Голосовой автоинформатор даты и времени, приятным женским голосом, русским языком, на базе asterisk? Легко

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



В этой публикации мы будем делать акцент на падежи порядковых числительных, так как произносимый текст должен быть связан и не резать слух. Попытаемся добиться следующего произношения:
Текущее время пятнадцать часов, двадцать одна минута, двадцать секунд. Сегодня среда, пятнадцатое октября.

или

Текущее время один час, тридцать пять минут, десять секунд. Сегодня четверг, шестнадцатое октября.

Для простоты и прозрачности внедрения мы не будем пользоваться AGI и попросим железную леди сообщать нам дату и время, по большому счету, поработав лишь с dialplan`ом и say.conf`ом. И если ваш asterisk до сих пор не говорит по-русски — не беда, этому мы его научим. Кому стало интересно, добро пожаловать под хабракат.


Который час?


Думаю, не стоит даже упоминать, что ваш сервер должен знать точное время, опираясь, например, на NTP.

В двух словах о настройке NTP
Время на серверах имеет свойство рассинхронизироваться, если его не подводить. Пакет ntp потребуется в любом случае. Для себя я выбрал первый вариант с ntpd, так как другие мои сервера подводят время внутри сети.
#yum -y install ntp

Вариант №1. Добавляем сервера по вкусу, в зависимости от региона, в котором находится asterisk. Чем быстрее отклик от сервера времени, тем точнее оно выставится, при прочих равных. Я выберу российский пул.
#grep "^server" /etc/ntp.conf
server 0.ru.pool.ntp.org
server 1.ru.pool.ntp.org
server 2.ru.pool.ntp.org
server 3.ru.pool.ntp.org

#chkconfig ntpd on
#service ntpd start

Проверяем, что демон слушает нужные порты:
#netstat -putln | grep ntpd

Если есть желание раздавать время своим серверам, не забудьте проверить файервол, порт UDP/123. Правило должно встать до завершающего REJECT`а. Не забудьте сохранить.

#iptables -nL --line-numbers
#iptables -I INPUT 4 -s 10.0.0.0/255.0.0.0 -p udp --dport 123 -j ACCEPT
#service iptables save


Вариант №2. Для разовой корректировки подойдет утилита ntpdate, входящая в тот же пакет ntp. Такой вариант годен, если раздача времени с сервера не планируется.

#ntpdate ru.pool.ntp.org
9 Oct 17:08:41 ntpdate[32744]: adjust time server 85.21.78.8 offset -0.183259 sec

Можно добавить в cron:
#echo -e "\n47 */1  *  *  * root /usr/sbin/ntpdate pool.ntp.org > /dev/null\n" >> /etc/crontab



Обучим леди русскому языку


Далее предположим, что asterisk установлен, но если это не так, то есть многоматериалов на эту тему. Так же будем считать, что первоначальная настройка хотя бы одного SIP-телефона или софтфона уже произведена.

Пакет русских звуков
Для Asterisk 1.4, если добавить languageprefix=yes в asterisk.conf, структура звуковых каталогов будет как в более новых версиях по умолчанию.
Стандартный каталог: /var/lib/asterisk/ в котором подпапки, зависящие от двух «xx» букв ISO кода страны (ru, nl, fr, de, it, pt, es ...)
        sounds/xx
        sounds/xx/digits
        sounds/xx/letters
        sounds/xx/phonetic

Создадим каталог, если его нет
 #mkdir /var/lib/asterisk/sounds/ru/


Загружаем русские звуки:
 #wget -O asterisk-sounds-additional-master.zip https://github.com/pbxware/asterisk-sounds-additional/archive/master.zip
 #wget -O asterisk-sounds-master.zip https://github.com/pbxware/asterisk-sounds/archive/master.zip


Распаковываем:
 #unzip asterisk-sounds-additional-master.zip
 #unzip asterisk-sounds-master.zip


Копируем на свое место:
 #cp -R ./asterisk-sounds-additional-master/* /var/lib/asterisk/sounds/ru/
 #cp -R ./asterisk-sounds-master/* /var/lib/asterisk/sounds/ru/


Посмотреть, какие фразы записаны можно в следующих файлах:
 #less ./asterisk-sounds-additional-master/additional-sounds-ru.txt
 #less ./asterisk-sounds-master/core-sounds-ru.txt



sip.conf


Укажем asterisk`у, использовать русский язык для SIP, добавив language=ru в [general]:
#cat /etc/asterisk/sip.conf
[general]
language=ru

