/ #linux 

IP-телефонія на базі Asterisk. Частина 4: Голосове меню

Цикл статей по розгортанню IP-телефонії на базі Asterisk. Фактично це моя проектна робота на отримання відповідного сертифікату одного з навчальних центрів. Робота була захищена, сертифікат отриманий, все написане тестувалось і є повтінстю робочою реалізацією. Інформація в статті і версії програмного забезбечення актуальні на 2017 рік.

4. Голосове меню

4.1 Створення багаторівневого голосового меню

Створюємо голосове меню з 3 рівнями:

  1. Головне меню - вітання, пропозиція набрати додатковий номер для дозвону в конкретний відділ компанії (1 - відділ продажів, 2 - відділ закупівель, 3 - питання доставки, 4 - з’єднання з секретарем, 5 - залишити голосове повідомлення, 0 - повторити меню).
    1. Меню відділу продажів - пропозиція набрати додатковий номер для з’єднання з фахівцем з конкретної товарної групи (1 - питання і консультації по товарній групі 1, 2 - питання і консультації по товарній групі 2, 3 - з’єднання з оператором, 0 - повторити меню, 9 - повернутися до попереднього меню).
    2. Меню відділу закупівель - пропозиція набрати додатковий номер для з’єднання з фахівцем із закупівель (1 - з’єднання з персональним менеджером, 2 - пропозиції щодо співпраці, 0 - повторити меню, 9 - повернутися до попереднього меню).
      1. Меню вибору додаткового номера менеджера із закупівель (1 - з’єднання з менеджером 1, 2 - з’єднання з менеджером 2, 0 - повторити меню, 9 - повернутися до попереднього меню, 8 - перейти в головне меню).
    3. Меню для питань пов’язаних з доставкою - пропозиція набрати додатковий номер для уточнення питань комплектації і доставки замовлень (1 - дізнатися статус замовлення за номером, 2 - з’єднання з оператором, 0 - повторити меню, 9 - повернутися до попереднього меню).

Описуємо вищевказане меню:

; головне меню
[ivr-mainmenu] 
exten => s,1,Answer() 
exten => s,2,Background(ivr/welcome) ; вітання
exten => s,3,GotoIfTime(18:00-9:00|mon-fri|*|*?ivr-holiday,s,1)
exten => s,4,GotoIfTime(*|sat-sun|*|*?ivr-holiday,s,1)
exten => s,5,Background(ivr/mainmenu) ; пропозиція набрати додатковий номер для дозвону в конкретний відділ компанії (1 - відділ продажів, 2 - відділ закупівель, 3 - питання доставки, 4 - з'єднання з секретарем, 5 - залишити голосове повідомлення, 0 - повторити меню)
exten => s,6,Background(ivr/make-a-choice) ; зробіть свій вибір 
; чекаємо протягом 5 секунд
exten => s,7,WaitExten(5) 
; якщо абонент обрав 1 - переходимо в меню відділу продажів
exten => 1,1,Goto(ivr-sales,s,1) 
; якщо абонент обрав 2 - переходимо в меню відділу закупівель
exten => 2,1,Goto(ivr-buying-1,s,1)  
; якщо абонент обрав 3 - переходимо в меню відділу доставки
exten => 3,1,Goto(ivr-warehouses,s,1)  
; якщо абонент обрав 4 - з'єднуємо з секретарем
exten => 4,1,Dial(SIP/601)
; якщо абонент обрав 5 - відправляємо на голосову пошту
exten => 5,1,Voicemail(601 @default)
; якщо абонент обрав 0 - повторюємо меню
exten => 0,1,Goto(s,5)  
; якщо обрано невірний номер - повторюємо меню
exten => i,1,Goto(s,5)  
; по таймауту пропонуємо ще раз зробити вибір
exten => t,1,Goto(s,6)  

; сценарій для відповіді абоненту в не робочий час
[ivr-holiday]
exten => s,1,Playback(ivr/holiday) ; нажаль Ви подзвонили в не робочий час, залиште нам голосове повідомлення
exten => s,2,Goto(ivr-mainmenu,5,1)

; меню відділу продажів
[ivr-sales] 
exten => s,1,Background(ivr/salesmenu) ; пропозиція набрати додатковий номер для з'єднання з фахівцем з конкретної товарної групи (1 - питання і консультації по товарній групі 1, 2 - питання і консультації по товарній групі 2, 3 - з'єднання з оператором, 0 - повторити меню, 9 - повернутися до попереднього меню) 
exten => s,2,Background(ivr/make-a-choice) ; зробіть свій вибір
; чекаємо протягом 5 секунд
exten => s,3,WaitExten(5) 
; якщо абонент обрав 1 - з'єднуємо менеджерами, що відповідають за товарну групу 1
exten => 1,1,Dial(SIP/201&SIP/202,20)
exten => 1,n,Hangup()
; якщо абонент обрав 2 - з'єднуємо менеджерами, що відповідають за товарну групу 2
exten => 2,1,Dial(SIP/203&SIP/204,20)
exten => 2,n,Hangup()
; якщо абонент обрав 3 - з'єднуємо з оператором
exten => 3,1,Dial(SIP/201&SIP/202&SIP/203&SIP/204,20)
exten => 3,n,Hangup()

