Реализация голосового справочника на базе YandexSpeechKit

в 7:38, , рубрики: asterisk, python, speech recognition, многобуков, ненормальное программирование

На просторах интернета представлены различные реализации, но, на мой взгляд, все они достаточно простые. Хочу представить свой вариант голосового справочника под астериск.

Примечание: я не являюсь профессиональным программистом, и, возможно, некоторые решения могут показаться вам дикими. Некоторые приемы могут быть устаревшими. Я готов принять критику и исправить систему к лучшему.

Краткое описание возможностей:

Пользователь попадает в IVR, произносит свой запрос и, в большинстве случаев, попадает туда, куда ему надо. К системе также прикручена статистика с записью в таблицу mysql.
Коротко о компании и сети, в которой развернута данная система:
~1000 телефонов, около 50 отделов

Программные продукты, используемые системой:

  • Asterisk 13.10
  • YandexSpeechKit
  • python 2.6.6
  • Mysql, MSSQL
  • sox 14.2.0
  • curl 7.19.7
  • lame 3.99.5

Описание диалплана в астериск.

[officevoicerec]
exten => s,1,Answer()
same => n,Macro(hangercheck,${CALLERID(num)})
same => n,Set(ITERATIONS=1)
same => n,Set(HANGFLAG=TRUE)
same => n,Background(/var/lib/asterisk/sounds/ru/speechrec/zdravstvuite)

В данном фрагменте запускается макрос проверки того, бросил ли звонивший трубку едва услышав приветственное сообщение. Далее идет установка значения переменных:
ITERATIONS — необходима для повтора процесса распознавания заданного количества раз. HANGFLAG – данная переменная используется макросом hangercheck.

same => n(rec),Set(RECFILE=/tmp/${UNIQUEID}.wav)
same => n,Playback(/var/lib/asterisk/sounds/en/beep)
same => n,Record(${RECFILE},3,8)
same => n,AGI(pyreq8.py,${RECFILE})
same => n,GotoIf($["${NUMTOCALL}" = "repeat"]?repeat)
same => n,Set(HANGFLAG=FALSE)

Устанавливаем переменную файла записи, записываем файл. Запускаем agi-скрипт, отвечающий за отправку файла на распознавание и поиск номера (описание скрипта будет дано далее), проверка переменной NUMTOCALL (значение устанавливается скриптом), ставим признак HANGFLAG, означающий, что человек не бросил трубку раньше времени.

same => n,Macro(VRstat,${CALLERID(num)},${NUMTOCALL},${RSTATUS},${CHANNEL},${RECREZ})
same => n,GotoIf($[[${EXISTS(${FNAME})}]]?foundName:havenodescr)
same => n(foundName),Set(FILE_FNAME=${STRREPLACE(FNAME, ,)})
same => n,GotoIf($["${STAT(f,/var/lib/asterisk/sounds/ru/cache/${FILE_FNAME}.mp3)}"="1"]?havecache:nocache)

В данном фрагменте запускается Макрос проверки факта того, бросил ли звонивший трубку едва услышав приветственное сообщение, далее идет установка значения переменных, ITERATIONS — необходима для повтора процесса распознавания заданного количества раз. HANGFLAG – данная переменная используется макросом hangercheck.

same => n(rec),Set(RECFILE=/tmp/${UNIQUEID}.wav)
same => n,Playback(/var/lib/asterisk/sounds/en/beep)
same => n,Record(${RECFILE},3,8)
same => n,AGI(pyreq8.py,${RECFILE})
same => n,GotoIf($["${NUMTOCALL}" = "repeat"]?repeat)
same => n,Set(HANGFLAG=FALSE)

Устанавливаем переменную файла записи, записываем файл. Запускаем скрипт, отвечающий за отправку файла на распознавание и поиск номера (описание скрипта будет ниже), проверка переменной NUMTOCALL (значение устанавливается скриптом), ставим признак HANGFLAG, что человек не бросил трубку раньше времени.

