Files
2026-09-11 19:47:15 +03:00

70 lines
2.9 KiB
Python

"""Очистка ответа LLM перед озвучкой.
По ТЗ: ИИ должен говорить чистым «человеческим» текстом — без звёздочек,
решёток, списков, ссылок и эмодзи. Первая линия обороны — системный промпт
(запрещает разметку), эта функция — вторая (убирает то, что просочилось).
"""
from __future__ import annotations
import re
# Эмодзи и декоративные символы (TTS их либо молчит, либо читает мусором)
_EMOJI = re.compile(
"["
"\U0001F000-\U0001FAFF" # эмодзи и пиктограммы
"\U00002600-\U000027BF" # misc symbols
"\U0001F1E6-\U0001F1FF" # флаги
"\U00002B00-\U00002BFF" # стрелки/звёзды
"\U0000FE00-\U0000FE0F" # variation selectors
"\U00002190-\U000021FF" # стрелки
"]+"
)
_CODE_FENCE = re.compile(r"```[\s\S]*?(```|$)")
# Служебный маркер многочастных ответов: [ЧАСТЬ 2 ИЗ 5] — вырезается до озвучки
_PART_MARKER = re.compile(r"\s*\[ЧАСТЬ\s+\d+\s+ИЗ\s+\d+\]\s*", re.I)
_IMAGE = re.compile(r"!\[([^\]]*)\]\([^)]*\)")
_LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
_HEADER = re.compile(r"^[ \t]{0,3}#{1,6}[ \t]*", re.M)
_BOLD = re.compile(r"(\*\*\*|\*\*|__)(?=\S)(.+?\S)\1")
_ITALIC = re.compile(r"(?<![\w*\\])(\*|_)(?=\S)(.+?\S)\1(?![\w*])")
_STRIKE = re.compile(r"~~(.+?)~~")
_BULLET = re.compile(r"(?:^|\n)[ \t]*[-*+][ \t]+")
_NUMBERED = re.compile(r"(?:^|\n)[ \t]*\d{1,3}[.)][ \t]+")
_BLOCKQUOTE = re.compile(r"(?:^|\n)[ \t]*>[ \t]?")
_HRULE = re.compile(r"(?:^|\n)[ \t]*([-_=*])[ \t]*\1[ \t]*\1[\1 \t=-]*(?:\n|$)")
_INLINE_CODE = re.compile(r"`([^`]*)`")
_MULTI_WS = re.compile(r"[ \t\f\v]+")
_MULTI_NL = re.compile(r"\s*\n\s*")
def clean_for_speech(text: str) -> str:
"""Markdown/эмодзи → одна чистая строка, пригодная для TTS."""
if not text:
return ""
t = _CODE_FENCE.sub(" ", text)
t = _PART_MARKER.sub(" ", t) # служебный маркер части — не для ушей
t = _IMAGE.sub(r"\1", t)
t = _LINK.sub(r"\1", t)
t = _HEADER.sub("", t)
t = _BOLD.sub(r"\2", t)
t = _ITALIC.sub(r"\2", t)
t = _STRIKE.sub(r"\1", t)
t = _BULLET.sub(" ", t)
t = _NUMBERED.sub(" ", t)
t = _BLOCKQUOTE.sub(" ", t)
t = _HRULE.sub(" ", t)
t = _INLINE_CODE.sub(r"\1", t)
t = _EMOJI.sub(" ", t)
# Переносы строк — в пробелы (для озвучки это одна речь)
t = t.replace("\r", "")
t = _MULTI_WS.sub(" ", t)
t = _MULTI_NL.sub(" ", t)
t = t.strip()
# Двойные знаки препинания после чистки («…!» и т.п.)
t = re.sub(r"\s+([,.!?;:])", r"\1", t)
t = re.sub(r"([,.!?;:])\1+", r"\1", t)
return t