Files
local-transcriber/src/local_transcriber/quality.py
T
Dmitriy DementievandClaude Fable 5 b1dfd9dcca 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>
2026-07-09 22:21:07 +03:00

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