same => n,Macro(VRstat,${CALLERID(num)},${NUMTOCALL},${RSTATUS},${CHANNEL},${RECREZ})
same => n,GotoIf($[[${EXISTS(${FNAME})}]]?foundName:havenodescr)
same => n(foundName),Set(FILE_FNAME=${STRREPLACE(FNAME, ,)})
same => n,GotoIf($["${STAT(f,/var/lib/asterisk/sounds/ru/cache/${FILE_FNAME}.mp3)}"="1"]?havecache:nocache)
same => n(nocache),System(curl "https://tts.voicetech.yandex.net/generate?format=mp3&lang=ru-RU&speaker=zahar&emotion=neutral&speed=0.8&key=вашключ" -G --data-urlencode "text= ${FNAME}." > /tmp/speech-${UNIQUEID}.mp3)
same => n,System(/usr/local/bin/lame -S --scale 30 /tmp/speech-${UNIQUEID}.mp3 /var/lib/asterisk/sounds/ru/cache/${FILE_FNAME}.mp3)
same => n(havecache),Playback(/var/lib/asterisk/sounds/ru/cache/${FILE_FNAME})
same => n,Dial(Local/${NUMTOCALL}@common-context)

Запуск макроса vrstat (отвечает за статистику, описан не будет ввиду своей тривиальности). Проверяем есть ли описание FNAME (переменную задает pyreq8.py) у запроса. Если описание есть, задаем в переменную имя файла кеша и проверяем его наличие. Если файл не существует – синтезируем его, конвертируем в mp3, увеличиваем громкость и, далее(или если кеш существует), проигрываем его и вызываем абонента.

same => n(repeat),GotoIf($["${ITERATIONS}"="1"]?secretary)
same => n,Background(/var/lib/asterisk/sounds/ru/speechrec/1-wav)
same => n,Set(ITERATIONS=$[${ITERATIONS}+1])
same => n,Goto(rec)

Повторение распознавания. Если количество итераций превышает заданное, то переводим на секретарей.

 same => n(secretary),Macro(VRstat,${CALLERID(num)},${NUMTOCALL},${RSTATUS},${CHANNEL},${RECREZ})
same => n,Set(HANGFLAG=FALSE)
same => n,Dial(Local/1000@common-context)

Перевод на секретарей. Заносим в статистику, устанавливаем флаг, что трубку не бросили. Вызываем секретаря.

same => n(havenodescr),Playback(/var/lib/asterisk/sounds/ru/speechrec/wait2);thanks-wait)
same => n,Noop('no description')
same => n,Dial(Local/${NUMTOCALL}@common-context)
same => n,Hangup()

Вызов абонента, у которого отсутствует описание в справочнике. Проигрываем сообщение, заносим в статистику, вызываем.

exten => h,1,Gotoif($["${HANGFLAG}"="TRUE"]?exec:noop)
same => n(exec),Macro(VRstat,${CALLERID(num)},x,HANGER,${CHANNEL})
same => n(noop),Noop('exiting')

Обработка окончания вызова для работы макроса hangercheck.

Описание диалплана. Макросы.

[macro-hangercheck]
;${ARG1} -clid
exten => s,1,GotoIf($["${ARG1}"="anonymous"]?end)
exten => s,n,MYSQL(Connect connid SRV user password db utf8)
exten => s,n,MYSQL(SET NAMES utf8)
exten => s,n,MYSQL(Query resultid ${connid} SELECT IFNULL((SELECT clid from ivr_stat  where rstatus="HANGER" and calldate >=ADDDATE(NOW(),INTERVAL -48 HOUR) and clid="${ARG1}" order by calldate desc limit 1),"NF"))
exten => s,n,MYSQL(Fetch fetchid ${resultid} VAR)
exten => s,n,MYSQL(Clear ${resultid})
exten => s,n,MYSQL(Disconnect ${connid})
exten => s,n,GotoIf($["${VAR}"="NF"]?end)
exten => s,n,Macro(VRstat,${ARG1},x,H_RECALL,${CHANNEL})
exten => s,n,Dial(Local/1000@common-context)
exten => s,n,Hangup()
exten => s,n(end),Noop(Hanger check failed)