Применяем настройки:
#asterisk -rx "sip reload"

say.conf


#cat /etc/asterisk/say.conf
[ru-base](!)
    _[n]um:0X		=> num:${SAY:1}

    _[n]um:X 		=> digits/${SAY}
    _[n]um:[1-2]f	=> digits/${SAY:0:1}f
    _[n]um:[3-9]f	=> digits/${SAY:0:1}

    ; Tens
    _[n]um:1X		=> digits/${SAY:0:2}
    _[n]um:1Xf		=> digits/${SAY:0:2}

    _[n]um:[2-9]0       => digits/${SAY:0:2}
    _[n]um:[2-9]0f      => digits/${SAY:0:2}

    _[n]um:[2-9][1-2]   => digits/${SAY:0:1}0, num:${SAY:1}
    _[n]um:[2-9][1-2]f  => digits/${SAY:0:1}0, num:${SAY:1}

    _[n]um:[2-9][3-9]   => digits/${SAY:0:1}0, num:${SAY:1}
    _[n]um:[2-9][3-9]f  => digits/${SAY:0:1}0, num:${SAY:1}


    ; Hundreds
    _[n]um:0XX		=>	num:${SAY:1}
    _[n]um:0XXf		=>	num:${SAY:1}

    _[n]um:[1-9]00	=>	digits/${SAY:0:1}00
    _[n]um:[1-9]00f	=>	digits/${SAY:0:1}00

    _[n]um:XXX		=>	num:${SAY:0:1}00, num:${SAY:1}
    _[n]um:XXXf		=>	num:${SAY:0:1}00, num:${SAY:1}


    ; enumeration
    _e[n]um:X			=>	digits/h-${SAY}
    _e[n]um:X[n]		=>	digits/h-${SAY}
    _e[n]um:0X			=>	enum:${SAY:1}
    _e[n]um:0X[n]		=>	enum:${SAY:1}
    _e[n]um:1X			=>	digits/h-${SAY}
    _e[n]um:1X[n]		=>	digits/h-${SAY}
    _e[n]um:[2-9]0		=>	digits/h-${SAY}
    _e[n]um:[2-9]0[n]		=>	digits/h-${SAY}
    _e[n]um:[2-9][1-9]		=>	num:${SAY:0:1}0, digits/h-${SAY:1}
    _e[n]um:[2-9][1-9][n]	=>	num:${SAY:0:1}0, digits/h-${SAY:1}
    _e[n]um:[1-9]00		=>	digits/h-${SAY}
    _e[n]um:[1-9]00[n]		=>	digits/h-${SAY}
    _e[n]um:[1-9]XX		=>	num:${SAY:0:1}00, enum:${SAY:1}
    _e[n]um:[1-9]XX[n]		=>	num:${SAY:0:1}00, enum:${SAY:1}



[ru](ru-base)

    _chas:0		=>	num:${SAY}, digits/hours
    _chas:1		=>	digits/${SAY}, digits/hour
    _chas:[2-4]		=>	num:${SAY}, digits/hours-a
    _chas:[5-9]		=>	num:${SAY}, digits/hours
    _chas:0X		=>	chas:${SAY:1}
    _chas:1X		=>	num:${SAY}, digits/hours
    _chas:20		=>	num:${SAY}, digits/hours
    _chas:2[1-4]	=>	num:${SAY:0:1}0, chas:${SAY:1}

    _mi[n]uta:0		=>	num:${SAY}, digits/minutes
    _mi[n]uta:1		=>	digits/1f, digits/minute
    _mi[n]uta:2		=>	digits/2f, digits/minutes-i
    _mi[n]uta:[3-4]	=>	num:${SAY}, digits/minutes-i
    _mi[n]uta:[5-9]	=>	num:${SAY}, digits/minutes
    _mi[n]uta:0X	=>	minuta:${SAY:1}
    _mi[n]uta:1X	=>	num:${SAY}, digits/minutes
    _mi[n]uta:[2-5]0	=>	num:${SAY}, digits/minutes
    _mi[n]uta:[2-5][1-9]	=>	num:${SAY:0:1}0, minuta:${SAY:1}

    _seku[n]da:0	=>	num:${SAY}, seconds
    _seku[n]da:[5-9]	=>	num:${SAY}, seconds
    _seku[n]da:0X	=>	sekunda:${SAY:1}
    _seku[n]da:1X	=>	num:${SAY}, seconds
    _seku[n]da:[2-5]0	=>	num:${SAY}, seconds

    _dayofweek:[0-6]	=>	digits/day-${SAY} 

    _dayofmo[n]th:X	=>	enum:${SAY}n
    _dayofmo[n]th:XX	=>	enum:${SAY}n

    _mo[n]th:X		=>	digits/mon-$[${SAY} - 1]
    _mo[n]th:XX		=>	digits/mon-$[${SAY} - 1]



