Структура IVR в связке с биллинговой системой и Yandex Speech
Вы когда-нибудь реализовывали сложные схемы IVR? Нет, имеется ввиду не многослойность, а выбор направления в зависимости от полученной информации какого-нибудь стороннего сервиса. Уже сложнее и вы уже начинаете активно думать. В этой статье будет описана схема голосового меню, которое будет направлять на нужные пункты основываясь на полученном результате. Постановка задачи и схема голосового меню Поступила […]
Вы когда-нибудь реализовывали сложные схемы IVR? Нет, имеется ввиду не многослойность, а выбор направления в зависимости от полученной информации какого-нибудь стороннего сервиса. Уже сложнее и вы уже начинаете активно думать. В этой статье будет описана схема голосового меню, которое будет направлять на нужные пункты основываясь на полученном результате.
Постановка задачи и схема голосового меню
Поступила задача от клиента реализовать схему IVR в зависимости от баланса лицевого счета в используемой биллинговой системе. В ответ при обращении к биллинговой системе выдавались ответы наличия номера в системе, баланс в цифрах, а также наличие аварии на линии. Вот основываясь на этом необходимо реализовать схему, включающую несколько слоев:
- При наличии номера в биллинговой системе и положительном балансе
- При отрицательном балансе
- При аварии на линии. Биллинговая система уже была интегрирована с системой Zabbix
- При отсутствии номера
Совместно с клиентом, мы обсудили схему получилось примерно следующее:
На схеме видно, при поступлении вызова, необходимо сразу определять наличие звонящего в биллинговой системе. И при совпадении направлять звонок в IVR для существующих клиентов, где уже будет проверяться баланс или наличие аварии на линии.
Скрипт на биллинговой системе
На стороне биллинговой системы находится PHP файл, который обрабатывает входящие запросы в зависимости указанного значения параметра. Вот список принимаемых значений параметра action:
- get_vgroup_status — возвращает статус блокировки учетной записи абонента
- get_account_type — тип абонент — юридическое лицо или физическое
- get_balance — баланс абонента без копеек
- get_incident — Возвращается время работы оборудования. -1 — если ничего не найдено
- cid — номер звонящего

