В двух предыдущих частях нашего обзора мы описали современные системы распознавания речи для Linux и попытались установить и настроить одну из них — CMU Sphinx. В заключительной части попробуем использовать наши наработки на практике, прикрутив движок Pocketsphinx к Android-устройству и контроллеру умного дома. Стоит сразу предупредить — для выполнения всех описанных ниже действий необходимо базовое знание Java.
Оборудование и софт
Для организации голосового управления мы будем использовать установленное и настроенное ранее ПО — CMU Sphinx, дешевый Android-смартфон c установленным приложением Veravoice (см. далее) и ноутбук Dell Inspiron с Debian 8 на борту. И, что самое интересное, контроллер умного дома — специальное устройство, создающее беспроводную сеть, подобную Wi-Fi-сети, но использующую более низкие частоты. С виду оно напоминает обычный Wi-Fi-роутер и выступает в качестве посредника между ПДУ (в нашем случае смартфоном) и управляемым устройством. Подробнее о технологии, используемой в контроллерах умного дома, можно почитать тут. В данном обзоре мы не будем касаться вопросов настройки подобных устройств. Отметим лишь, что они подключаются к домашней локальной сети, как и роутеры, и ровно так же управляются из браузера. Цель обзора — не демонстрация возможностей контроллеров и уж тем более не их реклама. Просто без этого класса устройств не обойтись, как ни крути.
Целью автора было не создание умного дома. Это дорого и непрактично в рамках обычной квартиры. Автор стремился показать работоспособность CMU Sphinx. Показать, что системы распознавания речи для Linux ничуть не хуже проприетарных справляются с задачами распознавания русской речи и голосового управления оборудованием. Смартфон будет выступать в качестве универсального пульта дистанционного (до 300 м) управления техникой. Ноутбук был использован для обкатки CMU Sphinx и ее настройки, но управлять техникой легче с помощью смартфона, поэтому задача — перенести опыт и технологии с мобильного компьютера на мобильный телефон, при этом немного доработав софт под Android. Однако никто не запрещает тебе использовать для голосового управления ноутбук, например.
Сначала прикрутим к нашему смартфону движок Pocketsphinx — реализацию CMU Sphinx для Android. У Pocketsphinx есть версия для Windows Phone и порт под iOS. Главное преимущество этого способа перед облачным распознаванием речи (с помощью Google ASR или Yandex SpeechKit) в полной автономности, а также в скорости и точности распознавания. К тому же это дешевле — не нужно каждый раз обращаться к серверам и уж тем более создавать эти серверы.
Сразу следует отметить, что преимущества можно считать таковыми только с учетом специфики оборудования (телевизор, например). Также нужно понимать, что в этом случае у нас будет строго определенный словарь и грамматика. Для распознавания произвольного текста (к примеру, для набора СМС-сообщения или поискового запроса) лучше использовать онлайн-способ.
INFO
Весь код, который ты увидишь далее, уже реализован в приложении Veravoice и приводится исключительно для объяснения принципов организации голосового управления.
Матчасть
Задача — создать голосовой ПДУ для техники, работающий быстро и точно с расстояния в несколько метров, который при необходимости активируется голосом и работает даже на дешевых Android-устройствах с устаревшими версиями мобильной ОС. На практике это выглядит так: голосом активируется микрофон, далее произносится название нужного устройства (в нашем случае телевизор). Приложение Veravoice распознаёт голос и включает либо выключает в зависимости от их текущего состояния наши устройства. Также приложение может получать от устройств состояние и дополнительную информацию, такую как прогноз погоды или новости.
INFO
Автор статьи выражает огромную благодарность пользователю GitHub Дмитрию Чечёткину, автору приложения Veravoice, в котором реализована львиная доля всех наработок и способов взаимодействия с CMU Sphinx. Скачать версию приложения для Android можно отсюда.
Исходники Veravoice открыты. Благодаря этому задача упростилась на порядок. Дмитрий совместил API Pocketsphinx с Android SDK, в результате получилось вполне работоспособное приложение. С помощью этого приложения можно реализовать многие вещи, доступные лишь в дорогих системах умного дома. Если в каждой комнате повесить по дешевому смартфону, можно вечером, придя с работы, скомандовать: «Умный дом! Свет! Телевизор!» — и включить нужное оборудование, узнать прогноз погоды или температуру в помещении.
Pocketsphinx хорош еще и тем, что исходники его открыты и в нем реализована поддержка голосовой активации, как говорится, «из коробки». Микрофон будет активироваться либо голосом, либо нажатием на иконку микрофона, либо просто касанием рукой экрана. Экран может быть полностью выключенным.
Русская языковая модель и грамматика пользовательских запросов прикручиваются к движку довольно просто. Об этом написано в предыдущей части нашего обзора. Здесь мы остановимся лишь на некоторых мелких, но важных деталях. Самое главное, что наше приложение будет распознавать именно то, чему мы его обучим, и не более того. Лишнего не выдаст. Напомним, Pocketsphinx использует грамматику запросов JSGF — расшифровывается как Java Speech Grammar Format, или формат грамматики речи JavaScript. Разработан компанией Sun Microsystems, открытый, принят W3C в качестве стандарта. По сути, это представление грамматики того или иного языка в виде исходного кода. JSGF используется многими проектами распознавания речи, в том числе проприетарными. Он гибок и позволяет описать все необходимые варианты фраз, которые будет произносить пользователь. В нашем случае это будут названия устройств. Выглядит примерно так:
<commands> = телевизор | включить | погода;
Pocketsphinx также может работать со статистической моделью языка, позволяющей распознавать спонтанную речь. Но нам это не нужно: грамматика наших запросов будет состоять только из названий устройств. После распознавания Pocketsphinx вернет нам обычную строчку текста, где устройства будут идти одно за другим:
#JSGF V1.0;
grammar commands;
public <command> = <commands>+;
<commands> = лампа | монитор | температура;
Знак плюса означает, что пользователь может назвать не одно, а несколько устройств подряд. Veravoice получает список устройств от контроллера умного дома (см. далее) и формирует такую грамматику в классе Grammar. Грамматика — это то, ЧТО может говорить пользователь. Чтобы научить Pocketsphinx тому, КАК он будет произносить что-либо, необходимо обучить его правильно понимать произношение слов. В этом нам поможет транскрипция. Это называется словарь. Транскрипции описываются с помощью специального синтаксиса. Пример:
умный uu m n ay j
дом d oo m
В принципе, ничего сложного. Двойная гласная в транскрипции обозначает ударение. Двойная согласная — мягкую согласную, за которой идет гласная. Все возможные комбинации для всех звуков русского языка можно найти в самой языковой модели. Понятно, что заранее описать все транскрипции в приложении невозможно, потому что никому заранее не известны названия, которые пользователь даст своим устройствам. Поэтому в Veravoice подобные транскрипции генерируются на лету. Реализуется это через JavaScript-класс PhonMapper
, который может получать на вход строчку и генерировать для нее правильную транскрипцию. Подробности тут.
Собственно процесс
Научить движок распознавания речи все время «слушать эфир» и ждать произнесения заранее заданной фразы (или фраз), при этом отсеивая все другие звуки и речь — не то же самое, что описать грамматику и просто включить микрофон. Бессмысленно приводить здесь теорию этой задачи и описывать ее реализацию. Можно лишь отметить, что недавно программисты, работающие над Pocketsphinx, реализовали такую функцию и теперь она доступна из коробки в API. Отметим лишь, что для активации голосом нужно не только указать транскрипцию, но и подобрать подходящее значение порога чувствительности. Чересчур малое значение приведет к множеству ложных срабатываний (это когда ты не произносил активационную фразу, а система ее распознала), слишком высокое — к излишней невосприимчивости. Поэтому данная настройка особо важна. Как уже сообщалось выше, Pocketsphinx предоставляет удобный API для конфигурирования и запуска процесса распознавания. Это классы SpeechRecognizer
и SpeechRecognizerSetup
. Вот как это выглядит в коде приложения:
PhonMapper phonMapper = new PhonMapper(getAssets().open("dict/ru/hotwords"));
Grammar grammar = new Grammar(names, phonMapper);
grammar.addWords(hotword);
DataFiles dataFiles = new DataFiles(getPackageName(), "ru");
File hmmDir = new File(dataFiles.getHmm());
File dict = new File(dataFiles.getDict());
File jsgf = new File(dataFiles.getJsgf());
copyAssets(hmmDir);
saveFile(jsgf, grammar.getJsgf());
saveFile(dict, grammar.getDict());
mRecognizer = SpeechRecognizerSetup.defaultSetup()
.setAcousticModel(hmmDir)
.setDictionary(dict)
.setBoolean("-remove_noise", false)
.setKeywordThreshold(1e-7f)
.getRecognizer();
mRecognizer.addKeyphraseSearch(KWS_SEARCH, hotword);
mRecognizer.addGrammarSearch(COMMAND_SEARCH, jsgf);
Если взглянуть на код, сразу бросается в глаза несколько вещей. Во-первых, сперва на диск копируются все необходимые файлы (Pocketsphinx требует наличия на диске акустической модели, грамматики и словаря с транскрипциями). Во-вторых, конфигурируется сам движок распознавания. Указываются пути к файлам модели и словаря, а также некоторые параметры (порог чувствительности для активационной фразы). В-третьих, конфигурируется путь к файлу с грамматикой, а также активационная фраза. Из кода также видно, что один движок конфигурируется сразу и для грамматики, и для распознавания активационной фразы. Для чего это делается? А для того, чтобы мы могли быстро переключаться между тем, что в данный момент нужно распознавать. Вот как выглядит запуск процесса распознавания активационной фразы:
mRecognizer.startListening(KWS_SEARCH);
А вот так — распознавание речи по заданной грамматике:
mRecognizer.startListening(COMMAND_SEARCH, 3000);
Второй аргумент необязательный — это время в миллисекундах, после которого распознавание будет автоматически завершаться, если никто ничего не говорит. Как видишь, можно использовать только один движок для решения обеих задач.
Результат
Чтобы получить результат распознавания, необходимо также указать так называемого слушателя событий, который имплементирует интерфейс RecognitionListener
. У него есть несколько методов, которые вызываются при наступлении одного из перечисленных событий:
onBeginningOfSpeech
— движок услышал какой-то звук, возможно речь (а может быть, и нет);onEndOfSpeech
— звук закончился;onPartialResult
— есть промежуточные результаты распознавания. Для активационной фразы это значит, что она сработала; аргумент Hypothesis содержит данные о распознавании (строка и score);onResult
— конечный результат распознавания. Этот метод будет вызван после вызова метода stopSpeechRecognizer. Аргумент Hypothesis содержит данные о распознавании (строка и score).
Реализуя тем или иным способом методы onPartialResult
и onResult
, можно изменять логику распознавания и получать окончательный результат. Вот как это сделано в случае с нашим приложением:
@Override
public void onEndOfSpeech() {
Log.d(TAG, "onEndOfSpeech");
if (mRecognizer.getSearchName().equals(COMMAND_SEARCH)) {
mRecognizer.stop();
}
}
@Override
public void onPartialResult(Hypothesis hypothesis) {
if (hypothesis == null) return;
String text = hypothesis.getHypstr();
if (KWS_SEARCH.equals(mRecognizer.getSearchName())) {
startRecognition();
} else {
Log.d(TAG, text);
}
}
@Override
public void onResult(Hypothesis hypothesis) {
mMicView.setBackgroundResource(R.drawable.background_big_mic);
mHandler.removeCallbacks(mStopRecognitionCallback);
String text = hypothesis != null ? hypothesis.getHypstr() : null;
Log.d(TAG, "onResult " + text);
if (COMMAND_SEARCH.equals(mRecognizer.getSearchName())) {
if (text != null) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
process(text);
}
mRecognizer.startListening(KWS_SEARCH);
}
}
Если, когда получаем событие onEndOfSpeech
, при этом мы распознаем команду для выполнения, то необходимо остановить распознавание, после чего сразу будет вызван onResult
. В onResult
нужно проверить, что только что было распознано. Если это команда, то нужно запустить ее на выполнение и переключить движок на распознавание активационной фразы. В onPartialResult
нас интересует только распознавание активационной фразы. Если мы его обнаруживаем, то сразу запускаем процесс распознавания команды. Вот как он выглядит:
private synchronized void startRecognition() {
if (mRecognizer == null || COMMAND_SEARCH.equals(mRecognizer.getSearchName())) return;
mRecognizer.cancel();
new ToneGenerator(AudioManager.STREAM_MUSIC, ToneGenerator.MAX_VOLUME).startTone(ToneGenerator.TONE_CDMA_PIP, 200);
post(400, new Runnable() {
@Override
public void run() {
mMicView.setBackgroundResource(R.drawable.background_big_mic_green);
mRecognizer.startListening(COMMAND_SEARCH, 3000);
Log.d(TAG, "Listen commands");
post(4000, mStopRecognitionCallback);
}
});
}
Здесь мы сперва играем небольшой сигнал для оповещения пользователя, что мы его услышали и готовы к его команде. На это время микрофон должен быть выключен. Поэтому мы запускаем распознавание после небольшого тайм-аута (чуть больше, чем длительность сигнала, чтобы не услышать его эха). Также запускается поток, который остановит распознавание принудительно, если пользователь говорит слишком долго. В данном случае это три секунды. Далее превращаем распознанную строку в команды. Тут все уже специфично для конкретного приложения. В нашем примере мы просто вытаскиваем из строчки названия устройств, ищем по ним нужное устройство и либо меняем его состояние с помощью HTTP-запроса на контроллер умного дома, либо сообщаем его текущее состояние (как в случае с термостатом). Эту логику можно увидеть в классе Controller
.
Синтезируем речь. Синтез речи — это операция, обратная распознаванию. Здесь наоборот — нужно превратить строку текста в речь, чтобы ее услышал пользователь. Например, погода: мы должны заставить наше Android-устройство произнести текущую температуру. С помощью APITextToSpeech это сделать довольно просто (спасибо Google за прекрасный женский TTS для русского языка):
private void speak(String text) {
synchronized (mSpeechQueue) {
mRecognizer.stop();
mSpeechQueue.add(text);
HashMap<String, String> params = new HashMap<String, String>(2);
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UUID.randomUUID().toString());
params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, String.valueOf(AudioManager.STREAM_MUSIC));
params.put(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS, "true");
mTextToSpeech.speak(text, TextToSpeech.QUEUE_ADD, params);
}
}
Важно помнить, что перед синтезом нужно обязательно отключить распознавание. На некоторых устройствах вообще невозможно одновременно и слушать микрофон, и что-то синтезировать.
Окончание синтеза речи (то есть окончание процесса говорения текста синтезатором) можно отследить в слушателе:
private final TextToSpeech.OnUtteranceCompletedListener mUtteranceCompletedListener = new TextToSpeech.OnUtteranceCompletedListener() {
@Override
public void onUtteranceCompleted(String utteranceId) {
synchronized (mSpeechQueue) {
mSpeechQueue.poll();
if (mSpeechQueue.isEmpty()) {
mRecognizer.startListening(KWS_SEARCH);
}
}
}
};
В нем мы просто проверяем, нет ли еще чего-то в очереди на синтез, и включаем распознавание активационной фразы, если ничего больше нет.
Вместо заключения
Всё? Да! Как видишь, быстро и качественно распознавать речь прямо на устройстве совсем несложно — благодаря наличию таких замечательных проектов, как Pocketsphinx. Он предоставляет очень удобный API, который можно использовать в решении задач, связанных с распознаванием голосовых команд. В данном примере мы прикрутили распознавание к вполне конкретной задаче — голосовому управлению устройствами умного дома. За счет локального распознавания мы добились очень высокой скорости работы и минимизировали ошибки.
Понятно, что тот же код можно использовать и для других задач, связанных с голосом. Это необязательно должен быть именно умный дом. Все исходники, а также саму сборку приложения можешь найти в репозитории приложения Veravoice на GitHub.
И все же после всего сказанного автора не покидает чувство, что технология еще не то что далека от совершенства — она вообще сырая. Готовые решения (будь то распознавание речи для организаций или же системы типа «умный дом») стоят совсем недешево, а чтобы самому разобраться во всем, нужно иметь квалификацию начинающего программиста (это как минимум). Поэтому на данном этапе развития системы распознавания речи для Linux больше подходят истинным патриотам этой ОС, не боящимся экспериментов и головоломок.