В прошлой статье мы смогли добиться получения изображения с наших веб-камер в виде снимков раз в секунду. Теперь пришла пора взяться за обещанное — распознавание и синтез голоса.
Начиная с этой статьи я начну описывать свое ПО, которое занимается координированием всех подсистем «умного дома». Считаю необходимым отметить, что уже ушел достаточно далеко от описываемого в этой статье кода, с более новыми и функциональными версиями можно ознакомиться через trac — ссылка. Распространение осуществляется под лицензией GNU GPLv3. Если кто-то пожелает присоединиться к разработке — милости прошу ;)
Как я уже писал в первой статье, для синтеза и распознавания голоса мы воспользуемся сервисами компании Google. Я думаю, многие сталкивались на мобильных устройствах под управлением ОС Android с голосовым поиском. Как дополнительная функцию, этот самый голосовой поиск был добавлен в браузер Google Chrome. Следует заметить, что официального API для этого сервиса компания еще не анонсировала, но благодаря открытым исходникам Chrome, народные умельцы нашли, что и куда посылается и что и как отдается в ответ. Выглядит это так:
Ответ представляет собой нечто вида:
Нас интересуют в ответе лишь два последних поля — utterance и confidence. Первое является искомой распознанной словом/фразой, второе — достоверностью распознавания. Если confidence будет более 0.5, можно считать, что распознавание достоверно.
Синтез речи будет так же осуществляться через сервис Google и к нему так же, насколько я знаю, не анонсировано официального API. Чтобы получить звуковую фразу из текста нужно произвести совсем не сложную комбинацию действий:
Как видите, тут все совсем не сложно. Теперь реализуем эту информацию программно.
Как я уже писал, заниматься централизованным управлением нашего «умного дома» будет специально написанный демон на perl. Заранее прошу за качество кода не бить ногами, ибо ваш покорный слуга всего лишь сисадмин :)
Итак, определимся с кругом задач, которое должно выполнять данное ПО:
Возможно, я что-то забыл или пропустил, но, как мне кажется, это основные задачи ПО «умный дом». Теперь начнем реализовывать все это.
Для создания на perl TCP/IP-демона воспользуемся модулем Net::Server::Fork. Я буду исходить из предположения, что язык perl вам уже знаком.
Чем же занимается функция toText()? Да собственно, распознаванием речи!
Теперь поговорим, откуда же появляются загадочные файлы в flac в директории data. Тут все просто — этим занимается отдельный скрипт:
Команда rec делает короткие 4х секундные записи с рандомным числом в имени, которые пережимаются программой flac. После этого происходит соединение к нашему главному демону и передается команда text тот_самый_рандомный_номер. Для чего же я пишу 4х секундные короткие записи? Все дело в том, как компьютер будет записывать наш голос. Тут возможны два решения:
Второй вариант мне не подошел по разным причинам, в том числе из-за плохих микрофонов ;) Разберем подробнее первый вариант с постоянной записью. Мы разбиваем нашу запись на множество мелких кусков, которые постоянно отправляем на сервер гугла для распознавания. Я нашел, что все мои команды пока входят максимум в 3-4 секунды. Если мы запустим несколько (предположим, 5) копий скрипта с интервалом в 1 секунду, получим непрерывное распознавание голоса. Добавим этот функционал к нашей основной программе:
srv.pl
mic.pl
Дадим права на запуск нашим скриптам:
Запускаем скрипт srv.pl, ожидаем запуска всех процессов, произносим, скажем, фразу: «Система! Раз два три!». Слышим через несколько секунд: «Ваша команда — раз два три». Нужно заметить, что наша команда будет попадать в несколько звуковых файлов и, соответственно, несколько раз исполняться. Чтобы этого избежать, нужно ввести проверку на последнюю команду. Добавим этот функционал в следующей части.
В этой статье мы реализовали базу нашего ПО для управления системой «умный дом». Пока оно почти ничего не умеет, кроме распознавания и синтеза речи, но это временно ;)
В следующей статье я расскажу, как прикрутить к этому всему web-интерфейс с некоторыми вкусными плюшками и просмотром камер.
UPD: Часть 4
Небольшое отступление
Начиная с этой статьи я начну описывать свое ПО, которое занимается координированием всех подсистем «умного дома». Считаю необходимым отметить, что уже ушел достаточно далеко от описываемого в этой статье кода, с более новыми и функциональными версиями можно ознакомиться через trac — ссылка. Распространение осуществляется под лицензией GNU GPLv3. Если кто-то пожелает присоединиться к разработке — милости прошу ;)
Немного информации
Распознавание речи
Как я уже писал в первой статье, для синтеза и распознавания голоса мы воспользуемся сервисами компании Google. Я думаю, многие сталкивались на мобильных устройствах под управлением ОС Android с голосовым поиском. Как дополнительная функцию, этот самый голосовой поиск был добавлен в браузер Google Chrome. Следует заметить, что официального API для этого сервиса компания еще не анонсировала, но благодаря открытым исходникам Chrome, народные умельцы нашли, что и куда посылается и что и как отдается в ответ. Выглядит это так:
- Записываем wav-файл с частотой дискретизации звука 16000 Гц, моно
- Перекодируем получившийся файл в формат flac
- Отсылаем файл по адресу https://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=ru-RU, представлясь гуглу клиентом Chrome
- Получаем ответ в формате JSON
Ответ представляет собой нечто вида:
{"status":0,"id":"84e03bf4efe17fa7856333560d6faba4-1","hypotheses":[{"utterance":"раз два три","confidence":0.85437811}]}Нас интересуют в ответе лишь два последних поля — utterance и confidence. Первое является искомой распознанной словом/фразой, второе — достоверностью распознавания. Если confidence будет более 0.5, можно считать, что распознавание достоверно.
Синтез речи
Синтез речи будет так же осуществляться через сервис Google и к нему так же, насколько я знаю, не анонсировано официального API. Чтобы получить звуковую фразу из текста нужно произвести совсем не сложную комбинацию действий:
- Отправить запрос вида: http://translate.google.com/translate_tts?tl=ru&q=текст, представлясь браузером Google Chrome в заголовках
- Получить ответом поток в MP3-кодировании
Как видите, тут все совсем не сложно. Теперь реализуем эту информацию программно.
Немного кода
Как я уже писал, заниматься централизованным управлением нашего «умного дома» будет специально написанный демон на perl. Заранее прошу за качество кода не бить ногами, ибо ваш покорный слуга всего лишь сисадмин :)
Итак, определимся с кругом задач, которое должно выполнять данное ПО:
- Принимать запросы на распознавание звуковых файлов
- Определять состояния устройств, подавать им команды
- Выполнять какие-то действия, если обнаружена командная последовательность
- Реагировать заданным образом на данные с датчиков и камер
- Вести статистику, учет и логи
- Иметь удобный web-интерфейс для просмотра состояния, камер, дачи команд и пр.
Возможно, я что-то забыл или пропустил, но, как мне кажется, это основные задачи ПО «умный дом». Теперь начнем реализовывать все это.
Для создания на perl TCP/IP-демона воспользуемся модулем Net::Server::Fork. Я буду исходить из предположения, что язык perl вам уже знаком.
Кратко пробежимся, по тому, что тут написано. Мы объявляем себя модулем с именем iON на базе модуля Net::Server::Fork и запускаем сервер на порту 16000 на localhost с максимальным уровнем детализации логов и без режима «демон». Далее, перегружаем функцию process_request(). Она отвечает за обработку полученных данных от клиента. В нашем случае, если сервер видит текст формата text число — выполняется функция toText c параметрами в виде числа, которое послал нам клиент. С командой quit, думаю все ясно.#!/usr/bin/perl -w package iON; use strict; use utf8; use base qw(Net::Server::Fork); sub process_request { my $self = shift; while (<STDIN>) { if (/text (\d+)/) { toText($1); next; } if (/quit/i) { print "+OK - Bye-bye ;)\n\n"; last; } print "-ERR - Command not found\n"; logSystem("Неизвестная команда: $_", 0); } } iON->run(port => 16000, background => undef, log_level => 4, host => 'localhost'); 1;
Чем же занимается функция toText()? Да собственно, распознаванием речи!
В деталях описывать не буду — тут реализуется именно те действия, которые нужны для распознавания текста. Гуглу скармливается файл из субдиректории data с именем input-число.flac. Как он там образуется, чуть позже. После — читается ответ, и если его достоверность выше 0.5, распознанный текст передается в качестве параметра функции checkcmd(). В конец всего, звуковой файл удаляется. Отмечу, что необходимо будет установить программу curl и добавить еще модули в начало нашего скрипта:sub toText { my $num = shift; print "+OK - Trying recognize text\n"; my $curl = WWW::Curl::Easy->new; $curl->setopt(CURLOPT_HEADER,1); $curl->setopt(CURLOPT_POST,1); #$curl->setopt(CURLOPT_VERBOSE, 1); my @myheaders=(); $myheaders[0] = "Content-Type: audio/x-flac; rate=16000"; $curl->setopt(CURLOPT_HTTPHEADER, \@myheaders); $curl->setopt(CURLOPT_URL, 'https://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=ru-RU'); my $curlf = WWW::Curl::Form->new; $curlf->formaddfile("data/input-$num.flac", 'myfile', "audio/x-flac"); $curl->setopt(CURLOPT_HTTPPOST, $curlf); my $response_body; $curl->setopt(CURLOPT_WRITEDATA,\$response_body); # Starts the actual request my $retcode = $curl->perform; # Looking at the results... if ($retcode == 0) { $response_body =~ /\n\r\n(.*)/g; my $json = $1; my $json_xs = JSON::XS->new(); $json_xs->utf8(1); my @hypo = $json_xs->decode($json)->{'hypotheses'}; my $dost = $hypo[0][0]{'confidence'}; my $text = $hypo[0][0]{'utterance'}; $dost = 0.0 if !defined $dost; $text = "" if !defined $text; print "+OK - Text is: \"$text\", confidence is: $dost\n"; if($dost > 0.5) { checkcmd($text); } { print "+ERR - Confidence is lower, then 0.5\n"; #sayText("Комманда не распознана!"); } } else { # Error code, type of error, error message print("+ERR - $retcode ".$curl->strerror($retcode)." ".$curl->errbuf); } system("rm data/input-$num.flac"); }
Теперь о синтезе речи. Этим будет заниматься функция под названием sayText() в качетстве параметра, принимающая собственно тот текст, который необходимо будет озвучить. Но для начала добавим некоторые недостающие модули и глобальные переменные:use WWW::Curl::Easy; use WWW::Curl::Form; use JSON::XS;
Теперь сам код:require Encode; use URI::Escape; use LWP::UserAgent; our $mp3_data;
Как видно, ответ сервера в виде потока обрабатывается функцией callback(), которая добавляет данные в переменную $mp3_data. Данные передаются через пайп на программу splay которая запущена через программу padsp, отвечающую за эмулирование OSS (в Ubuntu OSS был выпилен). Ключ -M заставляет программу проигрывать данные со стандартного входа.sub sayText { my $text = shift; print "+OK - Speaking \"$text\"\n"; my $url = "http://translate.google.com/translate_tts?tl=ru&q=".uri_escape_utf8($text); my $ua = LWP::UserAgent->new( agent => "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.872.0 Safari/535.2"); $ua->get($url, ':content_cb' => \&callback); open (MP3, "|padsp splay -M") or die "[err] Can't save: $!\n"; print MP3 $mp3_data; close(MP3); $mp3_data = undef; print "+OK - Done!\n"; return; } sub callback { my ($data, $response, $protocol) = @_; $mp3_data .= $data; # }
Теперь поговорим, откуда же появляются загадочные файлы в flac в директории data. Тут все просто — этим занимается отдельный скрипт:
Как мы можем видеть, запись и преобразование форматов выполняют несколько вызываемых из скрипта программ:#!/usr/bin/perl use strict; use IO::Socket; while (1) { my $rnd = int(rand(1000)); `rec -q -c 1 -r 16000 ./data/input-$rnd.wav trim 0 4`; `flac -f -s ./data/input-$rnd.wav -o ./data/input-$rnd.flac`; `rm ./data/input-$rnd.wav`; my $sock = new IO::Socket::INET( PeerAddr => "localhost", PeerPort => 16000, Proto => 'tcp') || next; print $sock "text ".$rnd; undef $rnd; }
- rec (из дистрибутива программы sox)
- flac
Команда rec делает короткие 4х секундные записи с рандомным числом в имени, которые пережимаются программой flac. После этого происходит соединение к нашему главному демону и передается команда text тот_самый_рандомный_номер. Для чего же я пишу 4х секундные короткие записи? Все дело в том, как компьютер будет записывать наш голос. Тут возможны два решения:
- Постоянная запись
- Запись файла при превышении определенной громкости
Второй вариант мне не подошел по разным причинам, в том числе из-за плохих микрофонов ;) Разберем подробнее первый вариант с постоянной записью. Мы разбиваем нашу запись на множество мелких кусков, которые постоянно отправляем на сервер гугла для распознавания. Я нашел, что все мои команды пока входят максимум в 3-4 секунды. Если мы запустим несколько (предположим, 5) копий скрипта с интервалом в 1 секунду, получим непрерывное распознавание голоса. Добавим этот функционал к нашей основной программе:
Теперь нам осталось только реализовать функцию checkcmd() для того чтобы проверить работу всего комплекса. Нам нужно также адресное обращение, чтобы исключить ложные срабатывания.for(1..5) { system("perl mic.pl &>/dev/null"); sleep 1; }
Теперь, соберем это все в одну кучу. У нас получилось два скрипта, назовем их srv.pl и mic.pl, а так же субдиректория data для хранения наших звуковых файлов.sub checkcmd { my $text = shift; if($text =~ /система/) { sayText("Ваша команда - $text"); # if $text eq "раз два три"; } }
srv.pl
#!/usr/bin/perl -w package iON; use strict; use utf8; use WWW::Curl::Easy; use WWW::Curl::Form; use JSON::XS; use URI::Escape; use LWP::UserAgent; require Encode; use base qw(Net::Server::Fork); ## Инициализация ################################ $|=1; our $parent = $$; our $mp3_data; ################################ for(1..5) { system("perl mic.pl &>/dev/null"); sleep 1; } ## Параметры запуска сервера ############################### iON->run(port => 16000, background => undef, log_level => 4, host => 'localhost'); ################################ ################################ sub DESTROY { if($$ == $parent) { system("killall perl"); system("rm data/*.flac && rm data/*.wav"); } } ## Процесс обработки команды ################################ sub process_request { my $self = shift; while (<STDIN>) { if (/text (\d+)/) { toText($1); next; } if (/quit/i) { print "+OK - Bye-bye ;)\n\n"; last; } print "-ERR - Command not found\n"; } } ############################### ############################### sub toText { my $num = shift; print "+OK - Trying recognize text\n"; my $curl = WWW::Curl::Easy->new; $curl->setopt(CURLOPT_HEADER,1); $curl->setopt(CURLOPT_POST,1); #$curl->setopt(CURLOPT_VERBOSE, 1); my @myheaders=(); $myheaders[0] = "Content-Type: audio/x-flac; rate=16000"; $curl->setopt(CURLOPT_HTTPHEADER, \@myheaders); $curl->setopt(CURLOPT_URL, 'https://www.google.com/speech-api/v1/recognize?xjerr=1&client=chromium&lang=ru-RU'); my $curlf = WWW::Curl::Form->new; $curlf->formaddfile("data/input-$num.flac", 'myfile', "audio/x-flac"); $curl->setopt(CURLOPT_HTTPPOST, $curlf); my $response_body; $curl->setopt(CURLOPT_WRITEDATA,\$response_body); # Starts the actual request my $retcode = $curl->perform; # Looking at the results... if ($retcode == 0) { $response_body =~ /\n\r\n(.*)/g; my $json = $1; my $json_xs = JSON::XS->new(); $json_xs->utf8(1); my @hypo = $json_xs->decode($json)->{'hypotheses'}; my $dost = $hypo[0][0]{'confidence'}; my $text = $hypo[0][0]{'utterance'}; $dost = 0.0 if !defined $dost; $text = "" if !defined $text; print "+OK - Text is: \"$text\", confidence is: $dost\n"; if($dost > 0.5) { checkcmd($text); } { print "+ERR - Confidence is lower, then 0.5\n"; } } else { # Error code, type of error, error message print("+ERR - $retcode ".$curl->strerror($retcode)." ".$curl->errbuf); } system("rm data/input-$num.flac"); } ############################### ## Проверка на комманды ############################### sub checkcmd { my $text = shift; chomp $text; $text =~ s/ $//g; print "+OK - Got command \"$text\" (Length: ".length($text).")\n"; if($text =~ /система/) { sayText("Ваша команда - $text"); } return; } ## Озвучивание ############################### sub sayText { my $text = shift; print "+OK - Speaking \"$text\"\n"; my $url = "http://translate.google.com/translate_tts?tl=ru&q=".uri_escape_utf8($text); my $ua = LWP::UserAgent->new( agent => "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.872.0 Safari/535.2"); $ua->get($url, ':content_cb' => \&callback); open (MP3, "|padsp splay -M") or die "[err] Can't save: $!\n"; print MP3 $mp3_data; close(MP3); $mp3_data = undef; print "+OK - Done!\n"; return; } sub callback { my ($data, $response, $protocol) = @_; $mp3_data .= $data; # } ######################################## ######################################## 1;
mic.pl
#!/usr/bin/perl use strict; use IO::Socket; while (1) { my $rnd = int(rand(1000)); `rec -q -c 1 -r 16000 ./data/input-$rnd.wav trim 0 3`; `flac -f -s ./data/input-$rnd.wav -o ./data/input-$rnd.flac`; `rm ./data/input-$rnd.wav`; my $sock = new IO::Socket::INET( PeerAddr => "localhost", PeerPort => 16000, Proto => 'tcp') || next; print $sock "text ".$rnd; undef $rnd; }
Что получилось
Дадим права на запуск нашим скриптам:
chmod 755 srv.pl mic.plЗапускаем скрипт srv.pl, ожидаем запуска всех процессов, произносим, скажем, фразу: «Система! Раз два три!». Слышим через несколько секунд: «Ваша команда — раз два три». Нужно заметить, что наша команда будет попадать в несколько звуковых файлов и, соответственно, несколько раз исполняться. Чтобы этого избежать, нужно ввести проверку на последнюю команду. Добавим этот функционал в следующей части.
Итого
В этой статье мы реализовали базу нашего ПО для управления системой «умный дом». Пока оно почти ничего не умеет, кроме распознавания и синтеза речи, но это временно ;)
В следующей статье я расскажу, как прикрутить к этому всему web-интерфейс с некоторыми вкусными плюшками и просмотром камер.
UPD: Часть 4
