Zum Inhalt

tests/test_transcript_align.py

"""Tests for transcript_align — propagating segment.text edits into segment.words.

The Remotion renderer drives the per-word karaoke highlight from segment.words
and only falls back to segment.text when the word list is empty
(visualizer/src/DialogueComposition.tsx:644). A text edit that never reaches
words is therefore invisible in the rendered MP4.
"""

from itertools import pairwise


def test_replacing_one_word_keeps_the_surrounding_timings():
    from transcript_align import propagate_text_to_words

    segment = {
        "start": 0.0,
        "end": 2.0,
        "text": "wir deployen auf Kubernetes heute",
        "words": [
            {"word": "wir", "start": 0.0, "end": 0.3, "score": 0.9},
            {"word": "deployen", "start": 0.3, "end": 0.7, "score": 0.9},
            {"word": "auf", "start": 0.7, "end": 0.9, "score": 0.9},
            {"word": "Kubernetis", "start": 0.9, "end": 1.5, "score": 0.4},
            {"word": "heute", "start": 1.5, "end": 2.0, "score": 0.9},
        ],
    }

    propagate_text_to_words(segment)

    words = segment["words"]
    assert [w["word"] for w in words] == [
        "wir",
        "deployen",
        "auf",
        "Kubernetes",
        "heute",
    ]
    # The replaced word keeps the slot it was spoken in.
    assert words[3]["start"] == 0.9
    assert words[3]["end"] == 1.5
    # Untouched neighbours keep their timings byte-for-byte.
    assert words[0]["start"] == 0.0
    assert words[4]["end"] == 2.0


def test_deleted_word_disappears_and_leaves_the_others_untouched():
    from transcript_align import propagate_text_to_words

    segment = {
        "start": 0.0,
        "end": 1.0,
        "text": "wir deployen heute",
        "words": [
            {"word": "wir", "start": 0.0, "end": 0.3, "score": 0.9},
            {"word": "deployen", "start": 0.3, "end": 0.7, "score": 0.9},
            {"word": "jetzt", "start": 0.7, "end": 0.8, "score": 0.9},
            {"word": "heute", "start": 0.8, "end": 1.0, "score": 0.9},
        ],
    }

    propagate_text_to_words(segment)

    assert [w["word"] for w in segment["words"]] == ["wir", "deployen", "heute"]
    assert segment["words"][2]["start"] == 0.8


def test_segment_without_words_stays_empty():
    """WhisperX leaves some segments wordless; the renderer falls back to
    segment.text there. Inventing words would fake timings that were never
    measured — and the fake would then drive the karaoke highlight."""
    from transcript_align import propagate_text_to_words

    segment = {"start": 2.5, "end": 4.0, "text": "Sounds great to me.", "words": []}

    assert propagate_text_to_words(segment) == 0
    assert segment["words"] == []


def test_inserted_word_gets_a_highlightable_slot_between_gapless_neighbours():
    from transcript_align import propagate_text_to_words

    segment = {
        "start": 0.0,
        "end": 1.0,
        "text": "wir deployen das heute",
        "words": [
            {"word": "wir", "start": 0.0, "end": 0.3, "score": 0.9},
            {"word": "deployen", "start": 0.3, "end": 0.7, "score": 0.9},
            {"word": "heute", "start": 0.7, "end": 1.0, "score": 0.9},
        ],
    }

    propagate_text_to_words(segment)

    words = segment["words"]
    assert [w["word"] for w in words] == ["wir", "deployen", "das", "heute"]
    # Every word needs a non-empty window — the renderer highlights on
    # `nowMs >= startMs && nowMs < endMs`, so start == end never lights up.
    for w in words:
        assert w["end"] > w["start"], f"{w['word']} has an empty window"
    # Monotonic and inside the segment.
    assert all(a["end"] <= b["start"] for a, b in pairwise(words))
    assert words[0]["start"] >= segment["start"]
    assert words[-1]["end"] <= segment["end"]


def test_inserted_word_at_segment_head_gets_a_highlightable_slot():
    """WhisperX segments start exactly where their first word starts (measured:
    715 of 716 segments in a real run), so there is no room before the first
    word — the head insertion has to share the right neighbour's span."""
    from transcript_align import propagate_text_to_words

    segment = {
        "start": 0.0,
        "end": 1.0,
        "text": "also wir deployen heute",
        "words": [
            {"word": "wir", "start": 0.0, "end": 0.3, "score": 0.9},
            {"word": "deployen", "start": 0.3, "end": 0.7, "score": 0.9},
            {"word": "heute", "start": 0.7, "end": 1.0, "score": 0.9},
        ],
    }

    propagate_text_to_words(segment)

    words = segment["words"]
    assert [w["word"] for w in words] == ["also", "wir", "deployen", "heute"]
    for w in words:
        assert w["end"] > w["start"], f"{w['word']} has an empty window"
    assert all(a["end"] <= b["start"] for a, b in pairwise(words))
    assert words[0]["start"] >= segment["start"]
    # Only the inserted word is an edit; the re-timed neighbour keeps its text.
    assert words[0].get("_edited") is True
    assert "_edited" not in words[1]
    # Words after the borrowed neighbour are untouched.
    assert words[2] == {"word": "deployen", "start": 0.3, "end": 0.7, "score": 0.9}