"""Постобработка распознанного текста. Две задачи: чинить систематические ошибки модели на английских терминах (GigaAM обучена только на русском и коверкает их предсказуемым образом) и приводить типографику к принятому виду. """ import re from pathlib import Path __all__ = ["apply_replacements", "load_replacements", "normalize_typography", "split_sentences"] # Все виды тире, которые встречаются в выводе моделей, включая минус. _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"(? str: """Приводит типографику к принятому виду: тире в дефис, отбивка кавычек.""" text = _DASH_RE.sub(" - ", text) text = _GLUED_QUOTE_RE.sub(r" \1", text) text = _MULTISPACE_RE.sub(" ", text) return text.strip() # Конец предложения: точка, восклицательный, вопросительный или многоточие, # после которых идёт пробел. Пунктуацию расставляет сама модель GigaAM. _SENTENCE_END_RE = re.compile(r"(?<=[.!?…])\s+") def split_sentences(turns: list[dict]) -> list[dict]: """Режет реплики на предложения для последующей разметки ролей. Разделение по голосам на записях с одним микрофоном обычно не работает, и роли расставляет LLM по смыслу текста. Ей нужны именно предложения: в одну реплику диаризация складывает и вопрос одного человека, и ответ другого, поэтому разметить реплику целиком нельзя. Точного времени начала у предложения нет - внутри реплики оно делится поровну. Для сборки диалога обратно этого достаточно, для точной привязки к звуку - нет. """ out: list[dict] = [] for index, turn in enumerate(turns): parts = [p.strip() for p in _SENTENCE_END_RE.split(turn["text"]) if p.strip()] if not parts: continue span = (turn["end"] - turn["start"]) / len(parts) loudness = (turn.get("acoustics") or {}).get("loudness_db") for position, text in enumerate(parts): out.append({ "n": len(out) + 1, "text": text, "start": round(turn["start"] + position * span, 2), "loudness_db": loudness, "turn": index, }) return out