""" Response cleaner module. Removes markdown formatting and special characters from AI responses. Handles complex number-to-text conversion for Russian language. """ # Модуль очистки текста перед озвучкой. # 1. Убирает Markdown (жирный шрифт, ссылки), который генерирует AI, чтобы робот не читал спецсимволы. # 2. Преобразует числа в слова ("5 мая" -> "пятого мая", "5 рублей" -> "пять рублей"). # Это критически важно для качественного русского TTS. import re import pymorphy3 from num2words import num2words # Инициализация морфологического анализатора (для определения падежей) morph = pymorphy3.MorphAnalyzer() # Карта предлогов и падежей. # Помогает понять, в какой падеж ставить число после предлога. PREPOSITION_CASES = { "в": "loct", # В ком/чем? (Предложный) или Винительный. Часто loct для годов. "во": "loct", "на": "accs", # На какое число? (Винительный) - для дат. "о": "loct", "об": "loct", "обо": "loct", "при": "loct", "у": "gent", # У кого/чего? (Родительный) "от": "gent", "до": "gent", "из": "gent", "с": "gent", # Или Творительный. Но чаще Родительный (с 5 числа). "со": "gent", "без": "gent", "для": "gent", "вокруг": "gent", "после": "gent", "к": "datv", # К кому/чему? (Дательный) "ко": "datv", "по": "datv", "над": "ablt", # Над кем/чем? (Творительный) "под": "ablt", "перед": "ablt", "за": "ablt", "между": "ablt", } # Соответствие падежей pymorphy и библиотеки num2words PYMORPHY_TO_NUM2WORDS = { "nomn": "nominative", "gent": "genitive", "datv": "dative", "accs": "accusative", "ablt": "instrumental", "loct": "prepositional", "voct": "nominative", "gen2": "genitive", "acc2": "accusative", "loc2": "prepositional", } # Названия месяцев в родительном падеже (для поиска дат в тексте) MONTHS_GENITIVE = [ "января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря", ] def get_case_from_preposition(prep_token): """Определяет падеж по предлогу.""" if not prep_token: return None return PREPOSITION_CASES.get(prep_token.lower()) def convert_number(number_str, context_type="cardinal", case="nominative", gender="m"): """ Обертка над num2words для конвертации числа в строку. cardinal - количественное (один, два) ordinal - порядковое (первый, второй) """ try: # Обработка дробей (замена запятой на точку) if "." in number_str or "," in number_str: num_val = float(number_str.replace(",", ".")) else: num_val = int(number_str) return num2words(num_val, lang="ru", to=context_type, case=case, gender=gender) except Exception as e: print(f"Error converting number {number_str}: {e}") return number_str def numbers_to_words(text: str) -> str: """ Интеллектуальная замена цифр на слова с учетом контекста (даты, года, падежи). """ if not text: return "" # 1. Обработка годов: "в 1999 году", "2024 год" def replace_year_match(match): full_str = match.group(0) prep = match.group(1) # Предлог (в, с, к...) year_str = match.group(2) # Само число year_word = match.group(3) # Слово "год", "году" и т.д. # Определяем падеж слова "год" через pymorphy parsed = morph.parse(year_word)[0] case_tag = parsed.tag.case nw_case = PYMORPHY_TO_NUM2WORDS.get(case_tag, "nominative") # Конвертируем число в порядковое числительное (тысяча девятьсот девяносто девятом) words = convert_number( year_str, context_type="ordinal", case=nw_case, gender="m" ) prefix = f"{prep} " if prep else "" return f"{prefix}{words} {year_word}" # Регулярка для годов text = re.sub( r"(?i)\b((?:в|с|к|до|от)\s+)?(\d{3,4})\s+(год[а-я]*)\b", replace_year_match, text, ) # 2. Обработка дат: "25 июня", "с 1 мая" month_regex = "|".join(MONTHS_GENITIVE) def replace_date_match(match): prep = match.group(1) day_str = match.group(2) month_word = match.group(3) # По умолчанию родительный падеж ("двадцать пятого июня") case = "genitive" if prep: prep_clean = prep.strip().lower() # Специфичные правила для дат if prep_clean == "на": case = "accusative" # на пятое мая elif prep_clean == "по": case = "accusative" # по пятое elif prep_clean == "к": case = "dative" # к пятому elif prep_clean in ["с", "до", "от"]: case = "genitive" # с пятого else: morph_case = get_case_from_preposition(prep_clean) if morph_case: case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "genitive") # Используем средний род ('n') для дат (число - средний род: пятое, пятого) words = convert_number(day_str, context_type="ordinal", case=case, gender="n") prefix = f"{prep} " if prep else "" return f"{prefix}{words} {month_word}" # Конкатенация regex для месяцев (ВАЖНО: month_regex должен быть вставлен в строку) text = re.sub( r"(?i)\b((?:с|к|до|от|на|по)\s+)?(\d{1,2})\s+({month_regex})\b", replace_date_match, text, ) # 3. Обработка всех остальных чисел (Количественные: пять столов, десять минут) def replace_cardinal_match(match): prep = match.group(1) num_str = match.group(2) case = "nominative" if prep: morph_case = get_case_from_preposition(prep.strip()) if morph_case: case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "nominative") words = convert_number(num_str, context_type="cardinal", case=case) prefix = f"{prep} " if prep else "" return f"{prefix}{words}" text = re.sub( r"(?i)\b((?:в|на|о|об|обо|при|у|от|до|из|с|со|без|для|вокруг|после|к|ко|по|над|под|перед|за|между)\s+)?(\d+(?:[.,]\d+)?)\b", replace_cardinal_match, text, ) return text def clean_response(text: str, language: str = "ru") -> str: """ Основная функция очистки. Убирает Markdown, ссылки, мусор и преобразует числа. Args: text: Сырой текст от AI. language: Язык (для конвертации чисел, работает только для ru). """ if not text: return "" # Удаление ссылок на источники [1], [citation needed] text = re.sub(r"\x5B\d+\x5D", "", text) text = re.sub(r"\x5Bcitation\s*needed\x5D", "", text, flags=re.IGNORECASE) text = re.sub(r"\x5Bsource\x5D", "", text, flags=re.IGNORECASE) # Удаление жирного шрифта **text** и __text__ text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) text = re.sub(r"__(.+?)__", r"\1", text) # Удаление курсива *text* и _text_ text = re.sub(r"\*(.+?)\*", r"\1", text) text = re.sub(r"(? оставляем только text # \x5B = [, \x5D = ] text = re.sub(r"\x5B([^\x5D]+)\x5D\([^)]+\)", r"\1", text) # Удаление картинок ![alt](url) -> удаляем полностью text = re.sub(r"!\x5B([^\x5D]*)\x5D\([^)]+\)", "", text) # Удаление inline кода `code` text = re.sub(r"`([^`]+)`", r"\1", text) # Удаление блоков кода ```code``` text = re.sub(r"```[\s\S]*?```", "", text) # Удаление маркеров списков (-, *, 1.) text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.MULTILINE) text = re.sub(r"^\s*\d+\.\s+", "", text, flags=re.MULTILINE) # Удаление цитат > text = re.sub(r"^\s*>\s*", "", text, flags=re.MULTILINE) # Удаление горизонтальных линий --- text = re.sub(r"^[-*_]{3,}\s*$", "", text, flags=re.MULTILINE) # Удаление HTML тегов text = re.sub(r"<[^>]+>", "", text) # Remove informal slang greetings at the beginning of sentences/responses text = re.sub( r"^(Эй|Хэй|Слушай|Так|Ну|Короче|В\s+общем)[,!?:]?\s*", "", text, flags=re.IGNORECASE | re.MULTILINE, ) # Convert numbers to words only for Russian, and only if digits exist if language == "ru" and re.search(r"\d", text): text = numbers_to_words(text) # Remove extra whitespace text = re.sub(r"\n{3,}", "\n\n", text) text = re.sub(r" +", " ", text) return text.strip()