Донецкий техникум промышленной автоматики

Надшвидке розпізнавання мови без серверів на реальному прикладі

  1. З чого раптом?
  2. Навіщо нам щось ще крім Яндекса і Google?
  3. Так Android же вміє розпізнавати мову без інтернету!
  4. Що таке Pocketsphinx
  5. транскрипції
  6. Голосова активація
  7. запускаємо распознование
  8. Як синтезувати мова

У цій статті я детально розповім і покажу, як правильно і швидко прикрутити розпізнавання російської мови на движку Pocketsphinx (Для iOS порт OpenEars ) На реальному Hello World прикладі управління домашньою технікою.
Чому саме домашньою технікою? Та тому що завдяки такому прикладу можна оцінити ту швидкість і точність, якої можна досягти при використанні повністю локального розпізнавання мови без серверів типу Google ASR або Яндекс SpeechKit.
До статті я також додаю всі вихідні програми і саму збірку під Android .

З чого раптом?


Натрапивши недавно на статтю про прикручуванні Яндекс SpeechKit-а до iOS додатком , я задав запитання автору, чому для своєї програми він захотів використовувати саме серверне розпізнавання мови (на мою думку, це було зайвим і призводило до деяких проблем). На що отримав зустрічне запитання про те, чи не міг би я більш детально описати застосування альтернативних способів для проектів, де немає необхідності розпізнавати що завгодно, а словник складається з кінцевого набору слів. Та ще й з прикладом практичного застосування ...

Навіщо нам щось ще крім Яндекса і Google?


Як того самого «практичного застосування» я вибрав тему голосового управління розумним будинком.
Чому саме такий приклад? Тому що на ньому можна побачити ті кілька переваг повністю локального розпізнавання мови перед розпізнаванням з використанням хмарних рішень. А саме:
  • Швидкість - ми не залежимо від серверів і тому не залежимо від їх доступності, пропускної спроможності і т.п. факторів
  • Точність - наш движок працює тільки з тим словником, який цікавить наш додаток, підвищуючи тим самим якість розпізнавання
  • Вартість - нам не доведеться платити за кожен запит до сервера
  • Голосова активація - як додатковий бонус до перших пунктів - ми можемо постійно «слухати ефір», не витрачаючи при цьому свій трафік і не навантажуючи сервера

Примітка

Відразу обмовлюся, що ці переваги можна вважати перевагами тільки для певного класу проектів, де ми точно заздалегідь знаємо, яким словником і який граматикою буде оперувати користувач. Тобто, коли нам не треба розпізнати текст (наприклад, СМС повідомлення, або пошуковий запит). У зворотному випадку без хмарного розпізнавання не обійтися.


Так Android же вміє розпізнавати мову без інтернету!

Так-так ... Тільки на JellyBean. І тільки з півметра, не більше. І це розпізнавання - це та ж диктування, тільки з використанням набагато меншою моделі. Так що керувати нею і налаштовувати її ми теж не можемо. І що вона поверне нам в наступний раз - невідомо. Хоча для СМС-ок в самий раз!

Що будемо робити?



Будемо реалізовувати голосовий пульт управління побутовою технікою, який буде працювати точно і швидко, з кількох метрів і навіть на дешевому гальмівному непотребі дуже недорогих Android смартфонах, планшетах і годинах.
Логіка буде простий, але дуже практичною. Активуємо мікрофон і вимовляємо одне або кілька назв пристроїв. Додаток їх розпізнає і включає-вимикає їх в залежності від поточного стану. Або отримують від них стан і вимовляє його приємним жіночим голосом. Наприклад, поточна температура в кімнаті.
Мікрофон будемо активувати або голосом, або натисканням на іконку мікрофона, або навіть просто поклавши руку на екран. Екран в свою чергу може бути і повністю вимкненим.
Варіантів практичного застосування маса

Вранці, не відкриваючи очей, грюкнули долонею по екрану смартфона на тумбочці і командуємо «Доброго ранку!» - запускається скрипт, включається і дзижчить кавоварка, лунає приємна музика, розсуваються штори.
Повісимо по дешевому (тисячі по 2, не більше) смартфону в кожній кімнаті на стіні. Заходимо додому після роботи і командуємо в порожнечу «Розумний будинок! Світло, телевізор! »- що відбувається далі, думаю, говорити не треба.