; якщо абонент обрав 0 - повторюємо меню
exten => 0,1,Goto(s,1) 
; якщо абонент обрав 9 - переходимо до батьківського меню
exten => 9,1,Goto(ivr-mainmenu,s,5) 
; якщо обрано невірний номер - повторюємо меню
exten => i,1,Goto(s,1)  
; по таймауту пропонуємо ще раз зробити вибір
exten => t,1,Goto(s,2)  

; меню відділу закупівель 
[ivr-buying-1] 
exten => s,1,Background(ivr/buyingmenu-1) ; пропозиція набрати додатковий номер для з'єднання з фахівцем із закупівель (1 - з'єднання з персональним менеджером, 2 - пропозиції щодо співпраці, 0 - повторити меню, 9 - повернутися до попереднього меню) 
exten => s,2,Background(ivr/make-a-choice) ; зробіть свій вибір
; чекаємо протягом 5 секунд
exten => s,3,WaitExten(5) 
; якщо абонент обрав 1 - з'єднуємо з персональним менеджером, переводимо на меню с вибором персонального менеджера
exten => 1,1,Goto(ivr-buying-2,s,1)
; якщо абонент обрав 2 - з'єднуємо с менеджером для обговорення умов співробітництва
exten => 2,1,Dial(SIP/301&SIP/302,20)
exten => 2,n,Hangup()
; якщо абонент обрав 0 - повторюємо меню
exten => 0,1,Goto(s,1) 
; якщо абонент обрав 9 - переходимо до батьківського меню
exten => 9,1,Goto(ivr-mainmenu,s,5) 
; якщо обрано невірний номер - повторюємо меню
exten => i,1,Goto(s,1)  
; по таймауту пропонуємо ще раз зробити вибір
exten => t,1,Goto(s,2)

; меню з вибором персонального менеджера 
[ivr-buying-2] 
exten => s,1,Background(ivr/buyingmenu-2) ; пропозиція ввести 3-х значний номер персонального менеджера, 0 - повторити меню, 9 - повернутися до попереднього меню, 8 - перейти в головне меню
exten => s,2,Background(ivr/make-a-choice) ; зробіть свій вибір
; чекаємо протягом 5 секунд
exten => s,3,WaitExten(5) 
exten => _3XX,1,Dial(SIP/${EXTEN},20)
exten => _3XX,n,Hangup()
; якщо абонент обрав 0 - повторюємо меню
exten => 0,1,Goto(s,1) 
; якщо абонент обрав 9 - переходимо до батьківського меню
exten => 9,1,Goto(ivr-buying-1,s,3) 
; якщо обрано невірний номер - повторюємо меню
exten => i,1,Goto(s,1)  
; по таймауту пропонуємо ще раз зробити вибір
exten => t,1,Goto(s,2)

