语音控制的网络浏览






4.58/5 (9投票s)
2004年1月22日
6分钟阅读

90639

5505
本文介绍了如何使用 SAPI 5.1 和 ActiveX 来语音控制您的网站。
引言
本文介绍了一个 ActiveX 控件,它可以嵌入到 html 网页中,提供语音控制的菜单树。
要编译代码,您将需要 VC6、Microsoft 的 Speech SDK 5.1 和 Internet Explorer 头文件。(如果您有 WINXP,您可能已经拥有所需的文件)
演示程序
此软件包的演示是一个简单的网页,其中包含两个 <iframe> 元素:第一个 <iframe> 嵌入了 ActiveX 控件,第二个显示页面内容。
编译并注册 WebVoiceCtl.dll 后,查找名为 demo
的文件夹,然后双击其中的名为 WebVoice.html 的文件。您应该会在左侧框架中看到树形控件,如上所示。按 Voice 按钮,然后耐心等待大型语音引擎加载。
加载完成后,您可以说 "go to class one" 开始导航。控件应响应 "Please confirm class one",您可以回答 "positive"。然后,请求的项目将显示在右侧框架中。
随时说 "help" 以获取活动命令列表。如果您刚导航到一个页面,帮助响应将是 "[scroll] up, down, top bottom; go back or navigate"。说出您的滚动命令,然后说:"navigate" 返回导航模式。
提示: 将扬声器的音量调低,以避免麦克风产生反馈。
背景
本文附带的代码演示了以下技术:
- ATL、ActiveX(以及宽字符字符串操作)
- 树形视图的搜索、展开和折叠
- 所有者绘制按钮、编辑控件和静态控件
- 图像列表、叠加(以及在 ATL 复合控件内部绘制)
- 使用 Microsoft 的 MSXML 解析器加载和操作 XML 文件
- 使用 C++ 与 Web 浏览器和 html 页面进行交互
- SAPI 5.1、语音识别和文本到语音引擎以及 Visemes
当然,您不必理解以上所有项目即可在您的项目中使用此控件,但您可能会发现其中一些解决方案(其中几个归功于其他 Code Project 文章)很有趣。
创建您自己的菜单树
您的菜单项将从文件 "data/WebVoice.xml"(名称当前是硬编码的)中读取,该文件包含菜单树和 SAPI 语法的信息。其内容存储在 KEY
结构数组中以供稍后检索。下面显示了一个简短的 XML 示例文件和 KEY
结构。
<!-- WebVoice.xml -->
<menu>
<item>
<mid>1</mid> <!- menu item id -->
<pid>0</pid> <!- parent id -->
<txt>Class One</txt> <!- menu text and grammar phrase -->
<ref>../html/class1.html</ref> <!- hyperlink reference -->
</item>
<item>
<mid>2</mid>
<pid>1</pid>
<txt>Source One</txt>
<ref>../html/src1.html</ref>
</item>
<!- more items here -- >
</menu>
typedef struct tag_key { int mid; int pid; int chd; HTREEITEM hItem; HTREEITEM hParent; char txt[32]; char ref[128]; }KEY; KEY aKeys[NUMBER_OF_KEYS];
您必须仔细确保菜单项 ID 按顺序编号,并且父 ID 指向树中位于当前项之上的项。加载时目前不执行任何错误检查,因此无效的 XML 文件将导致控件崩溃。
SAPI 初始化
WebVoice 控件在 InitSapi()
函数中按如下方式处理 SAPI 初始化:
- 创建语音引擎。
- 创建识别上下文。
- 设置一个通知机制(Windows 消息),用于从识别引擎进行回调。
- 设置识别事件兴趣。
- 加载特定的语法文件。
- 创建文本到语音引擎 (TTS)。
- 设置 TTS 事件兴趣。
- 设置一个通知机制(Windows 消息),用于从 TTS 引擎进行回调。
- 设置活动规则。
Speech SDK 文档和示例清楚地展示了所需的 SAPI 初始化调用,因此我在此不再赘述。但是,静态语法文件和动态语法需要一些解释。
SAPI 语法
SAPI 语法可以从 XML 文件静态加载,也可以在运行时动态加载。WebVoice
控件同时使用这两种方法。静态部分从 Grammar.xml 加载,其格式如下:
<GRAMMAR LANGID="409">
<DEFINE>
<ID NAME="RID_Tree" VAL="1001"/>
<ID NAME="RID_MenuItem" VAL="1004"/>
</DEFINE>
<RULE ID="RID_Tree" TOPLEVEL="ACTIVE">
<L>
<P>open</P>
<P>go to</P>
</L>
<RULEREF REFID="RID_MenuItem" />
</RULE>
<RULE ID="RID_MenuItem" DYNAMIC="TRUE">
<L PROPID="RID_MenuItem">
<P VAL="1">Dummy Item</P>
</L>
</RULE>
<!-more rules -->
</GRAMMAR>
如您所见,此文件片段创建了两个规则:第一个规则 RID_Tree
定义了起始导航短语,然后引用第二个规则 RID_MenuItem
。第二个规则包含一个占位符短语,该短语将在运行时替换为您的菜单项名称。此文件由 SAPI 的 gc.exe 编译为 Grammar.cfg,然后加载到 DLL 中的资源中。动态规则添加如下:
HRESULT CWebVoice::LoadGrammar() { USES_CONVERSION; HRESULT hr; SPPROPERTYINFO pi; ZeroMemory(&pi,sizeof(SPPROPERTYINFO)); pi.ulId = RID_MenuItem; // property ID pi.vValue.vt = VT_UI4; // add menu items to the dynamic grammar rule for(int i=0; i < m_nNumKeys; i++) { pi.vValue.ulVal = i+1; // Property_Value == data_index + 1 hr=m_cpGrammar->AddWordTransition(hRule,NULL, T2W(aKeys[i].txt),L" ",SPWT_LEXICAL,1,&pi); if(FAILED(hr)) return hr; } // add a wildcard phrase pi.vValue.ulVal = 0; hr=m_cpGrammar->AddWordTransition(hRule, NULL, L"*", L" ", SPWT_LEXICAL, 1, &pi); if(FAILED(hr)) return hr; hr=m_cpGrammar->Commit(NULL); if(FAILED(hr)) return hr; hr=m_cpGrammar->SetGrammarState(SPGS_ENABLED); if(FAILED(hr)) return hr; return hr; }
请注意,每个新短语(取自 aKeys[i].txt
)都被分配了属性 ID RID_MenuItem
和一个唯一的属性值(介于 1 和 m_nNumKeys 之间
),然后使用 AddWordTransition()
函数添加到语法中。另请注意,在末尾添加了一个通配符规则 ("*")
以捕获语法未涵盖的语音短语。
致谢
识别引擎将您的语音与活动语法规则进行比较。当引擎进行识别或误识别时,将调用您的回调例程来处理请求。以下显示了识别处理程序的一部分:
void CWebVoice::ExecuteCommand(ISpRecoResult *pPhrase, HWND hWnd) { USES_CONVERSION; SPPHRASE *pElements; static int ind; int pos; if (SUCCEEDED(pPhrase->GetPhrase(&pElements))) { m_cpRecoCtxt->Pause(NULL); // pause recognition while loading switch (pElements->Rule.ulId ) { case RID_Tree: pos=pElements->pProperties->vValue.ulVal; ind=pos-1; // store the index into the data array SetActiveRule(RID_Confirm); // change the active rule wcscpy(wcs,L"Please confirm: \r\n"); wcscat(wcs,T2W(aKeys[ind].txt)); HandleReply(0,wcs); break; case RID_Confirm: pos=pElements->pProperties->vValue.ulVal; switch(pos) { case 1: HandleConfirm(ind); // expand the tree and navigate to item SetActiveRule(RID_View); // change the active rule break; case 2: default:SetActiveRule(RID_Tree); HandleReply(MID_Tree,NULL); break; break; } // more cases for other rules default: SetActiveRule(RID_Tree); HandleReply(RID_Tree,NULL); break; } ::CoTaskMemFree(pElements); m_cpRecoCtxt->Resume(NULL); } }
当匹配到导航规则时,其属性值将存储在静态变量 ind
中,并在确认后传递给 HandleConfirm(ind)
函数,该函数使用它来索引数据数组 (aKeys[ind]
) 并检索正确的数据项。如果成功,树形视图将打开以显示选择,并且超链接将被导航。
关注点
每次我使用 ATL 编写 ActiveX 控件或 Web 浏览器插件时,我都必须重新学习如何使用宽字符字符串;SAPI 完全使用宽字符字符串。如果您的代码不需要在 Win98 上运行,您可以只定义 UNICODE,只要您的字符串定义为 TCHAR*
,常规的 API 调用就可以正常工作。但是,如果不能放弃 Win98 用户,那么在每次使用 Win32 API 时,您都将被迫从多字节字符串转换为宽字符串。幸运的是,ATL 在 <atlbase.h> 中定义了一套出色的转换宏。您只需在需要转换字符串的每个函数开头放置宏 USES_CONVERSION
,然后使用 W2T()
或 T2W()
宏执行转换。我毫不怀疑这些宏的开销会很高——毕竟,它们每次转换都需要分配内存、复制字符串然后释放内存。然而,这些宏非常方便和整洁,以至于我甚至开始将 <atlbase.h> 包含到我的 MFC 程序中。
我遇到的另一个问题是需要使用所有者绘制按钮——标准的对话框灰色在网页上无法接受。在 MFC 中,我会覆盖 WM_CTLCOLOR
消息并在那里更改背景颜色。在 ATL 中,我发现我必须将按钮设置为所有者绘制,然后处理 WM_DRAWITEM
消息。一切都很好,但后来我发现我既需要一个切换按钮也需要一个瞬时按钮,并且现在我需要自己编写所需的响应。这一切都很有趣,但花了一些时间我才能够进入代码的 SAPI 部分。
Microsoft Speech SDK 5.1 是一个 68 MB 的下载文件,如果您需要将 SAPI 运行时模块与您的代码一起打包,则必须下载完整的重新分发包,该包为 131.58 MB。
不幸的是,Microsoft 不单独打包运行时模块。您的客户必须下载 SDK(包括额外的 30 MB 的开发代码和文档),或者您必须准备一个运行时模块包,作为您应用程序的单独下载。
修订
- 2004 年 1 月 29 日 - 子类化图片控件以避免 Win98 问题,并修复了演示中的小脚本错误。