Введение
В последние несколько лет голосовые интерфейсы окружают нас все плотнее. То, что когда-то демонстрировалось только в фильмах о далеком будущем, оказалось вполне реальным. Дело дошло уже до встраивания движков для синтеза (Text To Speech — TTS) и распознавания (Automatic Speech Recognition — ASR) речи в мобильные телефоны. Более того, появились вполне доступные API для встраивания ASR и TTS в приложения.
Ныне создавать программы с голосовым интерфейсом может любой желающий (не поскупившийся заплатить за движок). Наш обзор будет посвящен именно использованию имеющихся движков (на примере Nuance) а не созданию таковых. Также будут даны общие сведения необходимые каждому программисту впервые сталкивающемуся с речевыми интерфейсами. Статья также может быть полезна руководителям проектов, пытающимся оценить целесообразность интеграции голосовых технологий в их продукты.
Итак, начнем…
Но для затравки — анекдот:
Урок русского языка в грузинской школе.
Учитель говорит: «Дети, запомните: слова сол, фасол и вермишел пишутся с мягким знаком, а слова вилька, булька, тарелька – без мягкого знака. Дети, запомните, потому что понять это невозможно!»
Раньше этот анекдот казался мне смешным. Теперь — скорее жизненным. Почему так? Сейчас постараюсь объяснить…
1. Фонемы
Говоря о речи (уже смешно) нам прежде всего придется разобраться с понятием фонемы. Говоря попросту — фонема это отдельный звук, который может быть произнесен и распознан человеком. Но такого определения конечно мало, ибо произнести можно очень много звуков, а набор фонем в языках ограничен. Хочется иметь более строгое определение. А значит — нужно идти на поклон к филологам. Увы, филологи и сами не могут сойтись во мнениях что это такое (да им и не особо надо), но имеют несколько подходов. Один связывает фонемы со смыслом. Например, английская Wiki говорит нам «The smallest contrastive linguistic unit which may bring about a change of meaning». другие — с особенностями восприятия. Так, наш соотечественник Н. Трубецкой писал «Фонологические единицы, которые с точки зрения данного языка невозможно разложить на более краткие следующие друг за другом фонологические единицы мы называем фонемами». И в том и в другом определении есть важные для нас уточнения. С одной стороны, изменение фонемы может (но не обязано) поменять смысл слова. Так, «код» и «кот» — будут восприняты как два разных слова. С другой — вы можете произнести «музей» или «музэй» и смысл не изменится. Разве что ваши собеседники смогут как-то классифицировать ваш акцент. Важна и неделимость фонем. Но, как верно замечено у Трубецкого — она может зависеть от языка. Там, где человек одной национальности слышит один звук, кто-то другой может услышать два, следующих друг за другом. Хочется, однако, иметь фонетические инварианты, пригодные для всех языков, а не только какого-то одного.
2. Фонетический алфавит
Чтобы как-то утрясти определения еще в далеком 1888 году был создан международный фонетический алфавит (IPA). Алфавит этот хорош тем, что не зависит от конкретного языка. Т.е. рассчитан как бы на «сверхчеловека» который может произносить и распознавать звуки практически всех имеющихся живых (и даже мертвых) языков. Алфавит IPA постепенно изменялся вплоть до наших дней (2005 год). Поскольку создан он был в основном в докомпьютерную эпоху, то филологи рисовали символы обозначающие звуки как бог на душу положит. Они конечно как-то ориентировались на латинский алфавит, но весьма и весьма условно. Как результат сейчас символы IPA хоть и есть в Unicode, но вводить их с клавиатуры весьма непросто. Тут читатель может спросить — а зачем вообще нужен IPA простым людям? Где я могу увидеть хотя бы примеры слов, записанных фонетически? Мой ответ — простому человеку IPA в общем-то знать не обязательно. Но, при всем этом увидеть его можно очень легко — во многих статьях Wiki, касающихся географических названий, фамилий и имен собственных. Зная IPA вы всегда можете сверить правильность произношения того или иного названия на незнакомом вам языке. Например, хотите произносить «Париж» как француз? Вот вам пожалуйста — [pa?i].
3. Фонетическая транскрипция
Внимательный пользователь Wiki может правда заметить, что иногда странные значки фонетического алфавита стоят внутри квадратных скобок — [m??skva], а иногда — внутри косых черт (слэшей) — /?l?nd?n/. В чем же разница? В квадратных скобках записывается т.н. narrow, сиречь «узкая» транскрипция. В отечественной литературе она зовется фонетической. В слэшах же пишется broad, т.е. «широкая» или фонематическая транскрипция. Практический смысл здесь следующий: фонетическая транскрипция дает предельно точное произношение, которое в некотором смысле идеально и независимо от акцента говорящего. Иными словами — имея фонетическую транскрипцию мы можем сказать «кокни произнесет это слово так». Фонематическая же транскрипция позволяет вариации. Так, австралийском и канадском английском произносимый звук при одинаковой записи в // может быть другим. По правде говоря, даже узкая транскрипция все еще не однозначна. Т.е. довольно далека от waw-файла. Мужской, женский и детский голос произнесут одну и ту же фонему по-разному. Также не принимается во внимание общая скорость речи, ее громкость и базовая высота голоса. Собственно, эти отличия и делают задачу генерации и распознавания речи нетривиальной. Далее по тексту я всегда буду пользоваться IPA в узкой транскрипции, если не оговорено другое. При этом прямое использование IPA я постараюсь свести к разумному минимуму.
4. Языки
Каждому живому естественному языку свойственен свой набор фонем. Более точно — это свойство речи, ибо вообще говоря, можно знать язык не имея возможности произносить слов (как обучаются языку глухо-немые). Фонетический состав языков различен, примерно так же как различны алфавиты. Соответственно, разнится и фонетическая сложность языка. Она складывается из двух составляющих. Во-первых, сложность преобразования графем в фонемы (мы помним, что англичане пишут «Манчестер» а читают «Ливерпуль») и сложности произнесения самих звуков (фонем) во-вторых. Сколько фонем обычно содержит язык? Несколько десятков. С детства нас учили что русское произношение простое как три копейки, и все читается как пишется, в отличии от европейских языков. Конечно нас обманывали! Если читать слова буквально так, как они написаны, вас хоть и поймут, но не всегда верно. Но уж русским точно не посчитают. Кроме того, в дело вступает такая жуткая для европейца вещь как ударение. Вместо того, чтобы ставить его в начале (как англичане) или в конце (как французы) у нас оно гуляет по всему слову как бог на душу положит, при этом меняя смысл. Дoрoги и дороги — два различных слова, и даже части речи. Сколько фонем в русском языке? Nuance насчитывает их 54 штуки. Для сравнения — в английском всего 45 фонем, а во французском и того меньше — 34. Не зря аристократы считали его легким для изучения языком пару веков назад! Конечно — русский не самый сложный язык в Европе, но один из (заметьте, я еще молчу о грамматике).
5. X-SAMPA и LH+
Поскольку вводить фонетическую транскрипцию с клавиатуры людям хотелось давно, еще до широкого распространения Unicode, то были разработаны нотации, позволяющие обойтись только символами таблицы ASCII. Две наиболее распространенных из них это X-SAMPA — творение профессора Джона Уэлса, и LH+ — внутренний формат компании Lernout & Hauspie, технологии которой были в дальнейшем куплены Nuance Communications. Между X-SAMPA и LH+ есть довольно существенная разница. Формально, X-SAMPA — это просто нотация, позволяющая по определенным правилам записывать те же фонемы IPA, только с помощью ASCII. Иное дело LH+. В некотором смысле — LH+ является аналогом широкой (фонематической) транскрипции. Дело в том, что для каждого языка, один и тот же символ LH+ может обозначать разные фонемы IPA. С одной стороны — это хорошо, т.к. укорачивается запись, и не нужно кодировать все возможные символы IPA, с другой — возникает неоднозначность. И каждый раз для трансляции в IPA нужно держать перед собой таблицу соответствия. Однако, печальнее всего то, что строку записанную в LH+ может правильно произнести только «голос» для определенного языка.
6. Голоса
Нет, речь пойдет не о тех голосах, которые часто слышат в голове программисты, которые в прошлом написали слишком много плохого кода. Скорее о тех, которые так часто ищут на трекерах и файлопомойках обладатели навигаторов и прочих мобильных устройств. Голоса эти имеют даже имена. Слова «Милена» и «Катерина» многое говорят бывалому пользователю голосовых интерфейсов. Что же это такое? Грубо говоря — это наборы данных, подготовленные различным компаниями (типа той же Nuance) которые позволяют компьютеру преобразовывать фонемы в звук. Голоса бывают женские и мужские, и стоят немалых денег. В зависимости от платформы и фирмы-разработчика с вас могут потребовать 2-5 тыс. долларов за голос. Таким образом, если вы хотите создать интерфейс хотя бы на 5 наиболее распространенных европейских языках, то счет может пойти на десятки тысяч. Разумеется, речь именно о программном интерфейсе. Итак, голос специфичен для языка. Отсюда же происходит и привязка его к фонетической транскрипции. Это непросто осознать по-первости, но анекдот в начале статьи — сущая правда. Люди с одним родным языком обычно просто не в состоянии произнести фонемы другого, которых нет в их родном языке. И, что еще хуже — не только отдельные фонемы но и определенные их сочетания. Так, если в твоем языке слово никогда не заканчивается на мягкое «л» то и произнести его мы не сможем (поначалу).
То же самое и с голосами. Голос заточен на произнесение только тех фонем, которые имеются в языке. Более того — в конкретном диалекте языка. Т.е. голоса для канадского французского и французского французского будут не только отличаться по звучанию, но и иметь разный набор произносимых фонем. Это, кстати, удобно фирмам производителям движков ASR и TTS, т.к. каждый язык можно продавать за отдельные деньги. С другой стороны — можно понять и их. Создание голоса дело довольно трудоемкое, и затратное по деньгам. Возможно именно по этому до сих пор не существует сколь-нибудь широкого рынка Open Source решений для большинства языков.
Казалось бы, ничто не мешает создать «универсальный» голос, который будет уметь произносить все фонемы IPA, и таким образом решит проблему многоязычных интерфейсов. Но этого почему-то никто не делает. Скорее всего, это и невозможно. Т.е. говорить-то он может и будет, но каждый носитель языка будет недоволен недостаточной «натуральностью» произношения. Звучать это будет примерно как русский в устах мало практиковавшегося англичанина или английский в устах француза. Так что, если хотите многоязычности — приготовьтесь раскошелиться.
7. Пример использования TTS API
Чтобы дать читателю представление о том, как процесс работы с TTS выглядит на нижнем уровне (используется С++) я приведу пример синтеза речи на базе движка Nuance. Разумеется это неполный пример, его нельзя не только запустить но даже скомпилировать, но представление о процессе он дает. Все функции кроме TTS_Speak() нужны как «обвязка» для нее.
TTS_Initialize() — служит для инициализации движка
TTS_Cleanup() — для деинициализации
TTS_SelectLanguage — выбирает язык и настраивает параметры распознавания.
TTS_Speak() — собственно генерирует звуковые отсчеты
TTS_Callback() — вызывается, когда очередная порция звуковых данных готова к проигрыванию а также в случае других событий.
static const NUAN_TCHAR * _dataPathList[] = {
__TEXT("\\lang\\"),
__TEXT("\\tts\\"),
};
static VPLATFORM_RESOURCES _stResources = {
VPLATFORM_CURRENT_VERSION,
sizeof(_dataPathList)/sizeof(_dataPathList[0]),
(NUAN_TCHAR **)&_dataPathList[0],
};
static VAUTO_INSTALL _stInstall = {VAUTO_CURRENT_VERSION};
static VAUTO_HSPEECH _hSpeech = {NULL, 0};
static VAUTO_HINSTANCE _hTtsInst = {NULL, 0};
static WaveOut * _waveOut = NULL;
static WaveOutBuf * _curBuffer = NULL;
static int _volume = 100;
static int _speechRate = 0; // use default speech rate
static NUAN_ERROR _Callback (VAUTO_HINSTANCE hTtsInst,
VAUTO_OUTDEV_HINSTANCE hOutDevInst,
VAUTO_CALLBACKMSG * pcbMessage,
VAUTO_USERDATA UserData);
static const TCHAR * _szLangTLW = NULL;
static VAUTO_PARAMID _paramID[] = {
VAUTO_PARAM_SPEECHRATE,
VAUTO_PARAM_VOLUME
};
static NUAN_ERROR _TTS_GetFrequency(VAUTO_HINSTANCE hTtsInst, short *pFreq) {
NUAN_ERROR Error = NUAN_OK;
VAUTO_PARAM TtsParam;
/*-- get frequency used by current voicefont --*/
TtsParam.eID = VAUTO_PARAM_FREQUENCY;
if (NUAN_OK != (Error = vauto_ttsGetParamList (hTtsInst, &TtsParam, 1)) ) {
ErrorV(_T("vauto_ttsGetParamList rc=0x%1!x!\n"), Error);
return Error;
}
switch(TtsParam.uValue.usValue)
{
case VAUTO_FREQ_8KHZ: *pFreq = 8000;
break;
case VAUTO_FREQ_11KHZ: *pFreq = 11025;
break;
case VAUTO_FREQ_16KHZ: *pFreq = 16000;
break;
case VAUTO_FREQ_22KHZ: *pFreq = 22050;
break;
default: break;
}
return NUAN_OK;
}
int TTS_SelectLanguage(int langId) {
NUAN_ERROR nrc;
VAUTO_LANGUAGE arrLanguages[16];
VAUTO_VOICEINFO arrVoices[4];
VAUTO_SPEECHDBINFO arrSpeechDB[4];
NUAN_U16 nLanguageCount, nVoiceCount, nSpeechDBCount;
nLanguageCount = sizeof(arrLanguages)/sizeof(arrLanguages[0]);
nVoiceCount = sizeof(arrVoices) /sizeof(arrVoices[0]);
nSpeechDBCount = sizeof(arrSpeechDB)/sizeof(arrSpeechDB[0]);
int nVoice = 0, nSpeechDB = 0;
nrc = vauto_ttsGetLanguageList( _hSpeech, &arrLanguages[0], &nLanguageCount);
if(nrc != NUAN_OK){
TTS_ErrorV(_T("vauto_ttsGetLanguageList rc=0x%1!x!\n"), nrc);
return 0;
}
if(nLanguageCount == 0 || nLanguageCount<=langId){
TTS_Error(_T("vauto_ttsGetLanguageList: No proper languages found.\n"));
return 0;
}
_szLangTLW = arrLanguages[langId].szLanguageTLW;
NUAN_TCHAR* szLanguage = arrLanguages[langId].szLanguage;
nVoice = 0; // select first voice;
NUAN_TCHAR* szVoiceName = arrVoices[nVoice].szVoiceName;
nSpeechDB = 0; // select first speech DB
{
VAUTO_PARAM stTtsParam[7];
int cnt = 0;
// language
stTtsParam[cnt].eID = VAUTO_PARAM_LANGUAGE;
_tcscpy(stTtsParam[cnt].uValue.szStringValue, szLanguage);
cnt++;
// voice
stTtsParam[cnt].eID = VAUTO_PARAM_VOICE;
_tcscpy(stTtsParam[cnt].uValue.szStringValue, szVoiceName);
cnt++;
// speechbase parameter - frequency
stTtsParam[cnt].eID = VAUTO_PARAM_FREQUENCY;
stTtsParam[cnt].uValue.usValue = arrSpeechDB[nSpeechDB].u16Freq;
cnt++;
// speechbase parameter - reduction type
stTtsParam[cnt].eID = VAUTO_PARAM_VOICE_MODEL;
_tcscpy(stTtsParam[cnt].uValue.szStringValue, arrSpeechDB[nSpeechDB].szVoiceModel);
cnt++;
if (_speechRate) {
// Speech rate
stTtsParam[cnt].eID = VAUTO_PARAM_SPEECHRATE;
stTtsParam[cnt].uValue.usValue = _speechRate;
cnt++;
}
if (_volume) {
// Speech volume
stTtsParam[cnt].eID = VAUTO_PARAM_VOLUME;
stTtsParam[cnt].uValue.usValue = _volume;
cnt++;
}
nrc = vauto_ttsSetParamList(_hTtsInst, &stTtsParam[0], cnt);
if(nrc != NUAN_OK){
ErrorV(_T("vauto_ttsSetParamList rc=0x%1!x!\n"), nrc);
return 0;
}
}
return 1;
}
int TTS_Initialize(int defLanguageId) {
NUAN_ERROR nrc;
nrc = vplatform_GetInterfaces(&_stInstall, &_stResources);
if(nrc != NUAN_OK){
Error(_T("vplatform_GetInterfaces rc=%1!d!\n"), nrc);
return 0;
}
nrc = vauto_ttsInitialize(&_stInstall, &_hSpeech);
if(nrc != NUAN_OK){
Error(_T("vauto_ttsInitialize rc=0x%1!x!\n"), nrc);
TTS_Cleanup();
return 0;
}
nrc = vauto_ttsOpen(_hSpeech, _stInstall.hHeap, _stInstall.hLog, &_hTtsInst, NULL);
if(nrc != NUAN_OK){
ErrorV(_T("vauto_ttsOpen rc=0x%1!x!\n"), nrc);
TTS_Cleanup();
return 0;
}
// Ok, time to select language
if(!TTS_SelectLanguage(defLanguageId)){
TTS_Cleanup();
return 0;
}
// init Wave out device
{
short freq;
if (NUAN_OK != _TTS_GetFrequency(_hTtsInst, &freq))
{
TTS_ErrorV(_T("_TTS_GetFrequency rc=0x%1!x!\n"), nrc);
TTS_Cleanup();
return 0;
}
_waveOut = WaveOut_Open(freq, 1, 4);
if (_waveOut == NULL){
TTS_Cleanup();
return 0;
}
}
// init TTS output
{
VAUTO_OUTDEVINFO stOutDevInfo;
stOutDevInfo.hOutDevInstance = _waveOut;
stOutDevInfo.pfOutNotify = TTS_Callback; // Notify using callback!
nrc = vauto_ttsSetOutDevice(_hTtsInst, &stOutDevInfo);
if(nrc != NUAN_OK){
ErrorV(_T("vauto_ttsSetOutDevice rc=0x%1!x!\n"), nrc);
TTS_Cleanup();
return 0;
}
}
// OK TTS engine initialized
return 1;
}
void TTS_Cleanup(void) {
if(_hTtsInst.pHandleData){
vauto_ttsStop(_hTtsInst);
vauto_ttsClose(_hTtsInst);
}
if(_hSpeech.pHandleData){
vauto_ttsUnInitialize(_hSpeech);
}
if(_waveOut){
WaveOut_Close(_waveOut);
_waveOut = NULL;
}
vplatform_ReleaseInterfaces(&_stInstall);
memset(&_stInstall, 0, sizeof(_stInstall));
_stInstall.fmtVersion = VAUTO_CURRENT_VERSION;
}
int TTS_Speak(const TCHAR * const message, int length) {
VAUTO_INTEXT stText;
stText.eTextFormat = VAUTO_NORM_TEXT;
stText.szInText = (void*) message;
stText.ulTextLength = length * sizeof(NUAN_TCHAR);
TraceV(_T("TTS_Speak: %1\n"), message);
NUAN_ERROR rc = vauto_ttsProcessText2Speech(_hTtsInst, &stText);
if (rc == NUAN_OK) {
return 1;
}
if (rc == NUAN_E_TTS_USERSTOP) {
return 2;
}
ErrorV(_T("vauto_ttsProcessText2Speech rc=0x%1!x!\n"), rc);
return 0;
}
static NUAN_ERROR TTS_Callback (VAUTO_HINSTANCE hTtsInst,
VAUTO_OUTDEV_HINSTANCE hOutDevInst,
VAUTO_CALLBACKMSG * pcbMessage,
VAUTO_USERDATA UserData) {
VAUTO_OUTDATA * outData;
switch(pcbMessage->eMessage){
case VAUTO_MSG_BEGINPROCESS:
WaveOut_Start(_waveOut);
break;
case VAUTO_MSG_ENDPROCESS:
break;
case VAUTO_MSG_STOP:
break;
case VAUTO_MSG_OUTBUFREQ:
outData = (VAUTO_OUTDATA *)pcbMessage->pParam;
memset(outData, 0, sizeof(VAUTO_OUTDATA));
{
WaveOutBuf * buf = WaveOut_GetBuffer(_waveOut);
if(buf){
VAUTO_OUTDATA * outData = (VAUTO_OUTDATA *)pcbMessage->pParam;
outData->eAudioFormat = VAUTO_16LINEAR;
outData->pOutPcmBuf = WaveOutBuf_Data(buf);
outData->ulPcmBufLen = WaveOutBuf_Size(buf);
_curBuffer = buf;
break;
}
TTS_Trace(_T("VAUTO_MSG_OUTBUFREQ: processing was stopped\n"));
}
return NUAN_E_TTS_USERSTOP;
case VAUTO_MSG_OUTBUFDONE:
outData = (VAUTO_OUTDATA *)pcbMessage->pParam;
WaveOutBuf_SetSize(_curBuffer, outData->ulPcmBufLen);
WaveOut_PutBuffer(_waveOut, _curBuffer);
_curBuffer = NULL;
break;
default:
break;
}
return NUAN_OK;
}
Как может заметить читатель, код довольно громоздок, и простая (казалось бы) функциональность требует большого числа предварительных настроек. Увы, это обратная сторона гибкости движка. Разумеется, API других движков и для других языков может быть существенно проще к компактнее.
8. Снова фонемы
Поглядев на API, читатель может спросить — а зачем нам вообще нужны фонемы, если TTS (Text-To-Speech) умеет прямо конвертировать текст в речь. Умеет, но тут есть одно «но». Хорошо преобразуются в речь знакомые движку слова. Гораздо хуже дело обстоит со словами «незнакомыми». Такими как топонимы, имена собственные и т.п. Это особенно хорошо видно на примере многонациональных стран, таких к примеру, как Россия. Названия городам и весям на территории одной вечно-шестой части суши давались разными людьми, на разных языках и в разное время. Необходимость записать их русскими буквами сыграла плохую шутку с национальными языками. Фонемы татар, ненцев, абхазцев, казахов, якутов, бурят оказались втиснуты в прокрустово ложе русского языка. В котором хоть и много фонем, но все же недостаточно чтобы передать все языки народов бывшего Союза. Но дальше хуже — если фонетическая запись хоть как-то похожа на оригинал, то прочтение движком TTS названия типа «Кючук-Кайнарджи» ничего кроме смеха не вызывает.
Однако, наивно думать что это только проблема русского языка. Аналогичные сложности есть и в более однородных по населению странах. Так, во французском языке буквы p, b, d, t, s в конце слов обычно не читаются. Но если мы возьмем топонимы, то тут уже вступают в силу местные традиции. Так, в слове Paris 's' в конце дейстивтельно не произносится, а в слове 'Valluris' — наоборот. Разница в том, что Париж расположен на севере Франции, а Валлорис — на юге, в Провансе, где правила произношения несколько другие. Именно поэтому, все же желательно иметь фонетическую транскрипцию для слов. Обычно карты поставляются с ней. Правда, единства в формате не наблюдается. Так, NavTeq традиционно использует транскрипцию X-SAMPA, а TomTom — LH+. Хорошо, если ваш TTS-движок воспринимает обе, а если нет? Тут приходится извращаться. Например — конвертировать одну транскрипцию в другую, что само по себе нетривиально. Если же фонетической информации вовсе нет, то движок имеет собственные методы ее получения. Если говорить о движке Nuance — это «Data Driven Grapheme To Phoneme» (DDG2P) и «Common Linguistic Component» (CLC). Однако использование этих вариантов — уже крайняя мера.
9. Специальные последовательности
Nuance дает не только возможность произносить текст или фонетическую запись, но и динамически переключаться между ними. Для этого используется escape-последовательность вида: <ESC>/+
Вообще, с помощью escape-последовательностей можно задавать множество параметров. В общей форме это выглядит так:
<ESC>\<param>=<value>
Например,
\x1b\rate=110\ — устанавливает скорость произношения
\x1b\vol=5\ — устанавливает громкость
\x1b\audio=«beep.wav»\ — вставляет в звуковой поток данные из wav-файла.
Аналогичным образом можно заставлять движок читать слово по буквам, вставлять паузы, менять голос (например — с мужского на женский) и многое другое. Конечно, не все последовательности могут вам пригодится, но в целом это очень полезная возможность.
10. Словари
Иногда требуется произносить какой-то набор слов определенным образом (сокращения, аббревиатуры, имена собственные, etc), но н хочется в каждом случае заменять текст на фонетическую транскрипцию (да и не всегда это возможно). В этом случае на помощь приходят словари. Что такое словарь в терминологии Nuance? Это файл с набором пар: <текст> <транскрипция>. Этот файл компилируется и затем загружается движком. При произношении, движок проверяет присутствует ли слово/текст в словаре и в случае положительного ответа заменяет на его фонетическую транскрипцию. Вот например, словарь содержащий названия улиц и площадей Ватикана.
[Header] Name=Vaticano Language=ITI Content=EDCT_CONTENT_BROAD_NARROWS Representation=EDCT_REPR_SZZ_STRING [Data] "Largo del Colonnato" // 'lar.go_del_ko.lo.'n:a.to "Piazza del Governatorato" // 'pja.t&s:a_del_go.ver.na.to.'ra.to "Piazza della Stazione" // 'pja.t&s:a_de.l:a_sta.'t&s:jo.ne "Piazza di Santa Marta" // 'pja.t&s:a_di_'san.ta_'mar.ta "Piazza San Pietro" // 'pja.t&s:a_'sam_'pjE.tro "Piazzetta Chateauneuf Du Pape" // pja.'t&s:e.t:a_Sa.to.'nef_du_'pap "Salita ai Giardini" // sa.'li.ta_aj_d&Zar.'di.ni "Stradone dei Giardini" // stra.'do.ne_dej_d&Zar.'di.ni "Via dei Pellegrini" // 'vi.a_dej_pe.l:e.'gri.ni "Via del Fondamento" // 'vi.a_del_fon.da.'men.to "Via del Governatorato" // 'vi.a_del_go.ver.na.to.'ra.to "Via della Posta" // 'vi.a_de.l:a_'pOs.ta "Via della Stazione Vaticana" // 'vi.a_de.l:a_sta.'t&s:jo.ne_va.ti.'ka.na "Via della Tipografia" // 'vi.a_de.l:a_ti.po.gra.'fi.a "Via di Porta Angelica" // 'vi.a_di_'pOr.ta_an.'d&ZE.li.ka "Via Tunica" // 'vi.a_'tu.ni.ka "Viale Centro del Bosco" // vi.'a.le_'t&SEn.tro_del_'bOs.ko "Viale del Giardino Quadrato" // vi.'a.le_del_d&Zar.'di.no_kwa.'dra.to "Viale Vaticano" // vi.'a.le_va.ti.'ka.no
11. Распознавание
Распознавание речи является еще более сложной задачей чем её синтез. Если синтезаторы кое-как работали еще в старые добрые времена, то толковое распознавание стало доступно только сейчас. Причин тут несколько, первая из которых очень напоминает проблемы обычного живого человека столкнувшегося с незнакомым языком, вторая — столкновение с текстом из незнакомой области.
Воспринимая колебания звука, напоминающие нам голос мы сначала пытаемся делить его на фонемы, вычленять знакомы звуки, которые должны у нас сложиться в слова. Если язык знаком нам, то это легко получается, если нет — то скорее всего даже «правильно» разложить речь на фонемы не удастся (помните историю про «Алла, я в бар!»). Там где нам слышится одно, произносящему — совсем другое. Происходит это потому, что годами наш мозг «натаскивался» на определенные фонемы, и со временем привык воспринимать только их. Встречая незнакомый звук, он пытается подобрать фонему родного языка [языков] наиболее близкую к услышанной. В каком-то смысле это похоже на метод векторного квантования применяемый в кодеках речи типа CELP. Не факт, что такая аппроксимация будет удачной. Именно поэтому, «удобными» для нас будут именно «родные» фонемы.
Помните, еще при СССР, учась в школе, и встречаясь с иностранцами мы пытались «транслитерировать» свое имя, говоря:
«Май нэйм из бОрис пEтрофф»
Учителя тогда ругали нас, говоря — зачем коверкаешь свою фамилию? Думаешь ему от этого понятней будет? Говори по-русски!
Увы, они и тут нас обманывали или заблуждались… Если вы смогли произнести свое имя на английский/немецкий/китайский лад, то носителю языка будет действительно легче его воспринять. Китайцы поняли это давно, и для общения с западными партнерами берут себе специальные «европейские» имена. В машинном распознавании, тот или иной язык описывается так называемой акустической моделью. Перед распознаванием текста, мы должны загрузить акустическую модель определенного языка, тем самым дав понять программе, какие фонемы ей ждать на входе.
Вторая проблема не менее сложна. Вернемся снова к нашей аналогии с живым человеком. Слушая собеседника мы подсознательно выстраиваем в голове модель того, что он скажет дальше, сиречь, создаем контекст разговора. И если ВНЕЗАПНО вставлять в повествование слова выпадающие из контекста (например «эвольвента», когда речь идет о футболе), можно вызвать у собеседника когнитивный диссонанс. Грубо говоря, у компьютера этот самый диссонанс происходит постоянно, ибо он никогда не знает чего ожидать от человека. Человеку проще — он может переспросить собеседника. А что делать компьютеру? Чтобы решить эту проблему и дать компьютеру верный контекст, применяются грамматики.
12. Грамматики
Грамматики (обычно заданные в форме BNF) как раз дают компьютеру (точнее движку ASR) представление о том, чего ждать от пользователя в данный конкретный момент. Обычно — это несколько альтернатив объединенных через 'или', но возможны и более сложные грамматики. Вот пример грамматики для выбора станций метро Казани:
#BNF+EM V1.0; !grammar test; !start <metro_KAZAN_stations>; <metro_KAZAN_stations> : "Аметьево" !id(0) !pronounce("^.'m%je.t%jjI.vo-") | "Авиастроительная" !id(1) !pronounce("^v%jI'astro-'it%jIl%jno-j^") | "Горки" !id(2) !pronounce("'gor.k%jI") | "Козья слобода" !id(3) !pronounce("'ko.z%jj^_slo-.b^.'da") | "Кремлевская" !id(4) !pronounce("kr%jIm.'l%jof.sko-.j^") | "Площадь Габдуллы Тукая" !id(5) !pronounce("'plo.S%jIt%j_go-.bdu.'li0_'tu.ko-.j^") | "Проспект победы" !id(6) !pronounce("pr^.'sp%jekt_p^.'b%je.di0") | "Северный вокзал" !id(7) !pronounce("'s%je.v%jIr.ni0j_v^g.'zal") | "Суконная слобода" !id(8) !pronounce("'su.ko-.no-.j^_slo-.b^.'da") | "Яшлек" !id(9) !pronounce("ja.'Sl%jek");
Как видим, каждая строка представляет собой одну из альтернатив, состоящих из собственно текста, целочисленного id и фонемы. Фонема в общем-то не обязательна, но с ней распознавание будет точнее.
Насколько велика может быть грамматика? Достаточно велика. Скажем, в наших экспериментах 37 тысяч альтернатив распознаются на приемлемом уровне. Гораздо хуже обстоит дело со сложными и разветвленными грамматиками. Время распознавания растет, а качество падает, причем зависимость от длинны грамматики нелинейна. Поэтому мой совет — избегайте сложных грамматик. Во всяком случае пока.
Грамматики (как и контексты) бывают статическими и динамическими. Пример статической грамматики вы уже видели, она компилируется заранее и хранится во внутреннем бинарном представлении движка. Однако порой контекст меняется во время интеракции с пользователем. Характерные пример для навигации — выбор города по первым буквам. Множество возможных вариантов для распознавания здесь меняется с каждой введенной буквой, соответственно, контекст распознавания надо постоянно перестраивать. Для этих целей и используются динамические контексты. Грубо говоря, программист компилирует грамматики «на лету» и подсовывает их движку прямо по ходу выполнения программы. Разумеется, если речь идет о мобильном устройстве, скорость обработки будет не слишком велика, так что придется ограничиться грамматиками небольшого размера (порядка 100 слов) чтобы пользовательский интерфейс не подвисал.
13. Пример использования ASR API
Распознавание текста не так однозначно, как синтез. Если пользователь просто помолчит перед микрофоном — нам придется распознавать окружающий шум. Если произнесет что-то типа «э-э-э-э» то распознавание тоже скорее всего будет неуспешным. В лучшем случае, ASR обычно возвращает нам набор вариантов (называемых еще гипотезами). Каждая гипотеза имеет определенный вес. Если грамматика большая, то вариантов распознавания может быть довольно много. В этом случае имеет смысл последовательно произнести гипотезы (например, первые пять по убыванию достоверности) и попросить пользователя выбрать одну из них. В идеале, при короткой грамматике («да»|«нет») нам вернется один вариант с высоким показателем достоверности.
Пример приведенный ниже содержит следующие функции:
ConstructRecognizer() — создает «распознаватель» и настраивает его параметры
DestroyRecognizer() — уничтожает «распознаватель»
ASR_Initialize() — инициализирует движок ASR
ASR_UnInitialize() — деинициализирует движок ASR
evt_HandleEvent — обрабатывает события генерируемые thread-ом «распознавателя»
ProcessResult() — печатает результаты распознавания
typedef struct RECOG_OBJECTS_S {
void *pHeapInst; // Pointer to the heap.
const char *acmod; // path to acmod data
const char *ddg2p; // path to ddg2p data
const char *clc; // path to clc data
const char *dct; // path to dct data
const char *dynctx; // path to empty dyn ctx data
LH_COMPONENT hCompBase; // Handle to the base component.
LH_COMPONENT hCompAsr; // Handle to the ASR component.
LH_COMPONENT hCompPron; // Handle to the pron component (dyn ctx)
LH_OBJECT hAcMod; // Handle to the AcMod object.
LH_OBJECT hRec; // Handle to the SingleThreadedRec Object
LH_OBJECT hLex; // Handle to lexicon object (dyn ctx)
LH_OBJECT hDdg2p; // Handle to ddg2p object (dyn ctx)
LH_OBJECT hClc; // Handle to the CLC (DDG2P backup)
LH_OBJECT hDct; // Handle to dictionary object (dyn ctx)
LH_OBJECT hCache; // Handle to cache object (dyn ctx)
LH_OBJECT hCtx[5]; // Handle to the Context object.
LH_OBJECT hResults[5]; // Handle to the Best results object.
ASRResult *results[5]; // recognition results temporary storage
LH_OBJECT hUswCtx; // Handle to the UserWord Context object.
LH_OBJECT hUswResult; // Handle to the UserWord Result object.
unsigned long sampleFreq; // Sampling frequency.
unsigned long frameShiftSamples; // Size of one frame in samples
int requestCancel; // boolean indicating user wants to cancel recognition
// used to generate transcriptions for dyn ctx
LH_BNF_TERMINAL *pTerminals;
unsigned int terminals_count;
unsigned int *terminals_transtype; // array with same size as pTerminals; each value indicates the type of transcription in pTerminal: user-provided, from_ddg2p, from_dct, from_clc
SLOT_TERMINAL_LIST *pSlots;
unsigned int slots_count;
// reco options
int isNumber; // set to 1 when doing number recognition
const char * UswFile; // path to file where userword should be recorded
char * staticCtxID;
} RECOG_OBJECTS;
// store ASR objects
static RECOG_OBJECTS recogObjects;
static int ConstructRecognizer(RECOG_OBJECTS *pRecogObjects,
const char *szAcModFN, const char * ddg2p, const char * clc, const char * dct, const char * dynctx) {
LH_ERROR lhErr = LH_OK;
PH_ERROR phErr = PH_OK;
ST_ERROR stErr = ST_OK;
LH_ISTREAM_INTERFACE IStreamInterface;
void *pIStreamAcMod = NULL;
LH_ACMOD_INFO *pAcModInfo;
LH_AUDIOCHAINEVENT_INTERFACE EventInterface;
/* close old objects */
if(!lh_ObjIsNull(pRecogObjects->hAcMod)){
DestroyRecognizer(pRecogObjects);
}
pRecogObjects->sampleFreq = 0;
pRecogObjects->requestCancel = 0;
pRecogObjects->pTerminals = NULL;
pRecogObjects->terminals_count = 0;
pRecogObjects->pSlots = NULL;
pRecogObjects->slots_count = 0;
pRecogObjects->staticCtxID = NULL;
pRecogObjects->acmod = szAcModFN;
pRecogObjects->ddg2p = ddg2p;
pRecogObjects->clc = clc;
pRecogObjects->dct = dct;
pRecogObjects->dynctx = dynctx;
EventInterface.pfevent = evt_HandleEvent;
EventInterface.pfadvance = evt_Advance;
// Create the input stream for the acoustic model.
stErr = st_CreateStreamReaderFromFile(szAcModFN, &IStreamInterface, &pIStreamAcMod);
if (ST_OK != stErr) goto error;
// Create the AcMod object.
lhErr = lh_CreateAcMod(pRecogObjects->hCompAsr, &IStreamInterface, pIStreamAcMod, NULL, &(pRecogObjects->hAcMod));
if (LH_OK != lhErr) goto error;
// Retrieve some information from the AcMod object.
lhErr = lh_AcModBorrowInfo(pRecogObjects->hAcMod, &pAcModInfo);
if (LH_OK != lhErr) goto error;
pRecogObjects->sampleFreq = pAcModInfo->sampleFrequency;
pRecogObjects->frameShiftSamples = pAcModInfo->frameShift * pRecogObjects->sampleFreq/1000;
// Create a SingleThreadRec object
lhErr = lh_CreateSingleThreadRec(pRecogObjects->hCompAsr, &EventInterface, pRecogObjects, 3000, pRecogObjects->sampleFreq, pRecogObjects->hAcMod, &pRecogObjects->hRec);
if (LH_OK != lhErr) goto error;
// cretae DDG2P & lexicon for dyn ctx
if (pRecogObjects->ddg2p) {
int rc = InitDDG2P(pRecogObjects);
if (rc<0) goto error;
} else if (pRecogObjects->clc) {
int rc = InitCLCandDCT(pRecogObjects);
if (rc<0) goto error;
} else {
// TODO: what now?
}
// Return without errors.
return 0;
error:
// Print an error message if the error comes from the private heap or stream component.
// Errors from the VoCon3200 component have been printed by the callback.
if (PH_OK != phErr) {
printf("Error from the private heap component, error code = %d.\n", phErr);
}
if (ST_OK != stErr) {
printf("Error from the stream component, error code = %d.\n", stErr);
}
return -1;
}
static int DestroyRecognizer(RECOG_OBJECTS *pRecogObjects) {
unsigned int curCtx;
if (!lh_ObjIsNull(pRecogObjects->hUswResult)){
lh_ObjClose(&pRecogObjects->hUswResult); pRecogObjects->hUswResult = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hUswCtx)){
lh_ObjClose(&pRecogObjects->hUswCtx); pRecogObjects->hUswCtx = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hDct)){
lh_ObjClose(&pRecogObjects->hDct); pRecogObjects->hDct = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hCache)){
lh_ObjClose(&pRecogObjects->hCache); pRecogObjects->hCache = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hClc)){
lh_ObjClose(&pRecogObjects->hClc); pRecogObjects->hClc = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hLex)){
lh_LexClearG2P(pRecogObjects->hLex);
lh_ObjClose(&pRecogObjects->hLex); pRecogObjects->hLex = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hDdg2p)){
lh_DDG2PClearDct (pRecogObjects->hDdg2p);
lh_ObjClose(&pRecogObjects->hDdg2p); pRecogObjects->hDdg2p = lh_GetNullObj();
}
for(curCtx=0; curCtx<sizeof(recogObjects.hCtx)/sizeof(recogObjects.hCtx[0]); curCtx++){
if (!lh_ObjIsNull(pRecogObjects->hCtx[curCtx])){
lh_RecRemoveCtx(pRecogObjects->hRec, pRecogObjects->hCtx[curCtx]);
lh_ObjClose(&pRecogObjects->hCtx[curCtx]); pRecogObjects->hCtx[curCtx] = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hResults[curCtx])){
lh_ObjClose(&pRecogObjects->hResults[curCtx]); pRecogObjects->hResults[curCtx] = lh_GetNullObj();
}
}
if (!lh_ObjIsNull(pRecogObjects->hRec)){
lh_ObjClose(&pRecogObjects->hRec); pRecogObjects->hRec = lh_GetNullObj();
}
if (!lh_ObjIsNull(pRecogObjects->hAcMod)){
lh_ObjClose(&pRecogObjects->hAcMod); pRecogObjects->hAcMod = lh_GetNullObj();
}
return 0;
}
int ASR_Initialize(const char * acmod, const char * ddg2p, const char * clc, const char * dct, const char * dynctx) {
int rc = 0;
size_t curCtx;
LH_HEAP_INTERFACE HeapInterface;
// Initialization of all handles.
recogObjects.pHeapInst = NULL;
recogObjects.hCompBase = lh_GetNullComponent();
recogObjects.hCompAsr = lh_GetNullComponent();
recogObjects.hCompPron = lh_GetNullComponent();
recogObjects.hAcMod = lh_GetNullObj();
for(curCtx=0; curCtx<sizeof(recogObjects.hCtx)/sizeof(recogObjects.hCtx[0]); curCtx++){
recogObjects.hCtx[curCtx] = lh_GetNullObj();
recogObjects.hResults[curCtx] = lh_GetNullObj();
}
recogObjects.hRec = lh_GetNullObj();
recogObjects.hLex = lh_GetNullObj();
recogObjects.hDdg2p = lh_GetNullObj();
recogObjects.hClc = lh_GetNullObj();
recogObjects.hCache = lh_GetNullObj();
recogObjects.hDct = lh_GetNullObj();
recogObjects.hUswCtx = lh_GetNullObj();
recogObjects.hUswResult = lh_GetNullObj();
recogObjects.sampleFreq = 0;
recogObjects.requestCancel = 0;
recogObjects.pTerminals = NULL;
recogObjects.terminals_count= 0;
recogObjects.pSlots = NULL;
recogObjects.slots_count = 0;
recogObjects.staticCtxID = NULL;
// Construct all components and objects needed for recognition.
// Connect the audiochain objects.
if (acmod) {
// initialize components
// Create a base and an ASR component. (+pron for dyn ctx)
if(LH_OK != lh_InitBase(&HeapInterface, recogObjects.pHeapInst, LhErrorCallBack, NULL, &recogObjects.hCompBase))
goto error;
if(LH_OK != lh_InitAsr(recogObjects.hCompBase, &HeapInterface, recogObjects.pHeapInst, &recogObjects.hCompAsr))
goto error;
if(LH_OK != lh_InitPron(recogObjects.hCompBase, &HeapInterface, recogObjects.pHeapInst, &recogObjects.hCompPron))
goto error;
rc = ConstructRecognizer(&recogObjects, acmod, ddg2p, clc, dct, dynctx);
if (rc<0) goto error;
}
return rc;
error:
// An error occured. Close the engine.
CloseOnError(&recogObjects);
return -1;
}
int ASR_UnInitialize(void)
{
int rc;
// Disconnects the audiochain objects.
// Closes all objects and components of the vocon recognizer.
rc = DestroyRecognizer(&recogObjects);
// Close the PRON component.
lh_ComponentTerminate(&recogObjects.hCompPron);
// Close the ASR and Base component.
lh_ComponentTerminate(&recogObjects.hCompAsr);
lh_ComponentTerminate(&recogObjects.hCompBase);
return 0;
}
int evt_HandleEvent(void *pEvtInst, unsigned long type, LH_TIME timeMs) {
RECOG_OBJECTS *pRecogObjects = (RECOG_OBJECTS*)pEvtInst;
if ( type & LH_AUDIOCHAIN_EVENT_BOS ){
// ask upper level for beep
printf ("Receiving event LH_AUDIOCHAIN_EVENT_BOS at time %d ms.\n", timeMs);
}
if ( type & LH_AUDIOCHAIN_EVENT_TS_FX ) {
printf ("Receiving event LH_AUDIOCHAIN_EVENT_TS_FX at time %d ms.\n", timeMs);
}
if ( type & LH_AUDIOCHAIN_EVENT_TS_REC ) {
printf ("Receiving event LH_AUDIOCHAIN_EVENT_TS_REC at time %d ms.\n", timeMs);
}
if ( type & LH_AUDIOCHAIN_EVENT_FX_ABNORMCOND ) {
LH_ERROR lhErr = LH_OK;
LH_FX_ABNORMCOND abnormCondition;
printf ("Receiving event LH_AUDIOCHAIN_EVENT_FX_ABNORMCOND at time %d ms.\n", timeMs);
// Find out what the exact abnormal condition is.
lhErr = lh_FxGetAbnormCondition(pRecogObjects->hRec, &abnormCondition);
if (LH_OK != lhErr) goto error;
switch (abnormCondition) {
case LH_FX_BADSNR:
printf ("Abnormal condition: LH_FX_BADSNR.\n");
break;
case LH_FX_OVERLOAD:
printf ("Abnormal condition: LH_FX_OVERLOAD.\n");
break;
case LH_FX_TOOQUIET:
printf ("Abnormal condition: LH_FX_TOOQUIET.\n");
break;
case LH_FX_NOSIGNAL:
printf ("Abnormal condition: LH_FX_NOSIGNAL.\n");
break;
case LH_FX_POORMIC:
printf ("Abnormal condition: LH_FX_POORMIC.\n");
break;
case LH_FX_NOLEADINGSILENCE:
printf ("Abnormal condition: LH_FX_NOLEADINGSILENCE.\n");
break;
}
}
// LH_AUDIOCHAIN_EVENT_FX_TIMER
// It usually is used to get the signal level and SNR at regular intervals.
if ( type & LH_AUDIOCHAIN_EVENT_FX_TIMER ) {
LH_ERROR lhErr = LH_OK;
LH_FX_SIGNAL_LEVELS SignalLevels;
printf ("Receiving event LH_AUDIOCHAIN_EVENT_FX_TIMER at time %d ms.\n", timeMs);
lhErr = lh_FxGetSignalLevels(pRecogObjects->hRec, &SignalLevels);
if (LH_OK != lhErr) goto error;
printf ("Signal level: %ddB, SNR: %ddB at time %dms.\n", SignalLevels.energy, SignalLevels.SNR, SignalLevels.timeMs);
}
// LH_AUDIOCHAIN_EVENT_RESULT
if ( type & LH_AUDIOCHAIN_EVENT_RESULT ){
LH_ERROR lhErr = LH_OK;
LH_OBJECT hNBestRes = lh_GetNullObj();
LH_OBJECT hCtx = lh_GetNullObj();
printf ("Receiving event LH_AUDIOCHAIN_EVENT_RESULT at time %d ms.\n", timeMs);
// Get the NBest result object and process it.
lhErr = lh_RecCreateResult (pRecogObjects->hRec, &hNBestRes);
if (LH_OK == lhErr) {
if (LH_OK == lh_ResultBorrowSourceCtx(hNBestRes, &hCtx)){
int i;
int _ready = 0;
for(i=0; i<sizeof(pRecogObjects->hCtx)/sizeof(pRecogObjects->hCtx[0]); i++){
if(!lh_ObjIsNull(pRecogObjects->hCtx[i])){
if(hCtx.pObj == pRecogObjects->hCtx[i].pObj){
if(!lh_ObjIsNull(pRecogObjects->hResults[i])){
lh_ObjClose(&pRecogObjects->hResults[i]);
}
pRecogObjects->hResults[i] = hNBestRes;
hNBestRes = lh_GetNullObj();
_ready = 1;
break;
}
} else {
break;
}
}
if (_ready) {
for (i=0; i<sizeof(pRecogObjects->hCtx)/sizeof(pRecogObjects->hCtx[0]); i++) {
if(!lh_ObjIsNull(pRecogObjects->hCtx[i])){
if(lh_ObjIsNull(pRecogObjects->hResults[i])){
_ready = 0;
}
}
}
}
ASSERT(lh_ObjIsNull(hNBestRes));
if (_ready) {
ProcessResult (pRecogObjects);
for(i=0; i<sizeof(pRecogObjects->hResults)/sizeof(pRecogObjects->hResults[0]); i++){
if(!lh_ObjIsNull(pRecogObjects->hResults[i])){
lh_ObjClose(&pRecogObjects->hResults[i]);
}
}
}
}
// Close the NBest result object.
}
}
return 0;
error:
return -1;
}
static int ProcessResult (RECOG_OBJECTS *pRecogObjects) {
LH_ERROR lhErr = LH_OK;
size_t curCtx, i, k, count=0;
size_t nbrHypothesis;
ASRResult *r = NULL;
long lid;
// get total hyp count
for(curCtx=0; curCtx<sizeof(pRecogObjects->hCtx)/sizeof(pRecogObjects->hCtx[0]); curCtx++){
if(!lh_ObjIsNull(pRecogObjects->hResults[curCtx])){
if(LH_OK == lh_NBestResultGetNbrHypotheses (pRecogObjects->hResults[curCtx], &nbrHypothesis)){
count += nbrHypothesis;
}
}
}
// traces
printf ("\n");
printf (" __________RESULT %3d items max_______________\n", count);
printf ("| | |\n");
printf ("| result | confi- | result string [start rule]\n");
printf ("| number | dence |\n");
printf ("|________|________|___________________________\n");
printf ("| | |\n");
if (count>0) {
r = ASRResult_New(count);
// Get & print out the result information for each hypothesis.
count = 0;
curCtx = sizeof(pRecogObjects->hCtx)/sizeof(pRecogObjects->hCtx[0]);
for(; curCtx>0; curCtx--){
LH_OBJECT hNBestRes = pRecogObjects->hResults[curCtx-1];
if(!lh_ObjIsNull(hNBestRes)){
LH_HYPOTHESIS *pHypothesis;
if(LH_OK == lh_NBestResultGetNbrHypotheses (hNBestRes, &nbrHypothesis)){
for (i = 0; i < nbrHypothesis; i++) {
char *szResultWords;
// Retrieve information on the recognition result.
if (LH_OK == lh_NBestResultFetchHypothesis (hNBestRes, i, &pHypothesis)){
// Get the result string.
if (LH_OK == lh_NBestResultFetchWords (hNBestRes, i, &szResultWords)){
printf ("| %6lu | %6lu | '%s' [%s]\n", i, pHypothesis->conf, szResultWords, pHypothesis->szStartRule);
// Return the fetched data to the engine.
lh_NBestResultReturnWords (hNBestRes, szResultWords);
}
lh_NBestResultReturnHypothesis (hNBestRes, pHypothesis);
}
}
}
}
}
}
// traces
printf ("|________|________|___________________________\n");
printf ("\n");
return 0;
}
Очевидно, как и в случае TTS, код довольно велик, и предварительные действия занимают много места. И это еще не полностью рабочий код! При публикации я выкинул много лишнего. Все это еще раз показывает тем, кто дочитал до этого места, что использование технологии голосового ввода/вывода требует довольно высокого «порога вхождения».
14. Поточное распознавание (диктовка)
Последним словом техники сейчас является поточное распознавание, или dictation. Технология уже доступна на современных смартфонах под Android и iOS. В том числе — в виде API. Здесь программисту нет нужды указывать контекст распознавания создавая грамматики. На входе имеется речь — на выходе, распознанные слова. К сожалению, детали того, как этот метод работает пока мне недоступны. Процесс распознавания идет не на самом устройстве, а на сервере, куда голос передается и оттуда же получается результат. Хочется однако верить, что по прошествии лет технология будет доступна и на клиентской стороне.
Заключение
Вот пожалуй и все, что я хотел рассказать о технологиях ASR и TTS. Надеюсь, вышло не слишком скучно и достаточно информативно.В любом случае — вопросы приветствуются.
Комментарии (8)
Xelonic
12.08.2015 02:09А что думаете про опенсорсный Sphinx?
A_J
12.08.2015 11:53Sphinx выглядит многообещающе, но уже довольно давно :)
В принципе я верю, что его когда-нибудь допилят до нужной кондиции + добавят большинство распространенных языков. Но процесс идет как-то медленно. А может быть развитие пойдет по другому пути — все будут использовать облачные сервисы вместо встроенных…
Tseikovets
12.08.2015 02:33+1Чтобы дать читателю представление о том, как процесс работы с TTS выглядит на нижнем уровне (используется С++) я приведу пример синтеза речи на базе движка Nuance.
При этом следует отметить, что основные OS уже имеют системный высокоуровневый TTS API. Если детального контроля за генерацией речи по тексту не требуется, а нужно просто зачитывание строк рядового текста вслух, то обычно имеет смысл использовать именно эти годовые API.
Возможно именно по этому до сих пор не существует сколь-нибудь широкого рынка Open Source решений для большинства языков.
Ну к open source термин рынок вообще обычно не очень подходит…
Существует проект eSpeak, поддерживающий большое количество языков. Есть бесплатный, но закрытый MBROLA, в рамках которого есть практически уникальные голоса, например, классическая латынь. Есть RHVoice, покрывающий английский, русский и эсперанто.
Казалось бы, ничто не мешает создать «универсальный» голос, который будет уметь произносить все фонемы IPA, и таким образом решит проблему многоязычных интерфейсов. Но этого почему-то никто не делает. Скорее всего, это и невозможно.
eSpeak — это живой пример именно такого подхода. Там есть некий объём фонем, из которых собирается поддержка нового языка. Зачастую на фонемах уже существующих языков удаётся собрать новый язык. Если же каких-то отдельных фонем не хватает, то они добавляются в проект. Например, как раз для русского пришлось отдельно записывать и добавлять фонему для звука [р].
Вообще существует несколько подходов к созданию синтезатора речи, и у каждого из вариантов своя специфика.
Есть и такое понятие как псевдоязык, когда поддержку какого-то языка делают на базе фонетически схожего. Например, так часто делают синтез украинской речи, просто перед генерацией обрабатывая строку текста набором правил, типа заменить «е» на «э» и т.п. Ну это, конечно, уже не от хорошей жизни.A_J
12.08.2015 12:04По мне так подход eSpeak имеет право на жизнь. Я вообще не против robotic voice и считаю, что железка пусть звучит как железка. Но широкому кругу пользователей такая идея не близка. Боюсь что такое качество трудно будет продать.
Насчет псевдоязыков — да, я тоже баловался такими вещами, например моделируя канадский английский на базе американского английского. Радости мало, но порой приходится идти на компромисс.
Iceg
Было бы здорово примеры произношения разных движков, или даже демку с распознаванием.
Nuance сильно лучше дотнетовского SpeechSynthesizer?
A_J
У Nuance есть демо для TTS на web. А вот M$ движок я давно не слушал. Несколько лет назад он был в меру ужасен, но сейчас возможно что-то изменилось.