126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
"""Тесты чистой функции реконструкции фраз `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:
|
||
"""Пример A–B–A из §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"),
|
||
]
|