Files
vidconf/backend/tests/test_build_phrases.py

126 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тесты чистой функции реконструкции фраз `build_phrases` (ТЗ §1.3).
Синтетические таймлайны без БД/сети — алгоритм тестируется в изоляции (TDD).
"""
import uuid
from core.plugins.transcriber import Segment
from workers.transcription.phrases import (
INTERJECTION_THRESHOLD_S,
PhraseDraft,
build_phrases,
)
ALICE = uuid.uuid4()
BOB = uuid.uuid4()
def test_single_speaker_multiple_segments_merge_into_one_phrase() -> None:
"""Несколько сегментов одного спикера → одна фраза с границами min start / max end."""
segments = {ALICE: [Segment(0.0, 2.0, "a"), Segment(3.0, 5.0, "b")]}
offsets = {ALICE: 0.0}
result = build_phrases(segments, offsets)
assert result == [PhraseDraft(participant_id=ALICE, start=0.0, end=5.0, text="a b")]
def test_alice_bob_alice_example_from_spec() -> None:
"""Пример ABA из §1.3: три фразы в порядке t_start."""
segments = {
ALICE: [Segment(0.0, 3.0, "alice1"), Segment(6.0, 9.0, "alice2")],
BOB: [Segment(3.0, 6.0, "bob1")],
}
offsets = {ALICE: 0.0, BOB: 0.0}
result = build_phrases(segments, offsets)
assert result == [
PhraseDraft(participant_id=ALICE, start=0.0, end=3.0, text="alice1"),
PhraseDraft(participant_id=BOB, start=3.0, end=6.0, text="bob1"),
PhraseDraft(participant_id=ALICE, start=6.0, end=9.0, text="alice2"),
]
def test_long_overlap_keeps_both_phrases_with_intersecting_intervals() -> None:
"""Перекрытие речи ≥ порога → сохраняются обе фразы с пересекающимися интервалами."""
segments = {
ALICE: [Segment(0.0, 10.0, "alice-long")],
BOB: [Segment(3.0, 8.0, "bob-overlap")],
}
offsets = {ALICE: 0.0, BOB: 0.0}
assert 8.0 - 3.0 >= INTERJECTION_THRESHOLD_S
result = build_phrases(segments, offsets)
assert result == [
PhraseDraft(participant_id=ALICE, start=0.0, end=10.0, text="alice-long"),
PhraseDraft(participant_id=BOB, start=3.0, end=8.0, text="bob-overlap"),
]
# интервалы действительно пересекаются
alice_phrase, bob_phrase = result
assert bob_phrase.start < alice_phrase.end
assert bob_phrase.end > alice_phrase.start
def test_short_interjection_does_not_break_monologue_but_is_kept_separately() -> None:
"""Короткое «угу» (<1.5с) внутри чужого монолога: монолог одной фразой, вставка — отдельной."""
segments = {
ALICE: [Segment(0.0, 2.0, "one"), Segment(2.5, 5.0, "two")],
BOB: [Segment(2.1, 2.3, "угу")],
}
offsets = {ALICE: 0.0, BOB: 0.0}
assert 2.3 - 2.1 < INTERJECTION_THRESHOLD_S
result = build_phrases(segments, offsets)
assert result == [
PhraseDraft(participant_id=ALICE, start=0.0, end=5.0, text="one two"),
PhraseDraft(participant_id=BOB, start=2.1, end=2.3, text="угу"),
]
def test_silence_gap_does_not_break_single_speaker_phrase() -> None:
"""Тишина (большой зазор) между сегментами одного спикера без чужой речи фразу не рвёт."""
segments = {ALICE: [Segment(0.0, 2.0, "a"), Segment(20.0, 22.0, "b")]}
offsets = {ALICE: 0.0}
result = build_phrases(segments, offsets)
assert result == [PhraseDraft(participant_id=ALICE, start=0.0, end=22.0, text="a b")]
def test_empty_input_returns_empty_list() -> None:
"""Пустой вход → пустой список фраз."""
assert build_phrases({}, {}) == []
def test_participant_with_no_segments_contributes_nothing() -> None:
"""Участник без сегментов не создаёт фраз, но не ломает построение остальных."""
segments = {ALICE: [Segment(0.0, 2.0, "a")], BOB: []}
offsets = {ALICE: 0.0, BOB: 0.0}
result = build_phrases(segments, offsets)
assert result == [PhraseDraft(participant_id=ALICE, start=0.0, end=2.0, text="a")]
def test_track_offsets_shift_timeline() -> None:
"""Смещения треков (`track_offsets`) корректно сдвигают таймлайн сегментов."""
segments = {
ALICE: [Segment(0.0, 2.0, "alice-early-on-track")],
BOB: [Segment(0.0, 2.0, "bob-early-on-track")],
}
# трек Боба стартовал позже (offset больше) → на общем таймлайне сеанса
# его сегмент оказывается ПОСЛЕ сегмента Алисы, хотя на своих треках
# оба сегмента начинались с нуля.
offsets = {ALICE: 10.0, BOB: 20.0}
result = build_phrases(segments, offsets)
assert result == [
PhraseDraft(participant_id=ALICE, start=10.0, end=12.0, text="alice-early-on-track"),
PhraseDraft(participant_id=BOB, start=20.0, end=22.0, text="bob-early-on-track"),
]