SAPI SR引擎 xml语法与编程体验

花了2天的时间研究了一下SAPI5的Text-to-Speech 以及 Speech Recognition。总体来说,觉得这个库非常的强大。经过了一系列的测试发现,准确率很高的。

昨天主要是研究TTS,然后总结了一个类可供使用。我将在另外一篇文章里把代码贴出来,以供大家使用。

今天,看了一下自带的教程。写得非常详细,但是在关于识别的rule,property等和SAPI需要的xml文件有关的地方有一点疑惑。经过反复的试验,大致的情况如下所示。

首先,需要说的是,SR有两种识别方式,一种是Dictation,另外一种是基于xml的Command and Control方式。前者的优点是能够识别的词组范围很大,然而后者则是比较精确。这里主要讲一下后者。

请先看下面的SAPI 的xml源码。

代码
<GRAMMAR LANGID="409"> // 409是英文的代码
    
<DEFINE> //下面主要的作用是定义一些常量,以后要用到。
        
<ID NAME="VID_Counter" VAL="1"/> 

        
<ID NAME="VID_Single" VAL="10"/>
        
<ID NAME="VID_Double" VAL="11"/>

        
<ID NAME="VID_Short" VAL="14"/>
        
<ID NAME="VID_Tall" VAL="15"/>
 
        
<ID NAME="VID_Mocha" VAL="25"/>
        
<ID NAME="VID_Cappuch" VAL="28"/>
     
        
<ID NAME="VID_Place" VAL="253"/>
        
<ID NAME="VID_Navigation" VAL="254"/>
        
<ID NAME="VID_EspressoDrinks" VAL="255"/>

        
<ID NAME="VID_Shots" VAL="258"/>
        
<ID NAME="VID_Size" VAL="261"/>

        
<ID NAME="VID_DrinkType" VAL="262"/>
        
<ID NAME="VID_OrderList" VAL="263"/>


        
<ID NAME="VID_Shots_PRO" VAL="500"/>
        
<ID NAME="VID_Size_PRO" VAL="501"/>
        
<ID NAME="VID_DrinkType_PRO" VAL="502"/>
        
<ID NAME="VID_Place_PRO" VAL="503"/>
        
<ID NAME="VID_Shots_PRO2" VAL="504"/>
        
<ID NAME="VID_Size_PRO2" VAL="505"/>
        
<ID NAME="VID_DrinkType_PRO2" VAL="506"/>
    
</DEFINE>

    //VID_EspressoDrinks是规则的ID,TOPLEVEL...则表示这个规则是主要的、被采用的规则
    
<RULE ID="VID_EspressoDrinks" TOPLEVEL="ACTIVE"> 
        
<O>  
//O表示OPTIONAL,就是这个范围内的语句以后在识别的时候是可有可无的。作为用户,其实就是说我可以忽略以下这些词语
            
<L>
//L表示LIST,表示以下每一句由
<p></p>包着的都是作为可选的。可以出现May I have, May I have Can I have,或者什么也不出现(相当于在<O>中)
                
<P>May I have</P>
//P表示PHRASE,表示这是一句PHRASE
                
<P>Can I have</P>
                
<P>Can I get</P>
                
<P>Please get me</P>
                
<P>Get me</P>
                
<P>I'd like</P>
                
<P>I would like</P>
                <P>...</P>  
//省略号表示可以使用任何无关紧要的词汇,但是为了保证准确率和速度,上面7个还是必要的
            
</L>
        
</O>
        
<O>a</O>
        
<MIN="1" MAX="7">
//这里MIN和MAX表示,在如下的PHRASE里,如果要识别的话,最少要识别到1个单词,最多只能识别到7个单词
            
<RULEREF REFID="VID_OrderList"/>
//这里引用了一个规则,相当于引入了一个新的非中结符,这里应该跳转到ID为VID_OrderList的规则那里去
        
</P>
        
<O>please</O>
    
</RULE>

    
<RULE ID="VID_Navigation" TOPLEVEL="ACTIVE">
        
<O>Please</O>
        
<P>
            
<L>
                
<P>Enter</P>
                
<P>Go to</P>
            
</L>
        
</P>
        
<O>the</O>
        
<RULEREF REFID="VID_Place" /> 
    
</RULE>

    
<RULE ID="VID_Place" >
        
<PROPID="VID_Place_PRO">
            
<VAL="VID_Counter">counter</P>
            
<VAL="VID_Counter">shop</P>
            
