85 lines
3.4 KiB
Python
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.")
|