Files
talkscore-asr/app/text.py
T
Vladimir BryzgalovandClaude Opus 5 9dc67bda5c talkscore-asr 0.1.0: сервис транскрибации и диаризации
Локальный FastAPI-сервис поверх GigaAM v3 и sherpa-onnx: приём аудио,
очередь задач, разделение по говорящим, постобработка терминов.

Доставка на Windows - ZIP со встроенным Python, без установки чего-либо.
Обновление кода при запуске тянется из релизов Gitea: меняется только
папка app, десятки килобайт вместо всего пакета.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 21:41:04 +05:00

60 lines
2.8 KiB
Python

"""Постобработка распознанного текста.
Две задачи: чинить систематические ошибки модели на английских терминах
(GigaAM обучена только на русском и коверкает их предсказуемым образом)
и приводить типографику к принятому виду.
"""
import re
from pathlib import Path
__all__ = ["apply_replacements", "load_replacements", "normalize_typography"]
# Все виды тире, которые встречаются в выводе моделей, включая минус.
_DASHES = "‐‑‒–—―−"
_DASH_RE = re.compile(f"\\s*[{_DASHES}]\\s*")
# Кавычка-ёлочка, приклеенная к предыдущему слову без пробела.
_GLUED_QUOTE_RE = re.compile(r"(?<=[^\s])(«)")
_MULTISPACE_RE = re.compile(r"[ \t]{2,}")
def load_replacements(path: Path) -> dict[str, str]:
"""Читает словарь замен из файла вида `что искать = на что заменить`.
Пустые строки и строки, начинающиеся с #, игнорируются, как и строки
без разделителя - чтобы опечатка в файле не роняла сервис.
"""
if not Path(path).is_file():
return {}
rules: dict[str, str] = {}
for line in Path(path).read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
src, dst = line.split("=", 1)
src, dst = src.strip(), dst.strip()
if src:
rules[src.lower()] = dst
return rules
def apply_replacements(text: str, rules: dict[str, str]) -> str:
"""Заменяет термины по словарю, не трогая совпадения внутри слов.
Длинные правила применяются первыми, иначе «так менеджер» съел бы часть
«гугл так менеджер» и более точное правило уже не сработало бы.
"""
if not rules:
return text
for src in sorted(rules, key=len, reverse=True):
pattern = re.compile(rf"(?<!\w){re.escape(src)}(?!\w)", re.IGNORECASE)
text = pattern.sub(rules[src].replace("\\", r"\\"), text)
return text
def normalize_typography(text: str) -> str:
"""Приводит типографику к принятому виду: тире в дефис, отбивка кавычек."""
text = _DASH_RE.sub(" - ", text)
text = _GLUED_QUOTE_RE.sub(r" \1", text)
text = _MULTISPACE_RE.sub(" ", text)
return text.strip()