<VAL="VID_Counter">store *+</P>
//这里的*表示这里使用了内嵌听写(embedded dictation)(dictation区别于使用xml的Command and Control,前者可以随意识别内置词典中的词汇,后者着重xml里所定义的语法的词汇。但是后者比前者准确得多),而后面的“+”表示,这里可以识别多个单词
        
</L>
    
</RULE>


    
<RULE ID="VID_Shots" >
        
<PROPID="VID_Shots_PRO2">
            
<VAL="VID_Single">Single</P>
            
<VAL="VID_Double">Double</P>
        
</L>
    
</RULE>

    
<RULE ID="VID_Size" >
        
<PROPID="VID_Size_PRO2">
            
<VAL="VID_Short"> -Short</P//在词汇前面的“-”表示这个词可以不那么重视,只要接近即可
            
<VAL="VID_Tall">-Tall</P>
        
</L>
    
</RULE>


    
<RULE ID="VID_DrinkType" >
        
<PROPID="VID_DrinkType_PRO2">
            
<VAL="VID_Mocha">+Mocha</P> //在词汇前面的“+”(区别于在*号后面的+)表示着重需要识别的
            
<VAL="VID_Cappuch">+Cappuccino</P>
        
</L>
    
</RULE>

    
<RULE ID="VID_OrderList" >
        
<L>
            
<RULEREF REFID="VID_Shots" PROPID="VID_Shots_PRO"/> 
//这里将规则VID_Shots的PROPERTY_ID设置为VID_Shots_PRO
            
<RULEREF REFID="VID_Size" PROPID="VID_Size_PRO"/> 
            
<RULEREF REFID="VID_DrinkType" PROPID="VID_DrinkType_PRO"/> 
        
</L>
    
</RULE>
</GRAMMAR>

本来我是看得糊里糊涂的,后来手册上说这个xml其实就是为了表达上下文无关文法。仔细想一下,好像有点道理。现在推算如下

1. <P>x</P>   
表示A->x 
2. 
<O>y</O>   
表示A->y|E  (E表示空串)
3.
<L>
  
<P>x</P>
  
<P>y</P>
</L>
表示A->xA | yA | x | y ,必须得说明<L></L>中必须出现一个,不可以出现空串
4.
 
<P>x</P>  
<O>z</O>   
<L>
  
<P>x</P>
  
<P>y</P>
</L>
可以用
S -> xAB
A -> z|E
B -> xB | yB | x | y
来表示

(我没学过xml,可能理解不正确。)

说好了这一块东西,我就可以解释以下这段代码了。

先插一句,这段代码是示例程序的一个部分。之前已经初始化好SAPI并且获取了相应的句柄。现在要做的事情就是让SAPI引擎来告诉我们它识别到了什么。由于SAPI有自己的一套消息机制,虽然和通常情况下的Windows消息机制差不多,但是也有一点区别。区别如下

代码
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
        case WM_RECOEVENT:
            ProcessRecoEvent( hWnd );
            break;
        ...
    }
}

关键的区别是必须使用CSpEvent类,其中g_cpRecoCtxt是已经关联到SR引擎的一个上下文。这个其实和窗口过程的机制很像的。上下文其实就有点像窗口过程,但是它不一定要绑定到窗口,可以关联到menu等。原文是这样说的,
Alternatively, each part of the application may have a different context. Individual windows, dialog boxes, menu bars, or even menu items (such as the Open or Print menu items) may have their own context. Events or messages generated from these areas are processed by their own procedures.



void ProcessRecoEvent( HWND hWnd )
{
    CSpEvent event;  // Event helper class

    // Loop processing events while there are any in the queue
    while (event.GetFrom(g_cpRecoCtxt) == S_OK)
    {
        // Look at recognition event only
        switch (event.eEventId)
        {
            case SPEI_RECOGNITION:
                ExecuteCommand(event.RecoResult(), hWnd);
                break;

        }
    }
}

回到整体,观察一下的代码

