Мой блог
Создаем нормализацию чисел для голосового ИИ-агента на Python
Главная проблема современного голосового робота кроется вовсе не в тембре или интонациях синтезированного голоса, а в том, как именно он озвучивает цифры и даты. Когда телефонная линия сжимает звук до 8 кГц, разница между движками TTS практически стирается, зато некорректно прочитанная дата вроде «15.07.2026» или десятизначный номер телефона мгновенно выдают машину. Я подготовил компактное и эффективное решение на Python, которое решает эту задачу с минимальными затратами.
В основе этой разработки лежит простая библиотека из 155 строк кода с единственной внешней зависимостью. Она полностью закрывает потребности русского языка в корректном чтении дат, денежных сумм, процентов, телефонных номеров и номеров заказов.
Почему нормализация чисел выносится в отдельный слой
Первое важное архитектурное решение, которое я принял при разработке голосового агента, заключалось в том, что цифры вообще не должны доезжать до синтеза речи. Весь числовой контент разворачивается в текстовые эквиваленты заранее, до отправки в движок TTS.
Детерминизм и полная переносимость
Поведение различных движков синтеза при чтении символов вроде «1 490 ₽» часто непредсказуемо и зависит от версии ПО. Один сервис корректно обрабатывает денежные знаки, но ошибается в датах, другой ведет себя с точностью до наоборот. Зависеть от этих нюансов при каждом обновлении я не намерен. Очищенный от цифр текст любой движок озвучивает одинаково качественно.
Кроме того, мой голосовой помощник изначально работал на речевых моделях Яндекса, а затем был адаптирован под открытое решение. Выделенный слой нормализации позволил перенести код на новую архитектуру без единой правки, избавив меня от необходимости заново настраивать правила чтения внутри самого TTS.
Первая ловушка: библиотека num2words выдает мужской род
Для перевода чисел в слова я задействовал стандартную библиотеку num2words. Однако стандартный вызов метода для порядковых числительных возвращает форму мужского рода, тогда как для даты требуется средний род («пятнадцатое июля»), а для года — родительный падеж («две тысячи двадцать шестого года»).
Корректировка окончаний и особые случаи
Поскольку полноценное автоматическое склонение порядковых числительных — это чересчур громоздкая задача, я реализовал замену окончаний на основе мужского рода. При этом важно модифицировать исключительно последнее слово в конструкции, чтобы не испортить всю фразу целиком.
Отдельного внимания потребовало числительное «третий». Из-за мягкой основы стандартный алгоритм замены окончаний для него не срабатывал корректно, поэтому мне пришлось прописать этот исключительный случай вручную для обеих целевых функций.
Вторая ловушка: строгая последовательность замен
В сценариях диалогового агента одновременно присутствуют даты, денежные суммы, телефонные номера и обычные количественные показатели. Регулярные выражения, обрабатывающие эти категории, неизбежно конкурируют друг с другом за одни и те же цифры.
Принцип движения от частного к общему
Если запустить поиск голых чисел в первую очередь, дата в тексте будет полностью уничтожена регулярным выражением, так как исходных цифр там уже не останется. Именно поэтому порядок обработки строго зафиксирован в шапке модуля: сначала разбираются даты, затем телефоны, денежные знаки, проценты, знаки номера и лишь в самом конце — оставшиеся одиночные числа.
Третья ловушка: специфика чтения телефонных номеров
Диктовать телефонный номер как единое десятизначное число или побуквенно нельзя — живые люди всегда произносят его смысловыми группами. Главная сложность здесь заключается в правильной обработке групп цифр, начинающихся с ведущего нуля.
Обработка ведущих нулей
Если группа вроде «005» будет прочитана как обычное число, она превратится в «пять», что сделает записанный абонентом номер ошибочным. Поэтому алгоритм проверяет наличие ведущего нуля: если он есть, цифры озвучиваются по отдельности, а в остальных случаях применяется стандартное числовое преобразование.
Правильное склонение существительных после числительных
Следом за числительным всегда идет существительное, форма которого меняется в зависимости от числа: один рубль, два рубля, пять рублей. Отдельного упоминания заслуживает диапазон от одиннадцати до четырнадцати, где используется форма «рублей», несмотря на окончание на единицу.
Реализация функции склонения
Я написал компактную вспомогательную функцию для выбора правильного падежного окончания. Без этой простой логики фразы вроде «двадцать один рублей» сразу выдавали бы неестественную синтетическую речь.
Работа с ударениями и ограничения библиотеки
Для борьбы с омографами вроде «за́мок» и «замо́к» можно использовать тяжелую библиотеку RUAccent, однако я намеренно не стал делать ее обязательной зависимостью. Подключение реализовано через мягкую деградацию: если пакет присутствует в системе, ударения расставляются автоматически, а если нет — текст проходит обработку в исходном виде.
Честный обзор недочетов
Разрабатывая этот инструмент, я столкнулся с тем, что библиотека не учитывает синтаксический контекст. Например, в конструкции «до 03.03.2026» дата разворачивается в именительном падеже, хотя после предлога «до» требуется родительный падеж («третьего марта»). Для узких задач голосового агента это допустимо на слух, но требует понимания ограничений.
Кроме того, текущая версия кода не занимается разбором артикулов вроде «1С», времени в формате «14:30» или диапазонов значений. По мере необходимости эти правила можно легко интегрировать в общий конвейер.
Итоговый конвейер нормализации
Все регулярные выражения объединяются в единую последовательную функцию, которая последовательно преобразует входную строку перед отправкой в движок синтеза речи. Весь код покрыт быстрыми тестами, работает за доли секунды и распространяется под свободной лицензией MIT.
