Продолжение предыдущей статьи. (//infostart.ru/public/954564/)
Пример применения распознавания речи в исходящих звонках asterisk.
Задача:
Получить (не) подтверждение о приезде клиента на определенный момент.
Общая логика решения задачи:
1. Дозвониться
2. Прочитать текст
3. Записать речь отвечающей стороны в файл
4. Выполнить распознавание речи
5. Попрощаться
6. В зависимости от результата распознавания принять решение
7. В зависимости от принятия решения выполнить некие действия в 1С(двинуть бп дальше, оповестить ответственных лиц, закрыть задачи, освободить занятое время, и тд)
8. Сохранить историю событий asterisk
Технологии:
1. 1С (выборка данных, инициализация вызова, обработка завершения, бизнес логика)
2. asterisk (ami сервер) (телефония)
3. python (бэк, прокси сервер между фронтом и телефонией, ami клиент)
4. Yandex.SpeechKit (распознавание речи)
5. php (agi)
Общая принцип работы:
Средствами 1С регламентное задание делает выборку по предварительно сформированным задачам, уведомление клиента за 24 часа. Из выборки берется телефон для набора, и определяется путь к файлу, который будет в дальнейшем воспроизводится. 1С делает гет запрос в бэк, передавая параметры (телефон, путь к файлу). Бэк получает запрос, парсит параметры, подключается к ами asterisk, вызывает действие "Originate" с указанием параметров(телефон, путь к файлу, контекс диалплана, канала вызывающей стороны), подписывается на события ами, подключается к kafka для отправки событий(на самом деле подключается при старте и держит соединение). Asterisk после успешного поднятия трубки на отвечающей стороне читает файл, где в конце файла будет что-то вроде "скажите да если вы приедете, скажите нет если не приедете" (ну условно). Asterisk записывает в файл разговор отвечающей стороны. Делает пост запрос в бэк, передавая файл и ид звонка(каллер ид), затем прощается, завершает вызов. Бэк получает файл, передает его на asr.yandex. asr.yandex распознает возвращает xml. Бэк парсит xml генерирует событие в kafka. kafka при получении событий отправляет в http сервис 1С, для дальнейшей обработки. 1С обрабатывает состояние, реализует бизнес логику.
Синтез речи (генерация файлов для воспроизведения) был описан в предыдущей статье.
В отличии от предыдущей статьи по астериску, здесь код:
1. будет адаптирован под работу в микросервисе (kafka, предыдущая статья по kafka)
2. web часть будет перенесена на flask
Шаг 1. диалплан
Инициализация исходящего звонка будет происходить через asterisk ami, далее звонок идет в dial plan.
Для реализации задачи был добавлен контекст и макрос.(да, снова не ari)
[ng_ext_autodial_speech_to_text]
exten => _X.,1,Verbose(0,=> Outbound : ${CALLERID(num)} => ${EXTEN})
same => n,Dial(SIP/ng_ext/${EXTEN},,M(after-up-speech-to-text,${file_name}))
Так как в первой части
[macro-after-up-speech-to-text]
exten => s,1,Wait(0.2)
same => n,Goto(retry)
same => n(retry),NoOp
same => n(retry),NoOp
same => n,Playback(${ARG1})
same => n,Record(/usr/local/freeswitch/recordings/auto_call/${UNIQUEID}.wav, 2, 5)
same => n, AGI(speech_to_text.php, /usr/local/../recordings/auto_call/${UNIQUEID})
Воспроизводим файл.
Пишем ответ в файл.
Передаем в agi.
#!/usr/local/bin/php -q
<?php
$agivars = array();
while (!feof(STDIN)) {
$agivar = trim(fgets(STDIN));
if ($agivar === '')
break;
$agivar = explode(':', $agivar);
$agivars[$agivar[0]] = trim($agivar[1]);
}
$filename = $agivars['agi_arg_1'];
$channel = $agivars['agi_channel'];
$callerid = $agivars['agi_callerid'];
$cmd = exec('/usr/local/bin/curl --silent -F "Content-Type=audio/x-pcm;bit=16;rate=16000" -F "audio=@'.$filename.'.wav" 192.168.777.555:8090/speech_to_text?channel='.$channel.'&callerid='.$callerid, $res);
$res_str = implode($res);
echo 'SET VARIABLE TEXT_SP_RES "'.(int)$res_str.'"'."\n";
fgets(STDIN);
exit(0);
/*
echo 'VERBOSE ("'.$voice_text.'")'."\n";
fgets(STDIN);
*/
?>
Парсим параметры из STDIN (поток ввода).
Делаем пост запрос в бэк с передачей в теле аудио файла, и передачей параметров для определения звонка(ид звонка).
Получаем ответ распознавания.
Устанавливаем переменную(опционально).
Пишем в поток вывода.
Шаг 2. бэк
from server import app
if __name__ == '__main__':
HOST = '0.0.0.0'
PORT = 8090
app.run(HOST, PORT, threaded=True)
Стартер бэк енда.
AMI_SECRET = 'AMI_SECRET '
AMI_USER = 'AMI_USER '
AMI_PORT = 5038
AMI_ADDRESS = 'AMI_ADDRESS '
CONTEXT_LOCAL = 'local'
CONTEXT_AUTO_DIAL='ng_ext_autodial'
TIMEOUT_AUTO_DIAL = '45000'
ONES_SERVER_SOCKET = 'http://192.168.555.666'
APP = 'PlayBack'
DATA = '/usr/local/share/asterisk/..../auto/sps'
FILE_NAMES = {'kia': 'file_name=/usr/local/share/asterisk/..../auto/auto_kia',
'ford': 'file_name=/usr/local/share/asterisk/..../auto/auto_ford',
'renault': 'file_name=/usr/local/share/asterisk/..../auto/auto_renault',
'sps': 'file_name=/usr/local/share/asterisk/..../auto/sps',
'24h_ford_fed': 'file_name=/usr/local/share/asterisk/..../auto/24h_ford_fed',
'24h_ford_resp': 'file_name=/usr/local/share/asterisk/..../auto/24h_ford_resp',
'24h_kia': 'file_name=/usr/local/share/asterisk/..../auto/24h_kia',
'24h_reno_fed': 'file_name=/usr/local/share/asterisk/..../auto/24h_reno_fed',
'24h_reno_resp': 'file_name=/usr/local/share/asterisk/..../auto/24h_reno_resp',
'24h_citroen': 'file_name=/usr/local/share/asterisk/..../auto/24h_citroen',
'24h_fiat': 'file_name=/usr/local/share/asterisk/..../auto/24h_fiat',
'24h_gaz': 'file_name=/usr/local/share/asterisk/..../auto/24h_gaz',
'24h_gaz_dru': 'file_name=/usr/local/share/asterisk/..../auto/24h_gaz_dru',
'24h_gaz_sherb': 'file_name=/usr/local/share/asterisk/..../auto/24h_gaz_sherb',
'24h_jeep': 'file_name=/usr/local/share/asterisk/..../auto/24h_jeep',
'24h_jlr': 'file_name=/usr/local/share/asterisk/..../auto/24h_jlr',
'24h_niva': 'file_name=/usr/local/share/asterisk/..../auto/24h_niva',
'24h_opel': 'file_name=/usr/local/share/asterisk/..../auto/24h_opel',
'24h_pego': 'file_name=/usr/local/share/asterisk/..../auto/24h_pego',
'24h_subaru': 'file_name=/usr/local/share/asterisk/..../auto/24h_subaru',
'24h_suzuki': 'file_name=/usr/local/share/asterisk/..../auto/24h_suzuki',
'24h_sy': 'file_name=/usr/local/share/asterisk/..../auto/24h_sy',
'24h_uaz': 'file_name=/usr/local/share/asterisk/..../auto/24h_uaz',
'24h_volvo': 'file_name=/usr/local/share/asterisk/..../auto/24h_volvo'
...
Конфиги.
from flask import Flask
from flask_cors import CORS
#from http.client import HTTPConnection
import json
#import requests
from datetime import datetime, date
from kafka import KafkaProducer
import decimal
from orm.models import Statuses_AutoCall
EVENTS_NOT_LISTEN = ['RTCPSent', 'RTCPReceived', 'PeerStatus', 'NewAccountCode']
EVENTS_VARS_LISTEN = ['DIALSTATUS', 'MIXMONITOR_FILENAME', 'TEXT_SP_RES', 'RECORDED_FILE', 'PLAYBACKSTATUS']
USER_EVENTS = {}
AUTO_CALLS = {}
def ami_client_event(jdata, user):
key = user.encode('utf-8')
value = jdata.encode('utf-8')
#print(jdata)
try:
producer.send(f'ami_client_event', key=key, value=value)
except:
print(f"""send to kafka failed {jdata}""")
pass
def auto_call_event(jdata):
key = 'auto_call_set_status'.encode('utf-8')
value = jdata.encode('utf-8')
#print(jdata)
try:
#producer.send(f'ones_ws_event', key=key, value=value)
producer.send(f'ones_http_event', key=key, value=value)
except:
print(f"""send to kafka failed {jdata}""")
pass
def do_handle_event(event):
#print(f"""{str(event)}""")
if event.name == 'Dial':
for x in AUTO_CALLS:
if x in event.keys['Channel']:
Destination = event.keys.get('Destination')
if Destination is None:
continue
AUTO_CALLS[x]['Destination'] = Destination
d = {}
d['Действия'] = 'Asterisk_AutoCall_Event'
d['Данные'] = event.keys
d['Данные']['name'] = event.name
d['Данные']['id_ext'] = AUTO_CALLS[x]['id_ext']
d['Данные']['id'] = AUTO_CALLS[x]['id']
d['Данные']['event_date'] = datetime.now()
jdata = json.dumps(d, default=json_serial)
auto_call_event(jdata)
#print(f"""{str(event)}""")
elif event.name == 'DTMF':
for x in AUTO_CALLS:
if AUTO_CALLS[x].get('Destination') is None:
continue
if AUTO_CALLS[x]['Destination'] in event.keys['Channel']:
d = {}
d['Действия'] = 'Asterisk_AutoCall_Event'
d['Данные'] = event.keys
d['Данные']['name'] = event.name
d['Данные']['id_ext'] = AUTO_CALLS[x]['id_ext']
d['Данные']['id'] = AUTO_CALLS[x]['id']
d['Данные']['event_date'] = datetime.now()
jdata = json.dumps(d, default=json_serial)
auto_call_event(jdata)
#print(f"""{str(event)}""")
elif event.name == 'Newstate' or event.name == 'Hangup' or event.name == 'VarSet':
if event.name == 'VarSet':
if not event.keys['Variable'] in EVENTS_VARS_LISTEN:
return
for x in USER_EVENTS:
if USER_EVENTS[x].get('channel') is None:
continue
if USER_EVENTS[x]['channel'] in event.keys['Channel']:
d = {}
d['Действия'] = 'Asterisk_Event'
d['Данные'] = event.keys
d['Данные']['name'] = event.name
d['Данные']['id_ext'] = USER_EVENTS[x]['id_ext']
d['Данные']['event_date'] = datetime.now()
jdata = json.dumps(d, default=json_serial)
ami_client_event(jdata, USER_EVENTS[x]['user'])
#print(f"""{str(event)}""")
for x in AUTO_CALLS:
if AUTO_CALLS[x].get('Destination') is None:
continue
if x in event.keys['Channel'] or AUTO_CALLS[x]['Destination'] in event.keys['Channel']:
if event.name == 'Hangup':
if event.keys['CallerIDNum'] != AUTO_CALLS[x]['callerid']:
continue
d = {}
d['Действия'] = 'Asterisk_AutoCall_Event'
d['Данные'] = event.keys
d['Данные']['name'] = event.name
d['Данные']['id_ext'] = AUTO_CALLS[x]['id_ext']
d['Данные']['id'] = AUTO_CALLS[x]['id']
d['Данные']['event_date'] = datetime.now()
jdata = json.dumps(d, default=json_serial)
auto_call_event(jdata)
#print(f"""{str(event)}""")
def event_listener(event, **kwargs):
if event.name in EVENTS_NOT_LISTEN:
return
try:
do_handle_event(event)
except Exception as e:
print('error')
print(e)
print(event)
pass
def json_serial(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
if isinstance(obj, decimal.Decimal):
return float(obj)
pass
app = Flask(__name__)
producer = KafkaProducer(bootstrap_servers=['192.168.555.666:9092'])
CORS(app, support_credentials=True)
import server.views
from asterisk.ami import AMIClient, AMIClientAdapter, AutoReconnect
from server.conf import *
client = AMIClient(address=AMI_ADDRESS, port=AMI_PORT)
AutoReconnect(client)
future = client.login(username=AMI_USER, secret=AMI_SECRET)
client.add_event_listener(event_listener)
Подключение к ами астериск:
client = AMIClient(address=AMI_ADDRESS, port=AMI_PORT)
AutoReconnect(client) - для переподключения при потерях связи
future = client.login(username=AMI_USER, secret=AMI_SECRET)
client.add_event_listener(event_listener)
Подключение к кафка:
producer = KafkaProducer(bootstrap_servers=['192.168.555.666:9092'])
Подписка на события ами:
def event_listener(event, **kwargs):
Отправка в кафка для серверного вызова 1с:
def auto_call_event(jdata):
...
#producer.send(f'ones_ws_event', key=key, value=value) - событие для web сервиса 1с, я кстати ухожу от него в пользу 1с http сервисов
producer.send(f'ones_http_event', key=key, value=value) - событие http сервиса 1c
...
как это реализуется и что это такое в плане кода (обработка события и отправка в 1с, можно посмотреть здесь )
Отправка в кафка для вызова клиентского метода 1с (например фонер(звонилка) 1с):
def ami_client_event(jdata, user):
Обработка, докручивание событий астериск:
def do_handle_event(event):
Остальное здесь логика.
from datetime import datetime, date, timedelta
from flask import render_template, request
from server import app, USER_EVENTS, AUTO_CALLS, auto_call_event
import json
import decimal
from asterisk.ami import AMIClient, AMIClientAdapter
import time
from server.conf import *
import uuid
import requests
import xmltodict
from orm.models import Statuses_AutoCall
HEADERS = {"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "Content-Type"}
TOKEN = "MY_TOKEN"
ANSWERS_POSITIVE = ['да', 'приеду', 'согласен', 'соглаcна']
ANSWERS_NEGATIVE = ['нет', 'не приеду']
ANSWERS_RE_RECORD = ['перезапишите', 'перезаписать', 'перезапись']
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html')
@app.route('/make_call')
def make_call():
client = AMIClient(address=AMI_ADDRESS, port=AMI_PORT)
future = client.login(username=AMI_USER, secret=AMI_SECRET)
id = request.args.get('id', '')
channel = request.args.get('channel', '')
exten = request.args.get('exten', '')
caller_id = request.args.get('caller_id', '')
caller_id_name = request.args.get('caller_id_name', '')
user = request.args.get('user', '')
action_id = request.args.get('action_id', '')
d = {}
exten = '8' + exten
id_ext = uuid.uuid4()
id_ext = str(id_ext)
if future.response.is_error():
d['status'] = 'failed'
d['error'] = str(future.response)
res = json.dumps(d, default=json_serial)
return res, 200, HEADERS
adapter = AMIClientAdapter(client)
res_call = do_call_with_oper(adapter, channel, exten, caller_id, caller_id_name, action_id)
client.logoff()
d['status'] = 'ok'
d['id_ext'] = id_ext
res = json.dumps(d, default=json_serial)
USER_EVENTS[user] = {'id_ext': id_ext, 'id':id, 'user': user, 'channel':channel, 'exten':exten, 'caller_id':caller_id}
return res, 200, HEADERS
@app.route('/make_call_auto')
def make_call_auto():
client = AMIClient(address=AMI_ADDRESS, port=AMI_PORT)
future = client.login(username=AMI_USER, secret=AMI_SECRET)
id = request.args.get('id', '')
exten = request.args.get('exten', '')
file = request.args.get('file', '')
action_id = request.args.get('action_id', '')
exten = '8' + exten
d = {}
id_ext = uuid.uuid4()
id_ext = str(id_ext)
if future.response.is_error():
d['status'] = 'failed'
d['error'] = str(future.response)
res = json.dumps(d, default=json_serial)
return res, 200, HEADERS
adapter = AMIClientAdapter(client)
channel = f'Local/{exten}@ng_ext_autodial_speech_to_text'
d['channel'] = channel
variable = FILE_NAMES[file]
res_call = do_call_without_oper(adapter, channel, DATA, APP, action_id, variable, exten)
client.logoff()
d = {}
d['Действия'] = 'Asterisk_AutoCall_Event'
d['Данные'] = {}
d['Данные']['name'] = 'init'
d['Данные']['id_ext'] = id_ext
d['Данные']['event_date'] = datetime.now()
d['Данные']['status'] = 'ok'
d['Данные']['callerid'] = exten
d['Данные']['id'] = id.upper()
jdata = json.dumps(d, default=json_serial)
auto_call_event(jdata)
AUTO_CALLS[channel] = {'id_ext': id_ext, 'id':id, 'channel':channel, 'exten':exten, 'callerid':exten}
#period = datetime.now()
#source = id.upper()
#status = 'init'
#value = id_ext
#comment = res
#var = Statuses_AutoCall.create(period=period, source=source, status=status, value=value, comment=comment)
return jdata, 200, HEADERS
@app.route('/speech_to_text', methods=['GET', 'POST'])
def speech_to_text():
if len(request.files) == 0:
return '-1'
callerid = request.args.get('callerid', '')
id = None
for x in AUTO_CALLS:
if callerid == AUTO_CALLS[x]['callerid']:
id = AUTO_CALLS[x]['id']
id_ext = AUTO_CALLS[x]['id_ext']
_id_ext = str(id_ext).replace('-', '')
topic = 'queries'
url = f"""https://asr.yandex.net/asr_xml?uuid={_id_ext}&key={TOKEN}&topic={topic}&lang=ru-RU"""
audio = request.files['audio'].stream.read()
#headers={'Content-Type': 'audio/x-pcm;bit=16;rate=8000'}
headers={'Content-Type': 'audio/x-wav'}
response = requests.get(url, headers=headers, data=audio)
answer = response.text
comment = answer
print(answer)
res_parse = xmltodict.parse(answer)
#res_str = json.dumps(res_parse, default=json_serial)
recognition_res = res_parse.get('recognitionResults')
res = '0'
if not recognition_res is None:
variants = recognition_res.get('variant')
if not variants is None:
if type(variants) == type([]):
for var in variants:
answer = var.get('#text')
if not answer is None:
res = '0'
if answer in ANSWERS_POSITIVE:
res = '1'
for x in ANSWERS_POSITIVE:
if x in answer:
res = '1'
if answer in ANSWERS_NEGATIVE:
res = '2'
for x in ANSWERS_NEGATIVE:
if x in answer:
res = '2'
if answer in ANSWERS_RE_RECORD:
res = '3'
for x in ANSWERS_RE_RECORD:
if x in answer:
res = '3'
pass
else:
answer = variants.get('#text')
if not answer is None:
res = '0'
if answer in ANSWERS_POSITIVE:
res = '1'
for x in ANSWERS_POSITIVE:
if x in answer:
res = '1'
if answer in ANSWERS_NEGATIVE:
res = '2'
for x in ANSWERS_NEGATIVE:
if x in answer:
res = '2'
if answer in ANSWERS_RE_RECORD:
res = '3'
for x in ANSWERS_RE_RECORD:
if x in answer:
res = '3'
pass
else:
res = '1'
d = {}
d['Действия'] = 'Asterisk_AutoCall_Event'
d['Данные'] = {}
d['Данные']['name'] = 'speech_to_text'
d['Данные']['id_ext'] = id_ext
d['Данные']['id'] = id
d['Данные']['res'] = res
d['Данные']['comment'] = comment
d['Данные']['event_date'] = datetime.now()
jdata = json.dumps(d, default=json_serial)
auto_call_event(jdata)
return res
def callback_response(response):
#print(response)
return
def do_call_with_oper(adapter, channel, exten, caller_id, caller_id_name, action_id, timeout='', context=CONTEXT_LOCAL, priority=1, callback_response=None):
res = adapter.Originate(Channel=channel, Context=context, Exten=exten, ActionID=action_id, Priority=priority, CallerID=caller_id, CallerIDName=caller_id_name, Timeout=timeout, _callback=callback_response)
return res
def do_call_without_oper(adapter, channel, data, app, action_id, variable = '', exten='', timeout=TIMEOUT_AUTO_DIAL, context=CONTEXT_AUTO_DIAL, callback_response=None):
res = adapter.Originate(Channel=channel, Context=context, Application=app, Exten=exten, ActionID=action_id, Data=data, Timeout=timeout, _callback=callback_response, Variable=variable)
return res
def json_serial(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
if isinstance(obj, decimal.Decimal):
return float(obj)
raise TypeError("Type is not serializable %s" % type(obj))
Енд поинты.
Ами команды:
def do_call_with_oper(adapter, channel, exten, caller_id, caller_id_name, action_id, timeout='', context=CONTEXT_LOCAL, priority=1, callback_response=None):
def do_call_without_oper(adapter, channel, data, app, action_id, variable = '', exten='', timeout=TIMEOUT_AUTO_DIAL, context=CONTEXT_AUTO_DIAL, callback_response=None):
Апи:
def make_call(): - вызывает 1с, из фонера
парсит параметры, устанавливает клинское соединение с астериск, делает action originate, добавляет в соответствие пользователя для дальнейшего отслеживания, для последующей генерации событий в кафка.
def make_call_auto(): - вызывает 1с из регламентого задания, старт автообзвона
парсит параметры, устанавливает клинское соединение с астериск, делает action originate(по другому контексту и передает туда параметры), добавляет в соответствие ид звонка (ид задачи) для дальнейшего отслеживания, для последующей генерации событий в кафка, генерирует событие в кафка init, которое приезжает в 1с, там оно пишется в регистр сведений, типа задача зарегистрирована на обзвон. от прямой вставки в бд, отказался, в коде коммент остался.
def speech_to_text(): - вызывает аги астериск для распознавания речи
Парсит параметры, вытягивает из тела запроса файл, делает пост на asr.yandex передавая файл, токен. Получает ответ от аср, парсит иксмл, проверяет строку по таблице ответов (ANSWERS_POSITIVE, ANSWERS_NEGATIVE, ANSWERS_RE_RECORD), генерирует в кафка событие "speech_to_text" для 1с, передавая числовое значение(0, 1,2,3)
0 - ответ не распознан
1 - ответ позитивный
2 - ответ негативный
3 - прочее
Web admin:
def index(): - возвращает статичную html страницу(пока что, далее будет статистика, отчеты, админ фронт)
server/templates/index.html
<html><body><h1>It works!</h1></body></html>
Статика, админка в будущем.
#!/bin/sh
screen -dmUS asterisk_ami_client python3.5 runserver.py
Запуск в срине.
Шаг 3. 1C
Серверная часть:
Енд поинт 1с.
Функция v1get(Запрос)
УстановитьПривилегированныйРежим(Истина);
ИмяМетода = Запрос.ПараметрыЗапроса["method"];
Данные = Запрос.ПараметрыЗапроса["data"];
Результат = Неопределено;
...
ИначеЕсли ИмяМетода = "auto_call_set_status" Тогда
Результат = РаботаСВебСервисом.auto_call_set_status(Данные);
...
Результат = JSON.лЗаписатьJSON(Результат);
Ответ = Новый HTTPСервисОтвет(200);
Ответ.УстановитьТелоИзСтроки(Результат, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
Возврат Ответ;
КонецФункции
Функция v1post(Запрос)
Ответ = Новый HTTPСервисОтвет(500);
Ответ.УстановитьТелоИзСтроки("Метод не поддерживается");
Возврат Ответ;
КонецФункции
Далее идет бизнес логика средствами 1с.
Примеры работы:
Клиентская часть фонер 1с:
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
Если Не Источник = "EventListener" Тогда
Возврат;
КонецЕсли;
Если Не ИмяСобытия = "Asterisk_Event" Тогда
Возврат;
КонецЕсли;
Если Не Строка(Параметр["id_ext"])=id_ext Тогда
Возврат;
КонецЕсли;
Если Параметр["name"]="Newstate" Тогда
ОтключитьОбработчикОжидания("ЗакрытьФорму");
Если Параметр["ChannelStateDesc"]="Ringing" Тогда
УстановитьСтатусПользователя(Ложь);
УстановитьСтатус(0);
УстановитьТекстСообщения("Возьмите трубку телефон "+ТелефонВнутренний);
КонецЕсли;
Если Параметр["ChannelStateDesc"]="Up" Тогда
УстановитьСтатус(0);
УстановитьТекстСообщения("Отлично. Звоню клиенту "+НаименованиеКлиента+Символы.ПС);
Набор = " +7"+Телефон;
ПодключитьОбработчикОжидания("НаборНомера", 0.3, Истина);
КонецЕсли;
ИначеЕсли Параметр["name"]="VarSet" Тогда
Если Параметр["Variable"]="MIXMONITOR_FILENAME" Тогда
ДобавитьЗаписьРазговора(Параметр["Value"]);
ПоказатьОповещениеПользователя("Запись звонка "+Параметр["Value"]);
ИначеЕсли Параметр["Variable"]="DIALSTATUS" Тогда
Если Параметр["Value"]="ANSWER" Тогда
УстановитьСтатус(1);
УстановитьТекстСообщения("Успешно. Дозвонились");
ПоказатьОповещениеПользователя("Абонент ответил");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 10, Истина);
ИначеЕсли Параметр["Value"]="BUSY" Тогда
УстановитьСтатус(2);
УстановитьТекстСообщения("Абонент занят");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 10, Истина);
ИначеЕсли Параметр["Value"]="CANCEL" Тогда
УстановитьСтатус(2);
УстановитьТекстСообщения("Отмена звонка");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 10, Истина);
ИначеЕсли Параметр["Value"]="NOANSWER" Тогда
УстановитьСтатус(2);
УстановитьТекстСообщения("Абонент не ответил");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 10, Истина);
ИначеЕсли Параметр["Value"]="CONGESTION" Тогда
УстановитьСтатус(2);
УстановитьТекстСообщения("Ошибка связи");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 10, Истина);
ИначеЕсли Параметр["Value"]="CHANUNAVAIL" Тогда
УстановитьСтатус(2);
УстановитьТекстСообщения("Ошибка связи");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 10, Истина);
КонецЕсли;
КонецЕсли;
ИначеЕсли Параметр["name"]="Hangup" Тогда
//УстановитьСтатусПользователя(Истина);
УстановитьСтатус(3);
УстановитьТекстСообщения("Звонок завершен");
ПодключитьОбработчикОжидания("ЗакрытьФорму", 3, Истина);
ПоказатьОповещениеПользователя("Звонок завершен "+Телефон);
КонецЕсли;
КонецПроцедуры
Сюда приезжает через Оповестить из события в ОбработкаВнешнегоСобытия из модуля приложения кодом:
...
ИначеЕсли Действие = "Asterisk_Event" тогда
Если Данные["name"]="Hangup" Тогда
СтруктураЗаписи = Новый Структура;
СтруктураЗаписи.Вставить("Период", ТекущаяДата());
СтруктураЗаписи.Вставить("Пользователь", ПараметрыСеанса.ТекущийПользователь);
СтруктураЗаписи.Вставить("Статус", Перечисления.CRM_СтатусыАктивностиПользователей.Активен);
КлиентскийКешФормаСервиса.ДобавитьЗаписьВРегистрСведений(СтруктураЗаписи, "CRM_АктивностьПользователей", Истина, Ложь);
КонецЕсли;
Оповестить(Действие, Данные, "EventListener");
...
туда прилетает через вк 1с тисипи клиента от кафки (статья про кафку выше ссылка), в кафку отправляет бэк описанный в данной статье.
Так как данная статья написана все таки про автообзвон, делать описание реализации фонера 1с здесь не буду(реализаций на сайте(и не только) много, как делать звонки, подписываться на события, и обработку в контексте 1с показано выше).
Отступление:
Про методы распознавание речи. К использованию спич кита яндекса, пришел не сразу.
Были попытки использовать:
Локальные:
1.CMU Sphinx https://cmusphinx.github.io, сфинкс очень крутой!, может работать локально. Есть моменты в плане потребления ресурсов (оно и понятно почему), завести как нужно его у меня не хватило мозгов. Точность распознавания у меня получилась не достаточная.
2. Напедалить свою поделку ПО-БЫСТРОМУ на тему оцифровки и принятия решений (ML, буст деревья яндекса, нейронка, обучение) у меня тоже не хватило мозгов(возможно позже) (вся проблема будет в обучении нейронки)(как мне кажется)
3. Использовать голосовой движок дот нет стэка мс. Тут проблема в том что dev-машина мс windows 10, оттуда поддержка русского языка выехала(может подвезут позже) я об Microsoft.CognitiveServices.Speech возможно я что не понял или не так сделал но при указании региона ру(RU-ru) там была бага что типа не найдено установленного региона или что-то в этом духе.
4. Использовать гугловский распознаватель, тот который на андроиде (ведь там все нормально работает). Руки не дошли до него, идея была в том что сделать поделку для андроид которой скармливать файл, прогонять через распознавание гугла, выдавать результат,(поделку с андроидом развернуть в эмуляторе, докере и т д)
Он-лайновые:
1. спич кит яндекс - распознает прекрасно(по моему и не только мнения для рашен ориент, лучше нет), иногда лучше нас(когда прилетает статус 0 не распознано, периодически прослушиваем и думаем что можно изменить в алгоритме, в списках словах(позитив, негатив, прочее)), пока только слова добавляем. (Платный)
2. Распознавание он лайн от гугла. Руки не дошли до него(дошли бы остановился на яндекс спич кит). Вероятно, тоже невероятное качество распознавания. (Платный)
3. Прочие он-лайн сервисы. Руки не дошли до них(дошли бы, если бы не п1-п2)(вообще рассматривал читал маны по ним)
Отступление:
Да, автообзвон натыкался на ивр. Мило пообщались 2 робота, ничего страшно. Нам прилетел (с бэка) ответ: не распознан, люди далее обработали инцидент самостоятельно. Все, что прилетает со статусом "ответ не распознан", разбирают люди, прослушав запись ответа.