Yandex Speech
В нашей схеме мы видим в разделе с отрицательным балансом и с положительным балансом необходимо проигрывать сообщение с суммой на счету. Для реализации такого функционала было решено использовать Yandex Speech Kit.
С установкой и настройкой интеграции asterisk и yandex speech вы сможете ознакомиться в статье «Синтез речи средствами Yandex Speech Cloud+Asterisk. Text to Speech».
Скрипт принимает на вход номер телефона и команду say_balance. Получает баланс абонента по этому номеру и передает его Яндексу для озвучки.
Если такой баланс уже озвучивали, то обращения к Яндексу не происходит, применяется старый файл.
Сперва создадим файл скрипта. В данном примере скрипт будет располагаться по следующему пути
/opt/scripts/yandex_speechkit.
# vim /opt/scripts/yandex_speechkit/check.php
Начнем написание кода:
- Опишем функционал захвата значения аргумента и операции с полученным результатом.
if (preg_match("/^\+?[0-9]{11}$/", $argv[1])) {
if ($argv[2] == "say_balance") {
$balance = file_get_contents("https://LK.BILLING.COM/get_client.php?cid=".$argv[1]."&action=get_balance");
if ($balance == "-1") {
echo "fail";
exit();
}
$word = "рублей";
if ($balance == 1) { $word = "рубль"; }
if ($balance > 1 and $balance < 5) { $word = "рубля"; }
if (preg_match("/[^1]1$/", $balance)) { $word = "рубль"; }
$text = "$balance $word.\n";
$result = generate_voice($text, $balance);
if ($result == false) {
echo "fail";
exit();
} else {
echo $result;
}
}
} else {
echo "fail";
}
Остановлюсь на этом моменте подробнее и опишу что этот кусок кода делает. Первой строкой мы проверяем регулярным выражением имеет ли первый переданный аргумент номер телефона и если нет, то скрипт возвращает сообщение fail. Если первый аргумент удовлетворяет шаблону номера телефона, то выполняем следующие действия.
Сначала проверяем указан ли вторым аргументом say_balance и при истинном выражении в переменную $balance записываем полученный результат обращения к биллинговому файлу, рассмотренному в прошлом пункте. При отрицательном балансе выдаем fail и завершаем работу программы. В противном случае создается переменная $word в которой будет храниться спряжение слова рубль. Следующим логическим пунктом стоит отправка полученного ранее баланса и спряжения в yandex, для этого используется созданная функция generate_voice.
Полученное значение отправки мы записываем в переменную $result. Программа дополнительно завершает свое выполнение при двух условиях: если результат генерации голоса провалился и вернул False, или просто возвращает значение пути, куда сохранила распознанный звуковой файл.
Листинг:
#!/usr/bin/php
<?php
const FORMAT_PCM = "lpcm";
const FORMAT_OPUS = "oggopus";
date_default_timezone_set('Europe/Moscow');
function get_token() {
$now = time();
$token_file = "/tmp/yc_iam_token";
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "WEHE ARE HER-1.91", FILE_APPEND);
// Нет файла токена. Получим токен и сохраним его
if (!file_exists($token_file)) {
try {
$token = chop(shell_exec("/home/asterisk/yandex-cloud/bin/yc iam create-token"));
file_put_contents($token_file, $token);
return $token;
} catch (Exception $e) {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] TOKEN_FILE error: ". $token, FILE_APPEND);
return false;
}
}
$diff = $now - filemtime('/tmp/yc_iam_token');
// Прошло больше трех часов с обновления токена, пора полуичть новый
if ($diff > 10800) {
try {
$token = chop(shell_exec("/home/asterisk/yandex-cloud/bin/yc iam create-token"));
file_put_contents($token_file, $token);
return $token;
} catch (Exception $e) {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] time_diff error: ". $token, FILE_APPEND);
return false;
}
// Прошло менее трех часов с обновления токена, токен в файле еще актуален
} else {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] time_diff error: $diff". $token, FILE_APPEND);
return file_get_contents('/tmp/yc_iam_token');
}
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] TOKEN_FILE error: ". $token_file, FILE_APPEND);
return false;
}
function generate_voice($speech, $balance) {
// Если у нас уже был такой баланс, то можем сообщить его без обращения в Яндекс
if (file_exists("/opt/scripts/yandex_speechkit/voice/balance/$balance.wav")) {
file_put_contents("yc_aster.log","we are here-1.5\r\n",FILE_APPEND);
return "/opt/scripts/yandex_speechkit/voice/balance/$balance";
}
// Токен работает 12 часов
$token = get_token();
if ($token == false) {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] GET TOKEN error: ". $token, FILE_APPEND);
return false;
} else {
file_put_contents("yc_aster.log","we are here\r\n",FILE_APPEND);
}
$folderId = "YANDEX_FOLDER_ID"; # Идентификатор каталога
$url = "https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize";
$post = "text=" . urlencode($speech) . "&voice=jane&folderId=${folderId}&sampleRateHertz=8000&format=" . FORMAT_PCM;
$headers = ['Authorization: Bearer ' . $token];
$ch = curl_init();
curl_setopt($ch, CURLOPT_AUTOREFERER, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
if ($post !== false) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
//echo "$response";
if (curl_errno($ch)) {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] CURL error: ". $response, FILE_APPEND);
return false;
print "Error: " . curl_error($ch);
}
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) {
$decodedResponse = json_decode($response, true);
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] JSON error: ". $decodedResponse, FILE_APPEND);
return false;
echo "Error code: " . $decodedResponse["error_code"] . "\r\n";
echo "Error message: " . $decodedResponse["error_message"] . "\r\n";
} else {
try {
file_put_contents("/tmp/$balance.raw", $response);
} catch (Exception $e) {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "[ERROR] error to try add raw file: ". $e, FILE_APPEND);
return false;
}
}
curl_close($ch);
if (file_exists("/tmp/$balance.raw")) {
shell_exec("sox -r 8000 -b 16 -e signed-integer -c 1 /tmp/$balance.raw /opt/scripts/yandex_speechkit/voice/balance/$balance.wav");
if (file_exists("/opt/scripts/yandex_speechkit/voice/balance/$balance.wav")) {
return "/opt/scripts/yandex_speechkit/voice/balance/$balance";
} else {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "NOT file /opt/scripts/yandex_speechkit/voice/balance/$balance.wav exist", FILE_APPEND);
return false;
}
} else {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s"). "NOT file /tmp/$balance.raw exist", FILE_APPEND);
return false;
}
file_put_contents("yc_aster.log","we are here-6\r\n",FILE_APPEND);
}
if (preg_match("/^\+?[0-9]{11}$/", $argv[1])) {
if ($argv[2] == "say_balance") {
$balance = file_get_contents("https://LK.BILLING.COM/get_client.php?cid=".$argv[1]."&action=get_balance");
if ($balance == "-1") {
echo "fail";
exit();
}
$word = "рублей";
if ($balance == 1) { $word = "рубль"; }
if ($balance > 1 and $balance < 5) { $word = "рубля"; }
if (preg_match("/[^1]1$/", $balance)) { $word = "рубль"; }
$text = "$balance $word.\n";
$result = generate_voice($text, $balance);
if ($result == false) {
file_put_contents("yc_aster.log", date("Y-m-d H:i:s")." результат обюращения в яндекс: $result", FILE_APPEND);
echo "fail";
exit();
} else {
echo $result;
}
}
} else {
echo "fail";
}
Логика диалплана
Чтобы при входящем звонке, сразу отрабатывалось разпознавание номера, то нужно в параметрах транка изменить/добавить параметр context и его значение указать свое собственное. В данном примере — это контекст [from-trunk-pre]
Приступим к написанию диалплана укажем шаблон для входящих DID номеров. которые у вас на АТС. он может быть и один. И первым приоритетом отметим для себя “комментарий” в диалплане приложением NoOp. Следующим по списку будет поиск в биллинге номера и результат занесем в переменную exist c помощью команды Set. И в зависимости от полученных значений выбираем направление: ivr-1 или ivr-2.
[from-trunk-pre]
exten => s,1,Set(exist=${SHELL(/usr/bin/curl --max-time 1 -k https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)})})
same => n,GotoIf($[ "${exist}" =~ "^(not_exist|exist)$" ]?${exist})
same => n(not_exist),NoOp('Клиент - не найден')
same => n,Goto(ivr-1,s,1)
same => n(exist),NoOp('Клиент - найден')
same => n,Goto(ivr-2,s,1)
Для IVR, где клиента нет в биллинговой системе описывать её реализацию не буду. Как настроить IVR вы сможете прочитать в статье «Настройка Интерактивного голосового меню (IVR)».
Перейдем к описанию интерактивного голосового меню, при условии, когда номер нашелся. Первым приоритетом будем снова обращаться к биллинговой системе с параметром get_vgroup_status и вторым обращение получим баланс на лицевом счете.
exten => s,1,Set(blocked=${SHELL(curl --max-time 1 -k "https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)}&action=get_vgroup_status")})
same => n,Verbose(2, "get_vgroup_status returns: ${blocked}")
same => n,Set(balance=${SHELL(curl --max-time 1 -k "https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)}&action=get_balance")})
Теперь мы установим в переменную путь к звуковому файлу с сообщением о балансе и будем проверять полученный ответ в этой переменной. Будет это устанавливаться выполнением php скриптом, описанным в предыдущем пункте. При fail — направим на сообщение, которое озвучит что не удалось распознать баланс или такого клиента не существует. При противоположном результате, будем сверять проводить проверку с балансом и наличие блокировки.
same => n,Set(balance_voice=${SHELL(/opt/scripts/yandex_speechkit/check.php ${CALLERID(num)} say_balance)})
same => n,Verbose(2, "BALANCE: ${balance_voice}")
same => n,GotoIf($[ "${balance_voice}" = "fail" ]?fail:check_balance)
same => n(fail),Goto(app-announcement-2,s,1)
same => n(check_balance),GotoIf($["${blocked}" = 4 || ${balance} < 0]?blocked:continue)
Если пользователь заблокирован или у него отрицательный баланс, воспроизведем сообщение о балансе и отправим его в голосовое меню с информацией о блокировке, где будет участвовать стандартная логика IVR.
same => n(blocked),Playback(${balance_voice})
same => n,Goto(ivr-3,s,1)
Метка continue означает, что у звонящего с балансом всё отлично и воспроизведем ему файл. где предлагается выбрать 1 или 2, которые будут указывать на прослушивание его текущего счета и наличии аварии на линии соответственно
same => n(continue),NoOp(оставайтесь на линии)
same => n,Background(ivr-2)
same => n,WaitExten(5)
exten => 1,1,Playback(${balance_voice})
same => n,Hangup()
exten => 2,1,NoOp(====== Проверка наличия аварии на линии ==========)
same => n,Set(incident=${SHELL(curl --max-time 1 -k "https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)}&action=get_incident")})
Переменная ${incident} получит статусы аварии на линии, и он будет принимать одно из значений:
- -1 — Не найдено или пробелема с проверкой
- 0 — В наличии инцидент
- 1 — Все хорошо
same => n,GotoIf($[ "${incident}" = "-1" ]?notfound:moveon)
same => n(notfound),Goto(ivr-1,s,1)
same => n(moveon),GotoIf($[ "${incident}" = "0" ]?incident:allright)
same => n(allright),Goto(app-announcement-9,s,1) ; аварий нет
same => n(incident),Goto(app-announcement-10,s,1) ; проводятся ремонтные работы
Листинг диалплана:
[from-trunk-pre]
exten => s,1,Set(exist=${SHELL(/usr/bin/curl --max-time 1 -k https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)})})
same => n,GotoIf($[ "${exist}" =~ "^(not_exist|exist)$" ]?${exist})
same => n(not_exist),NoOp('Клиент - не найден')
same => n,Goto(ivr-1,s,1)
same => n(exist),NoOp('Клиент - найден')
same => n,Goto(ivr-2,s,1)
[ivr-2]
exten => s,1,Set(blocked=${SHELL(curl --max-time 1 -k "https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)}&action=get_vgroup_status")})
same => n,Verbose(2, "get_vgroup_status returns: ${blocked}")
same => n,Set(balance=${SHELL(curl --max-time 1 -k "https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)}&action=get_balance")})
same => n,Set(balance_voice=${SHELL(/opt/scripts/yandex_speechkit/check.php ${CALLERID(num)} say_balance)})
same => n,Verbose(2, "BALANCE: ${balance_voice}")
same => n,GotoIf($[ "${balance_voice}" = "fail" ]?fail:check_balance)
same => n(fail),Goto(app-announcement-2,s,1)
same => n(check_balance),GotoIf($["${blocked}" = 4 || ${balance} < 0]?blocked:continue)
same => n(blocked),Playback(${balance_voice})
same => n,Goto(ivr-3,s,1)
same => n(continue),NoOp(оставайтесь на линии)
same => n,Background(ivr-2)
same => n,WaitExten(5)
exten => 1,1,Playback(${balance_voice})
same => n,Hangup()
exten => 2,1,NoOp(====== Проверка наличия аварии на линии ==========)
same => n,Set(incident=${SHELL(curl --max-time 1 -k "https://LK.BILLING>COM/get_client.php?cid=${CALLERID(num)}&action=get_incident")})
same => n,GotoIf($[ "${incident}" = "-1" ]?notfound:moveon)
same => n(notfound),Goto(ivr-1,s,1)
same => n(moveon),GotoIf($[ "${incident}" = "0" ]?incident:allright)
same => n(allright),Goto(app-announcement-9,s,1) ; аварий нет
same => n(incident),Goto(app-announcement-10,s,1) ; проводятся ремонтные работы