Проверяем в базе статистики, звонил ли данный номер последние 48 часов, и, если он вешал трубку не дождавшись окончания распознавания – заносим в статистику и соединяем с секретарем.

Краткое описание используемых sql и mysql таблиц.

Sql таблица dbo.phrases.

id(PK, int), phrase (text), number(varchar(50))

Для данной таблицы поднят полнотекстовый поиск, используется для переключения по ключевым словам при получении длинной фразы. Например поступила фраза: «соедините меня пожалуйста с представителем рекламного отдела», если в бд есть запись, где phrase= «рекламный отдел», то звонящего соединится с соответствующим номером(number).

Mysql таблица recogStats.

id(PK, int), date (datetime), ctime(float), rtime(float),stime(float), phrase(varchar(60))

Данная таблица используется для хранения результатов распознавания и сбора статистики по времени распознавания. ctime – время затраченное на конвертацию аудио файла, rtime – время затраченное на загрузку и распознавание, stime – время затраченное на поиск

Mysql таблица ivr_stat.

id(PK, int), calldate (datetime),clid(varchar(15)),duration(int(20)),callednum(varchar(10)),rstatus(varchar(20)),channame(varchar30)),RECREZ(varchar(200))

Данная таблица используется для ведения учета результатов распознавания(RECREZ,rstatus), на кого ушел звонящий по результатам поиска распознанной фразы(callednum), для ведения статистики, сколько времени провел человек в меню распознавания(duration) и для отладки(channame)

Mysql таблица CustomRequests.

id(PK, int), nomer(int(10)), request(varchar(100))
Вспомогательный справочник дополнительных групп и отделов.

Mysql таблица numdescriptions.

Num(text), name(text)

Справочник c описанием внесенного номера для озвучки.

Mysql таблицы spravochnik_rus и spravochnik_rus_name_num_no_tops

nomer(varchar(11)), fio(varchar(100))

Справочники персон с номерами. Первый — полный, для внутреннего использования. У второго, для внешнего использования, номера директоров и руководителей завернуты на секретариат.

AGI скрипт осуществляющий конвертацию, отправку и поиск распознанной фразы.

Используемые библиотеки:

import difflib
from sys import exit
import uuid
import time
import os
import subprocess
import xml.etree.ElementTree
import MySQLdb
import pymssql
import string
import re
from itertools import permutations
from os import remove
import timeit

А также библиотека asterisk.agi, которую импортируем в зависимости от отладки(отладка производится на windows машине).

Описание переменных.

WINDEBUG = False

Флаг отладки.

_digits = re.compile('d')

Переменная скомпилированного регулярного выражения на цифры.

uniqid = str(uuid.uuid1()).replace('-', '')

Переменная уникального идентификатора.

dkey = '12345678-9101-1121-3141-51617181920'

API-ключ яндекс speech kit.

lang = 'ru-RU'

Опция языка для яндекс speechkit.

topic = 'queries'

Опция темы распознавания для яндекс speechkit.

callnumber = '222'

Номер для вызова по умолчанию.

setVar('NUMTOCALL', callnumber)

Установка номера для вызова по умолчанию, в случае, если что-нибудь пойдет не так.

persondic = dict()

Словарь ФИО – номер.

persondicFI=dict() 

Словарь ФИ — номер.

persondicF=dict()

Словарь Фамилия – номер.

otherdic = dict()

Словарь с прочими записями вида Запись – номер.

descriptions = dict()

Словарь с описаниями для генерации озвучки, вид Запись – номер.

duplicates = list()

Служебный список для фильтрации дублей.

nums = list()

Список со всеми внутренними номерами.

outfile = '/tmp/' + uniqid + '-pcm.wav'

Переменная временного выходного аудио файла.

mysqlhost='myhost'
mysqlpass='mypass'
mysqluser='myuser'
mysqldb='mydb'
mssqlhost='mshost'
mssqlpass='mspass'
mssqluser='msuser'
mssqldb='msdb'

Реквизиты для подключения к mysql и mssql.

