другая структура проекта + beads + александр повтори + комментарии везде + readme

This commit is contained in:
2026-01-09 04:14:50 +03:00
parent 242ead5355
commit ce28fede74
31 changed files with 1654 additions and 1333 deletions

279
app/core/cleaner.py Normal file
View File

@@ -0,0 +1,279 @@
"""
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"(?<!\w)_(.+?)_(?!\w)", r"\1", text)
# Удаление зачеркнутого ~~text~~
text = re.sub(r"~~(.+?)~~", r"\1", text)
# Удаление заголовков Markdown (# Header)
text = re.sub(r"^#{1,6}\s*", "", text, flags=re.MULTILINE)
# Удаление ссылок [text](url) -> оставляем только 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()