Применяем настройки:
#asterisk -rx "module reload app_playback.so"



Контекст [ru-base] в say.conf имеет завершающий (!) восклицательный знак в скобках означает, что это шаблон, который мы в дальнейшем включаем в [ru]

Попробуем разобрать одно правило. Первое, на что стоит обратить внимание, это символы X Z N. Они интерпретируются asterisk`ом как специальные и если эти литеры фигурируют в названии правила чтения, их следует взять в квадратные скобки, например mo[n]th.

_mo[n]th:XX		=>	digits/mon-$[${SAY} - 1]


Синтаксис достаточно прост и правило совпадает, если входные данные XX — две любые цифры. Проигрываем файл digits/mon-(XX-1), где (XX-1) это арифметическая операция. При X=02 (да, «переваривает» даже такие цифры, что нам очень поможет), 02-1=1, digits/mon-1: «Февраля».

Отдельно стоит упомянуть секунды. Во-первых, в записанных фразах есть только единственная запись: «секунд». Это значит, что на вход этой функции должны приходить округленные данные, например 0, 10, 20, и так далее. А во-вторых, по мнению автора, это облегчит восприятие полученной информации.

extensions.conf


#cat /etc/asterisk/extensions.conf
[my_regular_context]  ; Там где живет ваш SIP абонент/телефон.
exten => 100,1,Goto(informer_100,s,1)

[informer_100]
exten => s,1,Set(FreezeEPOCH=$[${EPOCH} + 15]) ; Добавляем 15 секунд к unixtime.
 same => n,Set(TimeNow=${STRFTIME(${FreezeEPOCH},,%Y%m%d%H%M.%S-%w-%j)})
 same => n,Playback(silence/1&at-tone-time-exactly) ;Тишина 1 секунда + Текущее время
 same => n,Playback(chas:${TimeNow:8:2},say) ; десять + часов
 same => n,Playback(minuta:${TimeNow:10:2},say) ; сорок + одна + минута
 same => n,Playback(sekunda:${TimeNow:13:1}0,say) ; двадцать + секунд
 same => n,Playback(silence/1&digits/today) ; тишина 1 секунда + сегодня
 same => n,Playback(dayofweek:${TimeNow:16:1},say) ; четверг
 same => n,Playback(dayofmonth:${TimeNow:6:2},say) ; шестнадцатое
 same => n,Playback(month:${TimeNow:4:2},say) ; октября
 same => n,Playback(silence/1&beep) ; тишина 1 секунда + короткий гудок
 same => n,Hangup()


Применяем настройки:
#asterisk -rx "dialplan reload"


same => n,Set(FreezeEPOCH=$[${EPOCH} + 15])

В контексте [informer_100] стоит объяснить строчку, где мы в переменной FreezeEPOCH добавляем 15 секунд к unixtime. Сделано это для компенсации времени, потраченного на проигрывание файлов, предшествующих секундам.

Далее мы формируем необходимый нам формат даты в переменной TimeNow. Она содержит данные в виде: 201410160043.34-4-289. При чтении мы выдергиваем из «массива» необходимые числа. Они всегда на своих местах и извлечение не составит труда. Более подробно о форматах можно посмотреть в #man strftime.

Про работу с переменными asterisk можно ознакомиться под спойлером
Полный синтаксис переменной ${AnyVariable:x:y}, где x — начальное положение, а y — количество цифр, которое должно быть возвращено. Пусть задана строка:
201410160043.34-4-289

Используя конструкцию ${AnyVariable:x:y}, можно извлечь следующие данные:

${AnyVariable:0:4} — будет возвращена строка 2014. Пропустить ноль символов слева и взять четыре символа.
${AnyVariable:4:8} — будет возвращена строка 10160043.
${AnyVariable:-3:3} — строка будет начинаться с третьего символа, считая с конца и включает три символа, что даст 289.
${AnyVariable:2} — если количество цифр, которое должно быть возвращено, не задано, будет возвращена вся оставшаяся строка, получим 1410160043.34-4-289.


Исходя из say.conf, секунды у нас должны округляться. Из двухзначного формата секунд мы выбираем первую цифру и добавляем к ней ноль: ${TimeNow:13:1}0

Возможно, кто-то из читателей захочет самостоятельно сформировать правила, например, для прочтения рублей. Объема готовых примеров должно быть достаточно, чтобы справиться с этой задачей. А для проверки произношения можно воспользоваться нижеприведенным dialplan`ом.

