From c316309fc693d248f5b27fc8bafef64b9baed71a Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Sun, 7 Jun 2026 22:54:09 -0700 Subject: [PATCH] feat(simulator): clips.py manifest model; retire selection fixtures Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/clips.py | 68 +++++++++++++++++++++++++++++++++++++++++ simulator/fixtures.py | 59 ------------------------------------ tests/test_clips.py | 69 ++++++++++++++++++++++++++++++++++++++++++ tests/test_fixtures.py | 40 ------------------------ 4 files changed, 137 insertions(+), 99 deletions(-) create mode 100644 simulator/clips.py delete mode 100644 simulator/fixtures.py create mode 100644 tests/test_clips.py delete mode 100644 tests/test_fixtures.py diff --git a/simulator/clips.py b/simulator/clips.py new file mode 100644 index 0000000..7d4de52 --- /dev/null +++ b/simulator/clips.py @@ -0,0 +1,68 @@ +"""The base-clip + variant + annotation manifest the simulator renders. + +Replaces simulator/fixtures.py (the selection-era synthetic catalog). Each base +clip carries: the raw base file, a map of pre-baked Right-strength variant files +(strength 0 is always the raw base), an authored Left annotation track (box + +label key + the minimum Left level at which it appears), and per-language string +tables. See the reconciled-simulator-alteration-slice design ยง3.2. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class Clip: + id: str + title: str + base_file: str + license: str + source: str + right_variants: dict # {"1": {"file": ...}, "4": {...}} (no "0") + annotations: list # [{"key", "box":[x,y,w,h], "min_level"}, ...] + strings: dict # {"en": {key: text}} + + def variant_file(self, strength: int) -> str: + """The video file for a Right strength; 0 and any unauthored strength + fall back to the raw base file.""" + entry = self.right_variants.get(str(strength)) + return entry["file"] if entry else self.base_file + + def to_dict(self) -> dict: + variants = {"0": {"file": self.base_file, "raw": True}} + for k, v in self.right_variants.items(): + variants[k] = v + return { + "id": self.id, + "title": self.title, + "base_file": self.base_file, + "license": self.license, + "source": self.source, + "right_variants": variants, + "annotations": self.annotations, + "strings": self.strings, + } + + +def _clip_from_dict(d: dict[str, Any]) -> Clip: + return Clip( + id=d["id"], + title=d["title"], + base_file=d["base_file"], + license=d.get("license", ""), + source=d.get("source", ""), + right_variants=d.get("right_variants", {}), + annotations=d.get("annotations", []), + strings=d.get("strings", {}), + ) + + +def load_manifest(path: str | Path) -> list[Clip]: + """Load the base-clip manifest. Raises FileNotFoundError if missing.""" + path = Path(path) + data = json.loads(path.read_text()) + return [_clip_from_dict(c) for c in data["clips"]] diff --git a/simulator/fixtures.py b/simulator/fixtures.py deleted file mode 100644 index 1d83344..0000000 --- a/simulator/fixtures.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Deterministic synthetic catalog so the selection model can be felt everywhere. - -The real catalog (catalog/library.jsonl) is empty; this generates one record per -cell of the 5x5 brain x 5x5 mood coordinate space (625 records), with a seeded -mix of content modes and review statuses and no real media attached. -""" - -from __future__ import annotations - -import random - -from hef.catalog import Record - -MODES = ("audio", "video", "av") -ARCHIVES = ("internet_archive", "musopen", "librivox", "nasa", "freesound") - -_LEFT_WORDS = ("Treatise", "Lecture", "Field Notes", "Reading", "Documentary") -_RIGHT_WORDS = ("Reverie", "Nocturne", "Bloom", "Drift", "Aurora") - - -def _title(left: int, right: int, dark: int, light: int, mode: str) -> str: - a = _LEFT_WORDS[left] if left >= right else _RIGHT_WORDS[right] - return f"{a} ({mode}) L{left}R{right}D{dark}Li{light}" - - -def generate_fixture_catalog(seed: int = 1729) -> list[Record]: - """One valid Record per coordinate cell (625 total), deterministic for a seed.""" - rng = random.Random(seed) - records: list[Record] = [] - n = 0 - for left in range(5): - for right in range(5): - for dark in range(5): - for light in range(5): - mode = rng.choice(MODES) - status = rng.choice(("proposed", "approved")) - is_video = mode in ("video", "av") - records.append( - Record( - id=f"fx-{n:04d}", - title=_title(left, right, dark, light, mode), - source_url=f"https://example.test/fx/{n:04d}", - source_archive=rng.choice(ARCHIVES), - license="public_domain", - mode=mode, - left=left, - right=right, - dark=dark, - light=light, - duration_s=rng.choice((300, 480, 600, 720, 900)), - file_path="", - review_status=status, - resolution="1920x1080" if is_video else "", - rationale=f"fixture at ({left},{right},{dark},{light})", - reviewed_at="2026-06-04T00:00:00Z" if status == "approved" else None, - ) - ) - n += 1 - return records diff --git a/tests/test_clips.py b/tests/test_clips.py new file mode 100644 index 0000000..a9507c2 --- /dev/null +++ b/tests/test_clips.py @@ -0,0 +1,69 @@ +import json + +import pytest + +from simulator.clips import Clip, load_manifest + + +def _manifest_dict(): + return { + "clips": [ + { + "id": "forest", + "title": "Yosemite Falls (neutral)", + "base_file": "forest/base.mp4", + "license": "poc-sample", + "source": "hef-poc", + "right_variants": { + "4": {"file": "forest/right4.mp4", "model": "sd-turbo+flow"}, + "1": {"file": "forest/right1.mp4"}, + }, + "annotations": [ + {"key": "detected.water", "box": [0.1, 0.2, 0.3, 0.4], "min_level": 1}, + {"key": "detected.conifer", "box": [0.6, 0.1, 0.2, 0.2], "min_level": 3}, + ], + "strings": {"en": {"detected.water": "flowing water", "detected.conifer": "conifer"}}, + } + ] + } + + +def test_load_manifest_parses_clips(tmp_path): + p = tmp_path / "manifest.json" + p.write_text(json.dumps(_manifest_dict())) + clips = load_manifest(p) + assert len(clips) == 1 + c = clips[0] + assert isinstance(c, Clip) + assert c.id == "forest" + assert c.base_file == "forest/base.mp4" + + +def test_clip_lists_variant_files_by_strength(tmp_path): + p = tmp_path / "manifest.json" + p.write_text(json.dumps(_manifest_dict())) + c = load_manifest(p)[0] + # variant 0 is always the raw base; authored strengths come from the manifest + assert c.variant_file(0) == "forest/base.mp4" + assert c.variant_file(4) == "forest/right4.mp4" + assert c.variant_file(1) == "forest/right1.mp4" + # an unauthored strength falls back to the raw base + assert c.variant_file(2) == "forest/base.mp4" + + +def test_clip_serializes_to_dict_for_the_api(tmp_path): + p = tmp_path / "manifest.json" + p.write_text(json.dumps(_manifest_dict())) + d = load_manifest(p)[0].to_dict() + assert d["id"] == "forest" + assert d["base_file"] == "forest/base.mp4" + assert d["annotations"][0]["key"] == "detected.water" + assert d["strings"]["en"]["detected.water"] == "flowing water" + # variant map is exposed keyed by strength string, including 0 -> base + assert d["right_variants"]["0"]["file"] == "forest/base.mp4" + assert d["right_variants"]["4"]["file"] == "forest/right4.mp4" + + +def test_missing_manifest_raises(tmp_path): + with pytest.raises(FileNotFoundError): + load_manifest(tmp_path / "nope.json") diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py deleted file mode 100644 index 8e0d8cd..0000000 --- a/tests/test_fixtures.py +++ /dev/null @@ -1,40 +0,0 @@ -from hef.catalog import validate_catalog -from hef.selection import CONTENT_MODES -from simulator.fixtures import generate_fixture_catalog - - -def test_fixture_catalog_is_valid(): - records = generate_fixture_catalog() - validate_catalog(records) # raises on any invalid record or duplicate id - - -def test_fixture_catalog_spans_the_coordinate_space(): - records = generate_fixture_catalog() - coords = {(r.left, r.right, r.dark, r.light) for r in records} - # all 625 cells of the 5x5 brain x 5x5 mood space are present - assert len(coords) == 625 - - -def test_fixture_catalog_has_every_content_mode(): - records = generate_fixture_catalog() - present = {r.mode for r in records} - assert CONTENT_MODES <= present - - -def test_fixture_catalog_mixes_review_statuses(): - records = generate_fixture_catalog() - statuses = {r.review_status for r in records} - assert statuses == {"proposed", "approved"} - - -def test_fixture_catalog_references_no_real_media(): - records = generate_fixture_catalog() - assert all(r.file_path == "" for r in records) - - -def test_fixture_catalog_is_deterministic(): - a = generate_fixture_catalog(seed=42) - b = generate_fixture_catalog(seed=42) - assert [r.id for r in a] == [r.id for r in b] - assert [r.mode for r in a] == [r.mode for r in b] - assert [r.review_status for r in a] == [r.review_status for r in b]