SophiaBot:使用 Vista 语音识别 API 创建会说话的人工智能个性






4.92/5 (23投票s)
2007 年 3 月 30 日
26分钟阅读

184676

9228
一个通过文字游戏来演示 Vista 和 .NET 3.0 Framework 的 SAPI 功能的应用程序。
引言
随附的 Sophia 项目旨在兼具指导性和趣味性。它在最基本层面上是一个带有语音合成和语音识别功能的聊天机器人应用程序。我最初打算将其展示为 System.Speech
命名空间的功能,但随着项目的进展,我开始沉迷于如何将人工个性的概念推向极致——我能做些什么让这个个性看起来更真实?我能做些什么让它更灵活?等等。在此过程中,我得到了我三个孩子(4 到 8 岁)的帮助,他们经常不让我使用电脑,因为他们忙着玩演示应用程序。这个项目献给他们,尤其是最小的 Sophia,这个应用程序就是以她的名字命名的。
本文概述了 GrammarBuilder
类的各种功能,包括如何构建日益复杂的识别规则。我将介绍一些技巧,使机器人个性看起来更逼真。我还会尝试解开一些涉及将 SR 应用程序部署到 Windows XP 而不是 Vista 的问题。随附的演示在 Vista 上运行最佳。我编写 Sophia 也能在 Windows XP 上运行,但语音识别功能将必然禁用,因为并非所有通过 System.Speech
命名空间可用的方法都适用于 XP。本文还将重点介绍在使用 Vista 托管 Speech
API 时可能遇到的其他问题。最后,它将演示一种可扩展的设计,允许多个语音识别应用程序同时运行。
背景
聊天机器人是个人电脑最早适应的应用程序之一。聊天机器人只是一个人工个性,它试图使用预定义的脚本与用户进行对话。最早的例子之一是约瑟夫·魏森鲍姆在六十年代中期编写的 Eliza。它使用了一个脚本化的精神病医生角色,将用户在终端输入的任何内容改写为问题,然后将问题抛回去。80 年代早期流行的许多游戏都是基于文本的,人们非常注重让与电脑的文本对话既有参与感又沉浸。这很大程度上涉及一些技巧,在某种程度上欺骗用户,让他们相信自己正在玩的游戏实际上是智能的。Ada 通过包含足够的灵活性来做到这一点,使得对用户的回复看起来是自发的。Infocom 在其基于文本的冒险游戏中通过使用幽默甚至一定程度的脚本化自我意识来做到这一点——例如,游戏旁白有时会情绪化,从而影响接下来会发生什么。在这些游戏中,模拟智能始终是高优先级。
这些模拟中缺少的一件事是能够使用自然语言实际与计算机对话的能力。尽管当时的电影将这描绘成可以轻松实现的事情(还记得《战争游戏》吗?),但它从未实现。然而,随着语音识别技术变得越来越好,游戏行业也变得更加注重视觉,对人工智能个性所做的实验兴趣减少了。从那时到现在,基于文本的体验主要由爱好者维持,他们继续为 Infocom 创建的 Z-machine 规范编写冒险游戏,以及多年来发展起来的新的聊天机器人脚本,这些脚本可以就更广泛的主题进行对话,并提供比原始 Eliza 更广泛的回复选择。
Sophia 项目只是一个尝试,旨在将语音识别和合成带入文本游戏体验。借助微软的语音识别技术以及 .NET 3.0 Framework 的 System.Speech
命名空间(以前是 SpeechFX
)提供的 API,不仅性能相当不错,而且实现起来也相对容易。随附的演示项目使用了 Nicholas H. Tollervey 创建的 AIMLBot 解释器。为了玩基于 Z-machine 的游戏,它使用了 Jason Follas 编写的 .NET ZMachine 程序集。用于赋予 Sophia 个性的 AIML 文件(AIML 代表人工智能标记语言)来自 ALICE A.I. Foundation,并基于 Richard Wallace 获奖的 A.L.I.C.E. AIML 集。您可以通过向 *AIML FILES* 子文件夹添加更多文件来扩展 AIML 机器人个性。要玩 ZMachine(有时称为 Frotz)游戏,只需将您的 * .dat* 或 * .z3* 文件放入 *...\Game Data\Data* 文件夹(遗憾的是,目前,演示只能玩在 ZMachine 规范第三版及以下版本上运行的游戏)。AIML 文件集和 Zmachine 基于文本的冒险数据文件在互联网上无处不在。
有关 Vista 和语音识别的许多资料也可以在我的入门文章 语音识别和合成托管 API 中找到。如果您觉得本文中我对 Vista 语音识别的某些方面讲解得太快,很可能原因是我已经在那里介绍过了。
玩演示
我将首先介绍演示应用程序的功能。然后,我将解释一些底层技术和模式。
该应用程序由一个文本输出屏幕、一个文本输入字段和一个默认的 Enter 按钮组成。最初的外观和感觉是 IBX XT 主题(我玩过的第一台电脑)。这可以通过语音命令更改,我稍后会介绍。最初有三个菜单可用。文件菜单允许用户将对话日志保存为文本文件。选择语音菜单允许用户从其机器上安装的任何合成语音中选择。Vista 最初附带“Anna”。Windows XP 附带“Sam”。其他 XP 语音取决于在该 OS 实例的生命周期内安装了哪些 Office 版本。如果用户运行的是 Vista,则语音菜单将允许他切换语音合成、听写和上下文无关语法。通过这样做,用户将能够与应用程序对话,并让应用程序与他对话。如果用户运行的是 XP,则只能使用语音合成,因为 .NET 3.0 提供且此应用程序使用的一些功能在 XP 上不起作用。
Vista 中的语音识别有两种模式:听写和上下文无关识别。听写使用上下文,即对先行词和给定语音识别目标后跟词的分析,以确定说话者意图的词。相比之下,上下文无关语音识别使用精确匹配和一些简单模式来确定是否已说出某些词或短语。这使得上下文无关识别特别适用于命令和控制场景,而听写特别适用于我们只是尝试将用户的发音转换为文本的情况。
您应该首先尝试使用文本框与 Sophia 开始对话,只是为了看看它是如何工作的,以及她作为对话者的局限性。Sophia 使用某些技巧来显得更逼真。首先,她会随机出现打字错误。她也比电脑实际应有的速度慢一点。这是因为区分电脑和人的一件事是它们处理信息的方式——电脑处理速度快,而人处理速度更悠闲。通过慢速打字,Sophia 帮助用户保持悬念。最后,如果您的计算机上安装了文本到语音引擎,Sophia 会在她打出回复时同步朗读。我不确定这为什么有效,但电影中电脑终端就是这样交流的,而且在这里似乎也效果很好。我将在下面介绍这种幻觉是如何产生的。
在 *Command\AIML\Game Lexicon* 模式下,应用程序会生成一些语法规则,这些规则有助于将语音识别引导到某些预期结果。请注意:最初加载 AIML 语法大约需要两分钟,并且在后台进行。您可以继续使用触控输入与 Sophia 对话,直到语音识别引擎加载完语法并启用语音识别。使用命令语法,用户可以使计算机执行以下操作:LIST COLORS(列出颜色)、LIST GAMES(列出游戏)、LIST FONTS(列出字体)、CHANGE FONT TO...(更改字体为...)、CHANGE FONT COLOR TO...(更改字体颜色为...)、CHANGE BACKGROUND COLOR TO...(更改背景颜色为...)。除了 IBM XT 配色方案之外,亚麻背景上的黑色纸莎草字体看起来也非常漂亮。您还可以说出命令“PLAY GAME”(玩游戏)来获取 * \Game Data\DATA* 子文件夹中可用的游戏文件列表。说出游戏的名称或游戏在列表中的数字位置(例如,“TWO”)即可玩游戏。要查看您选择的文本冒险游戏使用的关键字完整列表,请说“LIST GAME KEYWORDS”(列出游戏关键字)。当游戏最初被选中时,会根据游戏识别的关键字的不同两词组合创建一组新规则,以通过缩小必须查找的短语总数来帮助语音识别。
在听写模式下,底层语音引擎只是将您的语音转换为单词,并让核心 SophiaBot
代码以与处理输入的文本相同的方式处理它。对于非游戏语音识别,听写模式有时比上下文无关模式更好,这取决于您的操作系统上安装的语音识别引擎对您的语音模式的训练程度。上下文无关模式通常更适合游戏模式。命令和控制仅在上下文无关模式下工作。
使用代码
XP 与 Vista
SophiaBot
应用程序使用 Vista 的托管语音识别和合成 API(也称为 SpeechFX
)。SophiaBot
也能在 Windows XP 上运行,但仅通过隐式禁用语音识别(如果安装了正确的组件,语音合成将在 XP 上的 SophiaBot
中工作)。要理解为什么有些东西能工作而有些不能,有必要了解 SAPI 难题的各个部分。托管语音合成和识别 API 包含在 *System.Speech.dll* 中,这是构成 .NET 3.0 Framework 的库之一。.NET 3.0 反过来并不是 .NET Framework 的新版本,而是一组经过某种营销努力而命名的新库。因此,要运行 SophiaBot
,需要 .NET 3.0 和 .NET 2.0。语音库是 SAPI 5.3 的包装器,而 SAPI 5.3 又是 Speech Recognition Engine 8.0 的 COM 包装器。托管语音 API 实际上调用 SAPI 5.3 并直接调用 SR 引擎;它似乎使用前者进行语音识别,而直接使用后者进行语音合成——但这只是我的印象。由于 SAPI 5.3 只是可以安装在 XP 上的 5.1 API 的增强版,因此许多托管 API 调用也将在 XP 上工作。遗憾的是,我广泛使用的语法对象无法在 XP 上工作。
.NET 2.0、.NET 3.0、SAPI 5.3 和语音引擎都随 Vista 提供,因此无需安装任何额外组件即可在 Vista 上使 SpeechFX
正常工作。要在 XP 中获得部分功能,必须安装 SAPI 5.1 和 6.1 版的语音引擎。SAPI 5.1 可以从 Microsoft 网站下载,据我了解,它也作为 Windows XP Service Pack 2 的一部分提供。语音引擎随各种版本的 Microsoft Office 和 Outlook 一起安装。当然,必须在操作系统上安装 .NET 2.0 才能使 SpeechFX
在 XP 上正常工作(令人惊讶的是,经过一些测试后发现,似乎不需要安装 .NET 3.0,因为 *System.Speech.dll* 已包含在 Sophia 的安装中)。
Vista 附带安装了 Microsoft Anna 语音。通过安装简体中文语言包,可以获得额外的 Microsoft Lili 语音。据我所知,目前没有其他合成语音可用。
简化应用程序
以人类为衡量标准,计算机有些事情做得不好,有些事情做得好,有些事情做得太好。它做得太好的事情之一就是响应速度太快。这表明你正在与机器而不是人打交道,对于聊天机器人来说,这会破坏你实际上在与智能对话的错觉。为了弥补这一点,我放慢了响应速度,以便 Sophia 的响应模仿人打字。负责向 GUI 发送事件的代码最初会暂停,以模拟思考,然后遍历构成相应规则引擎提供的响应的字符,并以一次一个字符的方式向 GUI 发送更新事件,并伴有适当的间歇性暂停。
public delegate void GenericEventHandler<T>(T val);
public event GenericEventHandler<string> Write;
public void TypeSlow(string outputText)
{
if (null == Write)
return;
Thread.Sleep(500);
Write("Sophia: ");
Thread.Sleep(1000);
SpeakText(outputText);
for (int i = 0; i < outputText.Length; i++)
{
Write(outputText.Substring(i, 1));
Thread.Sleep(50);
}
Write(Environment.NewLine + Environment.NewLine);
}
这本身对支撑智能计算机个性的幻觉大有帮助。然而,从各种电影和电视节目来看,我们还清楚地期望计算机个性能够与我们对话,尽管声音也必须有些人工痕迹。例如,在《星际迷航》中,声音往往是单调的。在《2001太空漫游》中,HAL 的声音是人类的,但却异常平静。此外,计算机个性的说话速度通常与她打字的速度匹配,就好像她在打字时大声朗读,或者就好像我们在她构思回复时阅读她的思想一样。当然,所有这些都有点奇怪,因为我正在使用电影习惯用法来判断什么对最终用户来说是自然的——尽管如此,它似乎有效,就好像科幻电影与其说预测未来会怎样,不如说塑造了我们对未来的期望。
通过 SpeechFX
提供的语音合成器具有异步模式,我用它来使语音合成与打字同时发生,并且大致匹配打字的速度。
protected SpeechSynthesizer _synthesizer = new SpeechSynthesizer();
protected bool _isSpeechOn = true;
protected string _selectedVoice = string.Empty;
protected void SpeakText(string output)
{
if (_isSpeechOn)
{
_synthesizer.SelectVoice(SelectedVoice);
_synthesizer.SpeakAsync(output);
}
}
public string SelectedVoice
{
get { return _selectedVoice; }
set { _selectedVoice = value; }
}
高级语法
接下来,我想为我的应用程序添加语音识别功能,以便与 Sophia 进行双向对话。使用 SpeechFX
有几种方法可以做到这一点。在 Vista 上,我可以使用 System.Speech.Recognition.SpeechRecognizer
类,它允许访问 Vista 用于典型命令和控制场景的跨进程语音识别引擎,并且还提供了一个有吸引力的识别 GUI。
然而,我希望比跨进程 SR 引擎提供更多的控制,而且我也不希望我对引擎所做的操作影响其他应用程序,所以我决定改用进程内 System.Speech.Recognition.SpeechRecognitionEngine
。SpeechRecognizer
类总是创建对同一个共享识别引擎的引用,无论您从哪个应用程序调用它,而 SpeechRecognitionEngine
类允许您为拥有的每个 SR 应用程序创建多个特定引擎。
为了使语音识别引擎有效,您必须加载 System.Speech.Recognition.Grammar
对象,这些对象指示您希望语音识别引擎尝试匹配的单词模式。这又可以通过两种方式完成:您可以加载默认的听写语法,这将把您的应用程序变成一个自由听写应用程序,允许用户说任何他们想说的话,并且有很大的机会被理解;或者您可以创建自定义语法,将语音识别引擎引导到某些预期的短语。Sophia 实际上运行在这两种模式下;用户可以选择最适合他的模式。
创建听写语法相当直接。只需实例化听写语法的默认实例,从识别引擎中卸载所有其他语法,然后添加听写。
protected object grammarLock = new object();
protected void LoadDictation()
{
DictationGrammar dictationGrammar = new DictationGrammar();
dictationGrammar.SpeechRecognized +=
new EventHandler<speechrecognizedeventargs />
(recognizer_DictationRecognized);
lock (grammarLock)
{
_recognizer.UnloadAllGrammars();
_recognizer.LoadGrammar(dictationGrammar);
}
}
实际上有不止一个 SpeechRecognized
事件可以用来捕获成功的语音识别。从语法对象抛出的事件在分支线程上运行,允许您创建特殊的处理程序方法来处理捕获到的短语。这在运行多个语法时特别有用,并且希望每个语法以不同的方式处理语音命令。例如,如果除了主听写语法之外,您还想添加一个选定的命令和控制方法列表,例如“文件打开”和“文件保存”,您可以创建一个特殊方法,只处理命令和控制语音识别事件,但忽略听写语法识别的任何其他内容。
或者,您可以通过创建委托来拦截语音引擎本身的 SpeechRecognized
事件,而不是由特定语法抛出的事件,从而在一个地方处理所有语法的语音识别事件。与语法对象抛出的事件不同,此事件在主线程中抛出。
除了 SpeechRecognized
事件之外,语音识别引擎还会在口头短语被拒绝(因为它无法解决)时,以及在识别过程中语音识别引擎做出不同猜测以尝试找到适当匹配时抛出事件。
Sophia 捕获这些事件并将其显示在 GUI 中,以便用户可以观看语音识别过程的发生。识别成功以白色显示,拒绝以红色显示,而假设则为橙色。
然而,创建自定义语法比听写更有趣,并且提供了更大程度的控制。它在命令和控制场景中表现最佳,在这种场景中,您只需要匹配几个选定的短语即可实现基本命令。在这个演示项目中,我想看看我能将这种范式推向多远,因此我实现了识别大约 30,000 个短语的语法,以便使用语音识别玩旧的 Frotz 游戏,并为底层基于 AIML 的人工智能个性实现多达 70,000 个短语的语法。
命令和控制语法是最简单的,所以我将从这里开始。在处理语法时,记住 Grammar
对象是使用 GrammarBuilder
对象构建的非常重要。GrammarBuilder
对象反过来是建立在 Choices
对象上的。最后,Choices
可以由文本字符串、通配符甚至其他 GrammarBuilder
对象构建。
构建 Grammar
对象的一个简单示例涉及开发人员只希望语音识别引擎在几个短语之间进行选择的场景。这些短语中的每一个都是一个替代选择,因此应该成为 Choices
对象中的一个单独元素。以下是一些示例代码来涵盖这种特定情况
protected virtual Grammar GetSpeechCommandGrammar()
{
GrammarBuilder gb = new GrammarBuilder();
Choices choices = new Choices();
choices.Add("List Colors");
choices.Add("List Game Keywords");
choices.Add("List Fonts");
gb.Append(choices);
Grammar g = new Grammar(gb);
return g;
}
代码的另一部分可以为此语法设置优先级,以解决与其他语法可能存在的任何识别冲突(请记住,优先级较高的数字优先,而听写语法的优先级不能设置);它可以为语法命名,并且可以为 SpeechRecognized
事件添加事件处理程序,以处理这三个短语中任何一个的识别。
public override Grammar[] GetGrammars()
{
Grammar g = GetSpeechCommandGrammar();
g.Priority = this._priority;
g.Name = this._name;
g.SpeechRecognized += new EventHandler<speechrecognizedeventargs />
(SpeechCommands_SpeechRecognized);
return new Grammar[1]{g};
}
public void SpeechCommands_SpeechRecognized
(object sender, SpeechRecognizedEventArgs e)
{
string recognizedText = e.Result.Text;
if (recognizedText.IndexOf
("list colors", StringComparison.CurrentCultureIgnoreCase)>-1)
{
StringBuilder sb = new StringBuilder();
foreach (string knownColor in Enum.GetNames(typeof(KnownColor)))
{
sb.Append(", " + knownColor);
}
Write(sb.ToString().Substring(2));
}
else if (recognizedText.IndexOf
("list fonts", StringComparison.CurrentCultureIgnoreCase) > -1)
{
StringBuilder sb = new StringBuilder();
foreach (FontFamily font in
(new System.Drawing.Text.InstalledFontCollection()).Families)
{
sb.Append(", " + font.Name);
}
Write(sb.ToString().Substring(2));
}
else if (recognizedText.IndexOf
("list game keywords", StringComparison.CurrentCultureIgnoreCase) > -1)
{
if (_gameEngineBot != null)
{
Write( _gameEngineBot.ListGameKeywords());
}
else
Write("No game has been loaded.");
}
}
最后,可以将语法添加到进程内语音识别引擎。
然而,这是一个相当简单的场景,接下来我想介绍一些更复杂的语法。您可能希望识别一组特定的关键字,但不在乎之前或之后的内容。例如,如果您希望识别短语“Play Game”(玩游戏),以及“Let's Play Game”(我们玩游戏)甚至“Whoozit Play Game”,您可以使用 GrammarBuilder
类的 AppendWildcard()
方法创建捕获这些短语的语法。
以下示例正是这样做的,它使用语法构建器创建包含通配符的短语。然后将语法构建器添加到选择对象。选择对象被添加到另一个语法构建器对象,最后从该语法构建器创建一个语法。(应该指出的是,语音识别自然不区分大小写。我使用全大写来构建语法,以便当短语匹配并从 SpeechRecognized
处理程序返回到 GUI 时,匹配的短语,因为它们在 SpeechRecognizedEventArgs.Result.Text
字段中格式化,可以与其他短语区分开来,因为它们以与语法中相同的形式返回,即在本例中,首字母大写。)
protected virtual Grammar GetPlayGameGrammar()
{
Choices choices = new Choices();
GrammarBuilder playGameCommand = null;
//match "* Play Game"
playGameCommand = new GrammarBuilder();
playGameCommand.AppendWildcard();
playGameCommand.Append("PLAY GAME");
choices.Add(playGameCommand);
//match "Play Game *"
playGameCommand = new GrammarBuilder();
playGameCommand.Append("PLAY GAME");
playGameCommand.AppendWildcard();
choices.Add(playGameCommand);
//exact match for "Play Game"
choices.Add("PLAY GAME");
return new Grammar(new GrammarBuilder(choices));
}
AppendWildcard()
方法有一个问题。如果您使用它,将无法检索通配符位置中识别到的文本。相反,如果您检查 SpeechRecognizedEventArgs.Result.Text
字段,您会发现匹配的语音识别文本返回为“... PLAY GAME”,省略号替换了丢失的单词。
如果您需要知道丢失的单词,那么您应该使用 AppendDictation()
方法。AppendDictation()
基本上尝试在短语中添加它的位置匹配默认听写词汇表中大约十万个单词中的一个。如果在上面的代码中使用 AppendDictation()
而不是 AppendWildcard()
,那么您将能够捕获诸如“Let's play a game”甚至“Cat play a game”等短语中丢失的单词。然而,“Whoozit play a game”仍然永远不会在 SpeechRecognizedEventArgs
参数中返回,因为“Whoozit”不包含在听写词汇表中。在通配符占位符和听写占位符之间的选择冲突中,似乎(从我花在语法构建上的有限时间来看)听写占位符更有可能被识别。
到目前为止,您已经看到可以使用语法构建器对象来添加短语、添加通配符和添加听写占位符。在一个非常强大的变体中,您还可以附加一个 Choices
对象。这在您有一个短语很短,但希望短语的最后一个词来自一个列表的情况下很有用。例如,您可能希望创建一个语音命令,例如“我的家乡是...”,但随后不是将最后一个词设为通配符(因为这会阻止您捕获用户说的最后一个词)或听写(因为这仍然允许太多不适当的选项),而是希望将最后一个词限制为五十个合法答案之一。为了实现这一点,您将创建一个 Choices
对象来保存五十个州的名称,然后使用 Append()
方法将其添加到您的语法构建器中。类似地,下面的示例基于 MSDN 库中的示例代码,使用 KnownColor
枚举创建了一个语法,允许用户为活动字体选择新颜色。
GrammarBuilder gb = new GrammarBuilder();
Choices choices = new Choices();
GrammarBuilder changeColorCommand = new GrammarBuilder();
Choices colorChoices = new Choices();
foreach (string colorName in System.Enum.GetNames(typeof(KnownColor)))
{
colorChoices.Add(colorName.ToUpper());
}
changeColorCommand.Append("CHANGE COLOR TO");
changeColorCommand.Append(colorChoices);
choices.Add(changeColorCommand);
gb.Append(choices);
Grammar g = new Grammar(gb);
这种技术在构建 Frotz 游戏语法时特别有用。如果您还记得玩这些文本冒险游戏(我的青年时代,但也许不是您的),每个游戏都有大约 200 个单词的词汇表。乍一看,这似乎是构建语法所需的许多关键字,考虑到您可以从 200 个单词中创建的排列组合数量;但在实践中,所有有用的 Frotz 命令都是单字或两字组合。通过创建包含所有可从可用关键字构建的两字组合作为选择的语法,我最终得到了一个非常有效的语音识别工具,尽管最终的语法包含数万个选择。为了保险起见,我还将每个关键字添加为单字选择,以及关键字 + 听写组合。
protected virtual Grammar GetGameGrammar()
{
Choices choices = new Choices();
Choices secondChoices = new Choices();
GrammarBuilder before;
GrammarBuilder after;
GrammarBuilder twoWordGrammar;
foreach (string keyword in GameLexicon.GetAllItems())
{
//can't use this character in a grammar
if (keyword.IndexOf("\"") > -1)
continue;
string KEYWORD = keyword.ToUpper();
//wildcard before keyword
before = new GrammarBuilder();
before.AppendDictation();
before.Append(KEYWORD);
//wildcard after keyword
after = new GrammarBuilder();
after.Append(KEYWORD);
after.AppendDictation();
choices.Add(before);
choices.Add(after);
choices.Add(KEYWORD);
secondChoices.Add(KEYWORD);
}
foreach (string firstKeyword in GameLexicon.GetAllItems())
{
//can't use this character in a grammar
if (firstKeyword.IndexOf("\"") > -1)
continue;
string FIRSTKEYWORD = firstKeyword.ToUpper();
twoWordGrammar = new GrammarBuilder();
twoWordGrammar.Append(FIRSTKEYWORD);
twoWordGrammar.Append(secondChoices);
choices.Add(twoWordGrammar);
}
Grammar g = new Grammar(new GrammarBuilder(choices));
return g;
}
历史注释:当您在 Sophia 中玩 Frotz 游戏(也称为 Z-Machine 游戏)时,您会注意到关键字有时会被截断。例如,没有“lantern”这个无处不在的关键字,但有“lanter”这个关键字。这是原始游戏中用于处理通配符变体和拼写错误的一种技术。
机器人命令模式
在构建 SophiaBot
时,我使用了命令模式的一种变体,该模式在管理 SR 功能方面表现良好。该模式解决了几个问题。首先,每个实现 IBotServer
接口的对象都负责管理自己的语法以及响应已识别输入的所有规则。其次,如果给定的 IBotServer
实现未能充分处理某个短语,则应将识别短语传递给另一个 IBotServer
进行处理。对于 SophiaBot
,我构建了四个不同的机器人服务器(或者换句话说,Sophia 个性的替代人工个性)。AIMLBotAdapter
是一个聊天机器人,它使用包含的 AIML 文件(人工智能标记语言)来形成对用户输入的响应。SpeechCommandBot
处理一系列简单的命令,允许用户更改 GUI 的字体颜色或列出活动 WinFrotz 游戏使用的关键字命令。PlayGameTransition
是一个基于文本的对话框,允许用户从游戏目录中可用的游戏中选择一个游戏进行玩。最后,GameEngineBot
实际上会加载一个游戏进行玩,并根据所选文本冒险游戏的核心词汇创建一个语法。
此设计成功处理了至少两种情况:一种是通过主界面输入文本,另一种是特定机器人关联的特定语法识别到口头短语。当仅使用键盘输入文本时,无法知道哪个机器人包含正确的处理程序。在这种情况下,重要的是每个机器人以串行方式链接到另一个机器人。首先调用链中第一个机器人的 Read()
方法,它将输入的文本传递给其规则引擎。如果引擎无法找到适当的响应,机器人会将输入的文本传递给系列中下一个机器人的 Read()
方法,直到没有机器人为止。当使用专用语法启用语音识别时,文本不一定会从系列中的第一个机器人开始。相反,它会转到与最能匹配口头短语的语法对象关联的机器人,这可能是链中的第一个或第四个机器人。该语法的 SpeechRecognized
处理程序然后将识别到的文本传递给包含它的对象的 Read()
方法。例如,如果与 GameEngineBot
关联的语法识别到口头短语,那么 GameEngineBot
的 Read()
方法将尝试对输入给出适当的响应。只有当它无法给出响应时,它才会将输入作为文本传递给链中的下一个机器人。
IBotServer
接口还跟踪每个机器人的状态,并在机器人启动或停止时抛出事件。这很方便,因为它允许客户端对象确定在发生各种事件时如何管理语音识别引擎。例如,当游戏引擎停止时,我希望客户端实际删除其语法,然后在游戏引擎重新启动时重新加载它们,因为每个游戏都会有一组不同的关键字,因此需要不同的语法。另一方面,AIML 机器人始终使用相同的语法集,而且重新创建它们相当耗时。在这种情况下,我只想在引擎停止时简单地禁用所有语法,而不是将它们从语音识别引擎中完全移除。客户端仍然负责使用此模式确定机器人之间的大部分工作流和交互,但通用接口至少有助于减轻所涉及的一些复杂性。
_aimlEngine = new AIMLBotAdapter(aIMLFolderPath);
_aimlEngine.OnUserInput += new GenericEventHandler<string />(DisplayUserInput);
_aimlEngine.OnStart += new EventHandler(EnableSelectedGrammar);
_aimlEngine.OnBotInfoResponse += new GenericEventHandler(TypeVerbatim);
_aimlEngine.OnBotResponse += new GenericEventHandler(TypeSlow);
_aimlEngine.OnFinish += new EventHandler<finisheventargs />
(DisableSelectedGrammar);
_aimlEngine.OnTextRecognized += new GenericEventHandler<string />
(IBotServer_OnTextRecognized);
_aimlEngine.OnUpdateLoadStatus += new GenericEventHandler<string />
(IBotServer_OnUpdateStatus);
GameEngineBot gameEngine = new GameEngineBot();
gameEngine.SavedGamesFolderPath = savedGamesFolder;
gameEngine.OnUserInput +=new GenericEventHandler<string />(DisplayUserInput);
gameEngine.OnStart +=new EventHandler(LoadSelectedGrammar);
gameEngine.OnBotInfoResponse += new GenericEventHandler(TypeVerbatim);
gameEngine.OnBotResponse += new GenericEventHandler(TypeSlow);
gameEngine.OnFinish += new EventHandler<finisheventargs />
(UnloadSelectedGrammar);
gameEngine.OnTextRecognized += new GenericEventHandler<string />
(IBotServer_OnTextRecognized);
gameEngine.OnStart += new EventHandler(gameEngine_OnStart);
gameEngine.OnFinish += new EventHandler<finisheventargs />
(gameEngine_OnFinish);
...
_firstBot.AddNextBot(_dialogEngine);
_dialogEngine.AddNextBot(gameEngine);
gameEngine.AddNextBot(_aimlEngine);
_aimlEngine.Start(aIMLFolderPath);
_dialogEngine.Start(gameDataFolder);
_firstBot.Start();
陷阱!
对于这个应用程序,我想同时使用语音合成器的异步方法和语音识别器的异步方法,以便屏幕更新和文本输入可以与这些其他活动同时发生。这样做的一个问题是,合成器和识别器不能完全同时处理信息,如果尝试这样做会抛出错误,所以我不得不加入大量的同步锁,以确保在合成器激活时识别器被禁用,然后在合成器完成后再次启用。如果我只是简单地使用同步的 Speak()
和 Recognize()
方法,所有这些都会简单得多,但是,唉,我有点雄心勃勃,最终效果好得多,尽管我一直担心有一个死锁场景我还没有完全解决。另一个陷阱是语法并不总是在主线程上返回事件,因此必须经常使用主 GUI 表单的 Invoke()
和 BeginInvoke()
方法来处理任何源自 Grammar.SpeechRecognized
事件的委托。Invoke()
和 BeginInvoke()
确保这些事件在主线程而不是某个流氓线程中处理,并且调用因此是线程安全的。最后,加载和卸载语法不能在语音识别活动时进行,因此这需要添加更多检查,以确保在尝试这些操作时语音识别引擎未在识别。这不仅涉及取消语音识别器中任何正在进行的活动,还要确保当前正在处理 SpeechRecognized
事件的任何代码确实已完成。除非您非常擅长处理多线程应用程序(我不是特别擅长),否则我建议您在自己的 SR 应用程序中缓慢进行,一次添加一个功能,以确保所有线程都在您希望它们结束的地方结束,然后再转向更复杂的线程场景。
如果您在代码中遇到任何错误,提出了 IBotServer
接口的更好设计,或者只是有一个您认为在 Sophia 中会很好用的机器人,请给我留言。我期待阅读您对如何改进 Sophia 的见解。
延伸阅读
- 更多聊天机器人脚本
- 更多 zmachine 游戏
- Jason Follas(本项目中使用的 C# ZMachine 的作者)
- Nicholas H. Tollervey(本项目中使用的 AIML 解释器的作者)
Code Project 文章
- 人工智能 (AI) 聊天机器人 和 AI 聊天机器人向导:创建、训练和聊天 作者 dzzxyz
- AI 聊天机器人开发 作者 elagizy
- 聊天机器人 Eliza 作者 Gonzales Cenelia
- 博格知识同化器 作者 Sean Michael Murphy
- 使用正则表达式引擎构建 AI 聊天机器人 作者 MattsterP
文章历史
- 3/31/07 - [更正] Eliza 是原始聊天机器人的名称,不是 Ada
- 3/31/07 - [更正了指向其他文章的链接]
- 3/31/07 - 添加了阅读列表,以便所有以前涉足此领域的人都能得到适当的认可