mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
Fix Python padding bug: single-column images now tight-bound
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.
This commit is contained in:
@@ -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:
|
||||
<out_dir>/python_<sample>.png
|
||||
<out_dir>/rust_<sample>.png
|
||||
<out_dir>/compare_<sample>.png (side-by-side with labels)
|
||||
<out_dir>/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()
|
||||
+5
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user