feat(v1): render wire images as ASCII or braille#2034
Conversation
… 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
ApprovabilityVerdict: 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. |
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.
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.
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.
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.
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/rollout.py
# Conflicts: # 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.
|
Resolved all currently open review threads. The two newest correctness issues were valid and are fixed in |
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.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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.
# Conflicts: # pyproject.toml # uv.lock
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.

Summary
Experimental opt-in image → ASCII/braille rendering at the v1 interception server.
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
TextifyConfigonEnvConfig; v0/legacy configs reject enabled textify instead of silently ignoring itdescribe_textify(config)supports future self-supervised(image, params, rendering)data generationSafety / performance
asyncio.to_thread, never on the shared aiohttp loopTaskErrorsValidation
c8ad8b4fc:mmmu-v1, Physics validation,deepseek/deepseek-v4-flash, null harness--textify.enabled true --textify.width 160Note: full-repo Ruff currently reports an unrelated unused
BytesIOimport introduced on currentmaininenvironments/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:
TEXTIFY_PLAN.mdis 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
Note
Add ASCII and braille image rendering for wire images in v1 evaluations
TextifyConfigto configure image-to-text rendering (mode, width, height, gamma, invert, threshold, etc.) and exposes it fromverifiers.v1.image_to_textandrender_urlinverifiers/v1/utils/textify.pyto decode image data URLs and emit fenced ASCII or braille art.ChatDialect,AnthropicDialect, andResponsesDialectwithtextify_bodymethods that replace image parts with rendered text fences before forwarding to the model.pillowas a project dependency for image decoding.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.
TextifyConfigonEnvConfig(--textify.*/[textify]); legacy v0 configs error if enabled. Rendering uses Pillow + NumPy inasyncio.to_thread, with size/output limits and a bounded per-rollout render cache onRolloutSession.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-commitTaskErrors (e.g. bad images after a model turn) so SDK retries do not resample.Adds
pillow, docs/skills reference forTextifyConfig, 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.