На відео показано, що вийшло в результаті . Далі ж мова піде про технічну реалізації з витягами з реально працюючого коду і трохи теорії.

Що таке Pocketsphinx



Pocketsphinx - це движок розпізнавання з відкритим вихідним кодом під Android. У нього також є порт під iOS , WindowsPhone , і навіть JavaScript .
Він дозволить нам розпочати розпізнавання мови прямо на пристрої і при цьому налаштувати його саме під наші завдання. Також він пропонує функцію голосового активації «з коробки» (див далі).
Ми зможемо «згодувати» движку розпізнавання російську мовну модель (ви можете знайти її в исходниках) і граматику призначених для користувача запитів. Це саме те, що буде розпізнавати наш додаток. Нічого іншого воно розпізнати не зможе. А отже, практично ніколи не видасть щось, чого ми не очікуємо.
граматика JSGF Формат граматики JSGF використовується Pocketsphinx, як і багатьма іншими подібними проектами. У ньому можна з достатньою гнучкістю описати ті варіанти фраз, які буде вимовляти користувач. У нашому випадку граматика буде будуватися з назв пристроїв, які є в нашій мережі, приблизно так:
<Commands> = лапм | монітор | температура;

Pocketsphinx також може працювати по статистичної моделі мови, що дозволяє розпізнавати спонтанну мова, не описується контекстно-вільною граматикою. Але для нашої задачі це як раз не потрібно. Наша граматика буде складатися тільки з назв пристроїв. Після процесу розпізнавання Pocketsphinx поверне нам звичайну рядок тексту, де пристрої будуть йти один за іншим.
#JSGF V1.0; grammar commands; public <command> = <commands> +; <Commands> = лапм | монітор | температура;
Знак плюса позначає, що користувач може назвати не одне, а кілька пристроїв поспіль.
Додаток отримує список пристроїв від контролера розумного будинку (див далі) і формує таку граматику в класі Grammar .

транскрипції



Граматика описує те, що може говорити користувач. Для того, щоб Pocketsphinx знав, як він це буде вимовляти, необхідно для кожного слова з граматики написати, як воно звучить у відповідному мовному моделі. Тобто транскрипцію кожного слова. Це називається словник.
Транскрипції описуються за допомогою спеціального синтаксису. наприклад:
розумний uu mn ay j будинок d oo m
В принципі, нічого складного. Подвійна голосна в транскрипції позначає наголос. Подвійна згодна - м'яку приголосну, за якою йде голосна. Всі можливі комбінації для всіх звуків російської мови можна знайти в самій мовній моделі .
Зрозуміло, що заздалегідь описати все транскрипції в нашому додатку ми не можемо, тому що ми не знаємо заздалегідь тих назв, які користувач дасть своїм пристроям. Тому ми будемо гененріровать «на льоту» такі транскрипції за деякими правилами російської фонетики. Для цього можна реалізувати ось такий клас PhonMapper , Який зможе отримувати на вхід рядок і генерувати для неї правильну транскрипцію.

Голосова активація


Це можливість движка розпізнавання мови весь час «слухати ефір» з метою реакції на заздалегідь задану фразу (або фрази). При цьому всі інші звуки і мова будуть відкидатися. Це не те ж саме, що описати граматику і просто включити мікрофон. Наводити тут теорію цього завдання і механіку того, як це працює, я не буду. Скажу лише тільки, що недавно програмісти, які працюють над Pocketsphinx, реалізували таку функцію, і тепер вона доступна «з коробки» в API.
Одне варто згадати обов'язково. Для активационной фрази потрібно не тільки вказати транскрипцію, а й підібрати підходяще значення порога чутливості. Занадто мале значення призведе до безлічі помилкових спрацьовувань (це коли ви не говорили активаційну фразу, а система її розпізнає). А надто висока - до несприйнятливості. Тому дана настройка має особливу важливість. Приблизний діапазон значень - від 1e-1 до 1e-40 в залежності від активаційної фрази.
Активація по датчику наближення Це завдання специфічна саме для нашого проекту і безпосередньо до розпізнавання не має відношення. Код можна побачити прямо в головною активності .
Вона реалізує SensorEventListener і в момент наближення (значення сенсора менше максимального) включає таймер, перевіряючи після деякої затримки, заблоковано досі датчик. Це зроблено для виключення помилкових спрацьовувань.
Коли датчик знову не заблоковано, ми зупиняємо розпізнавання, отримуючи результат (див опис далі).