if not WINDEBUG:
    from asterisk.agi import *

    agi = AGI()
    infile = agi.env['agi_arg_1']
    caller = agi.get_variable('CALLERID(num)')
else:
    caller = '1064'
    infile = ''

Импорт библиотеки, установка переменной имени входного аудиофайла и номера звонящего в зависимости от отладки.

Описание функций

def verb(s):
    if not WINDEBUG:
        agi.verbose(s)
    else:
        print s

В зависимости от значения переменной отладки, выводим сообщения в консоль asterisk или в stdout.

def setVar(varname, varval):
    if not WINDEBUG:
        agi.set_variable(varname, varval)
    else:
        print "setting var " + varname + " with value " + varval

В зависимости от значения переменной отладки присваиваем значение переменной диалплана в астериск или выводим в stdout.

def contains_digits(s):
    verb('enter contains_digits')
    return bool(_digits.search(s))

Проверка содержит ли s цифры.

def return_digits(s):
    verb('return digits')
    pstr = s.encode("utf-8")
    all = string.maketrans('', '')
    nodigs = all.translate(all, string.digits)
    return unicode(pstr.translate(all, nodigs), "utf-8")

Возвращаем из s только цифры.

def check_dob(num):
    if int(num) in nums:
        verb('checkdob success')
        return True
    else:
        verb('checkdob fail' + num)
        return False

Проверка существования добавочного номера в списке nums.

def set_dob(strnum):
    buf = return_digits(strnum)
    if contains_digits(strnum):
        if check_dob(buf):
            verb('setting var ' + buf)
            setVar('RSTATUS', 'SAYDIAL')
            return buf
        else:
            return "repeat"
    else:
        return "repeat"

Функция установки добавочного номера.

def checkSize(infile):
    if int(os.stat(infile).st_size) <= 26364:
        setVar('NUMTOCALL', 'repeat')
        setVar('RSTATUS', 'SILENCE')
        verb('empty file received')
        remove(infile)
        exit(9)

Проверка полученного файла аудио-записи для распознавания, если размер слишком маленький, полагаем, что в трубку молчали.

def addSessionStat(ctime, rtime, stime, phrase):
    cdate = time.strftime("%Y-%m-%d %H:%M:%S")
    db = MySQLdb.connect(host=mysqlhost, user=mysqluser, passwd=mysqluser, db=mysqldb,
                         charset='utf8')
    cur = db.cursor()
    cur.execute(
        "INSERT INTO recogStats(date,ctime,rtime,stime,phrase) VALUES ('" + cdate + "','" + str(ctime) + "','" + str(
            rtime) + "','" + str(stime) + "','" + phrase + "')")
    db.commit()
    db.close()

Функция записи статистики распознавания.

def fillDics():
    db = MySQLdb.connect(host=mysqlhost, user=mysqluser, passwd=mysqlpass, db="central_cdr",
                         charset='utf8')
    cur = db.cursor()
    if len(caller) != 4:
        tbname = 'spravochnik_rus_name_num_no_tops'
    else:
        tbname = 'svravochnik_rus_persons'
    cur.execute("""SELECT nomer,fio from """ + tbname)
    for row in cur.fetchall():
        fullfio=row[1].lower()

        f=" ".join(fullfio.split(' ')[0:1])

        if not f in persondicF.keys():# and f not in duplicates :
            persondicF[f] = int(row[0])
        elif fullfio in persondic.keys():
            pass
        else:
            duplicates.append(f)

        if not fullfio in persondic.keys():
            persondic[fullfio] = int(row[0])

        fi=" ".join(fullfio.split(' ')[0:2])
        if not fi in persondicFI.keys():
            persondicFI[fi] = int(row[0])
        elif fullfio in persondic.keys():
            pass
        else:
            persondicFI.pop(fi)

    uniquedups=[x for x in list(set(duplicates)) if x != '']
    for item in uniquedups:
        for key in persondicF.keys():
            if item in key:
                persondicF.pop(key)

    cur.execute("""SELECT nomer,request from CustomRequests """)
    for row in cur.fetchall():
        otherdic[row[1].lower()] = row[0]

    cur.execute("""SELECT num,name from numdescriptions """)
    for row in cur.fetchall():
        descriptions[str(row[0])] = row[1]

    cur.execute("""SELECT nomer from spravochnik_rus """)
    for row in cur.fetchall():
        nums.append(int(row[0]))
    db.close()

