Привет, Хабр! Я Катя Саяпина, менеджер продукта МТС 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-уведомления и мониторинг метрик, чтобы контролировать качество доставки и быстро реагировать на сбои.