Add reference-anchored concept generation
Full build / Import + headless tests (push) Successful in 49s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-03 14:59:53 +01:00
1 parent 5b306b1c52
commit e21ff30783
8 files changed
+305 -31

No files matched your search

+85 -5
View File
@@ -323,30 +323,104 @@ def compose_prompt(name: str) -> str:
return f"{prompt}. {style}."
def generate_one(name: str, model: str, request_size: int) -> Path:
REFERENCE_PROMPT_SUFFIX = (
"Use the attached reference photograph as the fixed source of geometry and "
"composition: keep the building's proportions, column rhythm, band heights, "
"signage placement and camera angle exactly as photographed. Only restage "
"lighting, weather, people and surface condition where the prompt asks for it. "
"Do not invent extra storeys, wings or ornaments that the photo does not show."
)
REFS_DIR = ROOT / "docs" / "screens" / "refs"
def _reference_source(path_str: str, allow_unchecked: bool = False) -> dict:
"""Resolve and licence-check a reference photo for anchored generation.
A file under `docs/screens/refs/` must already be listed in that venue's
`SIDECAR.md` (the licence gate); its licence and Commons page are returned
for provenance. Anything else requires the explicit `--reference-unchecked`
override, which keeps unreviewed material out of the committed pipeline by
default.
"""
path = Path(path_str).expanduser().resolve()
if not path.is_file():
sys.exit(f"error: reference photo not found: {path}")
if REFS_DIR in path.parents:
sidecar = path.parent / "SIDECAR.md"
text = sidecar.read_text(encoding="utf-8") if sidecar.is_file() else ""
row = next((line for line in text.splitlines()
if f"`{path.name}`" in line), None)
if not row:
sys.exit(f"error: {path.name} is not listed in {sidecar}; add the "
"sidecar row (licence gate) before anchoring generation on it")
cells = [c.strip() for c in row.split("|")]
licence = next((c for c in cells if c and c.lower().startswith(("cc", "public"))), "?")
page = next((c.strip("<> ") for c in cells if c.startswith("<http")), "")
return {"path": path, "licence": licence, "page": page, "checked": True}
if not allow_unchecked:
sys.exit(f"error: {path} is outside docs/screens/refs/ and not licence-checked; "
"move it into a refs board with a SIDECAR row, or pass "
"--reference-unchecked to accept the licence responsibility")
return {"path": path, "licence": "unchecked", "page": "", "checked": False}
def _write_reference_provenance(out_path: Path, reference: dict, name: str,
model: str) -> Path:
"""Record which photo anchored a generated frame (`.refgen.json` sidecar)."""
sidecar = out_path.with_suffix(".refgen.json")
sidecar.write_text(json.dumps({
"concept": name,
"generated": out_path.name,
"model": model,
"reference": {"file": str(reference["path"].relative_to(ROOT))
if reference["path"].is_relative_to(ROOT) else str(reference["path"]),
"licence": reference["licence"],
"page": reference["page"],
"licence_checked": reference["checked"]},
"suffix": REFERENCE_PROMPT_SUFFIX,
}, indent=2) + "\n", encoding="utf-8")
return sidecar
def generate_one(name: str, model: str, request_size: int,
reference: dict | None = None) -> Path:
group = CATALOGUE[name][0]
prompt = compose_prompt(name)
if reference:
prompt = f"{prompt}\n\n{REFERENCE_PROMPT_SUFFIX}"
if group == "scene":
image = generate_textures.generate_image(prompt, model, request_size,
transparent=False, aspect_ratio="3:2")
transparent=False, aspect_ratio="3:2",
reference_path=reference["path"]
if reference else None)
image = ImageOps.fit(image.convert("RGB"), (SCENE_W, SCENE_H),
method=Image.Resampling.LANCZOS)
elif group == "backdrop":
image = generate_textures.generate_image(prompt, model, request_size,
transparent=True, aspect_ratio="3:2")
transparent=True, aspect_ratio="3:2",
reference_path=reference["path"]
if reference else None)
else:
image = generate_textures.generate_image(prompt, model, request_size,
transparent=False, aspect_ratio="1:1")
transparent=False, aspect_ratio="1:1",
reference_path=reference["path"]
if reference else None)
image = image.convert("RGB")
out = next_free_path(raw_path(name))
out.parent.mkdir(parents=True, exist_ok=True)
image.save(out)
if reference:
provenance = _write_reference_provenance(out, reference, name, model)
print(f"provenance {_rel(provenance)} <- {reference['licence']}")
print(f"wrote {_rel(out)} {image.size}")
return out
def cmd_generate(args) -> int:
_load_env_file()
reference = _reference_source(args.reference, args.reference_unchecked) \
if args.reference else None
names = _select(args.group, args.only)
if not names:
print("nothing selected", file=sys.stderr)
@@ -356,7 +430,7 @@ def cmd_generate(args) -> int:
for _ in range(max(1, args.variants)):
print(f"generating {name} ...", flush=True)
try:
generate_one(name, args.model, args.request_size)
generate_one(name, args.model, args.request_size, reference=reference)
except SystemExit as exc: # generate_textures exits on API errors
failed.append(name)
print(f" FAILED {name}: {exc}", file=sys.stderr)
@@ -1032,6 +1106,12 @@ def build_parser():
gen.add_argument("--variants", type=int, default=1)
gen.add_argument("--model", default=DEFAULT_MODEL)
gen.add_argument("--request-size", type=int, default=1536)
gen.add_argument("--reference", default="",
help="anchor generation on a licence-cleared reference photo "
"(must be listed in its refs-board SIDECAR.md)")
gen.add_argument("--reference-unchecked", action="store_true",
help="allow a reference photo from outside docs/screens/refs/ "
"(you take responsibility for its licence)")
gen.set_defaults(handler=cmd_generate)
proc = sub.add_parser("process")
proc.add_argument("--group", choices=("backdrop", "texture"))