Add the photo-anchored variant pass with night and overcast facade frames
Full build / Import + headless tests (push) Successful in 40s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-03 15:04:20 +01:00
1 parent 6e04c59589
commit ad91e71eeb
9 files changed
+183 -2

No files matched your search

+5 -2
View File
@@ -414,8 +414,11 @@ facade evidence is still the 2009 CC BY frame. The 8 downloaded candidates are i
`scene_concepts._reference_source` requiring a refs-board `SIDECAR.md` row (or an explicit `scene_concepts._reference_source` requiring a refs-board `SIDECAR.md` row (or an explicit
`--reference-unchecked`). Verified live on the El Capitan day concept anchored on the CC BY `--reference-unchecked`). Verified live on the El Capitan day concept anchored on the CC BY
2.0 2009 facade photo; tests in `ReferenceGateTests`. 2.0 2009 facade photo; tests in `ReferenceGateTests`.
- [ ] Add a sidecar-driven variant pass: for each real photo, generate day, night and weathered - [x] Add a sidecar-driven variant pass: `scene_concepts.py anchor --reference <photo>
variants of the same framing so the concept board stays comparable with the photo board. --treatments day,night,weathered` generates same-framing variants with treatment clauses,
one `.refgen.json` provenance sidecar per frame. Verified live on the facade photo (night +
overcast frames committed; the weathered treatment reads as mild overcast — noted on the
board). Tests in `AnchorTests`.
- [-] Generate facade, street and signage concept frames for the target block - [-] Generate facade, street and signage concept frames for the target block
(`scene_concepts.py generate --reference ...`), commit only the selected frames, and record (`scene_concepts.py generate --reference ...`), commit only the selected frames, and record
model, prompt, spend **and source photo** in the block sidecar. (First frame done: model, prompt, spend **and source photo** in the block sidecar. (First frame done:
+8
View File
@@ -207,6 +207,14 @@ generation writes a `.refgen.json` sidecar next to the frame (`model`, reference
geometry suffix) so a concept frame can always be traced back to its evidence photo. Where a geometry suffix) so a concept frame can always be traced back to its evidence photo. Where a
generated frame and its reference photo disagree, the photo wins for shape. generated frame and its reference photo disagree, the photo wins for shape.
### Photo-anchored variant pass
`tools/scene_concepts.py anchor --reference <photo> --context "..." --treatments day,night,weathered
--out-dir docs/screens/concepts/<block>` generates several restaged conditions (day, night,
weathered) of one reference photo's framing, each with its `.refgen.json` provenance sidecar, so a
concept board stays comparable with its photo board. The reference passes the same SIDECAR licence
gate as `--reference` on `generate`.
### Reference-board research (`refboard`) ### Reference-board research (`refboard`)
`tools/refboard.py` searches, fetches and documents Wikimedia Commons photos of a real location for `tools/refboard.py` searches, fetches and documents Wikimedia Commons photos of a real location for
@@ -22,6 +22,21 @@ The facade geometry (motto band, column rhythm, lattice windows, banner slots,
blade sign) follows the photograph; the street is restaged clean for art blade sign) follows the photograph; the street is restaged clean for art
direction. direction.
## Facade — night (photo-anchored)
![El Capitan facade at night, anchored on the facade photo](el_capitan_facade_night.png)
Same facade photo as the day frame, restaged to night with practical lighting
only (marquee, signage, street lamps) and wet pavement. The blade sign, banner
slots and lattice windows follow the photograph.
## Facade — overcast (photo-anchored)
![El Capitan facade under flat overcast light](el_capitan_facade_weathered.png)
Same framing under flat overcast light. The weathering requested came out
mild — treat this as the overcast target, not a decay reference.
## Entrance — night ## Entrance — night
![El Capitan Entertainment Centre entrance at night](hollywood_boulevard_entrance_night.png) ![El Capitan Entertainment Centre entrance at night](hollywood_boulevard_entrance_night.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,12 @@
{
"concept": "el_capitan_facade_night",
"generated": "el_capitan_facade_night.png",
"model": "google/gemini-2.5-flash-image",
"reference": {
"file": "docs/screens/refs/hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"licence": "CC BY 2.0",
"page": "https://commons.wikimedia.org/wiki/File:El_Capitan_Theater_frontal_fachade-The_Masonic_Temple.jpg",
"licence_checked": true
},
"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."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -0,0 +1,12 @@
{
"concept": "el_capitan_facade_weathered",
"generated": "el_capitan_facade_weathered.png",
"model": "google/gemini-2.5-flash-image",
"reference": {
"file": "docs/screens/refs/hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"licence": "CC BY 2.0",
"page": "https://commons.wikimedia.org/wiki/File:El_Capitan_Theater_frontal_fachade-The_Masonic_Temple.jpg",
"licence_checked": true
},
"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."
}
+74
View File
@@ -31,6 +31,7 @@ import argparse
import json import json
import math import math
import os import os
import re
import sys import sys
from pathlib import Path from pathlib import Path
@@ -1088,6 +1089,62 @@ def render_markdown(report: dict) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Treatment clauses for the photo-anchored variant pass: one framing, several
# restaged conditions, so a concept board stays comparable with its photo board.
TREATMENTS = {
"day": "clean midday daylight, soft haze fading the far street to beige",
"night": "night scene lit by practical lights only (marquee, signage, street "
"lamps), dark sky, wet pavement reflections",
"weathered": "overcast flat light, weathered and grimy facade surfaces, faded "
"signage, scattered litter near the kerb",
}
def _anchor_treatment_prompt(context: str, treatment: str) -> str:
if treatment not in TREATMENTS:
sys.exit(f"error: unknown treatment '{treatment}'; "
f"choose from {sorted(TREATMENTS)}")
return f"{context}\n\n{REFERENCE_PROMPT_SUFFIX}\n\nRestage the scene: {TREATMENTS[treatment]}"
def cmd_anchor(args) -> int:
"""Generate day/night/weathered variants of one reference photo's framing."""
_load_env_file()
reference = _reference_source(args.reference, args.reference_unchecked)
treatments = [t.strip() for t in args.treatments.split(",") if t.strip()]
unknown = [t for t in treatments if t not in TREATMENTS]
if unknown:
sys.exit(f"error: unknown treatment(s) {unknown}; "
f"choose from {sorted(TREATMENTS)}")
out_dir = ROOT / args.out_dir
out_dir.mkdir(parents=True, exist_ok=True)
stem = args.name or re.sub(r"-[0-9]{4}-[0-9]{2}-[0-9]{2}$", "",
reference["path"].stem)
stem = re.sub(r"[^a-z0-9_]+", "_", stem.lower()).strip("_")
failed = []
for treatment in treatments:
prompt = _anchor_treatment_prompt(args.context, treatment)
print(f"anchoring {stem}_{treatment} ...", flush=True)
try:
image = generate_textures.generate_image(
prompt, args.model, args.request_size,
reference_path=reference["path"])
image = ImageOps.fit(image.convert("RGB"), (SCENE_W, SCENE_H),
method=Image.Resampling.LANCZOS)
except SystemExit as exc: # generate_textures exits on API errors
failed.append(treatment)
print(f" FAILED {treatment}: {exc}", file=sys.stderr)
continue
out = next_free_path(out_dir / f"{stem}_{treatment}.png")
image.save(out)
_write_reference_provenance(out, reference, f"{stem}_{treatment}", args.model)
print(f"wrote {_rel(out)} {image.size}")
if failed:
print("failed: " + ", ".join(failed), file=sys.stderr)
return 1
return 0
def cmd_list(_args) -> int: def cmd_list(_args) -> int:
for name, (group, _) in CATALOGUE.items(): for name, (group, _) in CATALOGUE.items():
have = len(raw_variants(name)) have = len(raw_variants(name))
@@ -1123,6 +1180,23 @@ def build_parser():
prom.add_argument("--pick", default="", help="name=candidate overrides") prom.add_argument("--pick", default="", help="name=candidate overrides")
prom.set_defaults(handler=cmd_promote) prom.set_defaults(handler=cmd_promote)
sub.add_parser("audit").set_defaults(handler=cmd_audit) sub.add_parser("audit").set_defaults(handler=cmd_audit)
anchor = sub.add_parser("anchor",
help="generate day/night/weathered variants of one "
"reference photo's framing")
anchor.add_argument("--reference", required=True,
help="licence-cleared photo (listed in its refs SIDECAR.md)")
anchor.add_argument("--context", required=True,
help="what the photo shows and how to restage it")
anchor.add_argument("--treatments", default="day,night,weathered",
help="comma list from: " + ", ".join(sorted(TREATMENTS)))
anchor.add_argument("--out-dir", required=True,
help="concepts subfolder for the frames, relative to the repo root")
anchor.add_argument("--name", default="",
help="output stem (default: derived from the photo filename)")
anchor.add_argument("--model", default=DEFAULT_MODEL)
anchor.add_argument("--request-size", type=int, default=1536)
anchor.add_argument("--reference-unchecked", action="store_true")
anchor.set_defaults(handler=cmd_anchor)
real = sub.add_parser("realism") real = sub.add_parser("realism")
real.add_argument("--run", required=True) real.add_argument("--run", required=True)
real.add_argument("--judge", action="store_true") real.add_argument("--judge", action="store_true")
+57
View File
@@ -1,4 +1,5 @@
import json import json
import re
import tempfile import tempfile
import unittest import unittest
from unittest import mock from unittest import mock
@@ -245,3 +246,59 @@ class RealismTests(unittest.TestCase):
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
class AnchorTests(unittest.TestCase):
ROOT = Path(sc.__file__).resolve().parent.parent
def test_treatment_prompt_carries_context_suffix_and_clause(self):
prompt = sc._anchor_treatment_prompt("A 1920s masonic facade.", "night")
self.assertTrue(prompt.startswith("A 1920s masonic facade."))
self.assertIn("keep the building", prompt.lower())
self.assertIn("Restage the scene:", prompt)
self.assertIn("practical lights", prompt)
def test_unknown_treatment_names_are_rejected(self):
with self.assertRaises(SystemExit):
sc._anchor_treatment_prompt("c", "underwater")
def test_name_derivation_strips_the_capture_date(self):
reference = {"path": self.ROOT / "x" / "some-facade-2009-10-27",
"licence": "CC0", "page": "", "checked": True}
# exercised through cmd_anchor's stem logic via a tiny local copy
import re as _re
stem = _re.sub(r"-[0-9]{4}-[0-9]{2}-[0-9]{2}$", "",
"el-capitan-facade-day-wikimedia-2009-10-27")
self.assertEqual(stem, "el-capitan-facade-day-wikimedia")
def test_cmd_anchor_generates_each_treatment_with_provenance(self):
reference = {"path": self.ROOT / "docs/screens/refs/hollywood/"
"el-capitan-facade-day-wikimedia-2009-10-27.jpg",
"licence": "CC BY 2.0", "page": "https://commons/x",
"checked": True}
class FakeImage:
def convert(self, mode):
return Image.new("RGB", (1600, 900))
with tempfile.TemporaryDirectory() as tmp:
out_dir = Path(tmp) / "frames"
with mock.patch.object(sc, "_reference_source", return_value=reference), \
mock.patch.object(sc.generate_textures, "generate_image",
return_value=FakeImage()) as mocked, \
mock.patch.object(sc, "next_free_path",
side_effect=lambda p: p), \
mock.patch.object(sc, "_rel", side_effect=str):
args = mock.Mock(reference="r", reference_unchecked=False,
context="A facade.", treatments="day,night",
out_dir=str(out_dir), model="m",
request_size=1536)
args.name = "" # `name` is reserved on Mock; set it explicitly
self.assertEqual(sc.cmd_anchor(args), 0)
stems = [c.args[0] for c in mocked.call_args_list]
self.assertEqual(len(stems), 2)
for treatment, call in zip(("day", "night"), mocked.call_args_list):
self.assertIn("Restage the scene:", call.args[0])
self.assertEqual(call.kwargs["reference_path"],
reference["path"])
self.assertTrue((out_dir /
f"el_capitan_facade_day_wikimedia_{treatment}.png")
.exists())