代码
void ExecuteCommand(ISpPhrase *pPhrase, HWND hWnd)
{
    SPPHRASE *pElements;

    // Get the phrase elements, one of which is the rule id we specified in
    // the grammar.  Switch on it to figure out which command was recognized.
    if (SUCCEEDED(pPhrase->GetPhrase(
&pElements)))
    {        
        switch ( pElements->Rule.ulId )
        {
            case VID_EspressoDrinks:
            {
                ID_TEXT *pulIds = new ID_TEXT[MAX_ID_ARRAY];  
                // This memory will be freed when the WM_ESPRESSOORDER
                // message is processed
                const SPPHRASEPROPERTY *pProp = NULL;
                const SPPHRASERULE *pRule = NULL;
                ULONG ulFirstElement, ulCountOfElements;
                int iCnt = 0;

                if ( pulIds )
                {
                    ZeroMemory( pulIds, sizeof( ID_TEXT[MAX_ID_ARRAY] ) );
                    pProp = pElements->pProperties;  //获得第一个识别出来的单词的属性
                    pRule = pElements->Rule.pFirstChild; //第一个识别出来的单词的规则
                    // Fill in an array with the drink properties received
                    while ( pProp && iCnt 
< MAX_ID_ARRAY )
                    {
                        // Fill out a structure with all the property ids received as well
                        // as their corresponding text
                        // 试验下来,这里获得的是已经识别出来的那个单词在xml里面的<p VAL
=""></p>的VAL中
                            //  所指代的enum的数字形式,这个也是这里使用property的唯一用处了
                     pulIds[iCnt].ulId = static_cast
< ULONG >(pProp->pFirstChild->vValue.ulVal);
                        // Get the count of elements from the rule ref, not the actual leaf
                        // property
                        if ( pRule )
                        {
                                //取得所识别出来的单词在整个语句中的位置,0为第一个单词
                                //这里“整个”指的是符合上下文无关文法展开式的那个句子,比如说如果是
                         //I would like a single tall mocha的话,那么single就是第一个可以识别的词
                                //这是因为之前的那些都是在
<O></O>之间包着的。
                                ulFirstElement = pRule->ulFirstElement;
                            ulCountOfElements = pRule->ulCountOfElements;//这次识别出来几个单字?
                        }
                        else
                        {
                            ulFirstElement = 0;
                            ulCountOfElements = 0;
                        }
                        // This is the text corresponding to property iCnt - it must be
                        // released when we are done with it
                       // 获取识别出来那个词的文本,个人感觉是通过类似
<VAL="VID_Single">Single</P> 
                        // 这样的语句获得的,先给出VAL的值,然后找到字符文本
                           pPhrase->GetText( ulFirstElement, ulCountOfElements,
                                        FALSE, &(pulIds[iCnt].pwstrCoMemText), NULL);
                        
                    // 一下两句的作用是转到下一个已经识别出来的词的属性和规则上                        
                        // Loop through all properties
                        pProp = pProp->pNextSibling;
                        // Loop through rulerefs corresponding to properties
                        if ( pRule )
                        {
                            pRule = pRule->pNextSibling;
                        }
                        iCnt++;
                    }
               PostMessage( hWnd, WM_ESPRESSOORDER, NULL, (LPARAM) pulIds );                   
                }
            }
            break;
            ...
}
试验过程与结果
如果让SR识别的句子为“I would like a double short mocha”,那么三次pPro,pRule和pulIds[iCnt].ulId 的uId分别是
第一次 VID_SIZE_PRO, VID_ORDERLIST, VID_SHORT
第二次 VID_SHOTS_PRO, VID_ORDERLIST, VID_Double
第三次 VID_DrinkType_PRO, VID_OrderList, VID_Mocha
 

 最后总体说一下,整个过程是这样的。首先通过读取xml文件,我们大概可以知道需要说什么SR引擎才能够明白,在这里例子里,就是说<Size><SHOTS><COFFEE>,也就是类似于single tall mocha可以被识别的,其中的顺序是无所谓的。

然后呢,SR如果识别出来你的话了,也就是说你的话已经符合了由xml所定义的上下文无关文法。那么SR会为每一个需要识别的(比如说I would like to就是不需要识别的)词组织一系列的结构,比如说准备Rule和Property,对于一个词,就出现一对Rule和Property,其实这也很好理解的,因为SPPHRASE::pProperty指的是所识别的词在主规则所引用的辅助规则的属性,而SPPHRASE::Rule.pFirstChild指的是所识别的词的直接上级规则ID。在这个例子里,如果识别到了tall,根据xml文件,由于它是在RULE ID为VID_Size的规则里定义的,而这个规则又是在VID_OrderList规则里被引用的,而VID_OrderList又是被主规则VID_EspressoDrinks所引用的规则。所以它的SPPHRASE::pProperty就是VID_Size,而SPPHRASE::Rule.pFirstChild就是VID_OrderList。通过这些变量其实就可以很方便得找到需要的东西了。

-
原文地址:https://www.cnblogs.com/aicro/p/1944995.html