В этой статье хотелось бы описать свой опыт по применению web speech api в браузере Google Chrome для реализации голосового поиска и автоматического воспроизведения видеороликов с канала Youtube. Для демонстрации данного функционала нам понадобиться сделать следующие шаги:

  1. Установить набор: Apache2, PHP5(пакет curl обязательно).
  2. Иметь в наличии мультимедиа центр Dune HD или установить XBMC и настроить его для работы в сети INTERNET.
  3. Получить Youtube API Key для выполнения поисковых запросов.

Как сделать все вышеперечисленное, здесь описывать не буду, так как на эти темы полно статей. Принцип реализации такой:

  1. Распознаем фразу с помощью скрипта, написанного на JavaScript — работать будет только в Google Chrome.
  2. Ищем ролики, соответствующие поисковому запросу.
  3. Получаем прямые ссылки на ролики.
  4. Создаем плейлист из ссылок и названий роликов.
  5. Отправляем плейлист для воспроизведения на устройство.

Топология сети: Internet приходит в wan порт Wi-Fi роутера, а к нему подключаются:

  • устройство, с которого будем управлять (планшет, смартфон, ноутбук).
  • компьютер с web сервером Apache (если управление будет производится с него, то первое устройство может отсутствовать).
  • собственно сам мультимедиа центр (программный — XBMC, или аппаратный — Dune HD).

Скрипт распознавания речи на JavaScript - index.html:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">
<head>
<title>Умный Дом</title>
<script language="javascript" type="text/javascript">
/* Создание нового объекта XMLHttpRequest для общения с Web-сервером */
var xmlHttp = false;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
try {
  xmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
  try {
    xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
  } catch (e2) {
    xmlHttp = false;
  }
}
@end @*/

if (!xmlHttp && typeof XMLHttpRequest != 'undefined') {
  xmlHttp = new XMLHttpRequest();
}
</script>
<style>
  * {
    font-family: Verdana, Arial, sans-serif;
    font-size: 20px;
  }
  a:link {
    color:#000;
    text-decoration: none;
  }
  a:visited {
    color:#000;
  }
  a:hover {
    color:#33F;
  }
  body {
      text-align: center;
  }
  .button {
    background: -webkit-linear-gradient(top,#008dfd 0,#0370ea 100%);
    border: 1px solid #076bd2;
    border-radius: 3px;
    color: #fff;
    display: none;
    font-size: 13px;
    font-weight: bold;
    line-height: 1.3;
    padding: 8px 25px;
    text-align: center;
    text-shadow: 1px 1px 1px #076bd2;
    letter-spacing: normal;
  }
  
  .final {
    color: black;
    padding-right: 3px;
  }
  .interim {
    color: gray;
  }
  .info {
    font-size: 14px;
    text-align: center;
    color: #777;
    display: none;
  }
  
  .sidebyside {
    display: inline-block;
    width: 45%;
    min-height: 40px;
    text-align: left;
    vertical-align: top;
  }
  #headline {
    font-size: 40px;
    font-weight: 300;
  }
  #info {
    font-size: 20px;
    text-align: center;
    color: #777;
    visibility: hidden;
  }
  #results {
    font-size: 14px;
    font-weight: bold;
    border: 1px solid #ddd;
    padding: 15px;
    text-align: left;
    min-height: 30px;
    width: 500px;
    margin: 0 auto;
  }
  #start_button {
    border: 0;
    padding: 0;
    background: url(images/mic.gif);
    width: 50px;
    height: 50px;
    cursor: pointer;
    vertical-align: top;
  }

#info_speak_now,
#info_no_speech,
#info_no_microphone,
#info_upgrade {
    display: none;
}
  
