/** * Подготовка текста для TTS и распознавание команд сброса. * Порт satellite/text.py + satellite/llm.py (RESET_PATTERNS, FILLER_PATTERNS). * * Отличие от Python-версии: время «HH:MM» раскрывается в слова всегда в * именительном падеже (без pymorphy3). Звучит чуть менее естественно в * конструкциях вроде «встретимся в 14:30», но без зависимости от морфологии. */ const RESET_PATTERNS = new RegExp( '(начни|начать|создай|открой|давай).{0,10}(новую|новый|чистую|чистый).{0,10}' + '(сессию|сессия|диалог|разговор|чат)' + '|' + '(сбрось|очисти|обнови).{0,10}(сессию|диалог|разговор|чат|историю|контекст)', 'i' ) const FILLER_PATTERNS = new RegExp( '(?:(?:сейчас посмотрю|дай мне секунду|дай секунду|проверяю|загружаю|узнаю' + '|смотрю|одну секунду|я сейчас посмотрю|я проверю|попробую другой источник' + '|нужны конкретные числа|дай мне загрузить)[^.!?]*[.!?]?\\s*)+', 'gi' ) export function isResetCommand(text: string): boolean { return RESET_PATTERNS.test(text) } export function stripFillers(text: string): string { return text.replace(FILLER_PATTERNS, '').trim() } // ── числа в слова (рус, именительный, кардинальные 0..99) ───────────── const ONES_M = ['', 'один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять'] const ONES_F = ['', 'одна', 'две', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять'] const TEENS = ['десять', 'одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать'] const TENS = ['', '', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто'] function numToWords(n: number, gender: 'm' | 'f' = 'm'): string { if (n === 0) return 'ноль' if (n < 0 || n > 99) return String(n) const ones = gender === 'f' ? ONES_F : ONES_M if (n < 10) return ones[n] if (n < 20) return TEENS[n - 10] const t = Math.floor(n / 10) const o = n % 10 return o === 0 ? TENS[t] : `${TENS[t]} ${ones[o]}` } function hoursWord(n: number): string { const last2 = n % 100 const last1 = n % 10 if (last2 >= 11 && last2 <= 14) return 'часов' if (last1 === 1) return 'час' if (last1 >= 2 && last1 <= 4) return 'часа' return 'часов' } function minutesWord(n: number): string { const last2 = n % 100 const last1 = n % 10 if (last2 >= 11 && last2 <= 14) return 'минут' if (last1 === 1) return 'минута' if (last1 >= 2 && last1 <= 4) return 'минуты' return 'минут' } function formatTime(h: number, mm: number): string { let out = `${numToWords(h, 'm')} ${hoursWord(h)}` if (mm > 0) out += ` ${numToWords(mm, 'f')} ${minutesWord(mm)}` return out } // ── единицы измерения со слэшем ─────────────────────────────────────── const UNIT_SLASH: Array<[RegExp, string]> = [ [/\bкм\s*\/\s*ч\b/gi, 'километров в час'], [/\bм\s*\/\s*с\b/gi, 'метров в секунду'], [/\bкм\s*\/\s*с\b/gi, 'километров в секунду'], [/\bмб\s*\/\s*с\b/gi, 'мегабит в секунду'], [/\bгб\s*\/\s*с\b/gi, 'гигабит в секунду'], [/\bруб\s*\/\s*мес\b/gi, 'рублей в месяц'], [/\bр\s*\/\s*мес\b/gi, 'рублей в месяц'], ] const ABBR: Array<[RegExp, string]> = [ [/\bт\.\s*е\./gi, 'то есть'], [/\bт\.\s*к\./gi, 'так как'], [/\bт\.\s*д\./gi, 'так далее'], [/\bт\.\s*п\./gi, 'тому подобное'], [/\bи\s*т\.\s*д\./gi, 'и так далее'], [/\bи\s*т\.\s*п\./gi, 'и тому подобное'], ] export function cleanForSpeech(input: string): string { let t = input // markdown — emojis: supplementary planes + symbols + dingbats + misc-symbols t = t.replace(/[\u{1F000}-\u{1FFFF}☀-➿]/gu, '') t = t.replace(/\*+/g, '') t = t.replace(/#+\s/g, '') t = t.replace(/^- /gm, '') t = t.replace(/\[.*?\]\(.*?\)/g, '') t = t.replace(/\n+/g, '. ') for (const [pat, repl] of UNIT_SLASH) t = t.replace(pat, repl) // знаки перед числом t = t.replace(/(^|\s)\+(\d)/g, '$1плюс $2') t = t.replace(/(^|\s)-(\d)/g, '$1минус $2') t = t.replace(/±(\d)/g, 'плюс-минус $1') // время HH:MM (опционально с предлогом — предлог сохраняем как есть, время в номинативе) t = t.replace( /(?:\b(с|со|до|от|после|около|к|ко|в|во|на|через|за|перед|между|о|об|при)\s+)?(\d{1,2}):(\d{2})\b/gi, (_m, prep: string | undefined, hStr: string, mmStr: string) => { const h = parseInt(hStr, 10) const mm = parseInt(mmStr, 10) if (h < 0 || h > 23 || mm < 0 || mm > 59) return _m const words = formatTime(h, mm) return prep ? `${prep} ${words}` : words } ) // дроби и слэш t = t.replace(/(\d+)\s*\/\s*(\d+)/g, '$1 из $2') t = t.replace(/\//g, ' или ') // символы t = t.replace(/°\s*C\b/gi, ' градусов') t = t.replace(/°\s*F\b/gi, ' градусов Фаренгейта') t = t.replace(/°/g, ' градусов') t = t.replace(/%/g, ' процентов') t = t.replace(/№/g, ' номер ') t = t.replace(/&/g, ' и ') t = t.replace(/@/g, ' собака ') t = t.replace(/×/g, ' на ') for (const [pat, repl] of ABBR) t = t.replace(pat, repl) // схлопывание t = t.replace(/([:;,!?])\s*\./g, '$1') t = t.replace(/\.\s*\.+/g, '.') t = t.replace(/\s+/g, ' ') t = t.replace(/(\d+)\.(\s)/g, '$1$2') return t.trim() }