From a550ee8b6b614f447308e358f95731978744baba Mon Sep 17 00:00:00 2001 From: jenstandstad Date: Sat, 8 Aug 2026 11:19:57 +0200 Subject: [PATCH] Adding fix to the logo pause narration --- gnommo/transformer.py | 39 +++++++++++++++++++++++++-------------- tests/test_chunking_v2.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/gnommo/transformer.py b/gnommo/transformer.py index 0a98181..67dc96b 100644 --- a/gnommo/transformer.py +++ b/gnommo/transformer.py @@ -1506,21 +1506,32 @@ def _extract_video_events( # it), so a clip spanning a chunk boundary survives into the later chunk. if end_time <= range_start or start_time >= range_end: continue - # A clip that began before this window is already mid-playback at the seam; - # seek into it so it resumes at the right frame instead of restarting. skip_override = None - if start_time < range_start: - into = range_start - start_time # elapsed since the clip started - base = skip # resolved per-occurrence skip - playable = (video_source.duration - base) if video_source.duration else None - if playable and playable > 0 and into >= playable: - # the clip has looped by the window start → resume at the loop phase - skip_override = base + (into % playable) - else: - # still within the first play-through (or unknown length) → linear seek - skip_override = base + into - start_time = range_start # -> 0 after time_offset subtraction - end_time = min(end_time, range_end) + if pause_narration: + # A pause-narration CUTSCENE must render WHOLE in the single chunk where it + # starts. Its end = start + pause_narration deliberately overshoots the + # pre-pause timeline (the 18s freeze doesn't exist yet at extraction), so + # clamping it to this chunk's pre-pause range_end would truncate the freeze, + # AND it would otherwise ALSO be pulled into the next chunk as a "spanning" + # clip — duplicating the freeze (the video6 "two 18s pauses / logo shows one + # frame" bug). Own it only where it starts; never seek or clamp it. + if start_time < range_start: + continue # earlier chunk owns this cutscene + else: + # A clip that began before this window is already mid-playback at the seam; + # seek into it so it resumes at the right frame instead of restarting. + if start_time < range_start: + into = range_start - start_time # elapsed since the clip started + base = skip # resolved per-occurrence skip + playable = (video_source.duration - base) if video_source.duration else None + if playable and playable > 0 and into >= playable: + # the clip has looped by the window start → resume at the loop phase + skip_override = base + (into % playable) + else: + # still within the first play-through (or unknown length) → linear seek + skip_override = base + into + start_time = range_start # -> 0 after time_offset subtraction + end_time = min(end_time, range_end) events.append( VideoEvent( diff --git a/tests/test_chunking_v2.py b/tests/test_chunking_v2.py index 645fcd7..9b5524f 100644 --- a/tests/test_chunking_v2.py +++ b/tests/test_chunking_v2.py @@ -134,10 +134,44 @@ def test_video(): check("full render includes bg with no seek", bg3 is not None and bg3.skip_override is None) +def test_pause_cutscene_chunk_ownership(): + # A pause_narration cutscene must live WHOLLY in the chunk where it starts: its + # end = start + pause_narration overshoots the pre-pause timeline, so it must not be + # truncated by range_end nor duplicated into the next chunk (the video6 double-freeze + # / one-frame-logo bug). + print("pause cutscene chunk ownership:") + cutouts = {"fullscreen": CutoutDefinition(x=0, y=0, height=1080, width=1920)} + videos = { + "logo": VideoSource(source_file="logo.mov", cutout="fullscreen", layer="above", + duration=18.0, pause_narration=18.0), + } + slides = {f"S{i}": SlideDefinition(image=f"S{i}.png", type="slide") for i in range(1, 6)} + markers = [MarkerTiming(marker_id=f"S{i}", timestamp=(i - 1) * 30.0, context="", confidence=1.0) + for i in range(1, 6)] + # Cutscene triggers at t=100 (between S4=90 and S5=120). Its end = 100+18 = 118. + markers.append(MarkerTiming(marker_id="vftp:logo", timestamp=100.0, context="", confidence=1.0)) + total = 200.0 + + # Owning chunk [90, 110): starts inside it. End must NOT be clamped to 110 → full 18s. + evs, _ = _extract_video_events(markers, videos, cutouts, slides, total, time_range=(90.0, 110.0)) + lg = next((e for e in evs if e.video_id == "logo"), None) + check("cutscene INCLUDED in the chunk it starts in", lg is not None) + if lg is not None: + check("cutscene end NOT clamped to range_end (full pause length)", + abs((lg.end_time - lg.start_time) - 18.0) < 1e-6, + f"duration={lg.end_time - lg.start_time}") + + # Next chunk [110, 200): the cutscene started earlier → must be EXCLUDED (no dupe freeze). + evs2, _ = _extract_video_events(markers, videos, cutouts, slides, total, time_range=(110.0, 200.0)) + check("cutscene EXCLUDED from the next chunk (no duplicate freeze)", + not any(e.video_id == "logo" for e in evs2)) + + if __name__ == "__main__": test_audio() test_crossfade_phase() test_video() + test_pause_cutscene_chunk_ownership() print() if _fails: print(f"FAILED: {len(_fails)} check(s): {', '.join(_fails)}")