Данная функция заполняет словари Фамилий(persondicF), Фамилий Имен(persondicFI), ФИО(persondic), названий отделов(otherdic), описаний для синтеза речи(descriptions) и всех номеров компании(nums).
В зависимости от длины номера звонящего берется таблица, где содержатся номера директоров(внутренний звонящий) или не содержит(внешний звонящий).
Справочник фамилий проходит проверку на уникальность, чтобы исключить наличие в нем двух Ивановых с разными номерами.

def convert(infile, outfile):
    #convert file
    verb("Converting WAV " + infile)
    soxconvert = subprocess.Popen(['sox', infile, '-r', '16000', '-b', '16', '-c', '1', outfile],
                                  stdout=subprocess.PIPE)
    (out, err) = soxconvert.communicate()
    #    remove(infile)
    if soxconvert.returncode != 0:
        setVar('NUMTOCALL', callnumber)
        #  return ""
        exit(9)

Функция конвертации записанного в астериске файла, в случае, если sox выдает ошибку, устанавливаем номер по умолчанию и выходим из скрипта.

def sendRecog(file):

    verb("Sending file to yandex: " + outfile)
    proc = subprocess.Popen(['curl', '--max-time', '5', '--silent',
                             'asr.yandex.net/asr_xml?key=' + dkey + '&uuid=' + uniqid + '&topic=' + topic + '&lang=ru-RU',
                             '-F', 'Content-Type=audio/x-pcm;bit=16;rate=16000', '-F', 'audio=@' + outfile],
                            stdout=subprocess.PIPE)
    (out, err) = proc.communicate()
    verb("return code is: " + str(proc.returncode))
    if proc.returncode != 0:
        return ""
    remove(file)
    e = xml.etree.ElementTree.fromstring(out)
    if e.attrib['success'] == '1':
        verb(e._children[0].text)
        return e._children[0].text
    else:
        return ""

Функция отправки файла на распознавание и получения ответа от яндекса. В случае, если распознавание удалось, берем первый ответ. С библиотекой curl мне такое провернуть не удалось, по этой причине используется такой вариант.

def searchfiobyf(f):
    if len(f.split(" "))==2:
        limit=2
    elif len(f.split(" "))>=3:
        return f
    else:
        limit=1

    for fio,num in persondic.iteritems():
        cutfio=" ".join(fio.split(' ')[0:limit])
        if f == cutfio:
            return fio

Функция поиска полного ФИО в зависимости от того, пришла ли на вход только фамилия или фамилия и имя.

def combinationSearcher(phrase,pdic,quality):
        verb(u'searching in persons')
        result = difflib.get_close_matches(phrase, pdic.keys(), 1, quality)
        #verb(" ".join(result))
        if len(result) > 0:
            fullfio=searchfiobyf(result[0])
            verb(u'found ' + str(pdic[result[0]]))
            setVar('RSTATUS', 'NAMESUCCESS')
            setVar('FNAME', fullfio)
            return pdic[result[0]]
        else:
            return ""

Функция поиска комбинаций, пример: phrase = «Алексей Петр», в pdic содержится запись «Алексеев Пётр», функция get_close_matches вернет «Алексеев Пётр». Функция может выдавать и неправильные результаты, но, как показала практика, корректных срабатываний значительно больше. Параметр quality позволяет задать точность поиска похожих фраз.

