Add the photo-anchored variant pass with night and overcast facade frames
This commit is contained in:
1 parent
6e04c59589
commit
ad91e71eeb
9 files changed
+183
-2
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
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."
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in new issue
Block a user