четверг, 15 ноября 2012 г.

пользовательская Система Голосовых Команд

Сдается мне инженеры в NOKIA всегда были с просветленными головами, коль скоро даже на S40 платформе была прикручена система голосовых команд, вполне себе эффективно работающая. Почему современные смартфоны из коробки этого не делают, ума не приложу. Приходится либо пользоваться чьей-то бурной фантазией на тему, либо генерить интернет траффик своим собственным наколенным изваянием.
Разумеется никто не в силах объять все возможные варианты использования голосовых команд, хотя среднестатистическая фантазия очерчивает вполне ограниченный круг задач.
С теми, кому известно слово "флуктуация",  хочу поделиться своей очередной разминкой для мозгов.
Надоело боярину тыкать в экранные кнопки для вызова различных задачек в Tasker. Захотелось тыкать в одну или вовсе не тыкать. Пришлось запилить нижеследующее...



пользовательская Система Голосовых Команд (СГК)

Язык описания: JavaScript

Справочная информация: Современный учебник JavaScript 

Структура.

В основе СГК лежит возможность расширения функционала объекта через "наследование".

Атомарной единицей системы является объект Команда (vc). Объект Команда имеет свойство keyword, описывающее Ключевые Слова для детектора срабатывания. Проще говоря, строка в виде готового шаблона регулярного выражения. В строке перечислены все слова, при наличии которых во входной голосовой строке объект Команда должен отработать свою функцию Действие (action).
Второй уровень иерархии СГК представлен объектом Набор Команд (vcSet). При инициализации этого объекта создается объект Команда и дополняется фунцией Поиска (find), Действие по умолчанию (defaultAction), набором Команд (vcs), свойством vcsKeywords - состоит из Ключевых Слов всех Команд из набора. vcsKeywords введено для ускорения принятия решения о наличии в поступающем запросе правильной подстроки (подкоманды) для этого Набора Команд.




Третьим уровнем СГК является объект Система (vcSys). По сути, это тот же vcSet, только в набор Команд Системы включаются как объекты класса Команда, так и объекты класса Набор Команд.
Каждый Набор Команд может быть расширен как Командами, так и Наборами Команд. Класс Система введен для централизации и наглядности.
Каждый объект в составе СГК предоставляет два интерфейса: проверка принадлежности (check) и действие (action) и требует интерфейс для определения Ключевых Слов (keyword). Все остальные методы и свойства объектов не должны вызываться или адресоваться извне.
На каждом уровне иерархии Ключевые Слова должны быть уникальными. Иначе, срабатывать будет первый найденный объект с таким(и) КС.

Как это работает.

Средой сборки элементов системы выбрано приложение Tasker, т.к.:
1. позволяет автоматизировать вызов системы в контексте работы.
2. выполняет скрипты.
3. вызывает внешние приложения
4. и т.д. и т.п.
5. и пр.
В качестве системы распознавания голоса применяется Google Voice Search, т.к. он имеет интерфейсы для работы с Tasker.

Tasker вызывает GVS. Полученные варианты распознанного текста Tasker передает в JS-скрипт одной строкой, объединив их через запятую.
При вызове скрипта, создается объект Система. Строка, принятая от системы распознавания голоса проверяется на наличие в ней Ключевого Слова Системы - для избежания ложного срабатывания (если СГК работает в фоновом режиме, постоянно обрабатывая речь. Испытания проводились только при ручном запуске).
Если КС обнаружено, Система инициализируется дальше: заполняется Наборами Команд после чего запускается Действие Системы.
1. Входная строка анализируется на наличие КС набора команд Системы (уровень Наборов Команд). Если таковых не обнаруживается, срабатывает действие по умолчанию Системы и работа завершается.
2. Если п.1 пройден, то перебором ищется вызываемый Набор Команд или Команда. Управление передается первому найденному объекту.
3. Если найденный объект Команда, выполняется ее Действие и работа завершается. Если же в п.2 найден Набор Команд, то выполняются п.п. 1-3 применительно к Набору Команд.

Как сделать свою СГК.

Построение СГК проще начать с создания объектов подкоманд, которые будут входить в наборы или в Систему.
Далее создаются объекты Наборов Команд с указанием Ключевого Слова и заполнением набора команд простым включением их конструкторов в vcs.
Как вариант, подкоманды можно создать непосредственно на этапе создания наборов команд.
Для этого в каждом новом наборе заполняется собственно набор команд (подкоманды) с указанием Ключевых Слов для них, определяются Действия подкоманд, и Действие по умолчанию набора. А так же инициализируется составное свойство - ключевые слова подкоманд. Действия подкоманд определяются исходя из контекста работы системы.
Далее определяется вызываемая из Tasker функция инициализации СГК, в которой создается объект системы, объединяющий созданные наборы. См. "Как это работает"

