feat(simulator): clips.py manifest model; retire selection fixtures
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"]]
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user