import json import re import tempfile import unittest from unittest import mock from pathlib import Path from PIL import Image import scene_concepts as sc class CatalogueTests(unittest.TestCase): def test_groups_and_prompts(self): groups = {g for g, _ in sc.CATALOGUE.values()} self.assertEqual(groups, {"scene", "backdrop", "texture"}) for name in sc.CATALOGUE: prompt = sc.compose_prompt(name) self.assertGreater(len(prompt), 80, name) for day in ("skyline_day", "skyline_dusk", "skyline_night"): self.assertEqual(sc.CATALOGUE[day][0], "backdrop") def test_shots_point_at_scene_concepts(self): for shot, concept in sc.SHOTS.items(): self.assertEqual(sc.CATALOGUE[concept][0], "scene", shot) def test_paths_stay_in_their_folders(self): self.assertTrue(sc.raw_path("hollywood_boulevard_entrance_day").is_relative_to(sc.CONCEPT_DIR)) self.assertTrue(sc.raw_path("hollywood_boulevard_entrance_day").is_relative_to( sc.CONCEPT_DIR / "hollywood_boulevard")) self.assertTrue(sc.raw_path("joke_shop_entrance_day").is_relative_to( sc.CONCEPT_DIR / "joke_shop")) self.assertTrue(sc.raw_path("skyline_day").is_relative_to(sc.RAW_DIR)) self.assertTrue(sc.raw_path("asphalt").is_relative_to(sc.RAW_DIR)) class ReferenceGateTests(unittest.TestCase): ROOT = Path(sc.__file__).resolve().parent.parent def test_refs_photo_listed_in_sidecar_passes_with_provenance(self): result = sc._reference_source( self.ROOT / "docs/screens/refs/hollywood/" "el-capitan-facade-day-wikimedia-2009-10-27.jpg") self.assertTrue(result["checked"]) self.assertEqual(result["licence"], "CC BY 2.0") self.assertIn("commons.wikimedia.org", result["page"]) def test_refs_photo_missing_from_sidecar_is_refused(self): with tempfile.TemporaryDirectory(dir=self.ROOT / "docs/screens/refs") as tmp: path = Path(tmp) / "unlisted.jpg" path.write_bytes(b"x") with self.assertRaises(SystemExit): sc._reference_source(path) def test_outside_photo_requires_the_explicit_override(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "photo.jpg" path.write_bytes(b"x") with self.assertRaises(SystemExit): sc._reference_source(path) result = sc._reference_source(path, allow_unchecked=True) self.assertFalse(result["checked"]) self.assertEqual(result["licence"], "unchecked") def test_missing_file_is_refused(self): with self.assertRaises(SystemExit): sc._reference_source(self.ROOT / "docs/screens/refs/nope.jpg") def test_generate_one_writes_reference_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 = Path(tmp) / "hollywood_boulevard_entrance_day.png" with mock.patch.object(sc.generate_textures, "generate_image", return_value=FakeImage()) as mocked, \ mock.patch.object(sc, "next_free_path", return_value=out), \ mock.patch.object(sc, "_rel", side_effect=str): sc.generate_one("hollywood_boulevard_entrance_day", "m", 1536, reference=reference) call = mocked.call_args self.assertIn("keep the building", call.args[0].lower()) self.assertEqual(call.kwargs["reference_path"], reference["path"]) provenance = json.loads(out.with_suffix(".refgen.json").read_text()) self.assertEqual(provenance["reference"]["licence"], "CC BY 2.0") self.assertTrue(provenance["reference"]["licence_checked"]) class NoOverwriteTests(unittest.TestCase): def test_next_free_path_never_returns_an_existing_file(self): with tempfile.TemporaryDirectory() as tmp: base = Path(tmp) / "a.png" self.assertEqual(sc.next_free_path(base), base) base.write_bytes(b"x") second = sc.next_free_path(base) self.assertEqual(second.name, "a-v2.png") second.write_bytes(b"x") self.assertEqual(sc.next_free_path(base).name, "a-v3.png") class ProcessingTests(unittest.TestCase): def _skyline(self, opaque=False): image = Image.new("RGBA", (300, 200), (120, 170, 230, 255 if opaque else 0)) for x in range(300): top = 80 + (x * 7) % 50 for y in range(top, 200): image.putpixel((x, y), (40 + x % 30, 40, 50, 255)) return image def test_key_sky_makes_an_opaque_sky_transparent(self): keyed = sc.key_sky(self._skyline(opaque=True)) self.assertEqual(keyed.getpixel((5, 5))[3], 0) self.assertEqual(keyed.getpixel((5, 199))[3], 255) def test_band_keeps_tower_tops(self): band = sc._band(self._skyline()) self.assertAlmostEqual(band.size[0] / band.size[1], 2.0, delta=0.6) alpha = band.getchannel("A") self.assertEqual(alpha.getpixel((0, band.size[1] - 1)), 255) def test_processed_backdrop_audits_clean(self): with tempfile.TemporaryDirectory() as tmp: src = Path(tmp) / "raw.png" self._skyline().save(src) out = sc.process_backdrop(src, Path(tmp) / "skyline.png") result = sc.audit_backdrop(out) self.assertEqual(result["errors"], []) def test_texture_processing_writes_tile_and_normal(self): with tempfile.TemporaryDirectory() as tmp: src = Path(tmp) / "raw.png" Image.effect_noise((128, 128), 40).convert("RGB").save(src) out = sc.process_texture(src, Path(tmp) / "tile.png", size=64) self.assertEqual(Image.open(out).size, (64, 64)) normal = Image.open(out.with_name("tile_normal.png")) self.assertEqual(normal.size, (64, 64)) r, g, b = normal.getpixel((10, 10)) self.assertGreater(b, 128) # normals point out of the surface def _striped(period=64, size=256): image = Image.new("RGB", (size, size), (200, 190, 170)) for x in range(size): if (x // (period // 2)) % 2 == 0: for y in range(size): image.putpixel((x, y), (150, 40, 45)) return image class StripeTests(unittest.TestCase): def test_period_found_and_tile_wraps(self): tile = _striped() self.assertEqual(sc.stripe_period(tile), 64) aligned = sc.align_stripes(tile) self.assertEqual(aligned.size, tile.size) # The wrap step along x matches a normal neighbouring-column step. profile = sc._column_profile(aligned) wrap = sc._step(profile[-1], profile[0]) hardest = max(sc._step(profile[x], profile[x + 1]) for x in range(aligned.size[0] - 1)) self.assertLessEqual(wrap, hardest * 1.25 + 0.5) def test_flat_texture_has_no_period(self): self.assertEqual(sc.stripe_period(Image.new("RGB", (128, 128), (90, 90, 90))), 0) flat = Image.new("RGB", (64, 64), (128, 128, 128)) self.assertEqual(sc.align_stripes(flat).size, flat.size) def test_striped_audit_accepts_a_boundary_on_the_edge(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "awning_canvas.png" image = _striped(period=64, size=sc.TILE) image.save(path) result = sc.audit_striped_tile(path) self.assertEqual(result["errors"], []) class PromoteTests(unittest.TestCase): def test_promote_copies_and_never_overwrites(self): with tempfile.TemporaryDirectory() as tmp: picks = Path(tmp) / "picks" global_dir = sc.PICKS_DIR sc.PICKS_DIR = picks try: sc.READY_DIR = Path(tmp) Image.new("RGB", (16, 16), (10, 20, 30)).save(Path(tmp) / "src.png") Image.new("RGB", (16, 16), (128, 128, 255)).save(Path(tmp) / "src_normal.png") target = sc.promote("pick", "src") self.assertTrue(target.is_file()) self.assertTrue((picks / "pick_normal.png").is_file()) Image.new("RGB", (16, 16), (0, 0, 0)).save(Path(tmp) / "src2.png") again = sc.promote("pick", "src2") self.assertEqual(again, target) self.assertNotEqual(Image.open(target).getpixel((0, 0)), (0, 0, 0)) finally: sc.PICKS_DIR = global_dir sc.READY_DIR = Path(tmp).parent # restored by the loader below def test_picks_exist_in_catalogue(self): for name, source in sc.PICKS.items(): self.assertTrue(source.startswith(name), f"{name} <- {source}") class RealismTests(unittest.TestCase): def test_metrics_and_gap(self): with tempfile.TemporaryDirectory() as tmp: flat = Path(tmp) / "flat.png" busy = Path(tmp) / "busy.png" Image.new("RGB", (320, 180), (128, 128, 128)).save(flat) Image.effect_noise((320, 180), 60).convert("RGB").save(busy) m_flat, m_busy = sc.image_metrics(flat), sc.image_metrics(busy) self.assertLess(m_flat["edge_density"], m_busy["edge_density"]) self.assertEqual(sc.metric_gap(m_busy, m_busy), 0.0) def test_parse_realism_response_clamps_and_limits(self): payload = {"choices": [{"message": {"content": '{"scores": {"overall": 14, "lighting": "3"}, "fixes": ["a","b","c","d","e","f"]}'}}]} verdict = sc.parse_realism_response(payload) self.assertEqual(verdict["scores"], {"overall": 10.0, "lighting": 3.0}) self.assertEqual(len(verdict["fixes"]), 5) self.assertEqual(sc.parse_realism_response({})["scores"], {}) def test_summary_averages_per_set(self): report = {"sets": {"a": { "x": {"metric_gap": 0.2, "judge": {"scores": {"overall": 2.0}}}, "y": {"metric_gap": 0.4, "judge": {"scores": {"overall": 4.0}}}}}} summary = sc.summarize(report) self.assertEqual(summary["a"]["overall"], 3.0) self.assertAlmostEqual(summary["a"]["metric_gap"], 0.3) self.assertIn("Mean scores", sc.render_markdown( {"run": "r", "judge_model": None, "sets": report["sets"] | {}} | { "sets": {"a": {k: dict(v, target="t.png", metrics={ "edge_density": 1, "fine_detail": 1, "contrast": 1}) for k, v in report["sets"]["a"].items()}}})) def test_request_carries_both_images(self): with tempfile.TemporaryDirectory() as tmp: a = Path(tmp) / "a.png" Image.new("RGB", (8, 8)).save(a) body = sc.build_realism_request("m", a, a) parts = body["messages"][0]["content"] self.assertEqual([p["type"] for p in parts], ["text", "image_url", "image_url"]) 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())