Files
gnommo/tests/test_narration_slice.py

85 lines
3.4 KiB
Python

"""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.")