» » «Умный дом» собственными руками. Часть 3. Синтез и распознавание голоса с помощью Google

 

«Умный дом» собственными руками. Часть 3. Синтез и распознавание голоса с помощью Google

Автор: admin от 7-10-2011, 17:00, посмотрело: 4056

В прошлой статье мы смогли добиться получения изображения с наших веб-камер в виде снимков раз в секунду. Теперь пришла пора взяться за обещанное в прошлой статье — распознавание и синтез голоса.ссылка. Распространение осуществляется под лицензией 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 вам уже знаком.
    #!/usr/bin/perl -w

    package iON;

    use strict;
    use utf8;

    use base qw(Net::Server::Fork);

    sub process_request
    {
    my $self = shift;
    while ()
    {
    if (/text (d+)/) { toText($1); next; }
    if (/quit/i) { print "+OK - Bye-bye ;)nn"; last; }
    print "-ERR - Command not foundn";
    logSystem("Неизвестная команда: $_", 0);
    }
    }

    iON->run(port => 16000, background => undef, log_level => 4, host => 'localhost');

    1;
    Кратко пробежимся, по тому, что тут написано. Мы объявляем себя модулем с именем iON на базе модуля Net::Server::Fork и запускаем сервер на порту 16000 на localhost с максимальным уровнем детализации логов и без режима «демон». Далее, перегружаем функцию process_request(). Она отвечает за обработку полученных данных от клиента. В нашем случае, если сервер видит текст формата text число — выполняется функция toText c параметрами в виде числа, которое послал нам клиент. С командой quit, думаю все ясно.Чем же занимается функция toText()? Да собственно, распознаванием речи!
    sub toText
    {
    my $num = shift;
    print "+OK - Trying recognize textn";
    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 =~ /nrn(.*)/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: $dostn";
    if($dost > 0.5)
    {
    checkcmd($text);
    }
    {
    print "+ERR - Confidence is lower, then 0.5n";
    #sayText("Комманда не распознана!");
    }
    } else {
    # Error code, type of error, error message
    print("+ERR - $retcode ".$curl->strerror($retcode)." ".$curl->errbuf);
    }
    system("rm data/input-$num.flac");

    }
    В деталях описывать не буду — тут реализуется именно те действия, которые нужны для распознавания текста. Гуглу скармливается файл из субдиректории data с именем input-число.flac. Как он там образуется, чуть позже. После — читается ответ, и если его достоверность выше 0.5, распознанный текст передается в качестве параметра функции checkcmd(). В конец всего, звуковой файл удаляется. Отмечу, что необходимо будет установить программу curl и добавить еще модули в начало нашего скрипта:
    use WWW::Curl::Easy;
    use WWW::Curl::Form;
    use JSON::XS;
    Теперь о синтезе речи. Этим будет заниматься функция под названием sayText() в качетстве параметра, принимающая собственно тот текст, который необходимо будет озвучить. Но для начала добавим некоторые недостающие модули и глобальные переменные:
    require Encode;

    use URI::Escape;
    use LWP::UserAgent;

    our $mp3_data;
    Теперь сам код:
    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; #
    }
    Как видно, ответ сервера в виде потока обрабатывается функцией callback(), которая добавляет данные в переменную $mp3_data. Данные передаются через пайп на программу splay которая запущена через программу padsp, отвечающую за эмулирование OSS (в Ubuntu OSS был выпилен). Ключ -M заставляет программу проигрывать данные со стандартного входа.

    Теперь поговорим, откуда же появляются загадочные файлы в 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 секунду, получим непрерывное распознавание голоса. Добавим этот функционал к нашей основной программе:
    for(1..5)
    {
    system("perl mic.pl &>/dev/null");
    sleep 1;
    }
    Теперь нам осталось только реализовать функцию checkcmd() для того чтобы проверить работу всего комплекса. Нам нужно также адресное обращение, чтобы исключить ложные срабатывания.
    sub checkcmd
    {
    my $text = shift;
    if($text =~ /система/)
    {
    sayText("Ваша команда - $text"); # if $text eq "раз два три";
    }
    }
    Теперь, соберем это все в одну кучу. У нас получилось два скрипта, назовем их srv.pl и mic.pl, а так же субдиректория data для хранения наших звуковых файлов.

    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 ()
    {
    if (/text (d+)/) { toText($1); next; }
    if (/quit/i) { print "+OK - Bye-bye ;)nn"; last; }
    print "-ERR - Command not foundn";
    }
    }

    ###############################
    ###############################

    sub toText
    {
    my $num = shift;
    print "+OK - Trying recognize textn";
    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 =~ /nrn(.*)/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: $dostn";
    if($dost > 0.5)
    {
    logSystem("Распознан текст: $text, достоверность: $dost", 0);
    checkcmd($text);
    }
    {
    print "+ERR - Confidence is lower, then 0.5n";
    }
    } 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;
    }

    Что получилось


    Дадим права на запуск нашим скриптам:

    chown 755 srv.pl mic.pl

    Запускаем, произносим, скажем, фразу: «Система! Раз два три!». Слышим через несколько секунд: «Ваша команда — раз два три».

    Итого


    В этой статье мы реализовали базу нашего ПО для управления системой «умный дом». Пока оно почти ничего не умеет, кроме распознавания и синтеза речи, но это временно ;)

    В следующей статье я расскажу, как прикрутить к этому всему web-интерфейс с некоторыми вкусными плюшками и просмотром камер.

    Источник: Хабрахабр: Железо

    Теги: google chrome

    Категория: Железо

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *