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
@@ -31,6 +31,7 @@ import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
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:
|
||||
for name, (group, _) in CATALOGUE.items():
|
||||
have = len(raw_variants(name))
|
||||
@@ -1123,6 +1180,23 @@ def build_parser():
|
||||
prom.add_argument("--pick", default="", help="name=candidate overrides")
|
||||
prom.set_defaults(handler=cmd_promote)
|
||||
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.add_argument("--run", required=True)
|
||||
real.add_argument("--judge", action="store_true")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
@@ -245,3 +246,59 @@ class RealismTests(unittest.TestCase):
|
||||
|
||||
if __name__ == "__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