Skip to content

feat(v1): render wire images as ASCII or braille#2034

Draft
snimu wants to merge 28 commits into
mainfrom
sebastian/textify
Draft

feat(v1): render wire images as ASCII or braille#2034
snimu wants to merge 28 commits into
mainfrom
sebastian/textify

Conversation

@snimu

@snimu snimu commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Experimental opt-in image → ASCII/braille rendering at the v1 interception server.

[textify]
enabled = true       # disabled by default
# mode = "ascii"     # default when enabled; "braille" also available
width = 160

The transform runs before dialect parsing, so the provider/train renderer and the trace all see the same text. It catches data-URI images from task prompts, tool results, user simulators, and Chat/Responses/Anthropic wire formats without taskset or harness changes.

Design

  • TextifyConfig on EnvConfig; v0/legacy configs reject enabled textify instead of silently ignoring it
  • pure deterministic render core: ASCII + braille, explicit width/height, aspect correction, gamma, auto/manual inversion, ramp/threshold, hard character budget
  • in-place content-part replacement preserves message ordering and protocol structure
  • Responses file-backed images and HTTP(S) URLs pass through unchanged (no hot-path network fetch); computer screenshots become ordinary text observations with paired computer calls removed
  • traces record what the model actually saw; original task data remains unchanged for scoring
  • describe_textify(config) supports future self-supervised (image, params, rendering) data generation

Safety / performance

  • Pillow/NumPy rendering runs in asyncio.to_thread, never on the shared aiohttp loop
  • resize-first decoding avoids full-resolution float/RGBA intermediates
  • 25 MP decoded-image ceiling, 100 MB payload ceiling
  • 40k output-character default budget, 1M hard ceiling
  • bounded 32-entry per-rollout resend cache
  • malformed images consistently surface as recorded TaskErrors
  • fresh-process 4K PNG measurement: ~65 MB RSS over import baseline (vs ~536 MB before the resize-first refactor)

Validation

  • 27 focused textify/config tests pass
  • changed Python files pass Ruff; lockfile check passes; no type diagnostics in changed files
  • final Docker smoke, rebased onto c8ad8b4fc:
    • mmmu-v1, Physics validation, deepseek/deepseek-v4-flash, null harness
    • --textify.enabled true --textify.width 160
    • reward 1.0, no errors
  • without textify, this text-only model rejected MMMU image requests upstream (404); with textify, subprocess and Docker rollouts complete normally

Note: full-repo Ruff currently reports an unrelated unused BytesIO import introduced on current main in environments/mmmu_v1/mmmu_v1/taskset.py; this PR does not touch it.

Initial experiments

Six real MMMU images were rendered and tested blind with subagents:

  • shuffled image↔ASCII matching: 6/6 at widths 40, 120, 160; 4/6 at 80 (two sparse horizontal diagrams swapped — nearest-neighbor phase matters, not just width)
  • ASCII-only descriptions at width 160 retained coarse geometry but lost semantics/text: a leg anatomy cross-section was described as Saturn; current-carrying wires as frying pans; labels/equations were unreadable
  • tiny live MMMU slices were highly noisy across generation budgets, so this PR does not claim a universal minimum resolution; width 160 is a conservative initial default, with 120–160 a plausible coarse-geometry range

TEXTIFY_PLAN.md is a living design/experiment log. Future work explicitly covers an opt-in OCR/layout hybrid for text-heavy images (OCR text regions with coordinates/confidence; ASCII non-text regions), rather than merely increasing ASCII resolution.

Intentional limitations

  • lossy by design — not vision equivalence
  • data-URI images only; HTTP(S) and provider file IDs remain images
  • color is reduced to luminance
  • OCR/layout mode is future work

Note