Тонкие места.

В комплексе есть свои ограничения:
1. для работы GVS необходимо подключение к интернет;
Подключение должно быть максимально быстрым и устойчивым. От этого зависит как скорость, так и стабильность работы. К.О.
2. качество распознавания в шумной среде довольно низкое.
Второе ограничение можно смягчить путем увеличения количества возвращаемых GVS вариантов распознанного текста. Поэтому, если ключевые слова встречаются в разных вариантах, система должна сработать правильно. Для дополнительного увеличения вероятности срабатывания рекомендуется делать ключевые слова с альтернативами: "старт|начали|пошел", и.т.д. На этапе тренировки, отладки системы это позволяет определить набор слов и словосочетаний, которые распознаются GVS наиболее точно.

Скрипт

не знаю как прикрутить с меседжу файлик :(

// supplied AS IS
// For backtrack and comments: v.ozzzzzzz@gmail.com
// voice command core

function vc(keyword){
return {
keyword:keyword,
check: function(v){ re = new RegExp(this.keyword, 'i'); return re.test(v) },
action: function(){flash(this.keyword) }
}
}

function vcSet(keyword){
var set = vc(keyword)
// override inherited command's action
set.action = function(v){
if(set.checkSet(v)){
set.find(v)
}else{
set.defaultAction(v)
}
}
// add set's methods
set.find = function(v){
for(vc in set.vcs){
if(set.vcs[vc].check(v)){
set.vcs[vc].action(v)
break // comment it for batch commands
}
}
}
set.checkSet = function(v){ re = new RegExp(set.vcsKeywords, 'i'); return re.test(v) }
set.defaultAction = function(v){flash(set.keyword+': What? '+v)}
// add property
set.vcsKeywords = 'dummy'
// the Set
set.vcs = {}
return set
}

function vcSys(keyword){
return vcSet(keyword)
}

function sayit(s,e){
var engines = {en:'eng-USA', ru:'rus-RUS'}
e = e || 'ru'
say(s, 'com.svox.classic', engines[e], 'system', 5, 5)
}
// voice command core
все что ниже, может быть вынесено в отдельный файл/файлы

function voiceCommandSystem(c){
try{
sys = vcSys('command')
if(sys.check(c)){
sys.vcs = {setBP:BP(), setVR:VR(), setMap:Map()}
sys.vcsKeywords = sys.vcs.setBP.keyword+'|'+sys.vcs.setVR.keyword+'|'+sys.vcs.setMap.keyword
sys.action(c)
}
}catch(e){
popup('Exception',e.name+' '+e.message,true,'','',30);
}
}

function BP(){
var set = vcSet(global('vcBP'))
set.vcs = {c1:vc(global('vcBP1')), c2:vc(global('vcBP2'))}
set.vcsKeywords = set.vcs.c1.keyword+'|'+set.vcs.c2.keyword
set.vcs.c1.action = function(){performTask('m BeyondPod', 9, 'play')}
set.vcs.c2.action = function(){performTask('m BeyondPod', 9, 'pause')}
set.defaultAction = function(v){sayit('WTF BeyondPod','en')}
return set
}

function VR(){
var set = vcSet(global('vcVR'))
set.vcs = {c1:vc(global('vcVR1')), c2:vc(global('vcVR2'))}
set.vcsKeywords = set.vcs.c1.keyword+'|'+set.vcs.c2.keyword
set.vcs.c1.action = function(){shell('am broadcast -a rubberbigpepper.VideoReg.StartRecord && sleep 3 && renice 10 -p `pidof rubberbigpepper.VideoRegPro` && renice -12 -p `pidof mediaserver`', true, 20)}
set.vcs.c2.action = function(){shell('am broadcast -a rubberbigpepper.VideoReg.StopRecord', true, 20)}
set.defaultAction = function(v){sayit('WTF Video Reg','en')}
return set
}


function Map(){
var set = vc(global('vcMap'))
set.action = function(){
sendIntent( 'android.intent.action.VIEW' , 'activity', 'ru.yandex.yandexmaps' , '',
'browsable', 'geo:'+global('LOC')+'?z='+global('ZoomHigh')+'&q=(V.Oz.)' )}
return set
}