JSGF 是一种用于有限语音识别的语法格式标准,最早由 Sun 提出并捐赠给 W3C 组织。随后被广泛用于各种语音识别系统,尽管随着语音识别相关技术的发展。人们已经实现了高效率的无限制语音识别等技术,也出现了许多新的语音识别语法格式。但是 JSGF 因其成熟稳定、简单易用,仍然是许多语音识别系统的首选格式。
本文是对 W3C 标准当中 的 JSGF 标准的非官方翻译。笔者能力有限,难免存留错漏,望细心读者不吝斧正。
原文链接:JSpeech Grammar Format (w3.org)
以下是本文中出现的重要术语名词对照:
| 名词 | 翻译 |
|---|---|
| Rule Grammar | 规则语法 |
| Dictation Grammar | 听写语法 |
| Grammar Header | 语法头/语法标题 |
| Grammar Body | 语法体/语法正文 |
| Rulenames | 规则名 |
| Qualified Names | 限定名 |
| Fully-Qualified Names | 完全限定名 |
| Token | 词元 |
| Terminal symbol | 终极符 |
| Recognizer's vocabulary | 识别器词汇表 |
| Lexicon | 词典 |
| Pronunciation | 发音 |
| Quoted Tokens | 引用词元 |
| Expansions | 扩展 |
| Sequences | 序列 |
| Alternatives | 选项 |
| Weights | 权重 |
| Grouping | 分组 |
| Optional Grouping | 可选分组 |
| Unary Operators | 一元操作符 |
以下为正文:
JSpeech Grammar Format
发布于 2000 年 6 月 5 日
最新版本:www.w3.org/TR/jsgf
编辑:Andrew Hunt, Speech Works International <andrew.hunt@speechworks.com>
Copyright ©2000 Sun Microsystems, Inc.
Sun, Sun Microsystems, Inc., Java and all Java-based marks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
简介
JSpeech Grammar Format(JSGF)是一种与具体平台和供应商无关的,用于语音识别技术的一种语法文本表示形式。在语音识别中,语法用于描述用户可能说出的话语,供语音识别器用来确定它们应该监听什么内容。除了部分业内惯用的语法描述符号外,JSGF 主要采用 Java™ 编程语言的代码风格和编码约定。
本文档派生自 Java™ 语音 API 格式语法(Java™ Speech API),原始文档来自 Sun Microsystems 的网站: java.sun.com/products/ja…。
Sun Microsystems 希望将本文档提交 W3C 语音浏览器工作组(W3C Voice Browser Working Group)以致力于互联网语音技术标准的发展。我们期待由此产生的 W3C 推荐标准对开发人员社区起到非常重要的作用。
请参照 Sun Microsystems 关于知识产权的发言。
文档现状
本文档由 Sun Microsystems, Inc. 提交给 W3C 组织(参见:Submission Request, W3C Staff Comment),如需查看全部已确认的提交列表,请参见:Acknowledged Submissions to W3C。
本文档是 W3C 提供的仅供讨论之用的笔记,不代表该文档得到 W3C 成员的认可或共识,也不代表 W3C 已经、正在或将要分配任何资源来解决文档中提出的问题。本文档是一项正在进行中的工作,随时可能被更新或因其他文档的替换而变得过时。
本文档最初来源于 Sun 公司与其他公司一起开发的 Java™ Speech API 技术的一份语法规范,该规范被称为 Java 语音 API 语法格式(Java Speech API Grammar Format),您可以从 java.sun.com/products/ja… 获得更多的参考信息。本文档仅对原始的规范名称进行了变更,并未在技术上与之前的规范演化出差异。更改名称是为了保护 Sun 公司的商标。同时,我们也期望任何由商业化公司的产品派生出的规范与其原始文件具有不同的名称
如果未来需要对本文档进行任何更改,我们预计将会由 W3C 流程产生新的标准版本。而 Sun 公司对 JSGF 规范持有所有权,并保留独立维护和发展 JSGF 规范的权利,由此演化出的相关技术由 Sun 公司所有。
现行的 W3C 技术文档的列表可以在 Technical Reports 页面找到。
前言
适用范围
以下情况被认为可以通过计算机的语音识别接口实现的编程问题:
- 语法管理:语音识别器中加载、创建和删除语法,使用语法进行识别等语法管理机制
- 语法加载:通过 URL 从网站中加载语法,或通过其他方式加载语法的机制
- 识别结果:接收并处理语音识别器得到的识别结果的机制
- 异常处理:出现语法定义错误时的错误处理机制
- 词汇管理:标注自然语言的词汇发音等的机制
贡献
Sun Microsystems, Inc. received contributions to this specification from Apple Computer, Inc., AT&T, Dragon Systems, Inc., IBM Corporation, Novell, Inc., Philips Speech Processing and Texas Instruments Incorporated as well as from many internet reviewers.
JSpeech Grammar Format 规范
1. 引言
语音识别系统为计算机提供了聆听用户所说的自然语言,并转录对应的内容文本的能力。由于目前阶段技术上无法实现不受限制的语音识别(即在任何上下文中都能收听任何语音并转录的能力),为了能够实现合理的识别精度和响应时间,现有的语音识别器通过使用语法来限制他们所聆听的内容。
译注:在 JSGF 制定的时候(1998年)技术上还无法做到 不受限制的语音识别 ,但是随着软硬件技术的发展,以现在的技术已经可以实现不受限制的语音识别了。并且在移动设备甚至物联网设备上都可以实现不受限制的语音识别。
JSpeech Grammar Format(JSGF)定义了一种与具体平台和供应商无关的,用于语音识别的规则语法(Rule grammar),也被称作命令控制语法(Command and control grammar)或正则语法(Regular grammar)。它使用开发人员与计算机设备都可以阅读和编辑的文本形式来进行表示,并且可以导入到源代码中。另一种主要的语法类型——听写语法(Dictation grammar)不在本文档的讨论范围。
规则语法定义了用户可能说出的口语化或书面性语句,例如:一个简单的窗口控制语法可能需要监听“open window”和“close window”等命令。
用户可以说的内容取决于一定的上下文:用户想要控制电子邮件应用、读出信用卡号码或选择一个字体?应用程序知道这个上下文环境,所以能够为语音识别器提供特定的语法。
本文档就是对 JSGF 语法格式的规范性描述。本文档首先描述了语法基本的命名和结构,然后描述了由 语法头 和 语法体 组成的语法正文。语法头声明了语法的名称,以及需要导入的外部规则及其他语法。语法体则定义了规则语法可以匹配到的口语文本和引用的其他规则。最后本文档还提供了一些语法定义的简单示例。
1.1 相关主题
以下是一些对于理解和使用 JSGF 有所帮助的文档:
The JSpeech Grammar Format adopts some of the style and conventions of the JavaTM Programming Language. Readers interested in a comprehensive specification are referred to The JavaTM Language Specification, Gosling, Joy and Steele, Addison Wesley, 1996 (GJS96).
Grammars in the JSpeech Grammar Format may be written with the Unicode character set, as defined in The Unicode Standard, Version 2.0, The Unicode Consortium, Addison-Wesley Developers Press, 1996 (Uni96).
2. 定义
2.1 语法名(Grammar Names) 和 包名(Package Names)
每一个 JSGF 语法都要在语法头中声明一个唯一的语法名称,语法名称的合法结构有以下两种形式:
形式一
原文:packageName.simpleGrammarName
翻译:包名称.语法名称
形式二
原文:grammarName
翻译:语法名称
第一种形式(包名称 + 语法名称)是完整的语法名称声明形式,第二种形式(仅语法名称)是精简的语法名称声明形式。以下示例都是合法的语法名称声明形式:
com.sun.speech.apps.numbers
edu.unsw.med.people
examples
包名称 和 语法名称的格式,与 Java™ 程序语言的包名、类名相一致。一个完整的语法名称是一组由点号(“.”)分割开的标识符(关于 Java™ 中的标识符的更多信息,请参见"The Java Language Specification", Gosling, Joy and Steele, Addison Wesley, 1996, (GJS96) §3.8 and §6.5)。
语法名称的命名,也遵循 Java™ 编程语言中类名的命名约定(参见 GJS96),该约定能够将命名冲突的可能性降至最低,包名称应该为:
原文:reversedDomainName.localPackaging
翻译:反转域名.本地包名称
例如,“com.sun.speech.apps.numbers”这个例子中,“com.sun”部分是 Sun 的网络域名的序列反转,“speech.apps”部分是 Sun 内部划分的本地包名称,而“numbers”是简单的语法名称。
2.2 规则名(Rulenames)
语法由一组规则组成,这些规则共同定义了可以说的内容。规则由可以口述的自然语言文本以及对其他规则的引用而组成。每个规则都具有唯一的规则名称。对规则的引用由使用“<>”字符包裹起来的规则名来表示。
合法的规则名称类似于 Java 编程语言中的标识符,但允许一些额外的符号出现。一个合法的规则名称是由无限长度的 Unicode 字符序列及以下字符组成的:
- 在 Java 编程语言中允许用作标识符的任何合法字符
- 以下额外的标点符号:
+ - : ;, = |/ \ ( ) [ ] @ # % !^ & ~
语法开发人员应注意两个特定的约束:第一,规则名称 与 精确的 Unicode 字符串进行匹配,即规则名称是 大小写敏感 的,举个例子:<Name>、<NAME>和<name>是三个不同的规则名称。第二,空格不允许在规则名中出现。
<NULL>和<VOID>是两个保留的规则名称,下文中将详细讨论其特殊用途。
Unicode 字符集包含了世界上大多数语言的字符,因此规则名称可以用中文、日文、韩文、泰语、阿拉伯语、欧洲语言等来命名,以下示例均为合法的规则名称:
<hello><Zürich><user_test><$100><1+2=3><παβ>
2.2.1 限定名 和 完全限定名
尽管规则名称在一份语法中是唯一的,但是在不同的语法里面是可以重复使用相同的规则名称的。下面先来介绍 import 语句,import 允许在一个语法当中引用另一个语法里声明的规则。当在两个语法当中使用相同的规则名称时,对该规则名称的引用此时可能会产生歧义。限定名(Qualified Names) 和 完全限定名(Fully-Qualified Names)用于在不同的语法之间进行没有歧义的引用。
一个完全限定的规则名称包含了 完整的 语法名称 和 规则名称 ,例如:
<com.sun.greetings.english.hello><com.sun.greetings.deutsch.gutenTag>
一个限定的规则名称包含了 简单的 语法名称 和 规则名称 ,例如:
<english.hello><deutsch.gutenTag>
规则名称的使用需要遵循以下规则:
- 限定名称和完全限定名称不能用于规则的定义
import语句必须使用 完全限定 的规则名称- 本地规则时可以使用限定名称或完全限定名称进行引用,类似如下形式 :
<localGrammarName.ruleName>
2.2.2 解析规则名称
对于规则名称的不明确引用会导致异常发生,对规则引用的解析需要遵循以下行为:
-
存在同名的本地规则时候,本地规则优先:如果本地规则和导入的其他规则具备相同的规则名,则对简单规则名(
<ruleName>)的引用是指对本地规则的引用。 -
不存在同名的本地规则,存在多个导入的同名规则:如果使用
import语句导入的两个及以上的具有相同的规则名(<ruleName>),则使用简单规则名对其进行引用会引起歧义,这种引用是错误的。为了避免这个问题,对这种规则的引用必须使用限定名称或完全限定名称。 -
不存在同名的本地规则,存在多个导入的同名规则,并且他们的语法名称相同:如果使用
import语句导入的两个及以上的具有相同的规则名(<ruleName>),并且他们的语法名称(Grammar Name)相同(但是它们的包名肯定是不同的)。此时对这些规则的引用必须使用完全限定名称。 -
使用完全限定名称进行引用时永远不会造成歧义。
当无法解析一个规则的引用时(即该规则既没有在本地进行定义,也不是一个导入的 public 规则),对该规则的引用此时由识别器的软件结构自行处理(默认行为应当是抛出异常或者发出错误消息)。
2.2.3 保留的规则名称
JSGF 定义了两个保留的规则名,<NULL> 和 <VOID> 。这两个规则是通用的,它们可以在不使用 import 导入的前提下在任何规则中进行引用。并且在任何语法中都无法重新定义他们。这两个规则名本身就是完全限定的,因此对它们引用时,无需使用限定名称。
-
<NULL>定义一个自动匹配规则,它用于标识用户在此处没有说任何字词。 -
<VOID>定义了一个永远无法说出的规则,它用于插入序列当中使该序列被标记为不可能说出。
<NULL> 和 <VOID> 规则通常用于特殊情况,它们可以用来 禁用/启用 某些语法、控制递归或实现其他高级功能,有关它们的用途将在本文档后面介绍(参见:4.9 <NULL> 和 <VOID>)。
2.3 词元(Tokens)
词元(Token),也被称作终极符(Terminal symbol),是语法中定义的用户可能说出的内容。大多数时候,词元是一个单独出现的字词,例如:
hello
konnichiwa
词元也可以以由空格符分隔开来的字词序列的形式出现,例如:
this is a test
open the directory
在 JSGF 语法中,词元可以被空格、引号或其他符号分隔,以下符号都可以分隔词元:
; = | * + <> () [] {} /* */ //
同时,词元也是对识别器词汇表(Recognizer's vocabulary)中的条目的引用。识别器词汇表通常被称为词典(Lexicon),词典的作用是标注词元的发音(Pronunciation)。识别器通过被标注好的发音来聆听该词元。
JSGF 语法格式支持多语言语法,但是大多数识别器仅支持识别单语言。所以典型的语法将只包含一个语言,应用程序在将语法加载到识别器前需要确保该识别器支持语法中用到的语言。以下是一个简单的多语言规则:
<no> = no | nein | nao | non | nem;
大多数识别器对于它所支持的每种语言都有较为全面的词汇表来标注发音。但是却不可能做到包含该语言 100% 的词元,例如专有名词、技术术语和外来词汇往往在在词汇表中缺少对应的发音标记。为了解决这个问题,有三个可供参考的方案:
- 应用程序或用户可以将词元和发音添加到识别器的词典当中,以确保能够识别该词元。
- 优秀的识别器应当能够根据语言的发音规则推测出许多单词的发音。
- 如果以上两点都不可用,则对应词元的处理行为由对应的识别器软件自行决定。通常来说,未定义的词元是不会被说出的(相当于
<VOID>),否则会导致错误或异常的发生。
词元不一定是自然语言中的书面词汇,词元只要在识别器的词典中进行了定义就是可以被识别的。例如,如果需要处理单词 “read” 的两个不同的发音(过去时发音接近 “red” ,现在时发音接近 “Reed”),程序可以在词典中分别为 “read_past” 和 “read_present” 定义不同的发音。
2.3.1 引用词元
一个词元不一定是一个字词,词元也可以是由多个字词组成的序列(多字词词元)或特殊符号。可以使用引号将 多字词词元 或 特殊符号 括起来,用来标识它是一个完整的词元。例如:
the "New York" subway
"+"
当字词的发音会因上下文而发生变化时,这将会起到很重要的作用。多字词词元也可以用来简化处理结果,例如,从识别结果中直接获取到 “New York”、“Sydney”、“Rio de Janeiro” 等词元。
译注:如果 “New York” 以两个独立的词元 “New” 和 “York” 的形式在识别结果中返回的话,对于许多业务的处理将会变得十分复杂。业务上可能会需要某些字词一起出现。
引用词元的发音可以像其他的词元一样在识别器的词汇表中进行定义。识别器在词汇表找到 多字词词元 的对应发音规则时将使用其发音。找不到的情况下,默认会将引号内的内容以空格分隔为不同的词元发音。
在词元使用单引号时,需要在单引号的前面加上一个反斜杠 \。同样的道理,在词元中使用反斜杠时候,需要在反斜杠的前面再加一个反斜杠,以达到转义效果。下面两个例子分别表示 单个 反斜杠 和 单引号 字符:
"\\" "\""
空格在引用词元中非常重要。
2.3.2 特殊符号 和 标点符号
大部分语音识别器都能够处理常见的特殊符号和标点符号。例如,英语识别器通常能够处理省字符("Mary's", "it's")和连字符("new-age")。
但是依然有许多文本形式对于识别器而言任然难以做到无歧义的处理。这种时候,语法开发人员需要将词元尽量标注为更加接近口语上的发音形式,并且最好能够将其内置在识别器的词典中。以下是一些常见的示例:
- 数字:“012” 可以拓展出 “零一二” 或 “零幺二” 两种发音。同样的 “呼叫 110” 也可以拓展出 “呼叫 幺幺零”。
- 日期:“1997年08月15日” 可以拓展出 “九八年八月十五”。
- 缩写和首字母缩略词:“Mr.” 应当被标注为 “mister”,同样的 “St” 需要被标注为 “street”。
- 特殊符号:“&” 表示 “和”,“+” 表示 “加”,以此类推。
2.4 注释
注释可以出现在语法头或语法体当中。注释的形式采用 Java 编程语言(参见 GJS96)的形式。有两种形式的注释:
| 注释形式 | 描述说明 |
|---|---|
| /* text */ | 一个典型的注释,/* 和 */ 之间的所有字符都将被忽略 |
| // text | 一个单行注释,从 // 到行尾的所有字符都将被忽略 |
注释不支持嵌套,“//” 在 /* 和 */ 之间没有任何特殊意义,同样 “/” 和 “/” 在 // 后面也没有任何特殊意义。
注释不允许在词元、引用词元、规则名称、标签、权重中出现。语法中的其他位置则不受限制,都可以使用注释。
JSGF 支持与 Java 编程语言(参见 GJS96)有类似风格的特殊注释,这些特殊注释在(4.10 文档注释)中进行了详细定义。
3. 语法标题/语法头
单个文件仅用于定义单个语法,一个语法定义包含两个部分:语法头 和 语法体 ,语法头包含三个部分的内容:声明自识别标头、声明语法名称、导入来自其他语法的规则。语法体则用来定义公开或非公开的语法规则。公开语法规则在语法规则声明语句前使用 public 作为开头标识。
3.1 标头
所有的 JSGF 文档都以标头信息为开头,标头信息以井号 # 开头,分号 ; 结尾,且独占一行。标头内容分为四个部分:
- 固定的
JSGF字样; - 文档所使用的
JSGF 版本(目前为 “V1.0”); - 可选的
字符编码声明,用于标明文档所使用的字符集; - 可选的
区域信息声明,用于标明语言支持的国家和地区。
标头的四个内容部分之间使用空格分隔,标头中只允许出现 ASCII 字符。
一个完整的标头格式为:
#JSGF version char-encoding locale;
以下都是合法的标头示例:
#JSGF V1.0;
#JSGF V1.0 ISO8859-5;
#JSGF V1.0 JIS ja;
第一个例子中没有声明 字符编号 和 区域信息,因此将自动采用适当的默认值。如果在美国的话,字符编码的默认值可能是 ISO8859-1(标准字符集)区域信息的默认值可能是 en(英语区域)。
第二个例子中声明了字符编码为 ISO8859-5(西里尔字母)。但是没有声明区域信息,因此区域信息将采用默认值。
第三个例子中声明了字符编码为 JIS(一种日文字符集),并且声明了区域信息为 ja(日语地区)。
3.2 语法声明
语法名声明是语法文件的首个内容语句。语法声明为以下两种格式之一:
原文:grammar packageName.simpleGrammarName;
翻译:grammar 包名称.语法名称
原文:grammar grammarName;
翻译:语法名称
包名和语法名的格式在 2.1 语法名(Grammar Names) 和 包名(Package Names) 中有详细描述。
示例:
grammar com.sun.speech.apps.numbers;
grammar edu.unsw.med.people;
grammar examples;
3.3 导入
语法标头中可以包含导入声明。导入声明需要使用语法声明中声明的语法名,并且需要位于语法体(规则声明)之前。导入声明允许在当前语法中使用其他语法声明的一个或全部公开(public)语法规则。导入声明有以下两种格式:
原文:import <fullyQualifiedRuleName>;
翻译:import <完全限定规则名>;
原文:import <fullGrammarName.*>;
翻译:import <完全语法名.*>;
例如:
import <com.sun.speech.app.index.1stTo31st>;
import <com.sun.speech.app.numbers.*>;
第一个示例使用完全限定的语法名导入了单个单个语法规则,导入来自语法 <com.sun.speech.app.index> 中的公开语法规则 <1stTo31st>。
第二个示例中使用 星号 导入了语法 <com.sun.speech.app.numbers> 中的全部公开语法规则。如果该语法中有 <digits>、 <teens>、 <zeroToMillion> 三个公开语法规则,则这三个语法规则在当前语法中全部可以进行引用。
需要注意的是,由于 语法名称 和 (规则名 或 星号) 都是必须的,所以仅包含语法名称的导入声明是不合法的,例如:
import <ruleName>; // 不合法的导入声明
导入的语法规则可以通过三种方式进行引用:
- 通过简单语法规则名进行引用(
<digits>) - 通过限定语法规则名进行引用(
<numbers.digits>) - 通过完全限定语法规则名进行引用(
<com.sun.speech.apps.numbers.digits>)
规则名称的解析在本文 2.2.2 解析规则名称 中有详细说明。需要知道的,如果当前语法中的所有规则引用全部使用的语法规则的完全限定名形式,则导入声明语句是非必须的、可选的。
// 导入 com.sun.speech.app.numbers 的声明语句是可选的
<rule> = <com.sun.speech.app.numbers.digits>;
4. 语法正文/语法体(Grammar Body)
4.1 规则定义
语法正文用于定义语法规则。每个语法规则都只能在语法中进行一次定义,语法规则定义的先后顺序并不重要。
原注:这两条性质不同于某些自然语言或计算机语言。在那些语言的语法中允许非结束语句的重复定义,或者语句顺序是重要的
语法规则的定义有以下两种格式:
<ruleName> = ruleExpansion ;
public <ruleName> = ruleExpansion ;
一条语法规则定义由以下几部分组成:
- 可选的
public声明 - 定义的语法规则名称
<ruleName> - 等号
= - 语法规则的扩展(Expansion)
- 分号
;
在规则定义之前、public 关键字和规则名称之间、等号周围、分号周围的空格将被忽略,空格仅在扩展中具备重要意义。
规则扩展定义了规则的发音方式,规则扩展是一个符合逻辑的词元(可能被说出的文本)和对其他规则引用的组合。我们之所以使用 扩展(Expansion) 这个术语词汇,是因为扩展定义了一个规则的扩展方式——当被口语话说出时,一个规则可以扩展成很多种不同的口语说法。并且扩展本身也是可以扩展的,后面的章节将会 扩展 的扩展方式。
4.1.1 公开规则
语法中的任何规则,都可以通过 public 关键字将其标记为公开规则,公开规则有以下三种潜在用途:
- 公开规则可以在另一个语法的规则定义部分通过完全限定规则名进行引用。或者通过在导入声明部分进行导入,然后通过规则名进行无歧义的引用;
- 公开规则可用作识别的生效规则,也就是说,识别器可以通过公开规则确定用户可能说出的内容;
- 公开规则可以进行本地引用,即被当前语法文件的任何公开或非公开规则引用。
如果规则没有被声明为公开(public),则它默认被认为是私有(Private)规则。私有规则仅能被当前语法定义的其他规则引用。
4.2 规则扩展(Rule Expansions)
最简单的规则扩展是对词元的引用或对其他规则的引用,例如:
<a> = elephant;
<b> = <x>;
<c> = <com.acme.grammar.y>;
规则 <a> 的扩展为单个词元 “elephant” ,因此,想要识别词元 <a> 用户就必须说出 “elephant” 这个词。
规则 <b> 的扩展为对规则 <x> 的引用,因此,想要识别 <b> 用户就必须说出符合规则 <x> 的内容。同理,想要识别规则 <c> 用户就必须说出符合规则 <com.acme.grammar.y> 的内容。
原注:按照 2.2 规则名(Rulenames) 中的定义,如果
<x>和<com.acme.grammar.y>是导入的外部规则,则必须在当前的语法中对他们进行导入,并且这两个规则必须被声明为公开规则。
符合以下任一条件的都是合法的规则扩展:
- 任何词元;
- 对当前的语法中定义的任何公开或私有规则的引用;
- 对导入的语法中定义的任何公开规则的引用;
- 对其他的语法中定义的任何公开规则的完全限定名形式的引用(不论它是否导入)。
规则的引用策略是在本地定义的,也就是说,由当前规则的作用范围所定义。例如:语法 grammar1 中的规则可以合法的引用 grammar2 中的公开规则,而被引用的公开规则又可以引用 grammar2 中的非公开规则。也就是说,规则可以通过引用另一个语法的公开规则的方式来引用该语法中的私有规则。
空白扩展的规则定义是不合法的:
<d> = ; // 不合法
将规则扩展定义为 <NULL> 或 <VOID> 是合法的:
<e> = <NULL>; // 合法
<f> = <VOID>; // 合法
以下小结介绍了通过逻辑组合定义更加复杂规则扩展的方法,有以下逻辑组合方式:
- 组合(Composition):扩展 的有序序列,以及可替换的 扩展 集合
- 分组(Grouping):使用小括号或中括号对 扩展 进行分组
- 一元运算符(Unary Operators):用于定义 扩展 的重复次数的运算符
- 附加(Attachment):将带有特定含义的标签(Tags)附加到 扩展 上
4.3 组成(Composition)
4.3.1 序列(Sequences)
规则可以定义为由多个 扩展 组成的有序序列,序列由合法的 扩展 组成,使用空格符进行分隔。例如,由于词元和规则引用都是合法的规则 扩展 ,因此以下例子都是合法的规则定义:
<where> = I live in Boston;
<statement> = this <object> is <OK>;
要识别扩展序列,用户必须按序列中定义的扩展次序说出所有扩展。在第一个例子中,要识别 <where> 规则,用户必须按照单词顺序完整说出“I live in Boston”这句内容。第二个例子 <statement> 混合了词元和规则引用。要识别这个规则,用户必须先说出 “this”,然后说出能够匹配规则 <object> 的内容,接下来说出 “is”,最后说出能够匹配规则 <OK> 的内容。
序列中的子项可以是任何合法的规则扩展,也包括了接下来将要介绍的 选项、分组 等 扩展 结构。
4.3.2 选项(Alternatives)
规则可以定义为由竖线符(|)和可选的空格分隔开的 扩展 的无序集合,例如:
<name> = Michael | Yuriko | Mary | Duke | <otherNames>;
要识别规则 <name> ,用户必须说出 “Michael”、“Yuriko”、“Mary”、“Duke” 或符合规则 <otherNames> 的内容之一,并且仅需说出其中的一个。但是用户不能说“Mary Duke”。
序列的优先级高于选项,例如:
<country>= South Africa | New Zealand | Papua New Guinea;
规则 <country> 是一个有三个子项的选项(Alternatives),每个子项为一个国家或地区的名字。
4.3.3 权重(Weights)
不同的语法内容被用户说出的潜在可能性并非全部均等,权重(Weights)是可以附加到选项的子项中,用于标明该子项被说出的概率大小的元素。权重是由斜杠符(/)包裹起来的浮点数,例如 /3.14/。权重的数值越大,意味着该子项被用户说出的可能性也就越大。权重放在选项中每个子项之前,例如:
<size> = /10/ small | /2/ medium | /1/ large;
<color> = /0.5/ red | /0.1/ navy blue | /0.2/ sea green;
<action> = please (/20/save files |/1/delete all files);
<place> = /20/ <city> | /5/ <country>;
原注:Java 编程语言中要求浮点数使用 “f” 或 “F” 作为后缀,这在 JSGF 语法中不是必须的
权重反映了选项中不同子项的出现概率。在第一个例子中,语法编写者标记了 “small” 被说出的概率是 “large” 的 10 倍,是 “medium” 的 5倍。
使用权重时必须同时满足以下条件:
- 如果为选项中的一个子项指定了权重,则必须为该选项中的所有子项全部指定权重(要么全有,要么全无)
- 权重必须是浮点数。例如:
56、0.056、3.14e3、8f - 斜杠内只允许出现浮点数和空格
- 权重必须大于或等于零。权重用于标识子项可能被说出的概率,一个永远不可能被说出的子项可以被替换为
<VOID>(零权重在语法开发方面非常有用) - 选项中至少有一个非零的正权重
合适的权重往往很难确定,而猜测的权重又不一定能够提高识别能力。有效的权重通常是通过对语音数据和文本数据进行研究来确定的。
并非所有的识别器在识别过程中都是用权重,但是识别器至少会判断序列中的子项权重是否为零,以此来判断该子项是否永远不会被说出。
4.4 分组
4.4.1(括号)
任何合法的扩展都可以使用括号 () 来进行分组。分组具有很高的优先级,因此可以确保对规则的正确解释。例如,由于 序列 的优先级高于 选项 ,因此 分组 在以下规则的定义中是必须的,以确保 “please open” 和 “please close” 是合法的内容。
<action> = please (open | close | delete);
下面的例子定义了一个有三个子项的序列,每个子项都是使用括号进行正确分组的选项:
<command> = (open | close) (windows | doors)
(immediately | later);
要识别规则 <command> ,用户必须按顺序在三个选项分组中的每组各说出一个词,例如:“open window immediately” 或 “close doors later”。
译注:请注意此处规则
<command>的定义占了多行。在 JSGF 语法中,规则的定义并不限制在同一行,一个规则的定义可以横跨多行。
如果使用括号将单个扩展包裹成分组,则意味着将该内容定义为只有一个子项的序列,例如:
( start )
( <end> )
原注:这个定义为语法解析提供了和其他语法分析的一致性
括号内不允许为空,空的分组是不合法的。
( ) // 不合法
4.4.2 [可选分组]
任何合法的扩展都可以使用方括号 [] 标记为可选分组,用以标明方括号内的内容是可选的。其他方面可选分组和分组功能一致,它们具有相同的优先级。
例如:
<polite> = please | kindly | oh mighty computer;
public <command> = [ <polite> ] don't crash;
该语法定义允许用户说 “dont' crash”,并且可以选择性的添加一种礼貌性的修饰,例如 “oh mighty computer don't crash” 或 “kindly don't crash” 都是可以被识别到的。
方括号内不允许为空,空的可选分组是不合法的。
[ ] // 不合法
4.5 一元运算符(Unary Operators)
JSGF 语法有三个一元运算符:克莱尼星号、加号运算符、标签。一元运算符具有以下特点:
- 一元运算符可以附加到任何合法的规则扩展
- 一元运算符具有很高的优先级:它们紧密附加到运算符前面的规则扩展上
- 规则扩展只能附加一个一元运算符(标签除外)
- 规则扩展和一元运算符之间的空格将被忽略
由于一元运算符的优先级高于序列和选项,所以如果要将运算符应用于整个序列或分组,则需要用括号将整个序列或分组包裹起来。
4.5.1 * 克莱尼星号
一个规则扩展后跟着一个星号,表示该扩展可能被说出零次或多次。星号被称作克莱尼星号(以 Stephen Cole Kleene 的名字命名,他发明了该符号的使用)。例如:
<polite> = please | kindly | oh mighty computer;
<command> = <polite>* don't crash;
这个语法支持识别 “please don't crash”,“oh mighty computer please please don't crash”,“don't crash” 等内容。
作为一元运算符,该符号具有很高的优先级,例如:
<song> = sing New York *;
在这个例子中,运算符作用于它前面的一个合法扩展(词元“Yor”)。因此可以识别:“sing New York”,“sing New”,“sing New York New York New York”,但是不能识别“sing New York New York”。引用或者括号可以修改 * 运算符的作用域,例如:
<song> = sing (New York) *;
现在规则 song 可以匹配 “sing New York New York” 了。
4.5.2 + 加号运算符
一个规则扩展后跟着一个加号,表示该扩展可能被说出一次或多次。例如:
<polite> = please | kindly | oh mighty computer;
<command> = <polite>+ don't crash;
此时必须带有最少一个 <polite> 前缀才能被识别。例如 “please please don't crash” 可以被识别,但是 “don't crash” 不能被识别。
4.6 标签(Tags)
标签功能允许语法编写者将带有特定含义的标签(Tags)附加到语法扩展上。应用程序通常使用标签来简化或加强对识别结果的处理。
附加的标签不会影响语法的识别。事实上标签是附加到识别器返回给应用程序的识别结果上的。识别器的软件接口定义了标签的提供机制。
原注:最小的预期是语法结果树中应当带有附加的标签
标签是一元运算符,因此它可以附加到任何合法的扩展中。标签是使用大括号 {} 包裹起来的字符串。大括号内的所有字符都被视为标签的一部分,包括空格。因此,空括号 “{}” 是一个合法的标签。在这个情况下,标签被解释为长度为零的字符串(即空串,与 "" 在 Java 语言中的意义一致)。
如果需要在标签中使用右大括号,则需要使用反斜杠 \ 对右大括号进行转义(即 \});如果需要在标签中使用反斜杠,则需要再加一个反斜杠对其进行转移(即 \\)。例如:
{ {nasty \\looking\\ tag\} }
这个标签将被处理为以下字符串:
" {nasty \looking\ tag} "
标签紧密附加到它前面的一个规则扩展上(空格将被忽略),例如:
<rule> = <action> {tag in here};
<command>= please (open {OPEN} | close {CLOSE}) the file;
<country> = Australia {Oz} | (United States) {USA} |
America {USA} | (U S of A) {USA};
作为一元运算符,标签的优先级高于序列和选项。例如:
<action> = book | magazine | newspaper {thing};
在这个例子中,标签 {thing} 仅附加到了词元 newspaper 上。括号可以用于修改标签的附加行为,例如:
<action> = (book | magazine | newspaper) {thing};
与其他一元运算符不一样的是,一个规则扩展可以附加多个标签。例如:
<OKRule> = <action> {tag1} {tag2} {tag3}; // 合法
这可以被理解为使用了括号:
<OKRule> = ((<action> {tag1}) {tag2}) {tag3}; // 合法
这种语法便利性仅适用于标签,以下形式的一元运算符是不合法的:
<badRule> = <action> * {tag1}; // 不合法
<badRule> = <action> {tag1} +; // 不合法
4.6.1 使用标签
标签可以用于简化对识别结果的处理。标签的内容及使用方式完全取决于开发者。
标签的一个重要用途是应用程序国际化,下面定义了适用于四种不同自然语言的语法规则,程序将适当的时候将用户所说的语言加载到识别器中。标签在所有的语言中都保持不变,因此可以起到简化对识别结果的处理。一般来说语法名将包含语言标识符,以便能够以编程的方式定位和加载。
英语语法:
<greeting>= (howdy | good morning) {hi};
日语语法:
<greeting>= (ohayo | ohayogozaimasu) {hi};
德语语法:
<greeting>= (guten tag) {hi};
法语语法:
<greeting>= (bon jour) {hi};
4.7 优先级
JSGF 语法中规则扩展的各类实体的优先级如下列表(按优先级从高到低排列):
- 包含在尖括号内的规则名称,以及带引号和不带引号的词元
- 圆括号 “()” 表示分组,方括号 “[]” 表示可选分组
- 一元运算符(“+”、“*” 和 标签)应用于紧接着的前一个规则扩展(要将它们应用于序列或选项,请使用分组 “()” 或 可选分组“[]”)
- 序列:由多个规则扩展组成的有序序列
- 选项:由竖线符(
|)分隔开的扩展的无序集合
4.8 递归
递归是一个规则对它自身的一种特殊定义。递归是一个强大的工具,可以表示许多口语上的复杂语法形式。支持 JSGF 语法格式的识别器允许使用右递归。右递归中,规则最后一个部分可以是规则自身。例如:
<command> = <action> | (<action> and <command>);
<action> = stop | start | pause | resume | finish;
规则 <command> 能够匹配以下内容:“stop”、“stop and finish”、“start and resume and finish”。
嵌套右递归也是允许的。嵌套右递归是在一个规则中引用了另一个规则,被引用的规则由引用了引用它的那个规则。每个递归引用都是规则的最后一部分。例如:
<X> = something | <Y>;
<Y> = another thing <X>;
嵌套右递归可能在语法中出现。但是 强烈不建议 这样做,因为这会带来不必要的复杂性和潜在的维护问题。
任何右递归都可以使用 “*” 和 “+” 运算符来重写。例如,以下规则定义是等效的
<command> = <action> | (<action> and <command>);
<command> = <action> (and <action>) *;
尽管可以使用 “*” 和 “+” 运算符来重写正确的递归语法,但是 JSGF 语法上依然保留了递归的使用方式。因为递归允许对某些语法进行更简单、更优雅的表示。JSGF 语法不支持其他形式的递归(左递归(left recursion)、内嵌式递归(embedded recursion)),因为重写条件得不到保证。
4.9 <NULL> 和 <VOID> 的用途
本文档前面定义了两个特殊的规则名称,即 <NULL> 和 <VOID>。它们具备许多高级用途。
<NULL> 和任何规则组合成选项,可以达到和可选分组一样的效果。以下两条规则具备相同的效果:
<x> = a | <NULL>;
<x> = [ a ];
克莱尼星号和右递归可以映射为如下形式:
<x> = <NULL> | a <x>;
<x> = a*;
上面两个例子的语法上都具备相同的效果,可以认为它们是等效的。然而,当用户说出同样的内容时,识别器可能会产生不同的结果,最终命中不同的语法。
最后 <NULL> 和 <VOID> 还可以用作开关,用于控制语法的启用和禁用(生效与否)。程序可以定义一个名为 <gate> 的规则,将它放在需要需要开关的语法部分,并将 <gate> 的定义在 <NULL> 和 <VOID> 之间进行切换。
<gate> = <NULL>;
<gate> = <VOID>;
当 <gate> 为 <VOID> 时,引用了 <gate> 的所有语法将被禁用。
需要注意的时候,这个功能能够使用的前提是:识别器要能够支持重新编译已经定义好了的语法(重新编译 <gate> 的定义)。并非所有识别器都能够做到重新编译。
4.10 文档注释
JSGF 语法支持与 Java 编程语言中文档注释风格(GJS96, §18)类似的注释。注释可以出现在语法声明之前、导入语句和任何规则定义之前。超文本网页或其他形式的文档可以通过注释来自动生成。
文档注释以 /** 开头,并以 */ 结尾。每条注释行上前面的 * 字符和空格将被忽略。
注释的第一句话(以第一个句号字符结尾)应当是一个总结性的语句,用于对它所关联的实体进行描述。
和 Java 中的文档注释不一样的事,JSGF 注释中不允许使用 HTML 标记。因为 HTML 标签 和 JSGF 的规则名引用使用了相同的格式。例如,<font> 究竟是一个规则名还是一个 HTML 代码?
注释中的标签段落是注释行内容中以 @ 开头部分,后面紧跟着标签关键字。标签段落包括了接下来的所有注释行,直到出现了下一个标签段落开头标记(@)或到达了注释末尾(注释中的标签与规则定义中的标签无关)。
下面的例子展示了文档注释的基本结构。5.2 示例 3:文档注释 中展示了对简单语法进行文档注释。
/**
* Initial sentence provides a summary.
* Additional text and paragraphs can be inserted.
*
* @tag several types of tag are defined below
*/
4.10.1 注释标签:@author
@author 标签用于在文档注释中声明相应实体的作者。尽管 @author 标签没有定义结构,但是建议将每个作者都列在一个单独的 @author 中,单个文档注释可以包含多个 @author 标签。例如:
@author James Bond
@author Jose Alvarez
4.10.2 注释标签:@version
@version 标签用于在文档注释中声明语法版本,它没有特定的格式或结构要求,可以放置任意版本描述信息。例如:
@version versionInformation
4.10.3 注释标签:@see
@see 标签用于在文档注释中声明对其他规则(本地规则或导入规则)或其他语法的交叉引用。@see 标签中使用 .* 后缀来表示引用的是某个语法(而非语法中的某个具体规则)。例如:
@see <com.acme.number.zeroToTen>
@see <com.acme.number.*>
@see <zeroToTen>
4.10.4 注释标签:@example
@example 注释用于在文档注释中声明语法可以匹配的内容示例。适当的示例可以使语法和规则更加的易于理解。示例文本也可以用来通过语法工具校验语法的正确性。例如:
@example please say your name and age
@example I love "south america"
我们鼓励开发者在示例中使用与规则定义的词元相同内容。例如,上面第二个例子应该被解释为两个词元(而不是一个词元),因为使用引号将 \"south america\" 包裹起来了。然而,为了清晰可见工具开发人员应该充分考虑示例文本中可能包含可读性格式的可能性。对于英语来说,可读性格式包括了标点符号(.,?!!)、词元的大写、词元的修改(例如缺少引号)等。
@example 标签中可以包含对其他规则名的引用,例如:
@example I want a pizza with <topping>
该示例标签将展开为 <topping> 规则定义的每个示例标签("I want a pizza with pepperoni",、"I want a pizza with mushrooms"等等)。
5. 示例
通过将简单的规则组合在一起,可以构建更加复杂的语法来匹配用户说出的内容。以下是一些带有完整语法头和语法体的语法示例。
5.1 示例 1:简单的命令和控制
这个例子展示了如何定义语音命令的两种基本语法,这些语法用于控制应用窗口。包括可选的礼貌性前缀,用于体现更加接近自然对话的语音交互方式。
#JSGF V1.0;
grammar com.acme.politeness;
// Body
public <startPolite> = (please | kindly | could you |
oh mighty computer) *;
public <endPolite> = [ please | thanks | thank you ];
语法 politeness 本身并没有什么用,但是可以用于导入到其他语法中用于控制对话风格。
#JSGF V1.0 ISO8859-1 en;
grammar com.acme.commands;
import <com.acme.politeness.startPolite>;
import <com.acme.politeness.endPolite>;
/**
* Basic command.
* @example please move the window
* @example open a file
*/
public <basicCmd> = <startPolite> <command> <endPolite>;
<command> = <action> <object>;
<action> = /10/ open |/2/ close |/1/ delete |/1/ move;
<object> = [the | a] (window | file | menu);
语法 commands 定义了一个公开规则 <basicCmd>,该规则由两个导入规则(startPolite、endPolite)和三个私有规则(command、action、object)组成。规则 action 和 object 都是不同的 选项。并且 <action> 中带有 权重 声明,指示了 “open” 比其他单词会被用户用户说出的概率更大。command 规则定义了一个 序列,其中 <action> 后面可以跟着可选的 “the” 或 “a”,最后跟着规则 <object>。
因为 <com.acme.commands.basicCommand> 是可选规则,所以可以用于激活识别。当它被用做识别器中时候,用户可以说出以下命令:
- "open a window"
- "close file please"
- "oh mighty computer please open a menu"
5.2 示例 2:解析名称
这个例子定义了服装描述相关的语法,其中使用到了完全限定名。导入的两个语法中定义了 “pants” 和 “shirts” 的语法规则,包括 “pants” 和 “shirts” 可能具有的颜色列表,本地规则 <color> 是导入颜色列表的组合。由于对 <color> 的引用会产生歧义,因此必须使用限定名或完全限定名。
#JSGF V1.0;
grammar com.acme.selections;
import <com.acme.pants.*>;
import <com.sun.shirts.*>;
<color> = <com.acme.pants.color> |
<com.acme.shirts.color>;
public <statement> = I like <color>;
最后一个定义中的对 <color> 的引用不存在歧义。因为本地规则优先级高于导入的规则,所以此处引用的是本地定义的规则。在本地定义 <color> 的定义中,可以使用限定名(而非完全限定名)来进行明确的引用。类似于:
<color> = <pants.color> | <shirts.color>;
5.2 示例 3:文档注释
这个例子展示了如何在语法中使用文档注释。它展示了使用文档注释的三种不同形式:用于语法声明、用于导入语句、用于规则定义。
#JSGF 1.0 ISO8559-1;
/**
* Define simple travel directives.
*
* @author Mary Contrary
* @version 3.141beta
*/
grammar com.acme.travel;
/**
* Get a list of city names: <city>.
*/
import <com.acme.cities.*>;
/**
* A simple travel command
*
* @example go from sydney to tokyo to dublin
* @example go from "san francisco" to bangkok
*/
public <travel> = go from <city> ( to <city> )+;