; меню для питань пов'язаних з доставкою 
[ivr-warehouses] 
exten => s,1,Background(ivr/warehousemenu) ; пропозиція набрати додатковий номер для уточнення питань комплектації і доставки замовлень (1 - дізнатися статус замовлення за номером, 2 - з'єднання з оператором, 0 - повторити меню, 9 - повернутися до попереднього меню) 
exten => s,2,Background(ivr/make-a-choice) ; зробіть свій вибір
; чекаємо протягом 5 секунд
exten => s,3,WaitExten(5) 
; якщо абонент обрав 1 - пропонуємо ввести номер замовлення і повідомляємо його статус
exten => 1,1,Background(ivr/input-order-number) ; введіть номер замовлення
exten => 1,n,WaitExten(30)
; виводимо інформацію про замовлення в змінну ORDERSTATUS, використовуючи номер замовлення і номер абонента, інформацію дістаємо з БД скриптом на PHP
exten => 1,n,AGI(check_status.php, ${CALLERID(number)}, ${EXTEN})
exten => 1,n,Festival(${ORDERSTATUS},any)
exten => 1,n,Hangup()
; якщо абонент обрав 2 - з'єднуємо з оператором складу
exten => 2,1,Dial(SIP/401&SIP/402,20)
exten => 2,n,Hangup()
; якщо абонент обрав 0 - повторюємо меню
exten => 0,1,Goto(s,1) 
; якщо абонент обрав 9 - переходимо до батьківського меню
exten => 9,1,Goto(ivr-mainmenu,s,5) 
; якщо обрано невірний номер - повторюємо меню
exten => i,1,Goto(s,1)  
; по таймауту пропонуємо ще раз зробити вибір
exten => t,1,Goto(s,2)

Змінюємо контекст вхідних дзвінків із зовнішніх ліній (умовно на номер YYYYYY):

;вхідні звінки із зовнішніх ліній
[call-in]
exten => YYYYYY,1,Set(RECORDING=1)
exten => YYYYYY,1,Macro(recording,${CALLERID(num)},${EXTEN})
exten => YYYYYY,n,Goto(ivr-mainmenu,s,1) ; всі вхідні дзвінки адресуються на голосове меню

Файли для голосового меню зберігаємо в директорію /var/lib/asterisk/sounds/en/ivr

[root@asterisk-centos en]# ls -l /var/lib/asterisk/sounds/en/ivr
итого 1156
-rw-r--r-- 1 asterisk asterisk 171404 Май  7 10:56 buyingmenu-1.wav
-rw-r--r-- 1 asterisk asterisk 158924 Май  7 10:57 buyingmenu-2.wav
-rw-r--r-- 1 asterisk asterisk 105164 Май  7 10:54 holiday.wav
-rw-r--r-- 1 asterisk asterisk  42124 Май  7 11:00 input-order-number.wav
-rw-r--r-- 1 asterisk asterisk 208524 Май  7 10:52 mainmenu.wav
-rw-r--r-- 1 asterisk asterisk  40204 Май  7 10:53 make-a-choice.wav
-rw-r--r-- 1 asterisk asterisk 239564 Май  7 10:55 salesmenu.wav
-rw-r--r-- 1 asterisk asterisk 175244 Май  7 10:58 warehousemenu.wav
-rw-r--r-- 1 asterisk asterisk  30284 Май  7 10:51 welcome.wav

Перевіряємо работу голосового меню:

asterisk-centos*CLI> 
  == Using SIP RTP CoS mark 5
    -- Executing [YYYYYY@call-in:1] Set("SIP/zadarma-00000000", "RECORDING=1") in new stack
    -- Executing [YYYYYY@call-in:2] Goto("SIP/zadarma-00000000", "ivr-mainmenu,s,1") in new stack
    -- Goto (ivr-mainmenu,s,1)
    -- Executing [s@ivr-mainmenu:1] Answer("SIP/zadarma-00000000", "") in new stack
    -- Executing [s@ivr-mainmenu:2] BackGround("SIP/zadarma-00000000", "ivr/welcome") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/welcome.slin' (language 'ru')
       > 0xa2bd990 -- Probation passed - setting RTP source address to 185.45.152.162:14410
    -- Executing [s@ivr-mainmenu:3] GotoIfTime("SIP/zadarma-00000000", "18:00-9:00|mon-fri|*|*?ivr-holiday,s,1") in new stack
    -- Executing [s@ivr-mainmenu:4] GotoIfTime("SIP/zadarma-00000000", "*|sat-sun|*|*?ivr-holiday,s,1") in new stack
    -- Executing [s@ivr-mainmenu:5] BackGround("SIP/zadarma-00000000", "ivr/mainmenu") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/mainmenu.slin' (language 'ru')
    -- Executing [1@ivr-mainmenu:1] Goto("SIP/zadarma-00000000", "ivr-sales,s,1") in new stack
    -- Goto (ivr-sales,s,1)
    -- Executing [s@ivr-sales:1] BackGround("SIP/zadarma-00000000", "ivr/salesmenu") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/salesmenu.slin' (language 'ru')
    -- Executing [s@ivr-sales:2] BackGround("SIP/zadarma-00000000", "ivr/make-a-choice") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/make-a-choice.slin' (language 'ru')
    -- Executing [9@ivr-sales:1] Goto("SIP/zadarma-00000000", "ivr-mainmenu,s,5") in new stack
    -- Goto (ivr-mainmenu,s,5)
    -- Executing [s@ivr-mainmenu:5] BackGround("SIP/zadarma-00000000", "ivr/mainmenu") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/mainmenu.slin' (language 'ru')
    -- Executing [s@ivr-mainmenu:6] BackGround("SIP/zadarma-00000000", "ivr/make-a-choice") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/make-a-choice.slin' (language 'ru')
    -- Executing [s@ivr-mainmenu:7] WaitExten("SIP/zadarma-00000000", "5") in new stack
    -- Timeout on SIP/zadarma-00000000, going to 't'
    -- Executing [t@ivr-mainmenu:1] Goto("SIP/zadarma-00000000", "s,6") in new stack
    -- Goto (ivr-mainmenu,s,6)
    -- Executing [s@ivr-mainmenu:6] BackGround("SIP/zadarma-00000000", "ivr/make-a-choice") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/make-a-choice.slin' (language 'ru')
    -- Executing [s@ivr-mainmenu:7] WaitExten("SIP/zadarma-00000000", "5") in new stack
    -- Timeout on SIP/zadarma-00000000, going to 't'
    -- Executing [t@ivr-mainmenu:1] Goto("SIP/zadarma-00000000", "s,6") in new stack
    -- Goto (ivr-mainmenu,s,6)
    -- Executing [s@ivr-mainmenu:6] BackGround("SIP/zadarma-00000000", "ivr/make-a-choice") in new stack
    -- <SIP/zadarma-00000000> Playing 'ivr/make-a-choice.slin' (language 'ru')
  == Spawn extension (ivr-mainmenu, s, 6) exited non-zero on 'SIP/zadarma-00000000'

4.2 Налаштування голосовї пошти

Встановимо пакет ssmtp для відправлення повідомлень на електронну пошту:

yum install http://dl.fedoraproject.org/pub/fedora/linux/releases/25/Everything/i386/os/Packages/s/ssmtp-2.64-16.fc24.i686.rpm

Налаштовуємо відправлення через обліковий запис на pdd.yandex.ru. (!!! Ніякої “зради”, на момент виконання проекту яндекс ще не був заблокований, тому пишу все як було, принципово налаштування для інших поштових сервісів будуть мало чим відрізнятись !!!) Змінюємо файл /etc/ssmtp/ssmtp.conf:

mailhub=smtp.yandex.ru:465
UseTLS=YES
AuthUser=login@my-company.com.ua
AuthPass=password 
rewriteDomain=my-company.com.ua
hostname=localhost
FromLineOverride=NO
Root=login@my-company.com.ua
TLS_CA_File=/etc/pki/tls/certs/ca-bundle.crt

В файл /etc/ssmtp/revaliases додаємо рядок:

root:login@my-company.com.ua:smtp.yandex.ru:465

В файлі /etc/asterisk/voicemail.conf налаштовуємо голосову пошту в Asterisk:

[general] 
format = wav 
attach = yes 
delete = yes 
maxmsg = 1000 
envelope = yes 
skipms = 3000 
maxsilence = 10 
silencethreshold = 128 
maxlogins = 3 
emaildateformat = %A, %B %d, %Y at %r  
mailcmd=/usr/sbin/ssmtp -t 
fromstring=VoiceMail

[default] 
601 => 123456,office,login@my-company.com.ua,attach=yes

Для доступу до голосової пошти додаємо в контекст внутрішніх ліній наступний рядок:

exten => 999,1,VoiceMailMain()

4.3 Налаштування виводу голосових повідомлень (Festival)

Для програвання тексту встановимо пакет Festival:

cd /usr/src 
wget http://www.cstr.ed.ac.uk/downloads/festival/2.4/speech_tools-2.4-release.tar.gz
wget http://www.cstr.ed.ac.uk/downloads/festival/2.4/festival-2.4-release.tar.gz
tar zxvf festival-2.4-release.tar.gz
tar zxvf speech_tools-2.4-release.tar.gz
cd speech_tools 
./configure 
make && make install 
cd .. 
cd festival 
./configure  
make && make install

Додаємо в PATH шлях до бінарного файла festival:

export PATH=$PATH:/usr/src/festival/bin/

Встановлюємо мовні файли:

mkdir /usr/src/festival/lib/voices/ 
mkdir /usr/src/festival/lib/voices/russian/
cd /usr/src/ 
wget http://sourceforge.net/projects/festlang.berlios/files/msu_ru_nsh_clunits-0.5.tar.bz2 
tar xjfv msu_ru_nsh_clunits-0.5.tar.bz2 
mv /usr/src/msu_ru_nsh_clunits/ /usr/src/festival/lib/voices/russian

Далі додаємо на початок файлу /usr/src/festival/lib/languages.scm

(define (language_russian)  
"(language_russian)   
Set up language parameters for Russian."   
(set! male1 voice_msu_ru_nsh_clunits)   
(male1)   
(Parameter.set 'Language 'russian) 
)

додаємо перед

((equal? language 'americanenglish)
    (language_american_english))

наступний код

((equal? language 'russian)
    (language_russian))      

Встановлюємо мову на замовчування в файлі /usr/src/festival/lib/siteinit.scm

(set! voice_default 'voice_msu_ru_nsh_clunits)

Конфігураційний файл /etc/asterisk/festival.conf:

[general] 
host=localhost 
port=1314 
usecache=yes 
cachedir=/var/lib/asterisk/festivalcache/ 
festivalcommand=(tts_textasterisk "%s" 'file)(quit)\n

Для зберігання кешу створюємо відповідну папку

mkdir /var/lib/asterisk/festivalcache/
chown asterisk. /var/lib/asterisk/festivalcache/

Запускаємо сервер Festival

/usr/src/festival/bin/festival --server

Скрипт checkstatus.php для перевірки статуса заказу може виглядатити так:

<?php
$conn = mysql_connect("localhost", "mysql_user", "mysql_password");
mysql_select_db("myshop");
//пошук статуса за номером телефону клієнта і номером замовлення
$sql = "SELECT status FROM orders WHERE phone = ".$argv[1]." AND order_id = ".$argv[2];
$result = mysql_query($sql);
$row = mysql_fetch_assoc($result);
echo 'SET VARIABLE ORDERSTATUS '.$row["status"];
?>

В наступній статті навчимося зберігати статистику в MySQL.

Author

Олександр Бобилєв

Залишаю собі право використовувати ненормативну (але інформативну) лексику там, де звичайні слова втрачають сенс і не відображають всієї палітри почуттів, від споглядання навколишньої дійсності.