Остались вопросы?
Я - Кондрашин Игорь, менеджер компании Voxlink. Хотите уточнить детали или готовы оставить заявку? Укажите номер телефона, я перезвоню в течение 3-х секунд.
категории
- DECT
- Linux
- Вспомогательный софт при работе с Asterisk
- Интеграция с CRM и другими системами
- Интеграция с другими АТС
- Использование Elastix
- Использование FreePBX
- Книга
- Мониторинг и траблшутинг
- Настройка Asterisk
- Настройка IP-телефонов
- Настройка VoIP-оборудования
- Новости и Статьи
- Подключение операторов связи
- Разработка под Asterisk
- Установка Asterisk
VoIP оборудование
ближайшие курсы
Новые статьи
10 доводов в пользу Asterisk
Распространяется бесплатно.
Asterisk – программное обеспечение с открытым исходным кодом, распространяется по лицензии GPL. Следовательно, установив один раз Asterisk вам не придется дополнительно платить за новых абонентов, подключение новых транков, расширение функционала и прочие лицензии. Это приближает стоимость владения станцией к нулю.
Безопасен в использовании.
Любое программное обеспечение может стать объектом интереса злоумышленников, в том числе телефонная станция. Однако, сам Asterisk, а также операционная система, на которой он работает, дают множество инструментов защиты от любых атак. При грамотной настройке безопасности у злоумышленников нет никаких шансов попасть на станцию.
Надежен в эксплуатации.
Время работы серверов некоторых наших клиентов исчисляется годами. Это значит, что Asterisk работает несколько лет, ему не требуются никакие перезагрузки или принудительные отключения. А еще это говорит о том, что в районе отличная ситуация с электроэнергией, но это уже не заслуга Asterisk.
Гибкий в настройке.
Зачастую возможности Asterisk ограничивает только фантазия пользователя. Ни один конструктор шаблонов не сравнится с Asterisk по гибкости настройки. Это позволяет решать с помощью Asterisk любые бизнес задачи, даже те, в которых выбор в его пользу не кажется изначально очевидным.
Имеет огромный функционал.
Во многом именно Asterisk показал какой должна быть современная телефонная станция. За многие годы развития функциональность Asterisk расширилась, а все основные возможности по-прежнему доступны бесплатно сразу после установки.
Интегрируется с любыми системами.
То, что Asterisk не умеет сам, он позволяет реализовать за счет интеграции. Это могут быть интеграции с коммерческими телефонными станциями, CRM, ERP системами, биллингом, сервисами колл-трекинга, колл-бэка и модулями статистики и аналитики.
Позволяет телефонизировать офис за считанные часы.
В нашей практике были проекты, реализованные за один рабочий день. Это значит, что утром к нам обращался клиент, а уже через несколько часов он пользовался новой IP-АТС. Безусловно, такая скорость редкость, ведь АТС – инструмент зарабатывания денег для многих компаний и спешка во внедрении не уместна. Но в случае острой необходимости Asterisk готов к быстрому старту.
Отличная масштабируемость.
Очень утомительно постоянно возвращаться к одному и тому же вопросу. Такое часто бывает в случае некачественного исполнения работ или выбора заведомо неподходящего бизнес-решения. С Asterisk точно не будет такой проблемы! Телефонная станция, построенная на Asterisk может быть масштабируема до немыслимых размеров. Главное – правильно подобрать оборудование.
Повышает управляемость бизнеса.
Asterisk дает не просто набор полезных функций, он повышает управляемость организации, качества и комфортности управления, а также увеличивает прозрачность бизнеса для руководства. Достичь этого можно, например, за счет автоматизации отчетов, подключения бота в Telegram, санкционированного доступа к станции из любой точки мира.
Снижает расходы на связь.
Связь между внутренними абонентами IP-АТС бесплатна всегда, независимо от их географического расположения. Также к Asterisk можно подключить любых операторов телефонии, в том числе GSM сим-карты и настроить маршрутизацию вызовов по наиболее выгодному тарифу. Всё это позволяет экономить с первых минут пользования станцией.