def getNumByName(recognizedString):
    if u'добавочн' in recognizedString:
        verb('enter dobavochn')
        return set_dob(recognizedString)

    elif len(recognizedString.replace(" ", "")) == 4:
        verb('enter num say')
        return set_dob(recognizedString)

    if len(recognizedString) <= 5 and recognizedString.lower() not in [u"тула",u"химки",u"сочи"]:
        setVar('RSTATUS', 'SHORT')
        return "repeat"
    verb(u'start searching')
    #убираем повторы
    split = list(set(recognizedString.split(" ")))
    parts = len(split)
    fixedstring=" ".join(split)
    if parts >= 5:
        verb('Phrase is long. Using FTDB')
        buf = mssqlwrapper(fixedstring)
        if buf != '':
            buf = mssqlwrapper(fixedstring)[0]
            if str(buf) in descriptions.keys():
                setVar('FNAME', descriptions[str(buf)])
            setVar('RSTATUS', 'DEPTSUCCESS')
            return buf
        else:
            setVar('RSTATUS', 'REQUESTNOTFOUND')
            return "repeat"

    result=""
    if parts == 1:
        mssqlcheck=mssqlwrapper(fixedstring)
        if mssqlcheck!='':
            if mssqlcheck[2]>=80:
                buf=str(mssqlcheck[0])
                if buf in descriptions.keys():
                    setVar('FNAME', descriptions[str(buf)])
                setVar('RSTATUS', 'DEPTSUCCESS')
                result=buf
        else:
            result=combinationSearcher(fixedstring,persondicF,0.7)
    elif parts ==2:
        combs = list(permutations(split, parts))
        # (Иванов иван иванович, иван иванов иванович, иван иванович иванов.....)
        for item in combs:
            element = " ".join(item)
            result=combinationSearcher(element,persondicFI,0.7)
            if result!="":
                break
    elif parts ==3:

        combs = list(permutations(split, parts))
        for item in combs:
            element = " ".join(item)
            result=combinationSearcher(element,persondic,0.8)
            if result!="":
                break

    if result!="":
        return result

    verb('Low ftdbsearch')
    buf = mssqlwrapper(recognizedString)

    if buf != '':
        buf = mssqlwrapper(recognizedString)[0]
        if str(buf) in descriptions.keys():
            verb('it is')
            setVar('FNAME', descriptions[str(buf)])
        setVar('RSTATUS', 'DEPTSUCCESS')
        return buf
    else:
        verb(u'item not found ' + recognizedString)
        #добавляем не найденный запрос
        setVar('RSTATUS', 'REQUESTNOTFOUND')
        return callnumber

Основная функция поиска номера по фразе.

Разберем эту функцию по частям.

if u'добавочн' in recognizedString:
    verb('enter dobavochn')
    return set_dob(recognizedString)

Пример: на фразу «Пожалуйста добавочный 1234» вернет добавочный номер (если он есть).

elif len(recognizedString.replace(" ", "")) == 4:
    verb('enter num say')
    return set_dob(recognizedString)

В случае, если пользователь произнес четырёхзначный номер – вернуть соответствующий добавочный (если он есть).

if len(recognizedString) <= 5 and recognizedString.lower() not in [u"тула",u"химки",u"сочи"]:
    setVar('RSTATUS', 'SHORT')
    return "repeat"

Чтобы не делать лишние прогоны программы по коротким фразам (такое может случиться, когда яндекс услышал и распознал часть разговора, когда звонящий не слушал сообщение о распознавании).

if parts >= 5:
    verb('Phrase is long. Using FTDB')
    buf = mssqlwrapper(fixedstring)
    if buf != '':
        buf = mssqlwrapper(fixedstring)[0]
        if str(buf) in descriptions.keys():
            setVar('FNAME', descriptions[str(buf)])
        setVar('RSTATUS', 'DEPTSUCCESS')
        return buf
    else:
        setVar('RSTATUS', 'REQUESTNOTFOUND')
        return "repeat"
result=""

По длинным фразам(более пяти слов) мы сразу обращаемся к FT базе mssql.

if parts == 1:
    mssqlcheck=mssqlwrapper(fixedstring)
    if mssqlcheck!='':
        if mssqlcheck[2]>=80:
            buf=str(mssqlcheck[0])
            if buf in descriptions.keys():
                setVar('FNAME', descriptions[str(buf)])
            setVar('RSTATUS', 'DEPTSUCCESS')
            result=buf
    else:
        result=combinationSearcher(fixedstring,persondicF,0.7)

