first commit
This commit is contained in:
125
backend/tests/test_build_phrases.py
Normal file
125
backend/tests/test_build_phrases.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Тесты чистой функции реконструкции фраз `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"),
|
||||
]
|
||||
Reference in New Issue
Block a user