Задача:
Построить реал тайм диалог с возможностью распознавания.
Общая логика решения задачи:
1. Дозвониться
2. Прочитать текст
3. Записать речь в поток
4. Выполнить распознавание речи
5. В зависимости от результата распознавания принять решение либо перейти в п2, либо в п6
6. В зависимости от принятия решения выполнить некие действия в 1С(бизнес логику)
7. Сохранить историю событий asterisk
8. Завершить звонок
Технологии:
1. 1С (выборка данных, инициализация вызова, обработка завершения, бизнес логика)
2. asterisk(16) (ami сервер, agi клиент) (телефония)
3. python(3.6) (web сервер между фронтом и телефонией, ami клиент, agi сервер, web socket сервер)
4. Yandex.Cloud (распознавание речи)
5. С (extension asterisk, asterisk application, web socket клиент)
Общий принцип работы:
1) 1С регламентным заданием делает:
1) Запрашивает у бэка статусы по ранее отправленным задачам
2) Сохраняет статусы в регистре сведений
3) Выборку по предварительно сформированным задачам. Из выборки берется телефон для набора, и определяется путь к файлу, который будет в дальнейшем воспроизводится. 1С делает гет запрос в бэк, передавая параметры (телефон, путь к файлу). (фактически создает таск в бэк)
4) реализацию бизнес логики
2) Бэк:
1) При старте:
1) Запускает прослушку событий ами (ami)
2) Стартует вэб сервер (flask)
3) Стартует тисипи сервер(TCP) для обработки аги (agi)
4) Стартует веб сокет сервер (web-socket) для обработки потока с астериска
2) При получении события по ами делает запись в базу
3) При получении события по аги делает логику диалога звонка(воспроизведение файлов, распознование с потока, управление фреймами от веб сокет сервера, запуск миксмонитор(запись звонка для прослушки), ложит трубку)
4) При получении данных по веб сокету, хранит фреймы от астериска
5) При получении запроса по HTTP парсит параметры, вызывает действие "Originate" с указанием параметров(телефон, путь к файлу, контекст диалплана, канала вызывающей стороны)
3) Asterisk после успешного поднятия трубки на отвечающей стороне(через макрос):
1) запускает тисипи поток(канала с передачей голоса) через расширение астериск(вызов application)
2) "уходит" в аги
Отступление: 1Са в этой статье не будет, 1С был в предыдущих. С того времени мало что изменилось
Часть 1. Исследование
Изначально были попытки реализации через просто буферное чтение файлов записи разговора в аги вида: RECORD FILE {FILENAME} {FORMAT} {ESCAPE_DIGITS} {TIMEOUT} {OFFSET_SAMPLES} {BEEP} {S(количество секунд тишины до завершения)} - это аги команда из мануала по астеру. Проблема заключается в том что аги это блокирующий контекст. То есть пока пишется файл записи разговора НЕЛЬЗЯ:
1. прервать запись(по нормальному имеется ввиду)(может только вызываемая сторона по дтфм)
2. запустить что то на выполнение в диалплане(аги)(запустив рекорд файл(запись), плейбэк(воспроизведение) запустится после завершения рекорд файл)
С этим жить можно было бы( и решить), НО люди могут начать говорить в момент воспроизведения файла\либо наоборот после окна записи(3\5 сек) что составляло задержки и не подходило совсем(в файле часть слов, либо вообще файл пустой)
Так же были попытки использования производных AGI(fast, E, Async), в результате чего было принято решение делать именно стрим(как и почему будет ниже), а не парсить файлы с записью по тайм ауту. Так как проблема оставалась либо блокировка в аги, либо не попадание в окно записи + задержки
Помимо аги комманды RECORD FILE(запись в файл), были попытки использовать аги комманды(application) CHAN SPY, EXTENDED CHAN SPY, APP JACK, AUDIO HOOK (сравнение, обзор эксплуатации и мысли по ним приводить здесь не буду, там еще на статью слов наберется), и прочее с сайта wiki.asterisk.org/wiki/display/AST/Asterisk+16+AGI+Commands, так же были игры с NGINX отдачей побуферно без особого успеха, либо мозгов не хватило, либо поняли что то не так, короче на завелось.
+\- в это же время попалось на глаза https://wiki.asterisk.org/wiki/display/AST/AudioSocket что привело к размышлениям об идеи собрать и использовать модуль https://wiki.asterisk.org/wiki/display/AST/Asterisk+18+Application_AudioSocket так как в у астериска 16 его конечно же не было ни в чистом виде(.с), ни в бинарном(.so)
(*ули нам, кабанам) Касательно AudioSocket, удалось его собрать https://github.com/CyCoreSystems/audiosocket, запустить, даже поток валить начал, + есть обертка для питона https://github.com/NormHarrison/audiosocket_server но в нем был небольшой баг: в момент работы он грузил проц на 100 процентов, приложение стоящее внимание конечно.
Что было дальше?
Разбор https://github.com/CyCoreSystems/audiosocket/blob/master/asterisk/apps/app_audiosocket.c с целью понять как работает, откуда нагрузка проц, для того чтобы перепилить\напилить свою. Итог разбора был утешающим:
ответ на первый вопрос если коротко в audiosocket_run блок ast_audiosocket_send_frame.
ответ на второй вопрос там же всё это обернуто в сишный while (1) {...} без прерываний по таймаутам.
Ну собственно всё ясно +\-:
1. интрефейс вызова модуля(load_module, unload_module, audiosocket_run, audiosocket_exec + хидеры) со стороны кор(core) астериска(api application)
2. как вообще снять стрим с астериска
3. что нужно делать дальше(разбить на потоки, так как приложение запускается в контексте аги и в процессе коры (core) астериска, что может сыграть злую шутку(тормоза в голосе, общие проблемы с астериском))
Следующим шагом был поход в вики раздел для разработчиков, а потом и гит хаб астера поиск по ast_audiosocket_send_frame параллельно начали посещать идеи как разбить на потоки отправку, но до этого дело не дошло. В результате натыкаемся на ast_websocket_write https://asterisk-doxygen.osso.pub/master/api/db/db6/structast__websocket.html (int AST_OPTIONAL_API_NAME(ast_websocket_write)(struct ast_websocket *session, enum ast_websocket_opcode opcode, char *payload, uint64_t payload_size)). Немного погуглив внезапно находим https://github.com/nadirhamid/asterisk-audiofork смотрим в код, видим ast_pthread_create_detached_background(&thread, NULL, audiofork_thread, audiofork); треды, все дела, судя по коду то что нужно...
Ну в принципе ДА за исключением:
1. NOTE: you will need to use version 7.2.0 or below as there is currently a client side masking issue in res_http_websocket.c - здесь речь идет о не понятно с первого взгляда зачем использовать для ноды модуль сервера 7.2.0. если коротко не вникая в протокол веб сокетов могу сказать что он просто видоизменен, в том плане что в нем нет передачи бита маски со стороны клиента https://asterisk-doxygen.osso.pub/master/api/de/d4c/res__http__websocket_8c.html#a1a6e44db94d8b4e8c06bf456bb0174ad
2. не понятно как гуид в канал установить(я про переменную канала)(для того чтобы понимать какой поток какому звонку относится) в коде есть регистрация менеджера(ami), в мануале описан параметр, но так не получилось завести.
Решение 1 вопроса: пишем себ сокет сервер по реализацию клиента без учета маски https://github.com/dmarenin/asterisk-audiofork/blob/master/websocket_server.py, для сравнения полный протокол https://github.com/Pithikos/python-websocket-server/blob/master/websocket_server/websocket_server.py,
Решение 2 вопроса: добавляем отправку названия канала вида (SIP\xxx) первым сообщением https://github.com/dmarenin/asterisk-audiofork/commit/e1ca8142409e7ad11cdad501c7c5c5cbd68176c1
пример чтения фреймов с астера на веб сокет сервере https://github.com/dmarenin/asterisk-audiofork/blob/master/wss_sample.py
Собираем:
Запускаем:
same => n,Verbose(audio fork was started continuing call)
same => n,AudioFork(ws://192.168.555.222:8081/,D(out)i(WS_SOCKET_SESSION))
Проверяем:
Немного тестовых примеров:
https://yadi.sk/d/vAEREPOO4064cQ
https://yadi.sk/d/BZrdyE39Ln3Y4Q
https://yadi.sk/d/_L-0--p_7Z7Eqg
В следующей части будет рассмотрена техническая сторона вопроса