Началось все с запроса клиента который хотел шагать в ногу со временем и частично заменить живых операционистов на бездушного робота — сделать прием показаний счетчиков через телефон. С вводом через DTMF вроде все понятно и никакой изюминки в этом нет. Хотелось ввод голосом.
Под cut'ом небольшой мануал про прикручивание google ASR к одному из коробочных вариантов asterisk'a.
После некоторого мониторинга интернета было найдено несколько готовых решений на технологиях ЦРТ. Может и хорошо но ценник стартовал от 150 кило рублей, поэтому идем мимо (привет жаба).
Голый asterisk брать не стали, потому как хочется какой никакой гуй, FreePBX тоже пошел лесом ибо оно превращается в один большой custom-context, взяли коробку XVB — VirtualPBX потому как: cdr, TTS, gsm модемы, автоинформатор ( наш второй этап ) из коробки, ASR и база данных через http API. Берем бесплатную ( от платных отличается только ограничением на 10 одновременных звонков), версию — для наших 2 линий ТфОП и 2 3G свистков за глаза. В принципе к тому же switchvox'у прикручивание примерно такое же.
Поехали:
Качаем с сайта или зеркала ( стабильная но с чуть старым астериском тут ) большой tar.bz2 архив с готовым для работы имиджем для VMWare Player. Или если есть старая версия ставим апдейты.
Распаковываем и запускаем в VMWare плеере либо конвертируем в ESXi или делаем дамп на реальное железо. Ничего сложного. Первый запуск немного долгий ибо распаковываются звуковые файлы и проверяются апдейты.
Лирическое отступление:
Система задумывалась для создания
Заходим в интерфейс администратора, делаем пользователя, добавляем DID, меняем ему язык на Русский-Женщина ( это google TTS ) — эта виртуальная АТС и будет нашей системой самообслуживания. Более подробно про настройку писать наверное не стоит, тут уже писали.
Затем топаем в консоль и пишем cgi скрипт который будет к гуглу ходить. Берем пример и немного правим чтоб понимал CGI параметры, получаем что то типа:
#!/usr/bin/perl
########################################################################
use strict;
use CGI qw(:standard);
use LWP::UserAgent;
use JSON::XS;
my $lang = param('lang');
my $file = param('file');
my $var_name = param('var') || 'ASR_RESULT';
print "Content-type: text/plain; charset=utf-8nn";
my $url;
if ( lc($lang) eq 'ru' ) {
$url = "http://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=ru-RU";
} else {
$url = "http://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=en-US";
}
my $new_file = "/tmp/google-asr-$$.wav";
open(TMP,">$new_file");
my $buffer;
while ( !eof($file) ) {
read( $file, $buffer, 16384 );
print TMP $buffer;
}
system "ffmpeg -y -i $new_file $new_file.flac 2>/dev/null";
if ( $? ) {
print "$var_name=n";
die "Can't convert file $new_file to $new_file.flac: $?n";
} else {
$new_file .= '.flac';
}
my $file_info = `file $new_file`;
if ( $file_info =~ /FLAC audio.*s([d.]+)s*kHz/ ) {
$file_info = $1 * 1000;
} else {
unlink $new_file;
print "$var_name=n";
die "Incorrect FLAC file: $file_infon";
}
unless ( open( FILE, "<$new_file" ) ) {
print "$var_name=n";
die "Can't open input file[$file]: $!n";
}
undef $/; my $audio = <FILE>; $/ = "n";
close(FILE);
unlink $new_file;
my $ua = LWP::UserAgent->new( debug => 1 );
my $response = $ua->post($url, Content_Type => "audio/x-flac; rate=$file_info", Content => $audio);
if ( $response->is_success ) {
my $h_ref;
eval {
my $json = JSON::XS->new();
$h_ref = $json->decode($response->content());
};
if ( ref $h_ref eq 'HASH' and exists $h_ref->{'hypotheses'} ) {
my $data = $h_ref->{'hypotheses'}->[0]->{'utterance'};
my $map = {
'нoль' => 0,
'oдин' => 1,
'двa' => 2,
'три' => 3,
'четыре' => 4,
'пять' => 5,
'шесть' => 6,
'семь' => 7,
'вoсемь' => 8,
'девять' => 9,
'десять' => 10,
'да' => 'да',
'дальше' => 'да',
'верно' => 'да',
'нет' => 'нет',
'назад' => 'нет',
'газ' => 'газ',
'гас' => 'газ',
'вода' => 'вода',
'воду' => 'воду',
'свет' => 'свет',
'электричество' => 'свет',
'электро' => 'свет',
};
my $result = '';
foreach my $ch ( split(/s+/,$data) ) {
$result .= length($map->{$ch}) ? $map->{$ch} : $ch;
}
print "$var_name=$resultn";
exit;
}
}
print "$var_name=n";
Кроме чтения CGI параметров еще добавили `конвертацию в числа` из строк вида 'один','два' и тд и несколько 'альясов' на названия типов счетчиков и подтверждение ( да == да||дальше||верно ), потому как на некоторых линиях 'дальше' распознается лучше чем просто 'да'.
На входе скрипт принимает файл в wav формате и имя переменной куда записать значение (это передается в XVB и используется в диалплане), делаем еще 2 скрипта:
- Второй ( account_check.cgi ) проверяет корректность ввода лицевого счета,
- Третий ( commit.cgi ) записывает данные в базу.
Я не буду приводить их здесь потому, что там все будет зависеть от Вашей БД. Третий скрипт принимает номер лицевого счета / тип показаний / и показания и записывает их в БД и возвращает разницу. Мы проговариваем ее пользователю.
Сохраняем эти скрипты на той же машине ( или на другой ) в /var/www/cgi-bin/.
Далее происходит скучная настройка диал-плана из практически одинаковых кусков:
- запрос данных и передача в наш cgi-скрипт который возвращает из wav текстовую строку ( WebVariables )
- проверка введенных пользователем данных ( GotoIF )
- повтор введенных данных ( RoboText )
- подтверждение данных голосом ( WebVariables )
То есть для каждого показателя мы делаем:
- запрос данных ( запись звукового файла ).
- передача файла на веб сервер который декодирует введенные данные в текст и возвращает ответ в переменной.
- читаем пользователю то что он ввели и просим подтвердить ввода.
- записываем результат.
Как видно действия сильно нудные поэтому мы не стали делать все это руками а сгенерировали XML конфиг скриптом
(XVB поддерживает загрузку/выгрузку конфигурации из XML файла). Маленький кусок того что получилось:
<opt>
<IVR name="0"
EXT_NUMBER="0"
NAME="Приветствие."
GREET_REPEAT_CNT="1"
GREETING="Вас приветствует портал приема показаний счетчиков."
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_2*0"
TYPE="1"
WAITEXTENSION="0">
</IVR>
<IVR name="error"
EXT_NUMBER="error"
NAME="WEB ошибка"
GREETING="Произошла ошибка. Попробуйте позвонить позднее."
GREET_REPEAT_CNT="1"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="hangup"
TYPE="1"
WAITEXTENSION="0">
</IVR>
<IVR name="V5_2*0"
EXT_NUMBER="V5_2*0"
NAME="счетчик вода - холодная --------------"
GREET_REPEAT_CNT="0"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_2*1"
TYPE="1"
WAITEXTENSION="0">
</IVR>
<IVR name="V5_2*1"
EXT_NUMBER="V5_2*1"
NAME="счетчик вода - холодная / ввод цифр"
GOTO_IF_FAIL="error"
GREETING="Назовите показания счетчика холодной воды"
GREET_REPEAT_DELAY="0.00"
GREET_REPEAT_CNT="1"
MAX_MSG_DURATION="10"
MAX_SILENCE="2"
NEED_VOICE="1"
NEXTEXTENSION="V5_2*2"
TYPE="20"
WAITEXTENSION="0"
WEBVAR_URL="http://localhost/cgi-bin/gv.pl?lang=ru&file=% VAR:FILE_DATA %&var=ASR_RESULTV5_2">
</IVR>
<IVR name="V5_2*2"
EXT_NUMBER="V5_2*2"
NAME="счетчик вода - холодная / проверка ввода"
GREET_REPEAT_CNT="0"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_2*3"
TYPE="21"
WAITEXTENSION="0">
<_VB_DATA COND="==" FUNC="strlen" PRIORITY="5" REDIRECT_TO="V5_2*2*1" VAR_NAME="ASR_RESULTV5_2" VAR_VALUE="0"/>
</IVR>
<IVR name="V5_2*2*1"
EXT_NUMBER="V5_2*2*1"
NAME="счетчик вода - холодная / ошибка ввода"
GREETING="Мы не распознали ваш ввод, пожалуйста повторите еще раз."
GREET_REPEAT_CNT="1"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_2*1"
TYPE="1"
WAITEXTENSION="0">
</IVR>
<IVR name="V5_2*3"
EXT_NUMBER="V5_2*3"
NAME="счетчик вода - холодная / чтение ввода"
GREETING="Вы ввели"
GREET_REPEAT_CNT="1"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_2*4"
SAY_PATTERN="char"
SAY_PATTERN_ID="0"
TEXT_STR="% VAR:ASR_RESULTV5_2 %"
TYPE="25"
WAITEXTENSION="0">
</IVR>
<IVR name="V5_2*4"
EXT_NUMBER="V5_2*4"
NAME="счетчик вода - холодная / подтверждение"
GOTO_IF_FAIL="error"
GREETING="Подтвердите, сказав Да иилии Нет."
GREET_REPEAT_DELAY="0.00"
GREET_REPEAT_CNT="1"
MAX_MSG_DURATION="3"
MAX_SILENCE="2"
NEED_PARAMS="0"
NEED_VOICE="1"
NEXTEXTENSION="V5_2*4*1"
TYPE="20"
WAITEXTENSION="0"
WEBVAR_URL="http://localhost/cgi-bin/gv.pl?lang=ru&file=% VAR:FILE_DATA %">
</IVR>
<IVR name="V5_2*4*1"
EXT_NUMBER="V5_2*4*1"
NAME="счетчик вода - холодная / проверка подтверждения"
GREET_REPEAT_CNT="0"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_2*3"
TYPE="21"
WAITEXTENSION="0">
<_VB_DATA COND="==" FUNC="value" PRIORITY="5" REDIRECT_TO="V5_2*0" VAR_NAME="ASR_RESULT" VAR_VALUE="да"/>
<_VB_DATA COND="==" FUNC="value" PRIORITY="5" REDIRECT_TO="V5_2*1" VAR_NAME="ASR_RESULT" VAR_VALUE="нет"/>
</IVR>
<IVR name="V5_3*9"
EXT_NUMBER="V5_3*9"
NAME="счетчик вода - горячая / Сохранение"
GREET_REPEAT_CNT="0"
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="V5_3*9*1"
TYPE="6"
WAITEXTENSION="0"
QUIET_MODE="1"
GOTO_IF_FAIL="error"
TTS_URL="http://127.0.0.1/cgi-bin/commit.pl?level=V5_3&account=% VAR:ASR_RESULTV1 %&value1=% VAR:ASR_RESULTV5_1 %&value2=% VAR:ASR_RESULTV5_2 %&value3=% VAR:ASR_RESULTV5_3 %">
</IVR>
<IVR name="V5_3*9*1"
EXT_NUMBER="V5_3*9*1"
NAME="-----------------------"
GREET_REPEAT_CNT="1"
GREETING="Ваши показания сохранены. Спасибо. Если хотите выберите другой тип счетчика сейчас."
GREET_REPEAT_DELAY="0.00"
NEXTEXTENSION="0"
TYPE="1"
WAITEXTENSION="0">
</IVR>
</opt>
Можно сохранить выше приведенный конфиг в xxx.xml и залить в профиле пользователя через пункт восстановления конфигурации.
В вебе, в итоге у нас получилось, что то похожее на:
Собственно простейший рабочий прототип для сбора показаний голосом и через DTMF ( возможность организации самообслуживания вводом DTMF в VirtualPBX есть из коробки поэтому я не стал здесь это подробно описывать ) готов. Сейчас шлифуем свои скрипты и пишем нормальные звуковые приветствия.
Если кому интересно как оно распознает ( а распознает оно по разному ) то:
- Через скайп: skype: tsg812
- Через VoIP: tel:+1 253-243-1337
- Через dongle: tel: +7 987 391 6412
Донгл и скайп пока в фоваритах по качеству, к ТфОп пока не прикручивали.
P.S. Посмотрим сколько народу предпочтет голос DTMF'у, для меня DTMF вроде проще ( привычнее ) и быстрее если есть возможность ввести через DTMF.
Автор: Max1983