feat(quality): предупреждения о возможной потере содержания транскрипта
- Зачем: - транскрипция может тихо терять содержание: блоки галлюцинированных повторов и обрыв распознавания до конца файла выглядят как законченный транскрипт (класс ошибок из ADR-006), и читатель об этом не узнаёт. - Что: - новый модуль quality.py: детектор разрыва в хвосте (строго >120 с) и детектор блоков повторяющихся сегментов (серия >=4 для длинного текста, >=10 для короткого вроде «ага»). - предупреждения жёлтым в консоли (single и batch) и строками «Внимание» в шапке markdown-транскрипта; _format_duration переименована в публичную format_duration. - 27 новых тестов: границы порогов, нормализация текста, шапка formatter, точные строки CLI-сообщений с обрезкой «(+ ещё N)». - Проверка: - uv run pytest — 218 passed, 1 skipped. - ручной прогон записи 23:01 (gigaam-v3/onnx) — транскрипт полный, предупреждений нет. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
650e7580e5
commit
b1dfd9dcca
@@ -0,0 +1,125 @@
|
||||
import pytest
|
||||
|
||||
from local_transcriber.quality import (
|
||||
REPETITION_MIN_LEN,
|
||||
TAIL_GAP_WARN_S,
|
||||
_normalize,
|
||||
find_repetition_blocks,
|
||||
tail_gap,
|
||||
)
|
||||
from local_transcriber.types import Segment, TranscribeResult
|
||||
|
||||
|
||||
def _result(segments, duration):
|
||||
return TranscribeResult(
|
||||
segments=segments,
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=duration,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
|
||||
def _segments(texts, start=0.0):
|
||||
return [
|
||||
Segment(start=start + index, end=start + index + 1.0, text=text)
|
||||
for index, text in enumerate(texts)
|
||||
]
|
||||
|
||||
|
||||
def test_tail_gap_returns_positive_gap():
|
||||
result = _result([Segment(0.0, 10.0, "Текст")], duration=42.0)
|
||||
|
||||
assert tail_gap(result) == 32.0
|
||||
|
||||
|
||||
def test_tail_gap_empty_segments_returns_zero():
|
||||
assert tail_gap(_result([], duration=42.0)) == 0.0
|
||||
|
||||
|
||||
def test_tail_gap_negative_gap_returns_zero():
|
||||
result = _result([Segment(0.0, 43.0, "Текст")], duration=42.0)
|
||||
|
||||
assert tail_gap(result) == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gap", [119.99, 120.0])
|
||||
def test_tail_gap_boundary_does_not_warn(gap):
|
||||
result = _result([Segment(0.0, 10.0, "Текст")], duration=10.0 + gap)
|
||||
|
||||
assert tail_gap(result) <= TAIL_GAP_WARN_S
|
||||
|
||||
|
||||
def test_tail_gap_boundary_warns_above_threshold():
|
||||
result = _result([Segment(0.0, 10.0, "Текст")], duration=130.01)
|
||||
|
||||
assert tail_gap(result) > TAIL_GAP_WARN_S
|
||||
|
||||
|
||||
def test_normalize_removes_case_punctuation_and_collapses_spaces():
|
||||
assert _normalize(' «ПРИВЕТ…» — (мир) [тест] <да> ') == "привет мир тест да"
|
||||
assert _normalize("раз–два-три: да; нет!") == "раздватри да нет"
|
||||
|
||||
|
||||
def test_normalize_punctuation_only_returns_empty():
|
||||
assert _normalize('.,!?…:;—–-"\'«»()[]<> ') == ""
|
||||
|
||||
|
||||
def test_find_repetition_blocks_four_long_segments_with_normalization():
|
||||
segments = _segments(["Повтор!", "повтор", "«ПОВТОР»", "повтор…"])
|
||||
|
||||
blocks = find_repetition_blocks(segments)
|
||||
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0].start == 0.0
|
||||
assert blocks[0].end == 4.0
|
||||
assert blocks[0].count == 4
|
||||
assert blocks[0].text == "Повтор!"
|
||||
|
||||
|
||||
def test_find_repetition_blocks_three_long_segments_is_empty():
|
||||
assert find_repetition_blocks(_segments(["Повтор", "повтор", "повтор"])) == []
|
||||
|
||||
|
||||
def test_find_repetition_blocks_length_boundary():
|
||||
assert find_repetition_blocks(_segments(["пять5"] * 4)) == []
|
||||
|
||||
blocks = find_repetition_blocks(_segments(["шесть6"] * 4))
|
||||
|
||||
assert len("шесть6") == REPETITION_MIN_LEN
|
||||
assert len(blocks) == 1
|
||||
|
||||
|
||||
def test_find_repetition_blocks_short_text_boundary():
|
||||
assert find_repetition_blocks(_segments(["Ага."] * 9)) == []
|
||||
|
||||
blocks = find_repetition_blocks(_segments(["Ага."] * 10))
|
||||
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0].count == 10
|
||||
|
||||
|
||||
def test_find_repetition_blocks_empty_normalized_text_is_ignored():
|
||||
assert find_repetition_blocks(_segments([":", "", " "] * 10)) == []
|
||||
|
||||
|
||||
def test_find_repetition_blocks_two_separate_runs():
|
||||
segments = [
|
||||
*_segments(["Первый повтор"] * 4),
|
||||
Segment(10.0, 11.0, "Разрыв"),
|
||||
*_segments(["Второй повтор"] * 4, start=20.0),
|
||||
]
|
||||
|
||||
blocks = find_repetition_blocks(segments)
|
||||
|
||||
assert len(blocks) == 2
|
||||
assert blocks[0].text == "Первый повтор"
|
||||
assert blocks[1].text == "Второй повтор"
|
||||
|
||||
|
||||
def test_find_repetition_blocks_empty_list_is_empty():
|
||||
assert find_repetition_blocks([]) == []
|
||||
|
||||
|
||||
def test_find_repetition_blocks_single_segment_is_empty():
|
||||
assert find_repetition_blocks([Segment(0.0, 1.0, "Повтор")]) == []
|
||||
Reference in New Issue
Block a user