- Зачем: - транскрипция может тихо терять содержание: блоки галлюцинированных повторов и обрыв распознавания до конца файла выглядят как законченный транскрипт (класс ошибок из 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>
79 lines
2.2 KiB
Python
79 lines
2.2 KiB
Python
"""Эвристики качества транскрипта."""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from .types import Segment, TranscribeResult
|
|
|
|
TAIL_GAP_WARN_S = 120.0
|
|
REPETITION_MIN_RUN = 4
|
|
REPETITION_MIN_RUN_SHORT = 10
|
|
REPETITION_MIN_LEN = 6
|
|
|
|
_PUNCTUATION_TO_REMOVE = ".,!?…:;—–-\"'«»()[]<>"
|
|
_REMOVE_PUNCTUATION = str.maketrans("", "", _PUNCTUATION_TO_REMOVE)
|
|
|
|
|
|
@dataclass
|
|
class RepetitionBlock:
|
|
start: float
|
|
end: float
|
|
count: int
|
|
text: str
|
|
|
|
|
|
def tail_gap(result: TranscribeResult) -> float:
|
|
"""Возвращает непокрытый хвост записи в секундах."""
|
|
if not result.segments:
|
|
return 0.0
|
|
return max(0.0, result.duration - result.segments[-1].end)
|
|
|
|
|
|
def _normalize(text: str) -> str:
|
|
"""Нормализует текст сегмента для поиска межсегментных повторов."""
|
|
text = text.casefold()
|
|
text = text.translate(_REMOVE_PUNCTUATION)
|
|
return " ".join(text.split())
|
|
|
|
|
|
def find_repetition_blocks(segments: list[Segment]) -> list[RepetitionBlock]:
|
|
"""Находит серии подряд идущих одинаковых сегментов."""
|
|
blocks: list[RepetitionBlock] = []
|
|
run_start = 0
|
|
run_norm = ""
|
|
|
|
def append_run(run_end: int) -> None:
|
|
count = run_end - run_start
|
|
if not run_norm:
|
|
return
|
|
min_run = (
|
|
REPETITION_MIN_RUN
|
|
if len(run_norm) >= REPETITION_MIN_LEN
|
|
else REPETITION_MIN_RUN_SHORT
|
|
)
|
|
if count >= min_run:
|
|
blocks.append(
|
|
RepetitionBlock(
|
|
start=segments[run_start].start,
|
|
end=segments[run_end - 1].end,
|
|
count=count,
|
|
text=segments[run_start].text,
|
|
)
|
|
)
|
|
|
|
for index, segment in enumerate(segments):
|
|
norm = _normalize(segment.text)
|
|
if index == 0:
|
|
run_start = 0
|
|
run_norm = norm
|
|
continue
|
|
if norm == run_norm:
|
|
continue
|
|
append_run(index)
|
|
run_start = index
|
|
run_norm = norm
|
|
|
|
if segments:
|
|
append_run(len(segments))
|
|
|
|
return blocks
|