запускаємо распознование


Pocketsphinx надає зручний API для конфігурації і запуску процесу розпізнавання. Це класи SppechRecognizer і 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);
Тут ми спершу копіюємо всі необхідні файли на диск (Pocketpshinx вимагає наявності на диску акустичної моделі, граматики і словника з транскрипціями). Потім конфигурируется сам движок розпізнавання. Вказуються шляхи до файлів моделі і словника, а також деякі параметри (поріг чутливості для активационной фрази). Далі конфигурируется шлях до файлу з граматикою, а також активаційна фраза.
Як видно з цього коду, один движок конфигурируется відразу і для граматики, і для розпізнавання активационной фрази. Навіщо так робиться? Для того, щоб ми могли швидко перемикатися між тим, що в даний момент потрібно розпізнавати. Ось як виглядає запуск процесу розпізнавання активационной фрази:
mRecognizer.startListening (KWS_SEARCH);
А ось так - распозанваніе мови по заданій граматиці:
mRecognizer.startListening (COMMAND_SEARCH, 3000);
Другий аргумент (необов'язковий) - кількість мілісекунд, після якого розпізнавання буде автоматично завершуватися, якщо ніхто нічого не говорить.
Як бачите, можна використовувати тільки один движок для вирішення обох завдань.

Як отримати результат розпізнавання


Щоб отримати результат розпізнавання, потрібно також вказати слухача подій, імплементує інтерфейс RecognitionListener.
У нього є кілька методів, які викликаються pocketsphinx-му при настанні однієї з подій:
  • onBeginningOfSpeech - движок почув якийсь звук, може бути це мова (а може бути і немає)
  • onEndOfSpeech - звук закінчився
  • onPartialResult - є проміжні результати розпізнавання. Для активационной фрази це означає, що вона спрацювала. Аргумент Hypothesis містить дані про розпізнавання (рядок і score)
  • onResult - кінцевий результат розпізнавання. Цей метод буде викликати після виклику методу stop у SpeechRecognizer. Аргумент 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);}}); }
Тут ми спершу граємо невеликий сигнал для оповіщення користувача, що ми його почули і готові до його команді. На цей час мікрофон матиме однаковий бути вимкнений. Тому ми запускаємо розпізнавання після невеликого таймаута (трохи більше, ніж тривалість сигналу, щоб не почути його відлуння). Також запускається потік, який зупинить розпізнавання примусово, якщо користувач говорить занадто довго. В даному випадку це 3 секунди.

Як перетворити розпізнану рядок в команди


Ну тут все вже специфічно для конкретного додатка. У випадку з голим прикладом, ми просто витягуємо з рядка назви пристроїв, шукаємо по ним потрібний пристрій і або змінюємо його стан за допомогою HTTP запиту на контролер розумного будинку, або повідомляємо його поточний стан (як у випадку з термостатом). Цю логіку можна побачити в класі Controller .

Як синтезувати мова


Синтез мови - це операція, зворотна розпізнаванню. Тут навпаки - потрібно перетворити рядок тексту в мову, щоб її почув користувач.
У випадку з термостатом ми повинні змусити наше Android пристрій вимовити поточну температуру. За допомогою API TextToSpeech це зробити досить просто (спасибі Гуглу за прекрасний жіночий 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, який можна використовувати в рішенні задач, пов'язаних з розпізнаванням голосових команд.
В даному прикладі ми прикрутили розпізнавання до цілком кокрентной завданню - голосовому управління пристроями розумного будинку. За рахунок локального розпізнавання ми домоглися дуже високій швидкості роботи і мінімізували помилки.
Зрозуміло, що той же код можна використовувати і для інших завдань, пов'язаних з голосом. Це не обов'язково повинен бути саме розумний будинок.
Всі вихідні, а також саму збірку програми ви можете знайти в репозиторії на GitHub .
Також на моєму каналі в YouTube ви можете побачити деякі інші реалізації голосового управління, і не тільки системами розумних будинків.З чого раптом?
Навіщо нам щось ще крім Яндекса і Google?
Чому саме домашньою технікою?
З чого раптом?
Навіщо нам щось ще крім Яндекса і Google?
Чому саме такий приклад?
Що будемо робити?
Навіщо так робиться?
Null?
І це все?