"""Invariant tests for narration schedule slicing (chunked-render correctness). The property that must hold: render(A:C) uses the same per-segment source samples as render(A:B) ++ render(B:C). Since the render seeks each narration segment by its (sliced) skip, that reduces to: slice([A,C]) covers the same source spans as slice([A,B]) followed by slice([B,C]). """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from gnommo.narration import NarrationSegment, slice_schedule def seg(sid, skip, dur, offset): return NarrationSegment( seg_id=sid, source_path=Path(f"{sid}.mov"), skip=skip, take=dur, duration=dur, offset=offset, ) # Combined timeline: # s1: source skip 5, plays 100 -> combined [0,100], source [5,105] # s2: source skip 10, plays 200 -> combined [100,300], source [10,210] # s3: source skip 2, plays 50 -> combined [300,350], source [2,52] SCHED = [seg("s1", 5, 100, 0), seg("s2", 10, 200, 100), seg("s3", 2, 50, 300)] def _spans(segs): return [(s.seg_id, round(s.skip, 6), round(s.skip + s.take, 6)) for s in segs] def _merge(spans): """Fuse adjacent spans of the same segment (the boundary segment split across two chunks) so a chunked coverage can be compared to a single-window one.""" out = [] for sid, a, b in spans: if out and out[-1][0] == sid and abs(out[-1][2] - a) < 1e-6: out[-1] = (sid, out[-1][1], b) else: out.append((sid, a, b)) return out def check(name, cond): print(f" {'PASS' if cond else 'FAIL'} {name}") assert cond, name # Full-window slice is a no-op (full renders unaffected). full = slice_schedule(SCHED, 0, 350) check("full-window slice is identity", _spans(full) == _spans(SCHED)) # Window drops the out-of-range segment and trims the edges into the files. w = slice_schedule(SCHED, 150, 320) check("drops out-of-window segment s1", [s.seg_id for s in w] == ["s2", "s3"]) check("first kept seg seeks into its file (s2 skip 60, take 150, offset 0)", (w[0].skip, w[0].take, w[0].offset) == (60, 150, 0)) check("last kept seg trimmed to window (s3 skip 2, take 20, offset 150)", (w[1].skip, w[1].take, w[1].offset) == (2, 20, 150)) # Every seek stays within its own file's real bounds. for orig, sl in ((SCHED[1], w[0]), (SCHED[2], w[1])): check(f"{sl.seg_id} seek within file bounds", sl.skip >= orig.skip and sl.skip + sl.take <= orig.skip + orig.duration + 1e-6) # THE invariant: A:C == A:B ++ B:C in source coverage, for boundaries that land # mid-segment, on a segment edge, and spanning multiple segments. for A, B, C in [(150, 250, 340), (150, 300, 340), (50, 100, 350), (0, 300, 350)]: whole = _spans(slice_schedule(SCHED, A, C)) joined = _merge(_spans(slice_schedule(SCHED, A, B)) + _spans(slice_schedule(SCHED, B, C))) check(f"slice({A}:{C}) == slice({A}:{B})++slice({B}:{C})", joined == whole) # Offsets are contiguous and cover the window with no gap/overlap. for A, C in [(150, 320), (0, 350), (120, 340)]: sl = slice_schedule(SCHED, A, C) ok = abs(sl[0].offset) < 1e-6 for prev, cur in zip(sl, sl[1:]): ok = ok and abs((prev.offset + prev.duration) - cur.offset) < 1e-6 ok = ok and abs((sl[-1].offset + sl[-1].duration) - (C - A)) < 1e-6 check(f"contiguous coverage of window [{A},{C}]", ok) print("\nAll narration-slice invariants passed.")