dialplan для перебора цифр/порядковых номеров/чего-либо
exten => 101,1,Set(Number=0) ; от нуля
	same=>n(start),playback(enum:0${Number}n,say) ; читать enum с предшествующим нулем
	same=>n,Set(Number=$[ ${Number} + 1  ]) ; шаг в единицу
	same=>n,GotoIf($[${Number} <= 9 ]?start) ; до девяти
	same=>n,Hangup()



Бонус: прослушать готовый результат в живую можно по телефону:

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    –2
    Это можно сделать на VoxImplant гораздо быстрее и Астериск не понадобится :)
      0
      Часто такие вещи служат очень хорошим примером для решения аналогичных задач.
      Вместо времени вполне можно сообщать сумму денег на счету абонента, или состояние серверов (привет, голосовой Zabbix|Nagios)

      Или вон котировки какие. Выдача сложных фраз очень интересная задача.
        0
        Именно эту цель я преследовал при написании поста. Создать пример для asterisk. Надеюсь, это многим поможет.
      0
      +74951131111 — «Аппарат абонента не подключен к станции» :)
        0
        Ну так, тег же «информаторы долго не живут» )
          0
          А у меня сработало. Только с ударениями что-то не то немного.
            0
            К сожалению, в этом примере мы работаем с голосовыми записями, которые в данном варианте эксплуатации, немного не совпали по тональности и ударениям. Безусловно, если это требуется, можно записать собственные под каждую конкретную задачу.

            Механизмы для этого есть в самом Asterisk (функция Record()).

            Я предпочитаю пользоваться Audacity. В отличие от записи в asterisk (через трубку телефона), там можно не только записать качественным микрофоном, но и пропустить звуковой файл через фильтры, например, убрав шум и придать голосу вес, тональность или легкое эхо. Если это необходимо раскрыть более подробно, не стесняйтесь обращаться.
          0
          Какой-то это не юникс вей: заставить астериск самостоятельно синтезировать речь (пусть и из горы заготовок-вав-файлов).
          Нельзя ли как-то разнести задачи: пусть астериск вызывает скрипт-на-любимом-языке с параметрами (например, текущим временем), а этот скрипт дёргает TTS-движок или занимается рукоделием.
            0
            Конечно, можно разнести. Но это уже было неплохо освещено в предыдущих статьях.

            В данном посте я хотел показать как можно перенести всю «динамическую составляющую» непосредственно в asterisk и не пользоваться подпрограммами. В редких случаях это может быть затруднительным. А в некоторых случаях создавать дополнительную нагрузку.
            0
            А встроенная функция SayUnixTime?
              +1
              Мне playback показался чуть-чуть гибче в отношении прочтения числительных, отличных от времени, то есть более универсальным и интуитивным.
              Да, точно так же можно настроить SayUnixTime. У меня (A v1.8) по умолчанию эта функция читает не связанный текст на русском языке.
              0
              freeswitch, mod say_ru готовый автоинформатор даты и времени есть в примерах

              Only users with full accounts can post comments. Log in, please.