Add ASCII and braille image rendering for wire images in v1 evaluations

  • Adds TextifyConfig to configure image-to-text rendering (mode, width, height, gamma, invert, threshold, etc.) and exposes it from verifiers.v1.
  • Implements image_to_text and render_url in verifiers/v1/utils/textify.py to decode image data URLs and emit fenced ASCII or braille art.
  • Extends the ChatDialect, AnthropicDialect, and ResponsesDialect with textify_body methods that replace image parts with rendered text fences before forwarding to the model.
  • The interception server applies textify transforms to inbound model-turn and aux requests, serializes opening-message fetches, and caches post-commit errors for replay instead of re-sampling.
  • Adds pillow as a project dependency for image decoding.
  • Risk: Textify is rejected for legacy v0 environments via a model validator; enabling it for v0 raises ValueError.

Macroscope summarized 6d8e123. (Automatic summaries will resume when PR exits draft mode or review begins).


Note

Medium Risk
When enabled, every intercepted request pays thread-pool image work and changes what the model sees; retry/replay behavior on the hot path is more complex, though scoped to native v1 and off by default.

Overview
Textify is an opt-in path for native v1 evals: base64 images in task prompts, tool results, and user-simulator turns are turned into fenced ASCII or braille before dialect parsing, so text-only models and traces see the same rendered text without taskset changes.

TextifyConfig on EnvConfig (--textify.* / [textify]); legacy v0 configs error if enabled. Rendering uses Pillow + NumPy in asyncio.to_thread, with size/output limits and a bounded per-rollout render cache on RolloutSession.

Chat, Responses, and Anthropic dialects implement textify_body; Responses also textifies computer screenshots and drops paired computer calls. The interception server textifies model and aux requests, serializes opening-message textify under a lock, and extends the retry replay cache to post-commit TaskErrors (e.g. bad images after a model turn) so SDK retries do not resample.

Adds pillow, docs/skills reference for TextifyConfig, unit + e2e fixtures/tests, and routine lockfile bumps.

Reviewed by Cursor Bugbot for commit 6d8e123. Bugbot is set up for automated code reviews on this repo. Configure here.

snimu added 5 commits July 15, 2026 19:48
… server)

Living design doc for the experimental textify feature: flip any vision
environment into a text-only one with a config switch by rendering image
content parts to ascii/braille where all model traffic already passes.
Initial version preserves the original intent.
Add an opt-in TextifyConfig at the interception layer so any native v1
vision task can run against text-only models. Data-URI images from task
prompts, tool results, user simulators, and all three wire dialects are
rendered deterministically before tracing or inference. ASCII is the
default enabled mode; rendering is disabled by default.

Includes arbitrary dimensions/aspect correction, auto-inversion,
braille, output/decode safety limits, bounded per-rollout caching,
off-event-loop rendering, CLI/TOML config, docs, and focused tests.
Allow threshold=otsu to select a deterministic global luminance cutoff.
Braille uses it for dot activation; ASCII becomes a binary two-glyph
rendering. Fixed 0.5 remains the default.
# Conflicts:
#	verifiers/v1/interception/server.py
#	verifiers/v1/session.py
@snimu
snimu marked this pull request as ready for review July 16, 2026 17:40
Comment thread verifiers/v1/dialects/anthropic.py Outdated
Comment thread docs/v1/evaluation.md Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

This PR introduces a substantial new feature (Textify) for rendering images as ASCII/braille text, adding new configuration, utility modules, dialect methods, and interception server integration. New features introducing new processing pipelines and user-facing capabilities warrant human review.

You can customize Macroscope's approvability policy. Learn more.