Если распознанная фраза состоит из одного слова, сперва смотрим FT базу данных, при точности совпадения в 80%, ищем описание распознанного номера, выставляем переменную результата и переменную статуса номерного плана, иначе производим поиск похожих слов в словаре фамилий.

elif parts ==2:
    combs = list(permutations(split, parts))
    for item in combs:
        element = " ".join(item)
        result=combinationSearcher(element,persondicFI,0.7)
        if result!="":
            break

Если фраза состоит из двух слов, полагаем, что это фамилия и имя.
Составляем список комбинаций (иванов иван, иван иванов) и ищем похожие совпадения.

elif parts ==3:
    combs = list(permutations(split, parts))
    for item in combs:
        element = " ".join(item)
        result=combinationSearcher(element,persondic,0.8)
        if result!="":
            break

if result!="":
    return result

В случае получения фразы из трёх слов, полагаем что это ФИО, составляем список комбинаций и ищем в словаре с ФИО. Возвращаем result если переменная не пуста.

buf = mssqlwrapper(recognizedString)

if buf != '':
    buf = mssqlwrapper(recognizedString)[0]
    if str(buf) in descriptions.keys():
        verb('it is')
        setVar('FNAME', descriptions[str(buf)])
    setVar('RSTATUS', 'DEPTSUCCESS')
    return buf
else:
    verb(u'item not found ' + recognizedString)
    #добавляем не найденный запрос
    setVar('RSTATUS', 'REQUESTNOTFOUND')
    return callnumber

В случае, если мы не нашли совпадений, делаем поиск в FT базе данных, в случае нахождения соответствия возвращаем результат и описание, иначе – возвращаем номер по умолчанию и ставим статус о том, что фраза не найдена.

Основное тело программы.

fillDics()
if not WINDEBUG:
    checkSize(infile)

    start_time = timeit.default_timer()
    convert(infile, outfile)
    convert_elapsed = timeit.default_timer() - start_time

    start_time = timeit.default_timer()
    checkstring = sendRecog(outfile).lower()

    recog_elapsed = timeit.default_timer() - start_time

    verb('convert_elapsed = ' + str(recog_elapsed))

    if checkstring == "":
        verb('not recognized. using default.')
        setVar('NUMTOCALL', 'repeat')
        setVar('RSTATUS', 'SILENCE')
        exit(9)
    else:
        setVar('RECREZ', checkstring)

else:
    checkstring = u"проверка"

start_time = timeit.default_timer()
callnumber = getNumByName(checkstring)
search_elapsed = timeit.default_timer() - start_time

if not WINDEBUG:
    setVar('NUMTOCALL', str(callnumber))
    addSessionStat(convert_elapsed, recog_elapsed, search_elapsed, checkstring.lower())

Запускается функция заполнения словарей, если не находимся в отладке программы — заполняем переменные таймеров, проверяем размер файла, конвертируем, отправляем на распознавание, иначе – заполняем переменную checkstring проверочной фразой.
Далее, заполняем переменные таймеров для распознавания, проводим поиск фразы в нашей структуре. И, в случае боевого функционирования, устанавливаем переменную для номерного плана и заносим статистику.

Немного статистики.

Июнь 2018:
успешное распознавание — 1010
молчали в трубку — 78
распознавание не удалось (совпадение не найдено) — 79
короткий запрос — 4
распознанный департамент — 7
среднее время распознавания(среднее время ответа яндекса) — 2.6 сек

Июнь 2017:
успешное распознавание — 18
короткий запрос — 1271
распознавание не удалось (совпадение не найдено) — 127
короткий запрос — 9
молчали в трубку — 71
среднее время распознавания(среднее время ответа яндекса) — 1.5 сек

Данный сервис успешно используется в компании, где я работаю. Надеюсь мои наработки помогут другим людям в реализации своих идей или подобного функционала. Готов ответить на все вопросы.

Автор: nicknathanovich

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js