CEO-Synaptivo's picture
Synaps-Image 1.0: standalone toolkit package, wave-4 results, Apache-2.0, examples from the confirmation run
1b8f835 verified
Raw History Blame Contribute Delete
6.11 kB
"""PUP-T for one prompt: render, read the text back, rewrite the caption and render again (no training).
Same decisions as the measured research loop (p5/klein_rl/text_loop.py, wave 4):
- round 0 renders the PUP caption with the user seed;
- every candidate is read by the verifier and scored deterministically against the quoted texts of the prompt;
- the best candidate so far ranks by (1-CER, exact match), and ties keep the earliest;
- a prompt stops when its best candidate is solved (exact match and 1-CER = 1), when the rewrite is unusable, or
when the last round did not improve on the candidate already rewritten;
- otherwise the best caption is rewritten from the expected vs observed text, and CANDIDATES_PER_ROUND candidates
are rendered with seed, seed+1, ..., at most MAX_REFINES times.
Prompts without quoted text get the round-0 render only.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from .quotes import enforce_quote_guardrail, extract_quoted_literals
from .typography import parse_vlm_ocr_readout, score_typography_ocr
MAX_REFINES = 2
CANDIDATES_PER_ROUND = 2
CAPTION_MIN_CHARS = 20
CAPTION_MAX_CHARS = 4000
EPS = 1e-9
REVISE_TASK = """An image generator was given this caption:
{caption}
The image must show exactly these texts: {expected}
A careful reading of the generated image found: {observed}
Rewrite the caption so the generator renders every required text exactly, letter for letter: keep the scene and
everything else that is requested; put each required text in double quotes with its exact spelling, casing and
punctuation; place it prominently on a large plain high-contrast surface in a simple bold sans-serif font; state that each
required text appears exactly once and nowhere else, with no other text in the image; keep every other detail of the caption and do not shorten it.
Answer with one JSON object only: {{"revised_caption": "..."}}"""
@dataclass
class Candidate:
round: int
cand: int
caption: str
seed: int
image: object
observed: list
ocr_reward_01: float
exact_match_rate: float
@dataclass
class Result:
image: object
best: Candidate | None
caption: str
expected: list
candidates: list = field(default_factory=list)
revisions: list = field(default_factory=list)
stop: str = ''
@property
def renders(self) -> int:
return max(1, len(self.candidates))
def parse_revision(text: str) -> str:
"""revised_caption from the verifier's JSON; '' when absent, malformed or out of bounds."""
decoder = json.JSONDecoder()
text = text or ''
pos = text.find('{')
while pos >= 0:
try:
obj, _end = decoder.raw_decode(text, pos)
except (ValueError, RecursionError):
obj = None
if isinstance(obj, dict) and 'revised_caption' in obj:
value = obj['revised_caption']
value = value.strip() if isinstance(value, str) else ''
return value if CAPTION_MIN_CHARS <= len(value) <= CAPTION_MAX_CHARS else ''
pos = text.find('{', pos + 1)
return ''
def _rank(c: Candidate):
return (c.ocr_reward_01, c.exact_match_rate >= 1.0 - EPS)
def _solved(c: Candidate) -> bool:
return c.exact_match_rate >= 1.0 - EPS and c.ocr_reward_01 >= 1.0 - EPS
def _best(candidates):
best = None
for c in candidates: # rounds and candidates in order, so ties keep the earliest
if best is None or _rank(c) > _rank(best):
best = c
return best
def caption_for(prompt: str, upsample) -> str:
"""PUP caption with every quoted literal of the prompt kept verbatim (the measured guardrail)."""
repaired, _check = enforce_quote_guardrail(prompt, upsample(prompt))
return repaired
def generate(prompt: str, seed: int, *, upsample, render, read=None, revise=None, caption: str | None = None,
max_refines: int = MAX_REFINES) -> Result:
"""upsample(prompt) -> caption; render(caption, seed) -> image; read(image) -> raw OCR reply;
revise(caption, expected, observed) -> raw JSON reply. read/revise are needed only for prompts with quoted text."""
expected = list(extract_quoted_literals(prompt))
if int(max_refines) < 0:
raise ValueError('max_refines must be >= 0')
if caption is None and upsample is None:
raise ValueError('upsample is required unless a caption is given')
if expected and (read is None or revise is None):
raise ValueError('prompts with quoted text need read and revise')
caption = caption if caption is not None else caption_for(prompt, upsample)
image0 = render(caption, seed)
if not expected:
return Result(image=image0, best=None, caption=caption, expected=[], stop='no_quoted_text')
def check(rnd, cand, cap, s, image):
observed = parse_vlm_ocr_readout(read(image))
score = score_typography_ocr(expected, observed)
return Candidate(rnd, cand, cap, s, image, observed, float(score['ocr_reward_01']),
float(score['exact_match_rate']))
result = Result(image=image0, best=None, caption=caption, expected=expected)
result.candidates.append(check(0, 0, caption, seed, image0))
for rnd in range(max_refines):
best = _best(result.candidates)
if _solved(best):
result.stop = 'solved'
break
if best.round != rnd:
result.stop = 'no_improvement'
break
revised = parse_revision(revise(best.caption, expected, best.observed))
result.revisions.append({'round': rnd + 1, 'from': [best.round, best.cand], 'revised_caption': revised})
if not revised:
result.stop = 'unusable_rewrite'
break
for j in range(CANDIDATES_PER_ROUND):
result.candidates.append(check(rnd + 1, j, revised, seed + j, render(revised, seed + j)))
else:
result.stop = 'solved' if _solved(_best(result.candidates)) else 'max_refines'
result.best = _best(result.candidates)
result.image = result.best.image
return result