Synaps-Image 1.0: standalone toolkit package, wave-4 results, Apache-2.0, examples from the confirmation run
1b8f835 verified Download synaps_image/loop.py from Synaptivo/Synaps-Image-1.0: direct link, hf CLI and curl.
- Browser
- Download file 6.11 kB
-
https://huggingface.co/Synaptivo/Synaps-Image-1.0/resolve/main/synaps_image/loop.py
- Command line
-
hf download hf://Synaptivo/Synaps-Image-1.0/synaps_image/loop.py
-
curl -L -o loop.py https://huggingface.co/Synaptivo/Synaps-Image-1.0/resolve/main/synaps_image/loop.py
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": "..."}}""" | |
| class Candidate: | |
| round: int | |
| cand: int | |
| caption: str | |
| seed: int | |
| image: object | |
| observed: list | |
| ocr_reward_01: float | |
| exact_match_rate: float | |
| 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 = '' | |
| 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 | |