</style>
<meta charset="UTF-8" />
</head>
<body>
<div id="messages">
	 <input type="button" id="start_button" onclick="startButton(event);" />
	<!-- сообщения на разные случаи -->
	<p id="info_start">Кликни на микрофон чтобы начать раздавать команды.</p>
	<p id="info_speak_now">Командуй!</p>
	<p id="info_no_speech">Голос не обнаружен.</p>
	<p id="info_no_microphone">Микрофон не найден.</p>
	<p id="info_upgrade">Твой браузер не поддерживает Web Speech API.</p>
</div>
<div id="results">
  <span id="final_span" class="final"></span>
</div>
<script>
var start_button = document.getElementById('start_button'),
	recognizing = false, // флаг идет ли распознование
	final_transcript = '';

// проверяем поддержку speach api
if (!('webkitSpeechRecognition' in window)) {
	
	start_button.style.display = "none";
	showInfo("info_upgrade");
	
} else { /* инициализируем api */
 
 /* создаем объект 	*/
 var recognition = new webkitSpeechRecognition();
 
 /* базовые настройки объекта */
 
 recognition.lang = 'ru'; // язык, который будет распозноваться. Значение - lang code
 recognition.continuous = true; // не хотим чтобы когда пользователь прикратил говорить, распознование закончилось
 
 /* метод вызывается когда начинается распознование */
 recognition.onstart = function() {
	 
    recognizing = true;
	
    showInfo('info_speak_now'); // меняем инфо текст
    start_button.style.background = 'url(images/mic-animate.gif)'; // меняем вид кнопки
    
  };
  
  /* обработчик ошибок */
  recognition.onerror = function(event) {
    if (event.error == 'no-speech') {
      start_button.style.background = 'url(images/mic.gif)';
      showInfo('info_no_speech');
    }
    if (event.error == 'audio-capture') {
      start_button.style.background = 'url(images/mic.gif)';
      showInfo('info_no_microphone');
    }
  };
  
  /* метод вызывается когда распознование закончено */
  recognition.onend = function() {
 	recognizing = false;
 	//recognition.start();
	start_button.style.background = 'url(images/mic.gif)';
	showInfo('info_start'); 
  };
  
  /* 
  	метод вызывается после каждой сказанной фразы. Параметра event используем атрибуты:
	- resultIndex - нижний индекс в результирующем массиве
	- results - массив всех результатов в текущей сессии
 */
  recognition.onresult = function(event) {
	  
  
	  /* 
	  	обход результирующего массива
	  */
	  for (var i = event.resultIndex; i < event.results.length; ++i) {
		
		/* если фраза финальная (уже откорректированная) сохраняем в конечный результат */
      	if (event.results[i].isFinal) {
        	final_transcript += event.results[i][0].transcript.toLowerCase();
        	
     	 } 
     }
   
    final_span.innerHTML = final_transcript;
	 var newText2 = final_transcript.replace(/(^\s+|\s+$)/g,'');
	 var url = "/voice_search.php?q=" + encodeURI(newText2);
  	 xmlHttp.open("GET", url, true);
  	 xmlHttp.send(null);
	
	 final_transcript = '';	// очищаем рапознанный текст
	
  };
 
}

/* показ нужного сообщения */
function showInfo(id) {
		
	var messages = document.querySelectorAll('p');
	
	for(i=0; i<messages.length; i++) messages[i].style.display = 'none';

	document.getElementById(id).style.display = 'block';
}


/* обработчик клика по микрофону */
function startButton(event) {

  if (recognizing) { // если запись уже идет, тогда останавливаем
    recognition.stop();
	document.getElementById('final_span').innerHTML = '';
    return;
  }
 
  recognition.start();
  
}
</script>
</body>
</html>


Для полноценной работы скрипта, нужно еще создать папку images и положить в неё картинки с микрофончиками, которые можно взять здесь и здесь.

Данный скрипт делает всего два действия — распознает фразу и отправляет её AJAX запросом PHP скрипту. Также необходимо обратить внимание на то, что кодировка во всех скриптах должна быть UTF-8 (если делаете в Windows, то UTF-8 без ВОМ).

