test_shape_render.py
Language Python5.1 KB116 lines
import jsonfrom transcription_pipeline import renderfrom transcription_pipeline.shape import (_majority_speaker, _speaker_runs, rename_speakers,sentences_from_segments, turns)from .helpers import fake_resultdef shaped(speakers=True):return rename_speakers(sentences_from_segments(fake_result(speakers)["segments"]))def test_sentences_take_word_times_and_speakers():sents, names = shaped()assert [s["text"] for s in sents] == ["Hi, thanks for calling back.", "No problem.","Did the draft come through?"]assert [s["start"] for s in sents] == [0.5, 2.2, 3.5]assert all(s["timed"] for s in sents)# SPEAKER_01 talks first, so it becomes Speaker 1 whatever its raw label.assert [s["speaker"] for s in sents] == ["Speaker 1", "Speaker 2", "Speaker 2"]assert names == ["Speaker 1", "Speaker 2"]def test_turns_merge_consecutive_sentences():sents, _ = shaped()t = turns(sents)assert [x["speaker"] for x in t] == ["Speaker 1", "Speaker 2"]assert len(t[1]["sentences"]) == 2 and t[1]["end"] == 5.0def test_unaligned_segment_falls_back_to_interpolation():seg = {"start": 10.0, "end": 20.0, "text": "First part here. Second part here."}sents = sentences_from_segments([seg])assert sents[0]["start"] == 10.0 and not sents[0]["timed"]# "Second" starts at character 17 of 34, so halfway through the segment.assert sents[1]["start"] == 15.0def test_unpunctuated_speech_splits_at_speaker_change():words = [{"word": x, "start": i * 0.4, "end": i * 0.4 + 0.3, "speaker": s}for i, (x, s) in enumerate([("so", "A"), ("are", "A"), ("we", "A"), ("done", "A"),("yes", "B"), ("all", "B"), ("done", "B")])]seg = {"start": 0.0, "end": 3.0, "text": "so are we done yes all done", "words": words}sents, _ = rename_speakers(sentences_from_segments([seg]))assert [(s["speaker"], s["text"]) for s in sents] == [("Speaker 1", "so are we done"),("Speaker 2", "yes all done")]assert sents[1]["start"] == 1.6def test_one_word_flicker_is_not_a_speaker_change():labels = ["A", "A", "A", "B", "A", "A"]items = [(i, {"word": "w", "speaker": s}) for i, s in enumerate(labels)]runs = _speaker_runs(items)assert len(runs) == 1 and runs[0][0] == "A" and len(runs[0][1]) == 6def test_words_without_speaker_join_their_run():items = [(0, {"word": "a"}), (1, {"word": "b", "speaker": "A"}),(2, {"word": "c", "speaker": "A"}), (3, {"word": "d"}),(4, {"word": "e", "speaker": "B"}), (5, {"word": "f", "speaker": "B"})]runs = _speaker_runs(items)assert [(r[0], len(r[1])) for r in runs] == [("A", 4), ("B", 2)]def test_majority_tie_goes_to_first_speaker():words = [{"word": "one", "start": 0, "end": 1, "speaker": "X"},{"word": "two", "start": 1, "end": 2, "speaker": "Y"}]assert _majority_speaker(words) == "X"def test_no_diarization_means_no_labels():sents, names = shaped(speakers=False)assert names == [] and all(s["speaker"] is None for s in sents)assert render.to_txt(sents).splitlines()[0] == "[00:00:00] Hi, thanks for calling back."def test_txt_line_format():sents, _ = shaped()lines = render.to_txt(sents).splitlines()assert lines[0] == "[00:00:00] Speaker 1: Hi, thanks for calling back."assert lines[1] == "[00:00:02] Speaker 2: No problem."bare = render.to_txt(sents, timestamps=False).splitlines()assert bare[0] == "Speaker 1: Hi, thanks for calling back."assert render.stamp(3725.9) == "[01:02:05]"def test_markdown_is_a_vault_note():sents, names = shaped()md = render.to_markdown(sents, {"title": "Call", "recorded": "2026-09-01","transcribed": "2026-09-01T10:00:00", "duration": "0:00:05","engine": "local", "language": "en", "source": "call.m4a","speakers": names})head, body = md.split("---\n", 2)[1:]assert "type: transcript" in head and ' - "Speaker 2"' in headassert "**Speaker 1** [00:00:00]\nHi, thanks for calling back." in bodyassert "**Speaker 2** [00:00:02]\nNo problem. Did the draft come through?" in bodydef test_subtitles():sents, _ = shaped()srt = render.to_srt(sents)assert srt.startswith("1\n00:00:00,500 --> 00:00:01,700\n""Speaker 1: Hi, thanks for calling back.\n")vtt = render.to_vtt(sents)assert vtt.startswith("WEBVTT\n\n00:00:00.500 --> 00:00:01.700\n")# An untimed sentence with no end still gets a cue that moves forward.cue = list(render._cues([{"text": "x", "start": 4.0, "end": 4.0, "speaker": None}]))[0]assert cue[1] > cue[0]def test_json_carries_everything():sents, names = shaped()data = json.loads(render.to_json(sents, fake_result(), {"speakers": names}))assert data["meta"]["speakers"] == names and len(data["segments"]) == 2assert data["sentences"][0]["timed"] is True