Deploy / build (push) Successful in 19s
The consultant's review of the shipped set found two defects, both ours, and both fixed at the source rather than in the output: 1. The banner/thumb crop re-framing detected only the glowing core of the object. Column brightness measures 2-6 levels above the background on dark glass, so it boxed 8 % of the frame for an object group spanning 35 %+. The detector is now a background-plane deviation plus the largest connected components (`plan_banner_crops.py`, detector in `probe_band_crop.py`), 0 % false-positive on the frame's own negative space, stable across thresholds. The crop plan is frozen as `banner-plan.json` and consumed by the renderer; a thin subject is centred at 53.5 % of the banner width with the shortfall filled by the art's own edge pixels, one row at a time (a single averaged fill left a visible tonal step, tried and rejected). Measured on the delivered 72: 0 clipped, silhouette median 47.0 %, centre median 53.5 % (`verify_banner_crops.py`). 2. `art-meta.json` pinned two slugs to the hand-accepted sample frames — the very frames that carried the HUD readouts — which also kept them out of the crop plan. Pins removed. Prompt side: 9 frames still printed glyphs at the consultant's bar (>=12 px and >=2 characters). Every offending noun is measured and now asserted against: a literal `marked at 28` printed "28"; `storefront kiosk` printed "PRINATA"; `voucher card` printed "Voucher"; `market booths` printed "10MARKET"; `glass button` printed "OC"; `gateway pylon` printed "GATELY". 50 forbidden words, 0 hits across 72 captions, and all 9 frames re-rendered clean. Also: YAML escapes are honoured in the frontmatter reader and straight quotes become curly ones (the ZH cards shipped a literal backslash), the evidence line is capped at 65 characters, and the light theme gives dark artwork a hairline border plus a soft shadow (consultant option A) so it reads as a dark display case instead of a black slab. OCR gate at the consultant's bar: 72/72 frames clean (16 carry sub-threshold single glyphs, exempt as geometry noise).
120 lines
5.5 KiB
Python
120 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""sync_prompts_catalog.py — freeze the art inputs into the repo.
|
|
|
|
The metaphors and captions are authored in the Ideogram 4 tool dir
|
|
(`C:/AI/ideogram4/`), which is NOT in this repository. Without this script the
|
|
repository could not reproduce a single frame: `prompts_catalog.json` would still
|
|
describe the 21 shared metaphors that produced the near-duplicate v1-adjacent art.
|
|
|
|
This copies the authoring state into the repo so git alone is enough:
|
|
|
|
scripts/og-gen/metaphors.json slug -> the unique metaphor (72/72 distinct)
|
|
scripts/og-gen/captions/<slug>.json the exact structured caption sent to the model
|
|
scripts/og-gen/prompts_catalog.json same shape as before, refreshed from the above
|
|
scripts/og-gen/banner-plan.json the banner/thumb crop window per slug, computed by
|
|
`C:/AI/ideogram4/plan_banner_crops.py` (detector in
|
|
`probe_band_crop.py`). `render_from_art.mjs` reads
|
|
this by default; without it a slug falls back to a
|
|
weaker on-the-fly heuristic.
|
|
|
|
Usage:
|
|
python scripts/og-gen/sync_prompts_catalog.py # uses C:/AI/ideogram4
|
|
python scripts/og-gen/sync_prompts_catalog.py --tool-dir D:/other
|
|
python scripts/og-gen/sync_prompts_catalog.py --check # report only, no writes
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
OG = REPO / "scripts" / "og-gen"
|
|
|
|
|
|
def load_metaphors(tool_dir: Path) -> dict[str, str]:
|
|
src = tool_dir / "build_captions.py"
|
|
if not src.exists():
|
|
raise SystemExit(f"no build_captions.py under {tool_dir} (pass --tool-dir)")
|
|
spec = importlib.util.spec_from_file_location("build_captions", src)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return dict(mod.METAPHORS)
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--tool-dir", default="C:/AI/ideogram4")
|
|
ap.add_argument("--check", action="store_true", help="report only, write nothing")
|
|
args = ap.parse_args()
|
|
|
|
tool = Path(args.tool_dir)
|
|
metaphors = load_metaphors(tool)
|
|
|
|
posts = {p.stem for p in (REPO / "src" / "content" / "posts").glob("*.md")}
|
|
catalog = json.loads((OG / "prompts_catalog.json").read_text(encoding="utf-8"))
|
|
meta = json.loads((OG / "art-meta.json").read_text(encoding="utf-8"))
|
|
|
|
problems = []
|
|
if set(metaphors) != posts:
|
|
problems.append(f"slug mismatch: missing={sorted(posts - set(metaphors))[:4]} extra={sorted(set(metaphors) - posts)[:4]}")
|
|
if len(set(metaphors.values())) != len(metaphors):
|
|
problems.append(f"only {len(set(metaphors.values()))} distinct metaphors for {len(metaphors)} posts")
|
|
|
|
missing_captions = [s for s in metaphors if not (tool / "captions" / f"{s}.json").exists()]
|
|
if missing_captions:
|
|
problems.append(f"{len(missing_captions)} captions absent from {tool / 'captions'}")
|
|
missing_evidence = [s for s in metaphors if not meta.get(s, {}).get("evidence")]
|
|
if missing_evidence:
|
|
problems.append(f"{len(missing_evidence)} posts have no evidence line in art-meta.json")
|
|
|
|
plan_file = tool / "out" / "banner-plan.json"
|
|
plan = json.loads(plan_file.read_text(encoding="utf-8")) if plan_file.exists() else {"frames": {}}
|
|
plan_missing = sorted(set(metaphors) - set(plan.get("frames", {})))
|
|
if plan_missing:
|
|
problems.append(f"{len(plan_missing)} slugs have no banner crop plan "
|
|
f"(first: {plan_missing[:4]}) — run plan_banner_crops.py")
|
|
|
|
print(f"posts {len(posts)} · metaphors {len(metaphors)} · distinct {len(set(metaphors.values()))} · catalog {len(catalog)}")
|
|
print("problems:", problems or "none")
|
|
if problems:
|
|
sys.exit(1)
|
|
if args.check:
|
|
print("--check: nothing written")
|
|
return
|
|
|
|
(OG / "metaphors.json").write_text(
|
|
json.dumps({k: metaphors[k] for k in sorted(metaphors)}, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
dest = OG / "captions"
|
|
dest.mkdir(exist_ok=True)
|
|
for slug in metaphors:
|
|
shutil.copyfile(tool / "captions" / f"{slug}.json", dest / f"{slug}.json")
|
|
|
|
by_slug = {e["slug"]: e for e in catalog}
|
|
out = []
|
|
for slug in sorted(metaphors):
|
|
entry = by_slug.get(slug, {"slug": slug})
|
|
caption = json.loads((tool / "captions" / f"{slug}.json").read_text(encoding="utf-8"))
|
|
elems = caption.get("compositional_details", {}).get("elements", [])
|
|
entry["unique_elements"] = metaphors[slug]
|
|
entry["prompt"] = " ".join(e.get("desc", "") for e in elems).strip() or metaphors[slug]
|
|
entry["evidence_summary"] = meta.get(slug, {}).get("evidence", entry.get("evidence_summary", ""))
|
|
entry["caption"] = f"captions/{slug}.json"
|
|
out.append(entry)
|
|
(OG / "prompts_catalog.json").write_text(json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
(OG / "banner-plan.json").write_text(json.dumps(plan, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
fr = [f["silhouette"] for f in plan.get("frames", {}).values()]
|
|
if fr:
|
|
print(f"banner plan: {len(fr)} frames, silhouette "
|
|
f"{min(fr)*100:.0f}-{max(fr)*100:.0f}% of the banner width")
|
|
print(f"wrote metaphors.json, captions/ ({len(metaphors)} files), prompts_catalog.json, banner-plan.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|