snimu added 2 commits July 17, 2026 10:30
Only synthesize data URLs for explicit base64 image sources. Anthropic
file/provider-backed sources cannot be decoded locally and now pass
through unchanged, matching Responses file_id behavior.
Teach the evaluate-environments skill when and how to enable Textify,
including lossiness and pass-through caveats, and add the complete
TextifyConfig field table to its reference.
Comment thread verifiers/v1/interception/server.py Outdated
Comment thread verifiers/v1/utils/textify.py Outdated
snimu added 2 commits July 17, 2026 11:29
Store the raw simulator opening before the fallible textify step, so a
retry reuses it instead of calling respond("") again. Serialize concurrent
opening acquisition with a per-session lock as well.
Include threshold=otsu in the serialized conversion description when
ASCII binarization is active, so self-supervised image/config/rendering
examples state every parameter that affects output.
Comment thread verifiers/v1/session.py Outdated
Comment thread verifiers/v1/session.py Outdated
Comment thread verifiers/v1/interception/server.py
snimu added 3 commits July 17, 2026 12:53
Use a true recent-image LRU plus a fixed-memory Bloom admission filter.
Old images beyond capacity can re-render on retained-history scans, but
they no longer evict the recent working set and cascade into all misses.
Stress render_image from 16 worker threads across repeated image keys,
asserting deterministic output and bounded cache/filter state. The cache
implementation already serializes all metadata mutation with a lock.
Let textify_messages accept a renderer callback and pass the rollout's
render_image function from interception, so user-simulator openings and
turns reuse the same digest cache as native wire images.
Comment thread verifiers/v1/interception/server.py Outdated
Comment thread verifiers/v1/utils/textify.py
snimu added 2 commits July 17, 2026 13:23
Hold the opening lock through textification and cache a successful
transformation once. Attribute failures while locked, then let the HTTP
response path report them without rewriting session.error after a later
winning attempt clears it.
Catch base64 decoding failures separately, then enforce the decoded-byte
ceiling outside the exception handler so oversized images retain their
specific safety-limit diagnostic.
Comment thread verifiers/v1/dialects/anthropic.py
Comment thread verifiers/v1/utils/textify.py
snimu added 4 commits July 17, 2026 18:27
Run the existing Anthropic content-block walker over top-level system
blocks as well as messages. Base64/URL images render, file-backed sources
pass through, and absent system fields remain absent.
Apply the one-million-character ceiling to the actual rendered length,
including one line separator between each character row.
# Conflicts:
#	verifiers/v1/interception/server.py
Comment thread verifiers/v1/interception/server.py
Comment thread verifiers/v1/interception/server.py
Comment thread verifiers/v1/interception/server.py
Cache committed model responses before reporting later user-simulator or
typed-textify failures, resolve in-flight retries with that response, and
clear the post-commit failure when a retry replays it. Also clear a failed
opening textify error immediately when a serialized retry succeeds.
@snimu

snimu commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Resolved all currently open review threads. The two newest correctness issues were valid and are fixed in df085537b: committed model turns are cached/resolved before later simulator or typed-textify failures so retries replay rather than re-sample, and successful serialized opening textification clears any prior failure before an opening @stop. Added deterministic regressions for both paths; 29 focused Textify tests pass. All 14 inline review threads are now resolved.

Comment thread verifiers/v1/interception/server.py
snimu added 4 commits July 19, 2026 18:47
Cache post-commit simulator/textify failures as retry results instead of
replaying the committed model response as success. Completed and in-flight
SDK retries now receive the same error without re-sampling the committed
turn, and successful opening retries clear stale textify failures.
The runtime never consumes renderer descriptions, and the hypothetical
self-supervised data workflow is outside this feature. Remove the helper and
its public export to keep Textify's API limited to active behavior.
Keep the evaluation docs and skill focused on activation and core options,
and condense the configuration reference to short field descriptions.
Comment thread verifiers/v1/session.py
Replace overlapping helper tests with real evals covering prompt, user, and
tool-result images plus post-commit error retries. Keep a small focused suite
for rendering algorithms, safety limits, non-chat dialects, cache admission,
and the deterministic opening race.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2d24496. Configure here.

Comment thread tests/v1/fixtures/textify_v1.py
snimu added 3 commits July 20, 2026 20:00
Represent a completed retry as one response-or-error result, share the
off-thread Textify execution wrapper, and avoid resampling images already
decoded to their target grid.
@snimu
snimu marked this pull request as draft July 21, 2026 08:31
Capture the MMMU model-scale vision/ASCII comparison, per-prompt reward
variance, provider and truncation caveats, and the real-image Otsu rendering
analysis so the experimental PR remains a self-contained research artifact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant