From dc122fa0bf9e4e367178c82241aff1dbfab0988a Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Wed, 24 Jun 2026 07:57:38 -0700 Subject: [PATCH] =?UTF-8?q?feat(pipeline):=20manifest=20assembly=20?= =?UTF-8?q?=E2=80=94=20upsert=20clip=20+=20ring=20scale/edges=20(content?= =?UTF-8?q?=20pipeline=20=C2=A73.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_pipeline_manifest.py | 35 +++++++++++++++++++++ tools/pipeline/manifest.py | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 tests/test_pipeline_manifest.py create mode 100644 tools/pipeline/manifest.py diff --git a/tests/test_pipeline_manifest.py b/tests/test_pipeline_manifest.py new file mode 100644 index 0000000..44d9a2c --- /dev/null +++ b/tests/test_pipeline_manifest.py @@ -0,0 +1,35 @@ +from tools.pipeline.manifest import upsert_clip, add_ring_scale, rebuild_ring_edges + + +def _base(): + return {"clips": [{"id": "cosmos", "title": "Cosmos"}], + "ring": {"scales": [{"id": "cosmos", "clip_id": "cosmos"}], + "transitions": []}} + + +def test_upsert_clip_replaces_by_id_else_appends(): + m = _base() + m = upsert_clip(m, {"id": "reef", "title": "Reef"}) + assert [c["id"] for c in m["clips"]] == ["cosmos", "reef"] + m = upsert_clip(m, {"id": "reef", "title": "Coral Reef"}) # replace, not dup + assert [c["id"] for c in m["clips"]] == ["cosmos", "reef"] + assert m["clips"][1]["title"] == "Coral Reef" + + +def test_add_ring_scale_inserts_after_named_scale(): + m = _base() + m = add_ring_scale(m, "orbit", "orbit", after="cosmos") + assert [s["id"] for s in m["ring"]["scales"]] == ["cosmos", "orbit"] + + +def test_rebuild_ring_edges_one_transition_per_edge_with_wrap(): + m = _base() + m = add_ring_scale(m, "orbit", "orbit", after="cosmos") + m = add_ring_scale(m, "abyss", "abyss", after="orbit") + m = rebuild_ring_edges(m) + files = [t["file"] for t in m["ring"]["transitions"]] + assert files == [ + "transitions/cosmos-orbit.mp4", + "transitions/orbit-abyss.mp4", + "transitions/abyss-cosmos.mp4", # wrap edge + ] diff --git a/tools/pipeline/manifest.py b/tools/pipeline/manifest.py new file mode 100644 index 0000000..c9a36bd --- /dev/null +++ b/tools/pipeline/manifest.py @@ -0,0 +1,55 @@ +"""Stage 6 — assemble manifest entries (spec §3.6). Pure dict transforms over a +loaded manifest dict; the caller does the json read/write. Idempotent by id so +re-running earlier stages never clobbers authored labels.""" + +from __future__ import annotations + +import copy + + +def upsert_clip(manifest: dict, clip: dict) -> dict: + """Replace the clip with the same id, or append it. Returns a new manifest.""" + m = copy.deepcopy(manifest) + clips = m.setdefault("clips", []) + for i, c in enumerate(clips): + if c["id"] == clip["id"]: + clips[i] = clip + return m + clips.append(clip) + return m + + +def add_ring_scale(manifest: dict, scale_id: str, clip_id: str, + after: str | None = None) -> dict: + """Insert a scale into ring.scales after the named scale (or append if `after` + is None / not found). No-op if scale_id already present. Returns a new manifest.""" + m = copy.deepcopy(manifest) + ring = m.setdefault("ring", {"scales": [], "transitions": []}) + scales = ring.setdefault("scales", []) + if any(s["id"] == scale_id for s in scales): + return m + entry = {"id": scale_id, "clip_id": clip_id} + idx = next((i for i, s in enumerate(scales) if s["id"] == after), None) + if idx is None: + scales.append(entry) + else: + scales.insert(idx + 1, entry) + return m + + +def rebuild_ring_edges(manifest: dict) -> dict: + """Rebuild ring.transitions to one placeholder entry per ring edge (N scales -> + N edges incl. the wrap), preserving any existing model string for an edge. + Returns a new manifest.""" + m = copy.deepcopy(manifest) + ring = m.setdefault("ring", {"scales": [], "transitions": []}) + scales = ring.get("scales", []) + existing = {t["file"]: t.get("model", "") for t in ring.get("transitions", [])} + edges = [] + n = len(scales) + for i in range(n): + a, b = scales[i]["id"], scales[(i + 1) % n]["id"] + file = f"transitions/{a}-{b}.mp4" + edges.append({"file": file, "model": existing.get(file, "placeholder-zoom")}) + ring["transitions"] = edges + return m