Скрипт поиска видео роликов на PHP - voice_search.php:
<?php
// send info into core
function send_info($info)
{
	echo $info;
	
}
function send_req($url) 
	{
	   $ch = curl_init();
		curl_setopt($ch, CURLOPT_USERAGENT,				"Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1");
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 		FALSE);
		curl_setopt($ch, CURLOPT_HEADER,				false);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION,		true);
		curl_setopt($ch, CURLOPT_URL,					$url);
		curl_setopt($ch, CURLOPT_REFERER,				$url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER,		TRUE);
		$out = curl_exec($ch);
		curl_close($ch);
		return $out;
	}
function get_video_url($videoId) 
{ 	
 			$url = 'http://www.youtube.com/get_video_info?&video_id='.$videoId.'&asv=3&el=detailpage&hl=en_US';
 			
  			$first_found = "";
      	$last_found = "";
      	$first_quality = "";
      	$last_quality = "";
      	//$video_quality = 'medium';
			$video_quality = 'hd1080';
			$doc=send_req($url);
		
			$x=explode("&",$doc);
			$t=array(); $g=array(); $h=array();
			foreach($x as $r)
			{
				$c=explode("=",$r);
				$n=$c[0]; $v=$c[1];
				$y=urldecode($v);
				$t[$n]=$v;
		
			}
			$links = explode(',',urldecode($t['url_encoded_fmt_stream_map']));
			$dlinks = array();
			foreach ($links as $link) 
			{
				parse_str($link,$linkarr);
				$itag = $linkarr['itag'];
				$quality = $linkarr['quality'];
		
		
				if (in_array($itag, array('18', '22', '37', '38')))
				{
					if(isset($linkarr['s'])) 
					{
						$linkarr['signature'] = file_get_contents('http://dune-club.info/echo?message=' . $linkarr['s']);
						unset($linkarr['s']);	
						$dlinks[$linkarr['itag']] = $linkarr['url'] . "&signature=" . $linkarr['signature'];
					}
					else
					{
						$dlinks[$linkarr['itag']] = $linkarr['url'];
						$playback_url = $dlinks[$linkarr['itag']];
						if ($first_found === "") 
						{
        					$first_found = $playback_url;
        					$first_quality = $quality;
        				}
        				$last_found = $playback_url;
        				$last_quality = $quality;
       				if (($quality === $video_quality) || (($quality !== 'medium') && ($video_quality === 'hdonly'))) 
       				{
       					$playback_url=(urldecode($playback_url));
       					return $playback_url;
       				}
       			}
       		}
       				
       	}
			if (($last_found !== "") && ($video_quality !== 'hdonly')) 
			{
            if ($video_quality === 'hd1080') 
            {
              $first_found=(urldecode($first_found));
              return $first_found;
            } 
            else 
            {
                 $last_found=(urldecode($last_found));
                 return $last_found;
            }
			} 
			else 
			{
            //hd_print("--> video: $id; no mp4-stream.");
            return false;
			}
 
 
}
   if(isset($_GET['q']) == false or $_GET['q']=="" ) 
	{
		
			$url = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=голосовое%20управление%20телевизором&type=video&maxResults=10&key=Youtube_API_key";
		
 	}
	else 
	{
		$url = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=".urlencode($_GET['q'])."&type=video&maxResults=10&key=Youtube_API_key";
		
	}
 
  $res = json_decode(send_req($url));
  if(isset($res->items) == false or ($res->items)=="" ) 
	{
		$info="похоже что-то пошло не так!";																
		send_info($info);
 	}
	else 
	{
  		$res = $res->items;
  		//print_r($res);
  		$fp = fopen('play_list.m3u', 'w+t');
		$start="#EXTM3U\r\n";
		fwrite($fp, $start);
  		foreach ($res as $searchResult) 
  		{  			
  			$title=($searchResult->snippet->title);
  			$videoId = ($searchResult->id->videoId) ; 
  			$clip_url = get_video_url($videoId);
  			if(isset($clip_url) == false or $clip_url=="") 
  			{
				$info="клип не найден!";																
				send_info($info);
  			}
  			else 
  			{	
  				$clip="#EXTINF:-1,$title\r\n$clip_url\r\n";
				fwrite($fp, $clip);
			}
  		}
  		fclose($fp);
  		$info="плейлист создан";																
  		send_info($info);
		//url для Dune HD 
	       $url="http://ip_addr_dune/cgi-bin/do?cmd=launch_media_url&media_url=http://server_ip/play_list.m3u";
	      //url для XBMC 
	      $url="http://логин:пароль@ip-адрес:8080/jsonrpc?request={"jsonrpc":"2.0","id":"1","method":"Player.Open","params":{"item":{"file":"http://server_ip/play_list.m3u"}}}";
	      $curl = curl_init();
 	      curl_setopt($curl, CURLOPT_URL, $url);
 	      curl_setopt($curl, CURLOPT_RETURNTRANSFER,true);
             $out = curl_exec($curl);
	     curl_close($curl);
  	}
