Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

render-loop

Screenshot to code — where the product is the loop, not the first guess.

$ python -m render_loop target.rl

::::::::::::::::::::............................................
::::::::::::::::::::............................................
::::::.::::::...................................................
TTT.............................................................

outcome: budget
6 attempt(s), 6 render(s), best score 10.7500
  #1 score  12.0000  best  12.0000  1 matched, 3 missing, 0 extra
  ...
  #6 score  10.7500  best  10.7500  1 matched, 3 missing, 0 extra

The default generator got closer every round and never arrived, and the loop said so instead of returning its last attempt with a confident tone.

Generate markup, render it, compare the render to the target, correct, repeat. Every screenshot-to-code demo has that loop. Three things decide whether it is worth running, and none of them are the model.

Part of Agent Lab · Vol 2 · Project 01 — tools that watch, convert and triage. Standard library only. 25 tests, no API key, no network, no browser.


Quickstart

git clone https://github.com/dev48v/render-loop
cd render-loop
python -m venv .venv && . .venv/Scripts/activate   # Linux/macOS: . .venv/bin/activate
pip install -e ".[dev]"

python -m render_loop examples/target.rl     # render it, then try to reproduce it
pytest -q                                     # 25 tests

1. A pixel diff cannot converge

The obvious comparison is "how many pixels differ". It is also the one that guarantees the loop never finishes.

Two renders can be structurally identical and differ on thousands of pixels — a font advanced differently, an edge landed on a half-pixel. So the score never reaches zero, and worse, past a point it stops discriminating: every wrong answer looks equally wrong, so the signal stops pointing anywhere.

That is a testable claim, so it is a test:

def test_pixel_diff_saturates_where_structural_diff_still_discriminates():
    near = render(TARGET.replace("width=20", "width=34"))
    far  = render(TARGET.replace("width=20", "width=48"))

    assert s_far > s_near                                  # structural still ranks them
    assert abs(p_far - p_near) < (s_far - s_near) / 10     # pixel barely separates them

The structural comparison matches boxes to boxes and scores position, size and colour. It reaches exactly zero when the layouts agree, which is the property that lets a loop terminate at all.

assert structural_diff(boxes, boxes).value == 0.0

Both comparisons are implemented and both are measured, because "pixel diff is worse" is an opinion and a convergence curve is a finding.


2. Best-so-far, not last

Every correction can make things worse. A loop that returns its final attempt returns whatever its last mistake produced.

report = run_loop(TARGET, scripted([good, awful, awful]), patience=5)

assert report.best_source == good                  # not the last thing it built
assert report.attempts[-1].score > report.best_score

One variable. It is the difference between "here is my best attempt" and "here is where I happened to stop".


3. The generator never sees the answer

This is easier to get wrong by accident than it sounds — one shared object is enough. The proposer receives the previous source, its rendered boxes, and a score. Never the target markup.

def test_the_generator_never_receives_the_target_source():
    run_loop(TARGET, spy, max_attempts=2)
    assert TARGET not in seen        # only the previous source, never the target
    assert seen[0] == ""             # and the first call starts from nothing

There is a peeking() generator in the repo that does get handed the answer. It exists purely as a control: it matches on the first attempt, and nothing else in the suite is allowed to. A test that the honest loop is honest means very little without something that shows what cheating would have looked like.


Stopping conditions, named

outcome meaning
matched the render agrees with the target under the chosen comparison
no_improvement two rounds with no better score — it is going in circles
budget attempts exhausted while still improving; the last resort
gave_up the generator had nothing left to try
unrenderable the target does not parse, so nothing can be evaluated

unrenderable stops before a single candidate is generated. There is no signal to work against, so any output would be a guess.

And unrenderable output is an attempt, not a crash:

report = run_loop(TARGET, scripted(["blink width=2", "blink width=3"]), patience=2)
assert report.outcome == "no_improvement"
assert "did not render" in report.attempts[0].note

The renderer is real, and deliberately small

No browser, no fonts, no network. A block layout engine over four elements:

col gap=2
  box width=20 height=4 bg=#ff0000
  row gap=1
    box width=6 height=3 bg=#00ff00
    box width=6 height=3 bg=#0000ff
  text size=1 Hello

Layout is a pure function from document to positioned boxes — same input, same output, always — which is exactly what makes a structural comparison possible. Font metrics are two fixed constants, on purpose: real text shaping is the single largest source of pixel-level disagreement, and this project is about the loop.

Numeric attributes are validated at parse time rather than at layout time. A generator that emits width=wide should be told on the spot, not from a stack frame three calls away.


Swapping in a real model

def model_proposer(previous_source: str, boxes: list[Box], score: Score, n: int) -> str | None:
    reply = call_your_model(render_prompt(previous_source, describe(boxes), score))
    return extract_markup(reply)      # or None to give up

run_loop(target_source, model_proposer, max_attempts=12, patience=2)

The comparison, the best-so-far tracking and the five outcomes never learn what a model is. The shipped default (nudging) can only widen one box, and cannot solve a general target — on purpose, so the out-of-the-box run is an honest no_improvement rather than a demonstration that only works on the example.


Layout

src/render_loop/
  layout.py    parse -> tree -> positioned boxes -> raster.  A pure function.
  compare.py   pixel_diff and structural_diff, side by side
  loop.py      the loop, best-so-far, and the five outcomes
  __main__.py  python -m render_loop
tests/         25 tests, including the control for every honesty claim

Licence

MIT

About

Screenshot to code, where the product is the loop: a comparison that can actually converge, best-so-far rather than last, and a generator that never sees the answer. Agent Lab Vol 2 #01.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages