From 5c2f303cfdda51bfc3140264e9c9ee5f9e2f3939 Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Mon, 18 May 2026 11:58:04 -0400 Subject: [PATCH] Fix Python padding bug: single-column images now tight-bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/proxy.py:125 was computing image height as tallest = max(lines_per_col, last_col_lines) if c_needed == 1 else lines_per_col The max(lines_per_col, ...) made every single-column render pad to the full MAX_EDGE=1568 canvas height regardless of content, wasting ~99% of the image-token budget for small prompts. Fix: tight-bound the single-column case. tallest = max(1, last_col_lines) if c_needed == 1 else lines_per_col Multi-column (c_needed > 1) is unchanged — all earlier columns must be lines_per_col tall to fit them, so the image must be too. Measured impact (scripts/visual_compare.py): sample before after change ----------- ------------ ------------ ----------- 17 chars 241x1568 241x14 -99% pixels 176 chars 241x1568 241x42 -97% pixels 885 chars 241x1568 241x315 -80% pixels Verified edge cases hold: - empty/short text: still gated by MIN_COMPRESS_CHARS - 5000-char single line: 241x441 (63 wrapped lines, tight) - 80-char boundary: 241x14 (2 lines, tight) - 500-line multi-column: 731x1568 (unchanged, correctly padded) - 20KB realistic system prompt: 68.5% savings (matches existing 65-73%) Added scripts/visual_compare.py to inspect Python vs Rust renderings side-by-side with labels and dimensions. This is the immediate win that's independent of the Rust rewrite — every proxied request gets fewer image tokens starting at the next proxy restart. --- scripts/visual_compare.py | 150 ++++++++++++++++++++++++++++++++++++++ src/proxy.py | 7 +- 2 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 scripts/visual_compare.py diff --git a/scripts/visual_compare.py b/scripts/visual_compare.py new file mode 100644 index 0000000..0d4579b --- /dev/null +++ b/scripts/visual_compare.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +Render the same text through Python and Rust, save PNGs side-by-side, and +also stitch a single comparison image with labels for easy visual inspection. + +Usage: + python3 scripts/visual_compare.py [out_dir] + +Output: + /python_.png + /rust_.png + /compare_.png (side-by-side with labels) + /README.txt (dim + byte size summary) +""" + +import io +import json +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "src")) + +import proxy as py_proxy # noqa: E402 +from PIL import Image, ImageDraw, ImageFont # noqa: E402 + +RUST_BIN = ROOT / "src" / "rust" / "target" / "release" / "examples" / "diff_render" + +# Three representative samples +SAMPLES = { + "tiny": "echo hello world\n", + "code": ( + "def render_chunks(text):\n" + " \"\"\"Render text into 5pt PNG, multi-column.\"\"\"\n" + " font = get_font(FONT_SIZE)\n" + " lines = wrap(text, FIXED_COL_CHARS)\n" + " return paint(font, lines)\n" + ), + "tools_schema": json.dumps({ + "tools": [ + { + "name": "Bash", + "description": "Execute a bash command", + "input_schema": { + "type": "object", + "properties": { + "command": {"type": "string", "description": "the command"}, + "timeout": {"type": "integer", "description": "timeout in ms"}, + }, + "required": ["command"], + }, + }, + { + "name": "Read", + "description": "Read a file from disk", + "input_schema": { + "type": "object", + "properties": { + "file_path": {"type": "string"}, + "offset": {"type": "integer"}, + "limit": {"type": "integer"}, + }, + "required": ["file_path"], + }, + }, + ] + }, indent=2), +} + + +def label_image(img: Image.Image, label: str) -> Image.Image: + """Add a black bar at top with the label in white.""" + bar_h = 24 + out = Image.new("RGB", (img.width, img.height + bar_h), (0, 0, 0)) + out.paste(img.convert("RGB"), (0, bar_h)) + d = ImageDraw.Draw(out) + try: + f = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 14) + except Exception: + f = ImageFont.load_default() + d.text((6, 4), label, fill=(255, 255, 255), font=f) + return out + + +def main(): + out_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/cip_compare") + out_dir.mkdir(parents=True, exist_ok=True) + + if not RUST_BIN.exists(): + print(f"build rust first: cd src/rust && cargo build --release --example diff_render") + sys.exit(1) + + # Run rust + spec_path = out_dir / "spec.json" + spec_path.write_text(json.dumps(SAMPLES)) + rust_outdir = out_dir / "_rust" + rust_outdir.mkdir(exist_ok=True) + r = subprocess.run( + [str(RUST_BIN), str(spec_path), str(rust_outdir)], + capture_output=True, text=True, + ) + if r.returncode != 0: + print("RUST FAILED:", r.stderr); sys.exit(2) + rust_manifest = json.loads((rust_outdir / "manifest.json").read_text()) + + summary_lines = ["Visual compare: Python proxy.py vs Rust src/rust/", ""] + + for name, text in SAMPLES.items(): + # Python + py_pngs = py_proxy.render_chunks(text) + assert len(py_pngs) == 1, f"unexpected multi-chunk python output for {name}" + py_path = out_dir / f"python_{name}.png" + py_path.write_bytes(py_pngs[0]) + py_img = Image.open(io.BytesIO(py_pngs[0])) + + # Rust + rs_files = rust_manifest.get(name, []) + assert len(rs_files) == 1, f"unexpected multi-chunk rust output for {name}" + rs_bytes = (rust_outdir / rs_files[0]).read_bytes() + rs_path = out_dir / f"rust_{name}.png" + rs_path.write_bytes(rs_bytes) + rs_img = Image.open(io.BytesIO(rs_bytes)) + + # Side-by-side compare + py_labeled = label_image(py_img, f"PYTHON {py_img.width}x{py_img.height} ({len(py_pngs[0])}B)") + rs_labeled = label_image(rs_img, f"RUST {rs_img.width}x{rs_img.height} ({len(rs_bytes)}B)") + gap = 8 + cmp_w = max(py_labeled.width, rs_labeled.width) + cmp_h = py_labeled.height + gap + rs_labeled.height + cmp = Image.new("RGB", (cmp_w, cmp_h), (40, 40, 40)) + cmp.paste(py_labeled, (0, 0)) + cmp.paste(rs_labeled, (0, py_labeled.height + gap)) + cmp.save(out_dir / f"compare_{name}.png", "PNG") + + line = ( + f"[{name:14s}] py={py_img.width:4d}x{py_img.height:<4d} ({len(py_pngs[0]):>6d}B) " + f"rs={rs_img.width:4d}x{rs_img.height:<4d} ({len(rs_bytes):>6d}B) " + f"text_len={len(text)}" + ) + print(line) + summary_lines.append(line) + + (out_dir / "README.txt").write_text("\n".join(summary_lines) + "\n") + print(f"\nwrote PNGs to {out_dir}/") + + +if __name__ == "__main__": + main() diff --git a/src/proxy.py b/src/proxy.py index b150f1b..e789ac1 100644 --- a/src/proxy.py +++ b/src/proxy.py @@ -120,9 +120,12 @@ def render_chunks(text: str, font_size: int = FONT_SIZE) -> list[bytes]: tallest = lines_per_col if c_needed > 1 else len(chunk) else: tallest = len(chunk) - # All non-last columns are full lines_per_col; last col has the remainder + # All non-last columns are full lines_per_col; last col has the remainder. + # Single-column case → tight bound (just last_col_lines, no canvas pad). + # Multi-column case → all earlier cols are full lines_per_col tall, so + # the image must be lines_per_col rows tall to fit them. last_col_lines = len(chunk) - (c_needed - 1) * lines_per_col - tallest = max(lines_per_col, last_col_lines) if c_needed == 1 else lines_per_col + tallest = max(1, last_col_lines) if c_needed == 1 else lines_per_col chunk_img_h = line_h * tallest img = Image.new("L", (chunk_img_w, chunk_img_h), 255) d = ImageDraw.Draw(img)