?>


В данном скрипте в самом конце нужно будет отредактировать $url под настройки вашего мультимедиа центра и удалить лишний, а также исправить текст server_ip на ip адрес вашего Apache сервера и вставить свой Youtube_API_Key. Что происходит здесь: из скрипта распознавания речи сюда приходит текст распознанной фразы, далее с помощью Youtube API v3 производится поиск видео роликов подходящих под поисковой запрос, получив ссылки на ролики, мы пропускаем их через цикл, в котором извлекаются полные пути до видеофайлов, которые и записываются в плейлист play_list.m3u. Данный скрипт не претендует на идеальный код, так как написан чисто в ознакомительных целях, поэтому всевозможные проверки здесь отсутствуют.

Вот и всё, теперь заходим на наш web сервер по его ip адресу. Можно заходить с любого устройства: планшет, смартфон, ноутбук, единственное, что я заметил — в последнее время на смартфонах с Android, скрипт распознавания речи при отсутствии её, отправляет фразу повторно, с чем это связанно пока не понятно, но раньше такого не было.

На основе данного материала можно сделать еще много интересных вещей, таких как голосовой поиск музыки в VK и управления 1-wire устройствами. В общем пробуйте, если что не получится, то спрашивайте с удовольствием отвечу на все ваши вопросы.

P.S.: Статья написана по материалам:

W3C Web Speech API
YouTube api v3

Скрипт получения прямых ссылок взят из приложения YouTube для Dune HD и немного доработан под свои нужды. Для тех, кто хочет просто попробовать управлять мультимедиа центрами, без написания скриптов — можно почитать здесь. Результат моей работы на YouTube

Комментарии (5)


  1. BOBS13
    13.11.2015 16:59

    Идея интересная, я только и не особо понял XBMC — это программа? Или это какая-то аудио система со своим крутым ПО?


    1. arbuzmaster
      13.11.2015 17:26

      XBMC — мультимедиа центр, но только программный. То есть его можно установить практически на любую ОС. Новое название проекта xbmc — KODI. Это, что-то типа VLC только с большим функционалом. К стати VLC можно и здесь использовать, только нужно URL будет поменять, на тот который воспринимает VLC, там немного другие параметры.


      1. BOBS13
        13.11.2015 17:41

        Интересно, надо поробывать поискать его на мой телевизор — LG + Web OS)


        1. arbuzmaster
          13.11.2015 17:46

          На счет Web OS сказать ничего не могу — не знаю, но на Android точно есть, у меня на смартфоне стоит.


  1. denv
    13.11.2015 19:04

    Может быть кому интересно будет, perl + mojolicious = голосовой поиск сериалов на кино-дом + yandex speechkit, выдирает ссылку и запускает на XBMC. отрабатывает хорошо когда посторонних шумов нету. пробовал использовать pocketsphinx для выделения фразы «окей, дом» и запуска скрипта который уже идет через яндекс, так себе.