import re # Единицы измерения со слэшем — раскрываем до чтения слэша UNIT_SLASH = [ (r'\bкм\s*/\s*ч\b', 'километров в час'), (r'\bм\s*/\s*с\b', 'метров в секунду'), (r'\bкм\s*/\s*с\b', 'километров в секунду'), (r'\bмб\s*/\s*с\b', 'мегабит в секунду'), (r'\bгб\s*/\s*с\b', 'гигабит в секунду'), (r'\bруб\s*/\s*мес\b', 'рублей в месяц'), (r'\bр\s*/\s*мес\b', 'рублей в месяц'), ] def clean_for_speech(text: str) -> str: text = re.sub(r'\*+', '', text) # убрать **жирный** text = re.sub(r'#+\s', '', text) # убрать ## заголовки text = re.sub(r'- ', '', text) # убрать тире списков text = re.sub(r'\[.*?\]\(.*?\)', '', text) # убрать ссылки text = re.sub(r'\n+', '. ', text) # переносы → точки # составные единицы со слэшем — до общей замены `/` for pat, repl in UNIT_SLASH: text = re.sub(pat, repl, text, flags=re.IGNORECASE) # знаки перед числом: "+9", "-3", "±2" text = re.sub(r'(^|\s)\+(\d)', r'\1плюс \2', text) text = re.sub(r'(^|\s)-(\d)', r'\1минус \2', text) text = re.sub(r'±(\d)', r'плюс-минус \1', text) # дроби и отношения "12/15" → "12 из 15", "5/10" → "5 из 10" text = re.sub(r'(\d+)\s*/\s*(\d+)', r'\1 из \2', text) # одиночный слэш — как союз "или" text = text.replace('/', ' или ') # градусы и прочие символы text = re.sub(r'°\s*C\b', ' градусов', text, flags=re.IGNORECASE) text = re.sub(r'°\s*F\b', ' градусов Фаренгейта', text, flags=re.IGNORECASE) text = text.replace('°', ' градусов') text = text.replace('%', ' процентов') text = text.replace('№', ' номер ') text = text.replace('&', ' и ') text = text.replace('@', ' собака ') text = text.replace('×', ' на ') # распространённые сокращения в полную форму — иначе TTS буквоедит abbr = [ (r'\bт\.\s*е\.', 'то есть'), (r'\bт\.\s*к\.', 'так как'), (r'\bт\.\s*д\.', 'так далее'), (r'\bт\.\s*п\.', 'тому подобное'), (r'\bи\s*т\.\s*д\.', 'и так далее'), (r'\bи\s*т\.\s*п\.', 'и тому подобное'), (r'\bпо-\s*русски\b', 'по-русски'), ] for pat, repl in abbr: text = re.sub(pat, repl, text, flags=re.IGNORECASE) # схлопываем дубли пунктуации text = re.sub(r'([:;,!?])\s*\.', r'\1', text) text = re.sub(r'\.\s*\.+', '.', text) text = re.sub(r'\s+', ' ', text) # лишние пробелы text = re.sub(r'(\d+)\.(\s)', r'\1\2', text) return text.strip() def find_sentence_end(text: str, min_len: int = 60) -> int: """Ищет конец предложения, игнорируя ложные точки""" if len(text) < min_len: return -1 for match in re.finditer(r'[.!?]', text): pos = match.start() if pos < min_len: continue before_1 = text[max(0, pos-1):pos] # 1 символ до before_3 = text[max(0, pos-3):pos] # 3 символа до after_2 = text[pos+1:pos+3] # 2 символа после after_stripped = after_2.lstrip() # 1. Цифра.Цифра → "0.76", "3.14" if before_1.isdigit() and after_2[:1].isdigit(): continue # 2. Цифра. Цифра → "1. 2 ГБ" if before_1.isdigit() and after_stripped[:1].isdigit(): continue # 3. Аббревиатуры → "ГБ.", "МБ.", "км.", "шт.", "руб.", "млн.", "млрд." abbrevs = ["гб", "мб", "кб", "тб", "км", "см", "мм", "шт", "руб", "млн", "млрд", "тыс", "кг", "гр", "мл", "gb", "mb", "kb", "tb", "km", "ms", "kb"] if any(before_3.lower().endswith(a) for a in abbrevs): continue # 4. Одиночная заглавная буква → "А.", "В.", "США." (инициалы/аббр.) if len(before_3.strip()) == 1 and before_3.strip().isupper(): continue # 5. После точки строчная буква → "load avg. нормально" if after_stripped and after_stripped[0].islower(): continue # 6. Многоточие → "..." if text[pos:pos+3] == "...": continue # 7. Точка внутри URL или IP → "192.168.1.1", "example.com" if before_1.isdigit() or (after_2[:1].isdigit() and "." in before_3): continue # 8. Процент с точкой → "95.5%" if "%" in after_2[:2]: continue return pos return -1