Привет, Хабр! Я Катя Саяпина, менеджер продукта МТС Exolve. В прошлом посте я рассказывала, как подключить второй фактор аутентификации через звонок робота, который диктует код. А еще — как реализовать рабочее решение на Django с использованием API МТС Exolve на примере сайта бронирования.
Сегодня продолжим тему. Покажу, как это решение можно масштабировать и оптимизировать:
уменьшить затраты за счет сохранения аудиокодов;
повысить надежность доставки с помощью fallback-канала по SMS;
автоматически подобрать голос и язык диктовки.
Флоу аутентификации
В доработанной версии системы процесс выглядит так:
Пользователь запрашивает код для входа.
Через код автоматически определяются параметры диктовки:
язык — по профилю пользователя с помощью get_language() из Django.
голос — через внешний сервис Gender API.
Проверяем настройки использования аудиодороже��: если установлено переиспользование записей, проверяем, есть ли запись такого кода среди готовых файлов. Если ее нет, генерируем через МТС Exolve и сохраняем для повторного использования.
Если в настройках задано SAVE_RECORDS=false, совершаем звонок с генерацией озвучки всегда, как мы делали в прошлой публикации.
Делаем звонок через МТС Exolve, диктуем код.
Проверяем статус звонка. Если соединение с абонентом было выполнено, завершаем процесс. В противном случае можно запросить код в SMS как через резервный канал.
В итоге пользователю предлагается выбор между несколькими способами отправки кода. В случае неудачи он может вернуться на предыдущий этап аутентификации и выбрать другой способ.
Выбор языка и голоса для синтеза речи
Словарь GENDER_AND_LANGUAGE_MAP содержит список поддерживаемых языков и голосов для каждого языка. Метод SynthesizeAndSave, который мы используем для предварительной генерации и сохранения аудио, работает с шестью языками: русским, английским, немецким, ивритом, казахским и узбекским.
Синтезировать речь в режиме реального времени пока можно только на русском языке. Когда МТС Exolve добавит новые языки в синтез, их достаточно будет дописать в словарь, и мультиязычная поддержка заработает автоматически.
Мы не указывали расширенные настройки произношения — например, скорость, тембр или эмоциональную окраску речи, чтобы не усложнять пример.
GENDER_AND_LANGUAGE_MAP = {'ru': [1, 2], 'en': [17], 'de': [16], 'he': [18], 'kk': [19], 'uz': [21]} @dataclass class VoiceSettingsBase(JSONWizard): class _(JSONWizard.Meta): skip_defaults = True lang: int | None = None voice: int | None = None emotion: int | None = None speed: float | None = None def set_voice(self, name, language): dictor_indexes = 1 if not settings.SAVE_RECORDS: language = 'ru' # Only Russian is supported in online generation. if language in GENDER_AND_LANGUAGE_MAP.keys(): dictor_indexes = GENDER_AND_LANGUAGE_MAP.get(language, settings.LANGUAGE_CODE) dictor_id = dictor_indexes[0] if len(dictor_indexes) > 1 and len(name): gender = detect_gender(name) dictor_id = dictor_indexes[gender] self.voice = dictor_id @dataclass class TTS: text: str = ""
Функция set_voice автоматически выбирает подходящий голос в зависимости от языка и пола пользователя. Если для языка предусмотрен только один голос, используется он. Если несколько, определяем пол по имени через Gender API.
def detect_gender(name): r = requests.post(rf'https://gender-api.com/get?name={name}&key={gender_api_key}') if r.status_code == 200: data = json.loads(r.text) if data['gender'] == 'female': return 1 return 0
⚠️ Gender API — внешний сервис, 100 бесплатных запросов в день. Можно заменить на локальную логику или отказаться от точного распознавания, если это не критично.
Генерация и сохранение аудио
Теперь подготовим параметры TTS и создадим или повторно используем аудиозапись через МТС Exolve. Сначала собираем настройки — голос, язык, скорость и так далее — формируем уникальное имя full_name и проверяем его через GetList. Если запись найдена, используем ее resource_id, если нет — вызываем SynthesizeAndSave.
Имя формируем как voice_{voice}_code_{text}, но в продакшене лучше использовать хеш вместо текста для безопасности. Параметры speed, emotion, loudness_normalization фиксируем в кеше, чтобы каждый их вариант имел свою за��ись.
Функция check_existence() обеспечивает идемпотентность: повторный вызов с тем же именем вернет один и тот же resource_id.
Меняя шаблон фразы, обновляйте версию имени. При сетевых ошибках делайте повторы с паузами, для 4x ошибок — только логируйте.
В момент отправки кода система проверяет, есть ли в МТС Exolve запись с тем же кодом и созданная тем же диктором. Если нет, генерируется новое голосовое сообщение, а потом сохраняется для повторного использования.
@dataclass class VoiceSettings(VoiceSettingsBase): loudness_normalization: int | None = None @dataclass class SynthParams(JSONWizard, TTS): class _(JSONWizard.Meta): skip_defaults = True key_transform_with_dump = 'SNAKE' full_name: str = "" voice_settings: VoiceSettings = field(default_factory=VoiceSettings) def __post_init__(self): if self.full_name == "": voice = self.voice_settings.voice or 1 self.full_name = f'voice_{voice}_code_'+self.text
Мы создаем имя записи, оно состоит из двух частей: номера диктора и текста озвучиваемой фразы. Такой формат позволяет быстро находить нужную запись и избегать дублирования.
def check_existence(synth_params: SynthParams): name = synth_params.full_name r = requests.post(r'https://api.exolve.ru/media/v1/GetList', headers={'Authorization': 'Bearer ' + exolve_api_key}, data=json.dumps({'name': name})) ans = json.loads(r.text) if len(ans['media_records']): return ans['media_records'][0]["resource_id"] return "" def create_record(synth_params: SynthParams): resource_id = check_existence(synth_params) if len(resource_id): return resource_id synth_params = synth_params.to_json() r = requests.post(r'https://api.exolve.ru/media/v1/SynthesizeAndSave', headers={'Authorization': 'Bearer ' + exolve_api_key}, data=synth_params) if r.status_code != 200: raise ans = json.loads(r.text) return ans["resource_id"]
Если запись по уникальному имени в МТС Exolve найдена, возвращаем ее resource_id и используем для звонка. Если нет — создаем новую через SynthesizeAndSave, а потом сохраняем resource_id для следующих обращений. Такой подход обеспечивает предсказуемость и упрощает логику повторных вызовов.
Отправка звонка пользователю
Для формирования запроса на звонок и проигрывания аудио рассмотрим два сценария: с готовой аудиозаписью (SAVE_RECORDS=True) и синтез речи прямо во время вызова.
Класс CallParams собирает параметры вызова: исходящий и входящий номера, а также одно из двух полей — service_id или tts. В зависимости от сценария JSON в запросе включает только одно из них: service_id, если используется предзаписанное сообщение, или tts, если речь синтезируется во время звонка. Класс сам исключает поля со значениями по умолчанию, чтобы структура данных оставалась корректной. Перед отправкой номера проходят проверку и форматирование, а по��ом функция make_call отправляет запрос в МТС Exolve API для совершения звонка.
@dataclass class TTSCall(VoiceSettingsBase, TTS): volume: int | None = None @dataclass class CallParams(JSONWizard): class _(JSONWizard.Meta): skip_defaults = True key_transform_with_dump = 'SNAKE' source: str = "" destination: str = "" tts: TTSCall = field(default_factory=TTSCall) service_id: str = "" def __post_init__(self): self.source = verify_number(self.source) self.destination = verify_number(self.destination) def set_message_id(self, service_id: str): self.tts = TTSCall() # Erase information self.service_id = service_id
В параметрах звонка можно указать два варианта данных:
service_id — идентификатор заранее подготовленного аудиофайла, который уже хранится в МТС Exolve;
TTS — текст и настройки для генерации речи прямо во время звонка.
Это позволяет выбирать, использовать ли заранее сгенерированные сообщения или синтезировать новые на лету в зависимости от сценария.
def create_voice_SMS(resource_id: str): r = requests.post(r'https://api.exolve.ru/voice-message/v1/Create', headers={'Authorization': 'Bearer ' + exolve_api_key}, data=json.dumps({'media_id': resource_id, 'name': resource_id})) if r.status_code != 200: raise ans = json.loads(r.text) return ans["id"] def make_call(destination: str, text: str, name: str, language: str): tts_call = TTSCall(text=text) call_params = CallParams(source=application_phone, destination=destination, tts=tts_call) if settings.SAVE_RECORDS: voice_setts = VoiceSettings() voice_setts.set_voice(name, language) synth_params = SynthParams(text=text, voice_settings=voice_setts) resource_id = create_record(synth_params) message_id = create_voice_SMS(resource_id) call_params.set_message_id(message_id) else: tts_call.set_voice(name, language) call_params = call_params.to_json() r = requests.post(r'https://api.exolve.ru/call/v1/MakeVoiceMessage', headers={'Authorization': 'Bearer ' + exolve_api_key}, data=call_params) return r
Если SAVE_RECORDS=True, то при звонке используется уже подготовленное и сохраненное аудио, которое хранится в МТС Exolve и просто подставляется в вызов. Если False, речь синтезируется прямо в момент звонка и код проговаривается роботом в реальном времени.
Fallback: отправка SMS, если звонок не удался
Если звонок не проходит — например, абонент вне зоны действия сети, номер занят или пользователь не отвечает — важно иметь резервный способ доставки кода. В этой части мы добавляем fallback-канал в виде SMS. После завершения звонка проверяется его статус, и если результат неуспешный, формируем сообщение с кодом и отправляем его через API SMS-сервиса. Это гарантирует, что пользователь получит код даже при проблемах с голосовой доставкой.
В дальнейшем можно расширить логику: использовать разные тексты для разных ошибок, нескольких SMS-провайдеров или настраивать последовательность отправки: звонок, з��тем SMS, а при необходимости — push-уведомление.
@dataclass_json @dataclass class SMSParams: number: str = "" destination: str = "" text: str = ""
Функции отправки SMS и совершения звонка похожи:
def send_SMS(destination: str, text: str): payload = SMSParams(destination=destination, text=text).to_json() r = requests.post(r'https://api.exolve.ru/messaging/v1/SendSMS', headers={'Authorization': 'Bearer '+sms_api_key}, data=payload) return r
Соединяем код с бэкендом
Теперь подключим наши функции звонков и SMS к существующему бэкенду. Модифицируем gateways.py. Сначала импортируем методы совершения звонка и отправки сообщения:
from .utils import make_call, send_SMS from django.contrib.auth.models import User from django.utils.translation import get_language
В gateways.py определен класс Messages. Мы добавим в него методы make_call и send_sms для совершения звонка и отправки SMS. Все параметры обрабатываются через дата-классы, которые создаются внутри функций make_call и send_SMS.
Функция get_language() возвращает текущий язык интерфейса пользователя. Его имя извлекаем из таблицы User по индексу, связанному с используемым для аутентификации устройством.
@classmethod def make_call(cls, device, token): cls._add_message('Making call to %(number)s', device, token) destination = str(device.number) user_object = User.objects.get(id=device.user_id) name = user_object.username try: r = make_call(destination=destination, text=token, name=name, language=get_language()) if r.status_code != 200: cls._add_message('Failed to make call', device, token) except ValueError: cls._add_message('Wrong phone number', device, token) @classmethod def send_sms(cls, device, token): cls._add_message(_('Sending SMS to %(number)s'), device, token) try: r = send_SMS(destination=str(device.number), text=token) if r.status_code != 200: cls._add_message('Failed to send SMS', device, token) except ValueError: cls._add_message('Wrong phone number', device, token)
Настройка проекта
В settings.py добавим параметры: язык по умолчанию, список поддерживаемых языков и флаг SAVE_RECORDS. Эти настройки определяют, как обрабатываются голосовые сообщения:
если True — аудио создаются заранее и сохраняются на платформе;
если False — синтезируются на лету при каждом звонке.
# default language, it will be used, if django can't recognize user's language LANGUAGE_CODE = 'ru' # list of activated languages LANGUAGES = ( ('ru', 'Russian'), ('en', 'English'), ('de', 'German'), ('he', 'Hebrew'), ('kk', 'Kazakh'), ('uz', 'Uzbek'), ) SAVE_RECORDS = True
Что мы получили
Пора резюмировать! Сегодня мы с вами дополнили базовую реализацию двухфакторной аутентификации:
предусмотрели автоматический выбор голоса и языка;
добавили сохранение аудио для повторного использования записей и снижения нагрузки на систему;
реализовали резервный SMS-канал для случаев, когда звонок не проходит;
разделили логику на отдельные шаги: подготовка параметров, звонок, проверка статуса и fallback.
Теперь система стала более гибкой и готовой к масштабированию. В будущем стоит добавить логирование всех действий, ретраи и автоматизировать fallback, чтобы SMS отправлялось автоматически при неуспешном звонке. Еще можно подключить push-уведомления и мониторинг метрик, чтобы контролировать качество доставки и быстро реагировать на сбои.