Локальный FastAPI-сервис поверх GigaAM v3 и sherpa-onnx: приём аудио, очередь задач, разделение по говорящим, постобработка терминов. Доставка на Windows - ZIP со встроенным Python, без установки чего-либо. Обновление кода при запуске тянется из релизов Gitea: меняется только папка app, десятки килобайт вместо всего пакета. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60 lines
2.8 KiB
Python
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()
|