Powered by AppSignal & Oban Pro

Loop 2: image, EditScore, OmniGen2

notebooks/2-image-to-omnigen2.livemd

Loop 2: image, EditScore, OmniGen2

Mix.install([
  {:pythonx, "~> 0.4.9"},
  {:kino_pythonx, "~> 0.1.0"},
  {:kino, "~> 0.19.0"}
])
[project]
name = "weft_loop_notebook"
version = "0.0.0"
requires-python = "==3.11.*"
dependencies = [
  "pillow==11.1.0",
  "numpy==2.2.3",
  "matplotlib==3.10.1",
  "requests==2.32.3"
]

What this loop is

Propose an edit with OmniGen2, score it with EditScore against the image that went in, change one knob, run it again. Three rounds and a baseline.

The pins above are light on purpose. OmniGen2 needs torch==2.6.0+cu124 and EditScore needs cu128, and one embedded interpreter cannot hold two CUDA torch builds, so the two heavy stages run in the pixi environments that anny-render-corpus/pixi.toml already declares. This notebook's interpreter holds the loop, the plot and the provenance record.

The harness

import sys, json, subprocess
from pathlib import Path

HARNESS = Path(r"C:\weftspun-keypoint\7-service\service-livebook\priv\python")
CORPUS = Path(r"C:\weftspun-keypoint\6-datasource\anny-render-corpus")
sys.path.insert(0, str(HARNESS))

from weft_loop import run, pixi_run, plot, household, PreconditionFailed
print("harness loaded from", HARNESS)

Inputs

SOURCE = r"C:\anny_test\hv_0.png"
INSTRUCTION = "make the figure photographic, keep the pose exactly"
WORK = Path(r"C:\weftspun-keypoint\.loop2")
WORK.mkdir(exist_ok=True)

STEPS = [30, 40, 50]
CONTROL = SOURCE

CONTROL is what the baseline is measured on: the source scored against itself. A score with nothing to compare it to is not a measurement, so the harness refuses to return a history without one.

Propose and score

def propose(i):
    out = WORK / f"round_{i}.png"
    pixi_run("omnigen2", [
        str(CORPUS / "omnigen2_edit.py"),
        "--image", SOURCE,
        "--out", str(out),
        "--steps", str(STEPS[i - 1]),
        "--precision", "bf16",
    ])
    return str(out)


def score(path):
    out = WORK / (Path(path).stem + ".score.json")
    pixi_run("editscore", [
        str(HARNESS / "weft_score.py"),
        "--source", SOURCE,
        "--edited", str(path),
        "--instruction", INSTRUCTION,
        "--out", str(out),
        "--precision", "nf4",
        "--max-pixels", "262144",
    ])
    return json.loads(out.read_text())["overall"]

--precision bf16 on the generator and nf4 on the scorer is not an inconsistency. A quantised generator does not produce corpus data; a quantised verifier is permitted, and EditScore at NF4 peaks at 6.75 GiB against 8.60 GiB at 1024, which is why the pixel cap is here at all.

Run it

history = run(propose, score, control=CONTROL, rounds=3)
print(history.table())
plot(history)

The control that must fail

def score_wrong(path):
    out = WORK / (Path(path).stem + ".wrong.json")
    pixi_run("editscore", [
        str(HARNESS / "weft_score.py"),
        "--source", SOURCE,
        "--edited", str(path),
        "--instruction", "turn the figure into a bowl of soup",
        "--out", str(out),
        "--precision", "nf4",
    ])
    return json.loads(out.read_text())["overall"]


control_score = score_wrong(history.best.artifact)
print("wrong instruction:", control_score, "| best:", history.best.score)
assert control_score < history.best.score, "the scorer did not separate a wrong instruction from a right one"

A check that passes on known-broken input certifies the defect. This one asserts that the scorer can tell a matching instruction from a nonsense one; if it cannot, nothing above it means anything.

Provenance

record = {
    "loop": "2-image-to-omnigen2",
    "source": SOURCE,
    "instruction": INSTRUCTION,
    "generator": {"model": "OmniGen2/OmniGen2", "precision": "bf16", "steps": STEPS},
    "scorer": {"base": "Qwen/Qwen3-VL-8B-Instruct",
               "adapter": "EditScore/EditScore-Qwen3-VL-8B-Instruct",
               "precision": "nf4", "max_pixels": 262144},
    "baseline": history.baseline,
    "rounds": [r.__dict__ for r in history.rounds],
    "wrong_instruction_control": control_score,
}
path = history.to_json(WORK / "history.json")
(WORK / "provenance.json").write_text(json.dumps(record, indent=2), encoding="utf-8")
print("wrote", path)

Generated data carries its generator, checkpoint and conditioning, or it cannot be regenerated and its provenance cannot be answered later. That is condition 1, and this cell is where the loop satisfies it.