Adopt the realism pass and refresh screenshots/docs
- add the v2 runtime picks: per-phase skyline panoramas, brick, sidewalk pavers, dry/wet asphalt, awning canvas, and their normal maps - add the realism tooling and evidence: tools/scene_concepts.py, capture_realism.gd, concept frames, reports/sheets, and tests - retune phase lighting/fog/exposure, add sun yaw, brighten the night glow, add the flashlight beam mesh, and rebuild the planter/tree assets - regenerate all eleven staged screenshots from the current build; compare is 11 good / 0 bad - finish the visual-alignment closeout in DONE/TODO, update architecture, blender-assets, PLAN, and ignore rules (editable Blender sources untracked) - keep the judge retry and document the verified default judge model Verification: headless 227/227; Python tooling 86/86; check_screenshots 11/11.
This commit is contained in:
1 parent
26ecb93fd4
commit
d7e523f63c
70 files changed
+4042
-101
No files matched your search
@@ -0,0 +1,185 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
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("boulevard_skyline_day").is_relative_to(sc.CONCEPT_DIR))
|
||||
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 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()
|
||||
Reference in new issue
Block a user