Berbagai implementasi disajikan di Internet, tetapi, menurut saya, semuanya cukup sederhana. Saya ingin menyajikan versi panduan suara saya untuk tanda bintang.
Catatan: Saya bukan programmer profesional, dan mungkin beberapa solusi tampak liar bagi Anda. Beberapa trik mungkin sudah ketinggalan zaman. Saya siap menerima kritik dan memperbaiki sistem menjadi lebih baik.
Deskripsi singkat tentang fitur:
Pengguna memasuki IVR, membuat permintaannya dan, dalam kebanyakan kasus, sampai ke tempat ia perlu. Statistik juga kacau ke sistem dengan entri di tabel mysql.
Secara singkat tentang perusahaan dan jaringan tempat sistem ini digunakan:
~ 1000 telepon, sekitar 50 departemen
Produk perangkat lunak yang digunakan oleh sistem:
- Asterisk 13.10
- YandexSpeechKit
- python 2.6.6
- Mysql, MSSQL
- sox 14.2.0
- curl 7.19.7
- lumpuh 3.99.5
Deskripsi dialplan dalam tanda bintang.
[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)
Dalam fragmen ini, makro diluncurkan untuk memeriksa apakah pemanggil telah menutup telepon setelah mendengar pesan ucapan. Berikutnya adalah mengatur nilai variabel:
ITERASI - diperlukan untuk mengulangi proses pengakuan untuk beberapa kali. HANGFLAG - variabel ini digunakan oleh makro 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)
Atur variabel file rekaman, tulis file. Kami menjalankan skrip agi yang bertanggung jawab untuk mengirim file untuk pengakuan dan pencarian nomor (skrip akan dijelaskan nanti), memeriksa variabel NUMTOCALL (nilainya diatur oleh skrip), mengatur tanda HANGFLAG, yang berarti bahwa orang tersebut tidak menutup telepon sebelumnya.
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)
Dalam fragmen ini, makro dijalankan untuk memeriksa apakah penelepon menutup setelah mendengar pesan sambutan, maka variabel diatur, ITERASI diperlukan untuk mengulangi proses pengenalan beberapa kali. HANGFLAG - variabel ini digunakan oleh makro 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)
Atur variabel file rekaman, tulis file. Kami mulai skrip yang bertanggung jawab untuk mengirim file untuk pengakuan dan mencari nomor (deskripsi skrip akan di bawah), memeriksa variabel NUMTOCALL (nilai ditetapkan oleh skrip), mengatur tanda HANGFLAG bahwa orang tersebut tidak menutup telepon sebelumnya.
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)
Menjalankan makro vrstat (bertanggung jawab untuk statistik, tidak akan dijelaskan karena sifatnya yang sepele). Periksa apakah deskripsi FNAME (variabel diatur oleh pyreq8.py) dari permintaan. Jika ada deskripsi, setel nama file cache dalam variabel dan periksa keberadaannya. Jika file tidak ada, kami mensintesisnya, mengonversinya menjadi mp3, menambah volume dan kemudian (atau jika ada cache), mainkan dan panggil pelanggan.
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)
Ulangi pengenalan. Jika jumlah iterasi melebihi yang ditentukan, maka kami menerjemahkan menjadi sekretaris.
same => n(secretary),Macro(VRstat,${CALLERID(num)},${NUMTOCALL},${RSTATUS},${CHANNEL},${RECREZ}) same => n,Set(HANGFLAG=FALSE) same => n,Dial(Local/1000@common-context)
Pindah ke sekretaris. Kami memasukkan statistik, mengatur bendera yang tidak ditutup. Kami memanggil sekretaris.
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()
Panggil pelanggan yang tidak memiliki deskripsi di direktori. Kami kehilangan pesan, memasukkannya ke dalam statistik, menyebutnya.
exten => h,1,Gotoif($["${HANGFLAG}"="TRUE"]?exec:noop) same => n(exec),Macro(VRstat,${CALLERID(num)},x,HANGER,${CHANNEL}) same => n(noop),Noop('exiting')
Menangani penghentian panggilan agar makro hangercheck berfungsi.
Deskripsi Dialplan. Makro.
[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)
Kami memeriksa dalam database statistik apakah nomor yang diberikan disebut 48 jam terakhir, dan jika dia menutup telepon tanpa menunggu pengakuan selesai, kami memasukkannya ke dalam statistik dan menghubungkannya ke sekretaris.
Deskripsi singkat tentang tabel sql dan mysql yang digunakan.
Tabel sql dbo.phrases.
id(PK, int), phrase (text), number(varchar(50))
Pencarian teks lengkap telah dimunculkan untuk tabel ini, digunakan untuk beralih dengan kata kunci saat menerima frasa panjang. Misalnya, frasa: "sambungkan saya dengan perwakilan dari departemen periklanan," jika ada entri dalam database di mana frase = "departemen periklanan," maka penelepon akan terhubung ke nomor yang sesuai.
Tabel Mysql recogStats.
id(PK, int), date (datetime), ctime(float), rtime(float),stime(float), phrase(varchar(60))
Tabel ini digunakan untuk menyimpan hasil pengakuan dan mengumpulkan statistik pada waktu pengakuan. waktu yang dihabiskan untuk mengkonversi file audio, waktu yang dihabiskan untuk mengunduh dan mengenali, waktu yang dihabiskan untuk mencari
Tabel 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))
Tabel ini digunakan untuk melacak hasil pengakuan (RECREZ, rstatus), yang ditelepon pemanggil sesuai dengan hasil pencarian untuk frasa yang dikenali (dipanggil), untuk melacak berapa banyak waktu yang dihabiskan seseorang dalam menu pengenalan (durasi) dan untuk debugging (channame)
Tabel CustomRequests Mysql.
id(PK, int), nomer(int(10)), request(varchar(100))
Direktori bantu kelompok dan departemen tambahan.
Tabel numdescription Mysql.
Num(text), name(text)
Direktori dengan deskripsi nomor yang dimasukkan untuk akting suara.
Tabel Mysql spravochnik_rus dan spravochnik_rus_name_num_no_tops
nomer(varchar(11)), fio(varchar(100))
Referensi orang dengan angka. Yang pertama selesai, untuk penggunaan internal. Yang kedua, untuk penggunaan eksternal, jumlah direktur dan manajer dibungkus dalam sekretariat.
Skrip AGI yang mengonversi, mengirim, dan mencari frasa yang dikenal.
Perpustakaan yang digunakan:
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
Dan juga pustaka asterisk.agi , yang kami impor tergantung pada debugging (debugging dilakukan pada mesin windows).
Deskripsi variabel.
WINDEBUG = False
Bendera debug
_digits = re.compile('\d')
Menyusun variabel ekspresi reguler ke angka.
uniqid = str(uuid.uuid1()).replace('-', '')
Variabel pengidentifikasi unik.
dkey = '12345678-9101-1121-3141-51617181920'
Kunci API kit ucapan bahasa Yandex.
lang = 'ru-RU'
Opsi bahasa untuk bahasa Yandex.
topic = 'queries'
Opsi tema pengakuan untuk speechkit Yandex.
callnumber = '222'
Nomor panggilan default.
setVar('NUMTOCALL', callnumber)
Mengatur nomor panggilan standar jika terjadi kesalahan.
persondic = dict()
Nama Kamus - nomor.
persondicFI=dict()
Kamus FI - Angka.
persondicF=dict()
Kamus Nama Belakang - Nomor.
otherdic = dict()
Kamus dengan entri lain dari formulir Catatan - nomor.
descriptions = dict()
Kamus dengan deskripsi untuk generasi akting suara, ketik Rekam - nomor.
duplicates = list()
Daftar layanan untuk penyaringan diperlukan.
nums = list()
Daftar dengan semua ekstensi.
outfile = '/tmp/' + uniqid + '-pcm.wav'
File audio keluaran sementara variabel.
mysqlhost='myhost' mysqlpass='mypass' mysqluser='myuser' mysqldb='mydb' mssqlhost='mshost' mssqlpass='mspass' mssqluser='msuser' mssqldb='msdb'
Detail untuk menghubungkan ke mysql dan 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 = ''
Impor perpustakaan, tetapkan variabel untuk nama file audio input dan jumlah pemanggil, tergantung pada debugging.
Deskripsi Fitur
def verb(s): if not WINDEBUG: agi.verbose(s) else: print s
Bergantung pada nilai variabel debugging, kami menampilkan pesan di asterisk console atau di stdout.
def setVar(varname, varval): if not WINDEBUG: agi.set_variable(varname, varval) else: print "setting var " + varname + " with value " + varval
Bergantung pada nilai variabel debug, kami menetapkan nilai variabel dialplan ke asterisk atau menampilkannya di stdout.
def contains_digits(s): verb('enter contains_digits') return bool(_digits.search(s))
Periksa apakah s berisi digit.
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")
Kami hanya mengembalikan angka dari s.
def check_dob(num): if int(num) in nums: verb('checkdob success') return True else: verb('checkdob fail' + num) return False
Periksa keberadaan ekstensi dalam daftar nomor.
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"
Fungsi pengaturan ekstensi.
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)
Memeriksa file rekaman audio yang diterima untuk pengakuan, jika ukurannya terlalu kecil, kami menganggap bahwa mereka diam di handset.
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()
Merekam fungsi pengenalan statistik.
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():
Fungsi ini mengisi kamus Nama Belakang (persondicF), Nama Belakang (persondicFI), Nama (persondic), nama departemen (otherdic), deskripsi untuk sintesis pidato (deskripsi) dan semua nomor perusahaan (num).
Tergantung pada panjang nomor penelepon, sebuah tabel diambil yang berisi jumlah direktur (penelepon internal) atau tidak mengandung (penelepon eksternal).
Direktori nama keluarga diuji keunikannya untuk mengecualikan keberadaan dua Ivanovs dengan nomor yang berbeda di dalamnya.
def convert(infile, outfile):
Fungsi mengkonversi file yang direkam dalam tanda bintang, jika sox memberikan kesalahan, atur nomor default dan keluar dari skrip.
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 ""
Fungsi mengirim file untuk pengakuan dan menerima respons dari Yandex. Jika pengakuan berhasil, kami mengambil jawaban pertama. Saya tidak bisa melakukan ini dengan pustaka curl, untuk alasan ini opsi ini digunakan.
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
Fungsi mencari nama lengkap, tergantung apakah hanya nama belakang atau nama belakang dan nama depan yang masuk.
def combinationSearcher(phrase,pdic,quality): verb(u'searching in persons') result = difflib.get_close_matches(phrase, pdic.keys(), 1, quality)
Fungsi pencarian kombinasi, contoh: frase = "Alexey Petr", pdic berisi entri "Alexeyev Peter", fungsi get_close_matches akan mengembalikan "Alexeyev Peter". Fungsi ini dapat menghasilkan hasil yang salah, tetapi, seperti yang telah ditunjukkan oleh praktik, ada respons yang jauh lebih benar. Parameter kualitas memungkinkan Anda menentukan keakuratan pencarian frasa serupa.
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')
Fungsi utama menemukan angka dengan frase.
Mari kita menganalisis fungsi ini di beberapa bagian.
if u'' in recognizedString: verb('enter dobavochn') return set_dob(recognizedString)
Contoh: frasa "Tolong, ekstensi 1234" akan mengembalikan nomor ekstensi (jika ada).
elif len(recognizedString.replace(" ", "")) == 4: verb('enter num say') return set_dob(recognizedString)
Jika pengguna telah mengucapkan nomor empat digit, kembalikan ekstensi yang sesuai (jika ada).
if len(recognizedString) <= 5 and recognizedString.lower() not in [u"",u"",u""]: setVar('RSTATUS', 'SHORT') return "repeat"
Agar tidak menjalankan program yang tidak perlu dalam frasa pendek (ini bisa terjadi ketika Yandex mendengar dan mengenali bagian dari percakapan, ketika penelepon tidak mendengarkan pesan pengakuan).
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=""
Untuk frasa yang panjang (lebih dari lima kata), kami segera beralih ke basis 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)
Jika frasa yang dikenali terdiri dari satu kata, pertama-tama kita melihat pada basis data FT, dengan akurasi 80%, mencari deskripsi nomor yang dikenali, mengatur variabel hasil, dan status rencana, variabel rencana nomor, jika tidak, kita mencari kata-kata serupa di kamus nama keluarga.
elif parts ==2: combs = list(permutations(split, parts)) for item in combs: element = " ".join(item) result=combinationSearcher(element,persondicFI,0.7) if result!="": break
Jika frasa terdiri dari dua kata, kami menganggap bahwa itu adalah nama keluarga dan nama.
Kami membuat daftar kombinasi (Ivan Ivan, Ivan Ivan) dan mencari kecocokan serupa.
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
Dalam hal frasa tiga kata, kita asumsikan bahwa itu adalah nama lengkap, buat daftar kombinasi dan lihat di kamus dengan nama lengkap. Kami mengembalikan hasil jika variabel tidak kosong.
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)
Jika kami tidak menemukan kecocokan, kami melakukan pencarian di basis data FT, jika kami menemukan kecocokan, kami mengembalikan hasil dan deskripsi, jika tidak kami mengembalikan nomor default dan mengatur status bahwa frasa tidak ditemukan.
Bagian utama dari program.
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())
Fungsi mengisi kamus diluncurkan, jika kita tidak berada dalam program debugging - kita mengisi variabel pengatur waktu, memeriksa ukuran file, mengonversi, mengirimkannya untuk pengakuan, jika tidak - mengisi variabel checkstring dengan frase cek.
Selanjutnya, kita mengisi variabel timer untuk pengakuan, kita mencari frase dalam struktur kita. Dan, dalam hal fungsi tempur, kami menetapkan variabel untuk rencana penomoran dan memasukkan statistik.
Beberapa statistik.
Juni 2018:
pengakuan sukses - 1010
diam dalam tabung - 78
pengakuan gagal (tidak ditemukan kecocokan) - 79
permintaan pendek - 4
departemen yang diakui - 7
waktu pengenalan rata-rata (waktu respons Yandex rata-rata) - 2,6 detik
Juni 2017:
pengakuan sukses - 1271
departemen yang diakui - 18
pengakuan gagal (tidak ditemukan kecocokan) - 127
permintaan pendek - 9
diam dalam tabung - 71
waktu pengenalan rata-rata (waktu respons Yandex rata-rata) - 1,5 detik
Layanan ini berhasil digunakan di perusahaan tempat saya bekerja. Saya berharap prestasi saya akan membantu orang lain dalam mengimplementasikan ide-ide mereka atau fungsi serupa. Siap menjawab semua pertanyaan.