ruby_speech 2.2.2 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +50 -0
- data/ext/ruby_speech/ruby_speech.c +2 -2
- data/lib/ruby_speech/grxml.rb +24 -0
- data/lib/ruby_speech/grxml/builtins.rb +174 -0
- data/lib/ruby_speech/grxml/grammar.rb +1 -1
- data/lib/ruby_speech/version.rb +1 -1
- data/ruby_speech.gemspec +2 -0
- data/spec/ruby_speech/grxml/builtins_spec.rb +165 -0
- data/spec/ruby_speech/grxml/grammar_spec.rb +8 -0
- data/spec/ruby_speech/grxml/matcher_spec.rb +4 -0
- data/spec/ruby_speech/grxml_spec.rb +30 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/grammar_matchers.rb +79 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 891276a2d84f160536a3eb7bd4841b96a466a462
|
4
|
+
data.tar.gz: b97014a5a89012d01a7a7429aa4a0a0055af58e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54672c541a1203e51fa9058f7fff47f469854ddb551c90b0c786dd8c370bf2f9177360abe61d2bb357cf6d266b5b84e4e069851c499c7311cce9dc035acf119e
|
7
|
+
data.tar.gz: 9493a2b75a32fc7d2217708362848f131a69136fdade9f95fd9dff6d9b84c947567f56beb55e4535186a59fa18c66620e7933e44ca44f84d56814754044463fe
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# [develop](https://github.com/benlangfeld/ruby_speech)
|
2
2
|
|
3
|
+
# [2.3.0](https://github.com/benlangfeld/ruby_speech/compare/v2.2.2...v2.3.0) - [2013-09-30](https://rubygems.org/gems/ruby_speech/versions/2.3.0)
|
4
|
+
* Feature: Allow generation of a boolean, date, digits, currency, number, phone or time grammar including from URIs
|
5
|
+
* Bugfix: Ensure that rule refs can be reused when inlining grammars
|
6
|
+
|
3
7
|
# [2.2.2](https://github.com/benlangfeld/ruby_speech/compare/v2.2.1...v2.2.2) - [2013-09-03](https://rubygems.org/gems/ruby_speech/versions/2.2.2)
|
4
8
|
* Bugfix: Fix an exception message to include object type
|
5
9
|
|
data/README.md
CHANGED
@@ -135,6 +135,56 @@ which becomes
|
|
135
135
|
</grammar>
|
136
136
|
```
|
137
137
|
|
138
|
+
#### Built-in grammars
|
139
|
+
|
140
|
+
There are some grammars pre-defined which are available from the `RubySpeech::GRXML::Builtins` module like so:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
require 'ruby_speech'
|
144
|
+
|
145
|
+
RubySpeech::GRXML::Builtins.currency
|
146
|
+
```
|
147
|
+
|
148
|
+
which yields
|
149
|
+
|
150
|
+
```xml
|
151
|
+
<grammar xmlns="http://www.w3.org/2001/06/grammar" version="1.0" xml:lang="en-US" mode="dtmf" root="currency">
|
152
|
+
<rule id="currency" scope="public">
|
153
|
+
<item repeat="0-">
|
154
|
+
<ruleref uri="#digit"/>
|
155
|
+
</item>
|
156
|
+
<item>*</item>
|
157
|
+
<item repeat="2">
|
158
|
+
<ruleref uri="#digit"/>
|
159
|
+
</item>
|
160
|
+
</rule>
|
161
|
+
<rule id="digit">
|
162
|
+
<one-of>
|
163
|
+
<item>0</item>
|
164
|
+
<item>1</item>
|
165
|
+
<item>2</item>
|
166
|
+
<item>3</item>
|
167
|
+
<item>4</item>
|
168
|
+
<item>5</item>
|
169
|
+
<item>6</item>
|
170
|
+
<item>7</item>
|
171
|
+
<item>8</item>
|
172
|
+
<item>9</item>
|
173
|
+
</one-of>
|
174
|
+
</rule>
|
175
|
+
</grammar>
|
176
|
+
```
|
177
|
+
|
178
|
+
These grammars come from the VoiceXML specification, and can be used as indicated there (including parameterisation). They can be used just like any you would manually create, and there's nothing special about them except that they are already defined for you. A full list of available grammars can be found in [the API documentation](http://rubydoc.info/gems/ruby_speech/RubySpeech/GRXML/Builtins).
|
179
|
+
|
180
|
+
These grammars are also available via URI like so:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
require 'ruby_speech'
|
184
|
+
|
185
|
+
RubySpeech::GRXML.from_uri('builtin:dtmf/boolean?y=3;n=4')
|
186
|
+
```
|
187
|
+
|
138
188
|
#### Grammar matching
|
139
189
|
|
140
190
|
It is possible to match some arbitrary input against a GRXML grammar, like so:
|
@@ -50,7 +50,7 @@ static int is_match_end(pcre *compiled_regex, const char *input)
|
|
50
50
|
search_input[input_size] = *search++;
|
51
51
|
result = pcre_exec(compiled_regex, NULL, search_input, input_size + 1, 0, 0,
|
52
52
|
ovector, sizeof(ovector) / sizeof(ovector[0]));
|
53
|
-
if (result >
|
53
|
+
if (result > -1) return 0;
|
54
54
|
}
|
55
55
|
return 1;
|
56
56
|
}
|
@@ -82,7 +82,7 @@ static VALUE method_find_match(VALUE self, VALUE buffer)
|
|
82
82
|
}
|
83
83
|
return rb_funcall(self, rb_intern("match_for_buffer"), 1, buffer);
|
84
84
|
}
|
85
|
-
if (result == PCRE_ERROR_PARTIAL) {
|
85
|
+
if (result == PCRE_ERROR_PARTIAL || (int)strlen(input) == 0) {
|
86
86
|
VALUE PotentialMatch = rb_const_get(GRXML, rb_intern("PotentialMatch"));
|
87
87
|
return rb_class_new_instance(0, NULL, PotentialMatch);
|
88
88
|
}
|
data/lib/ruby_speech/grxml.rb
CHANGED
@@ -8,6 +8,7 @@ module RubySpeech
|
|
8
8
|
GRXML_NAMESPACE = 'http://www.w3.org/2001/06/grammar'
|
9
9
|
|
10
10
|
%w{
|
11
|
+
builtins
|
11
12
|
grammar
|
12
13
|
rule
|
13
14
|
item
|
@@ -29,5 +30,28 @@ module RubySpeech
|
|
29
30
|
def self.import(other)
|
30
31
|
Element.import other
|
31
32
|
end
|
33
|
+
|
34
|
+
URI_REGEX = /builtin:(?<class>.*)\/(?<type>\w*)(\?)?(?<query>(\w*=\w*;?)*)?/.freeze
|
35
|
+
|
36
|
+
#
|
37
|
+
# Fetch a builtin grammar by URI
|
38
|
+
#
|
39
|
+
# @param [String] uri The builtin grammar URI of the form "builtin:dtmf/type?param=value"
|
40
|
+
#
|
41
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar from the builtin set
|
42
|
+
#
|
43
|
+
def self.from_uri(uri)
|
44
|
+
match = uri.match(URI_REGEX)
|
45
|
+
raise ArgumentError, "Only builtin grammars are supported" unless match
|
46
|
+
raise ArgumentError, "Only DTMF builtins are supported" unless match[:class] == 'dtmf'
|
47
|
+
type = match[:type]
|
48
|
+
query = {}
|
49
|
+
match[:query].split(';').each do |s|
|
50
|
+
key, value = s.split('=')
|
51
|
+
query[key] = value
|
52
|
+
end
|
53
|
+
raise ArgumentError, "#{type} is an invalid builtin grammar" unless Builtins.respond_to?(type)
|
54
|
+
Builtins.send type, query
|
55
|
+
end
|
32
56
|
end # GRXML
|
33
57
|
end # RubySpeech
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module RubySpeech::GRXML::Builtins
|
2
|
+
#
|
3
|
+
# Create a grammar for interpreting a boolean response, where 1 is true and two is false.
|
4
|
+
#
|
5
|
+
# @param [Hash] options Options to parameterize the grammar
|
6
|
+
# @option options [#to_s] :y The positive/truthy/affirmative digit
|
7
|
+
# @option options [#to_s] :n The negative/falsy digit
|
8
|
+
#
|
9
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a boolean response.
|
10
|
+
#
|
11
|
+
# @raise [ArgumentError] if the :y and :n options are the same
|
12
|
+
#
|
13
|
+
def self.boolean(options = {})
|
14
|
+
truthy_digit = (options[:y] || options['y'] || '1').to_s
|
15
|
+
falsy_digit = (options[:n] || options['n'] || '2').to_s
|
16
|
+
|
17
|
+
raise ArgumentError, "Yes and no values cannot be the same" if truthy_digit == falsy_digit
|
18
|
+
|
19
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'boolean' do
|
20
|
+
rule id: 'boolean', scope: 'public' do
|
21
|
+
one_of do
|
22
|
+
item do
|
23
|
+
tag { 'true' }
|
24
|
+
truthy_digit
|
25
|
+
end
|
26
|
+
item do
|
27
|
+
tag { 'false' }
|
28
|
+
falsy_digit
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Create a grammar for interpreting a date.
|
37
|
+
#
|
38
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a date in the format yyyymmdd
|
39
|
+
#
|
40
|
+
def self.date(options = nil)
|
41
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'date' do
|
42
|
+
rule id: 'date', scope: 'public' do
|
43
|
+
item repeat: '8' do
|
44
|
+
one_of do
|
45
|
+
0.upto(9) { |d| item { d.to_s } }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Create a grammar for interpreting a string of digits.
|
54
|
+
#
|
55
|
+
# @param [Hash] options Options to parameterize the grammar
|
56
|
+
# @option options [#to_i] :minlength Minimum length for the string of digits.
|
57
|
+
# @option options [#to_i] :maxlength Maximum length for the string of digits.
|
58
|
+
# @option options [#to_i] :length Absolute length for the string of digits.
|
59
|
+
#
|
60
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a boolean response.
|
61
|
+
#
|
62
|
+
# @raise [ArgumentError] if any of the length attributes logically conflict
|
63
|
+
#
|
64
|
+
def self.digits(options = {})
|
65
|
+
raise ArgumentError, "Cannot specify both absolute length and a length range" if options[:length] && (options[:minlength] || options[:maxlength])
|
66
|
+
|
67
|
+
minlength = options[:minlength] || options['minlength'] || 0
|
68
|
+
maxlength = options[:maxlength] || options['maxlength']
|
69
|
+
length = options[:length] || options['length']
|
70
|
+
|
71
|
+
repeat = length ? length : "#{minlength}-#{maxlength}"
|
72
|
+
|
73
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'digits' do
|
74
|
+
rule id: 'digits', scope: 'public' do
|
75
|
+
item repeat: repeat do
|
76
|
+
one_of do
|
77
|
+
0.upto(9) { |d| item { d.to_s } }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Create a grammar for interpreting a monetary value. Uses '*' as the decimal point.
|
86
|
+
# Matches any number of digits, optionally followed by a '*' and up to two more digits.
|
87
|
+
#
|
88
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a monetary value.
|
89
|
+
#
|
90
|
+
def self.currency(options = nil)
|
91
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'currency' do
|
92
|
+
rule id: 'currency', scope: 'public' do
|
93
|
+
item repeat: '0-' do
|
94
|
+
ruleref uri: '#digit'
|
95
|
+
end
|
96
|
+
item repeat: '0-1' do
|
97
|
+
item { '*' }
|
98
|
+
item repeat: '0-2' do
|
99
|
+
ruleref uri: '#digit'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
rule id: 'digit' do
|
105
|
+
one_of do
|
106
|
+
0.upto(9) { |d| item { d.to_s } }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Create a grammar for interpreting a numeric value. Uses '*' as the decimal point.
|
114
|
+
# Matches any number of digits, optionally followed by a '*' and any number more digits.
|
115
|
+
#
|
116
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a numeric value.
|
117
|
+
#
|
118
|
+
def self.number(options = nil)
|
119
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'number' do
|
120
|
+
rule id: 'number', scope: 'public' do
|
121
|
+
item repeat: '0-' do
|
122
|
+
ruleref uri: '#digit'
|
123
|
+
end
|
124
|
+
item repeat: '0-1' do
|
125
|
+
item { '*' }
|
126
|
+
item repeat: '0-' do
|
127
|
+
ruleref uri: '#digit'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
rule id: 'digit' do
|
133
|
+
one_of do
|
134
|
+
0.upto(9) { |d| item { d.to_s } }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Create a grammar for interpreting a phone number. Uses '*' to represent 'x' for a number with an extension.
|
142
|
+
#
|
143
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a phone number.
|
144
|
+
#
|
145
|
+
def self.phone(options = nil)
|
146
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'number' do
|
147
|
+
rule id: 'number', scope: 'public' do
|
148
|
+
item repeat: '1-' do
|
149
|
+
one_of do
|
150
|
+
0.upto(9) { |d| item { d.to_s } }
|
151
|
+
item { '*' }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Create a grammar for interpreting a time.
|
160
|
+
#
|
161
|
+
# @return [RubySpeech::GRXML::Grammar] a grammar for interpreting a time.
|
162
|
+
#
|
163
|
+
def self.time(options = nil)
|
164
|
+
RubySpeech::GRXML.draw mode: :dtmf, root: 'time' do
|
165
|
+
rule id: 'time', scope: 'public' do
|
166
|
+
item repeat: '1-4' do
|
167
|
+
one_of do
|
168
|
+
0.upto(9) { |d| item { d.to_s } }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -118,7 +118,7 @@ module RubySpeech
|
|
118
118
|
def inline!
|
119
119
|
xpath("//ns:ruleref", :ns => GRXML_NAMESPACE).each do |ref|
|
120
120
|
rule = rule_with_id ref[:uri].sub(/^#/, '')
|
121
|
-
ref.swap rule.children
|
121
|
+
ref.swap rule.dup.children
|
122
122
|
end
|
123
123
|
|
124
124
|
query = "./ns:rule[@id!='#{root}']"
|
data/lib/ruby_speech/version.rb
CHANGED
data/ruby_speech.gemspec
CHANGED
@@ -10,6 +10,8 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.homepage = "https://github.com/benlangfeld/ruby_speech"
|
11
11
|
s.summary = %q{A Ruby library for TTS & ASR document preparation}
|
12
12
|
s.description = %q{Prepare SSML and GRXML documents with ease}
|
13
|
+
|
14
|
+
s.license = 'MIT'
|
13
15
|
|
14
16
|
s.rubyforge_project = "ruby_speech"
|
15
17
|
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RubySpeech::GRXML::Builtins do
|
4
|
+
describe "boolean" do
|
5
|
+
subject(:grammar) { described_class.boolean }
|
6
|
+
|
7
|
+
it { should max_match('1').and_interpret_as('true') }
|
8
|
+
it { should max_match('2').and_interpret_as('false') }
|
9
|
+
|
10
|
+
it { should not_match('0') }
|
11
|
+
it { should not_match('3') }
|
12
|
+
it { should not_match('10') }
|
13
|
+
|
14
|
+
context "with the true/false digits parameterized" do
|
15
|
+
subject { described_class.boolean y: 3, n: 7 }
|
16
|
+
|
17
|
+
it { should max_match('3').and_interpret_as('true') }
|
18
|
+
it { should max_match('7').and_interpret_as('false') }
|
19
|
+
|
20
|
+
it { should not_match('1') }
|
21
|
+
it { should not_match('2') }
|
22
|
+
it { should not_match('4') }
|
23
|
+
|
24
|
+
context "both the same" do
|
25
|
+
it "should raise ArgumentError" do
|
26
|
+
expect { described_class.boolean y: '1', n: 1 }.to raise_error(ArgumentError, /same/)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "date" do
|
33
|
+
subject(:grammar) { described_class.date }
|
34
|
+
|
35
|
+
it { should max_match('20130929').and_interpret_as('dtmf-2 dtmf-0 dtmf-1 dtmf-3 dtmf-0 dtmf-9 dtmf-2 dtmf-9') }
|
36
|
+
|
37
|
+
it { should potentially_match('130929') }
|
38
|
+
it { should potentially_match('0929') }
|
39
|
+
it { should potentially_match('29') }
|
40
|
+
it { should potentially_match('1') }
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "digits" do
|
44
|
+
subject(:grammar) { described_class.digits }
|
45
|
+
|
46
|
+
it { should match('1').and_interpret_as('dtmf-1') }
|
47
|
+
it { should match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
48
|
+
it { should match('1').and_interpret_as('dtmf-1') }
|
49
|
+
|
50
|
+
context "with a minimum length" do
|
51
|
+
subject { described_class.digits minlength: 3 }
|
52
|
+
|
53
|
+
it { should match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
54
|
+
it { should match('1234567890').and_interpret_as('dtmf-1 dtmf-2 dtmf-3 dtmf-4 dtmf-5 dtmf-6 dtmf-7 dtmf-8 dtmf-9 dtmf-0') }
|
55
|
+
|
56
|
+
it { should potentially_match('1') }
|
57
|
+
it { should potentially_match('11') }
|
58
|
+
it { should potentially_match('4') }
|
59
|
+
end
|
60
|
+
|
61
|
+
context "with a maximum length" do
|
62
|
+
subject { described_class.digits maxlength: 3 }
|
63
|
+
|
64
|
+
it { should match('1').and_interpret_as('dtmf-1') }
|
65
|
+
it { should match('12').and_interpret_as('dtmf-1 dtmf-2') }
|
66
|
+
it { should match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
67
|
+
|
68
|
+
it { should not_match('1111') }
|
69
|
+
it { should not_match('1111111') }
|
70
|
+
end
|
71
|
+
|
72
|
+
context "with an absolute length" do
|
73
|
+
subject { described_class.digits length: 3 }
|
74
|
+
|
75
|
+
it { should max_match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
76
|
+
it { should max_match('111').and_interpret_as('dtmf-1 dtmf-1 dtmf-1') }
|
77
|
+
|
78
|
+
it { should potentially_match('1') }
|
79
|
+
it { should potentially_match('12') }
|
80
|
+
|
81
|
+
it { should not_match('1234') }
|
82
|
+
it { should not_match('12345') }
|
83
|
+
end
|
84
|
+
|
85
|
+
context "when the min and max lengths are swapped" do
|
86
|
+
it "should raise ArgumentError" do
|
87
|
+
expect { described_class.digits minlength: 5, maxlength: 2 }.to raise_error(ArgumentError, /repeat/)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when the length and minlength are specified" do
|
92
|
+
it "should raise ArgumentError" do
|
93
|
+
expect { described_class.digits minlength: 5, length: 5 }.to raise_error(ArgumentError, /absolute length/)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when the length and maxlength are specified" do
|
98
|
+
it "should raise ArgumentError" do
|
99
|
+
expect { described_class.digits maxlength: 5, length: 5 }.to raise_error(ArgumentError, /absolute length/)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "currency" do
|
105
|
+
subject(:grammar) { described_class.currency }
|
106
|
+
|
107
|
+
it { should max_match('1*01').and_interpret_as('dtmf-1 dtmf-star dtmf-0 dtmf-1') }
|
108
|
+
it { should max_match('01*00').and_interpret_as('dtmf-0 dtmf-1 dtmf-star dtmf-0 dtmf-0') }
|
109
|
+
it { should max_match('100000000000*00').and_interpret_as('dtmf-1 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-star dtmf-0 dtmf-0') }
|
110
|
+
it { should max_match('0*08').and_interpret_as('dtmf-0 dtmf-star dtmf-0 dtmf-8') }
|
111
|
+
it { should max_match('*59').and_interpret_as('dtmf-star dtmf-5 dtmf-9') }
|
112
|
+
|
113
|
+
it { should match('0').and_interpret_as('dtmf-0') }
|
114
|
+
it { should match('0*0').and_interpret_as('dtmf-0 dtmf-star dtmf-0') }
|
115
|
+
it { should match('10*5').and_interpret_as('dtmf-1 dtmf-0 dtmf-star dtmf-5') }
|
116
|
+
it { should match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
117
|
+
it { should match('123*').and_interpret_as('dtmf-1 dtmf-2 dtmf-3 dtmf-star') }
|
118
|
+
|
119
|
+
it { should not_match('#') }
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "number" do
|
123
|
+
subject(:grammar) { described_class.number }
|
124
|
+
|
125
|
+
it { should match('0').and_interpret_as('dtmf-0') }
|
126
|
+
it { should match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
127
|
+
it { should match('1*01').and_interpret_as('dtmf-1 dtmf-star dtmf-0 dtmf-1') }
|
128
|
+
it { should match('01*00').and_interpret_as('dtmf-0 dtmf-1 dtmf-star dtmf-0 dtmf-0') }
|
129
|
+
it { should match('100000000000*00').and_interpret_as('dtmf-1 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-0 dtmf-star dtmf-0 dtmf-0') }
|
130
|
+
it { should match('0*08').and_interpret_as('dtmf-0 dtmf-star dtmf-0 dtmf-8') }
|
131
|
+
it { should match('*59').and_interpret_as('dtmf-star dtmf-5 dtmf-9') }
|
132
|
+
it { should match('0*0').and_interpret_as('dtmf-0 dtmf-star dtmf-0') }
|
133
|
+
it { should match('10*5').and_interpret_as('dtmf-1 dtmf-0 dtmf-star dtmf-5') }
|
134
|
+
it { should match('123*').and_interpret_as('dtmf-1 dtmf-2 dtmf-3 dtmf-star') }
|
135
|
+
it { should match('123*2342').and_interpret_as('dtmf-1 dtmf-2 dtmf-3 dtmf-star dtmf-2 dtmf-3 dtmf-4 dtmf-2') }
|
136
|
+
|
137
|
+
it { should not_match('#') }
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "phone" do
|
141
|
+
subject(:grammar) { described_class.phone }
|
142
|
+
|
143
|
+
it { should match('0').and_interpret_as('dtmf-0') }
|
144
|
+
it { should match('0123').and_interpret_as('dtmf-0 dtmf-1 dtmf-2 dtmf-3') }
|
145
|
+
it { should match('0123*456').and_interpret_as('dtmf-0 dtmf-1 dtmf-2 dtmf-3 dtmf-star dtmf-4 dtmf-5 dtmf-6') }
|
146
|
+
it { should match('0123*456*789').and_interpret_as('dtmf-0 dtmf-1 dtmf-2 dtmf-3 dtmf-star dtmf-4 dtmf-5 dtmf-6 dtmf-star dtmf-7 dtmf-8 dtmf-9') }
|
147
|
+
|
148
|
+
it { should potentially_match('') }
|
149
|
+
|
150
|
+
it { should not_match('#') }
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "time" do
|
154
|
+
subject(:grammar) { described_class.time }
|
155
|
+
|
156
|
+
it { should max_match('1235').and_interpret_as('dtmf-1 dtmf-2 dtmf-3 dtmf-5') }
|
157
|
+
|
158
|
+
it { should match('12').and_interpret_as('dtmf-1 dtmf-2') }
|
159
|
+
it { should match('4').and_interpret_as('dtmf-4') }
|
160
|
+
it { should match('123').and_interpret_as('dtmf-1 dtmf-2 dtmf-3') }
|
161
|
+
|
162
|
+
it { should not_match('*') }
|
163
|
+
it { should not_match('#') }
|
164
|
+
end
|
165
|
+
end
|
@@ -185,6 +185,8 @@ module RubySpeech
|
|
185
185
|
item :repeat => '4' do
|
186
186
|
ruleref :uri => '#digits'
|
187
187
|
end
|
188
|
+
item { '*' }
|
189
|
+
item { ruleref uri: '#digits' }
|
188
190
|
"#"
|
189
191
|
end
|
190
192
|
item do
|
@@ -205,6 +207,12 @@ module RubySpeech
|
|
205
207
|
0.upto(9) { |d| item { d.to_s } }
|
206
208
|
end
|
207
209
|
end
|
210
|
+
item { '*' }
|
211
|
+
item do
|
212
|
+
one_of do
|
213
|
+
0.upto(9) { |d| item { d.to_s } }
|
214
|
+
end
|
215
|
+
end
|
208
216
|
"#"
|
209
217
|
end
|
210
218
|
item do
|
@@ -27,6 +27,10 @@ module RubySpeech
|
|
27
27
|
input.should == '6'
|
28
28
|
end
|
29
29
|
|
30
|
+
it "should potentially match an empty buffer" do
|
31
|
+
subject.match('').should == GRXML::PotentialMatch.new
|
32
|
+
end
|
33
|
+
|
30
34
|
%w{1 2 3 4 5 7 8 9 10 66 26 61}.each do |input|
|
31
35
|
it "should not match '#{input}'" do
|
32
36
|
subject.match(input).should == GRXML::NoMatch.new
|
@@ -2,6 +2,36 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module RubySpeech
|
4
4
|
describe GRXML do
|
5
|
+
describe ".from_uri" do
|
6
|
+
context "with a builtin URI" do
|
7
|
+
it "should fetch a simple builtin grammar by type" do
|
8
|
+
subject.from_uri("builtin:dtmf/phone").should == GRXML::Builtins.phone
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should fetch a parameterized builtin grammar" do
|
12
|
+
subject.from_uri("builtin:dtmf/boolean?y=3;n=4").should == GRXML::Builtins.boolean(y: 3, n: 4)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "for speech" do
|
16
|
+
it "should raise ArgumentError" do
|
17
|
+
expect { subject.from_uri("builtin:speech/phone") }.to raise_error(ArgumentError, /Only DTMF/)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "that doesn't exist" do
|
22
|
+
it "should raise ArgumentError" do
|
23
|
+
expect { subject.from_uri("builtin:dtmf/foobar") }.to raise_error(ArgumentError, /invalid/)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with an http URI" do
|
29
|
+
it "should raise ArgumentError" do
|
30
|
+
expect { subject.from_uri("http://foo.com/grammar.grxml") }.to raise_error(ArgumentError, /builtin/)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
5
35
|
describe "#draw" do
|
6
36
|
let(:doc) { Nokogiri::XML::Document.new }
|
7
37
|
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rspec/expectations'
|
2
|
+
|
3
|
+
RSpec::Matchers.define :not_match do |input|
|
4
|
+
match do |grammar|
|
5
|
+
@result = RubySpeech::GRXML::Matcher.new(grammar).match(input)
|
6
|
+
@result.is_a?(RubySpeech::GRXML::NoMatch)
|
7
|
+
end
|
8
|
+
|
9
|
+
failure_message_for_should do |grammar|
|
10
|
+
"expected #{grammar} to not match #{input}, but received a #{@result.class}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec::Matchers.define :potentially_match do |input|
|
15
|
+
match do |grammar|
|
16
|
+
@result = RubySpeech::GRXML::Matcher.new(grammar).match(input)
|
17
|
+
@result.is_a?(RubySpeech::GRXML::PotentialMatch)
|
18
|
+
end
|
19
|
+
|
20
|
+
failure_message_for_should do |grammar|
|
21
|
+
"expected #{grammar} to potentially match #{input}, but received a #{@result.class}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
RSpec::Matchers.define :match do |input|
|
26
|
+
match do |grammar|
|
27
|
+
@result = RubySpeech::GRXML::Matcher.new(grammar).match(input)
|
28
|
+
@result.is_a?(RubySpeech::GRXML::Match) && (@interpretation ? @result.interpretation == @interpretation : true)
|
29
|
+
end
|
30
|
+
|
31
|
+
chain :and_interpret_as do |interpretation|
|
32
|
+
@interpretation = interpretation
|
33
|
+
end
|
34
|
+
|
35
|
+
description do
|
36
|
+
%{#{default_description} and interpret as "#{@interpretation}"}
|
37
|
+
end
|
38
|
+
|
39
|
+
failure_message_for_should do |grammar|
|
40
|
+
messages = []
|
41
|
+
unless @result.is_a?(RubySpeech::GRXML::Match)
|
42
|
+
messages << "expected #{grammar} to match, got a #{@result.class}"
|
43
|
+
end
|
44
|
+
|
45
|
+
if @result.respond_to?(:interpretation) && @result.interpretation != @interpretation
|
46
|
+
messages << %{expected interpretation to be "#{@interpretation}" but received "#{@result.interpretation}"}
|
47
|
+
end
|
48
|
+
|
49
|
+
messages.join(' ')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
RSpec::Matchers.define :max_match do |input|
|
54
|
+
match do |grammar|
|
55
|
+
@result = RubySpeech::GRXML::Matcher.new(grammar).match(input)
|
56
|
+
@result.is_a?(RubySpeech::GRXML::MaxMatch) && (@interpretation ? @result.interpretation == @interpretation : true)
|
57
|
+
end
|
58
|
+
|
59
|
+
chain :and_interpret_as do |interpretation|
|
60
|
+
@interpretation = interpretation
|
61
|
+
end
|
62
|
+
|
63
|
+
description do
|
64
|
+
%{#{default_description} and interpret as "#{@interpretation}"}
|
65
|
+
end
|
66
|
+
|
67
|
+
failure_message_for_should do |grammar|
|
68
|
+
messages = []
|
69
|
+
unless @result.is_a?(RubySpeech::GRXML::MaxMatch)
|
70
|
+
messages << "expected #{grammar} to max-match, got a #{@result.class}"
|
71
|
+
end
|
72
|
+
|
73
|
+
if @result.respond_to?(:interpretation) && @result.interpretation != @interpretation
|
74
|
+
messages << %{expected interpretation to be "#{@interpretation}" but received "#{@result.interpretation}"}
|
75
|
+
end
|
76
|
+
|
77
|
+
messages.join(' ')
|
78
|
+
end
|
79
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_speech
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Langfeld
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-09-
|
11
|
+
date: 2013-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -227,6 +227,7 @@ files:
|
|
227
227
|
- lib/ruby_speech.rb
|
228
228
|
- lib/ruby_speech/generic_element.rb
|
229
229
|
- lib/ruby_speech/grxml.rb
|
230
|
+
- lib/ruby_speech/grxml/builtins.rb
|
230
231
|
- lib/ruby_speech/grxml/element.rb
|
231
232
|
- lib/ruby_speech/grxml/grammar.rb
|
232
233
|
- lib/ruby_speech/grxml/item.rb
|
@@ -261,6 +262,7 @@ files:
|
|
261
262
|
- lib/ruby_speech/version.rb
|
262
263
|
- lib/ruby_speech/xml/language.rb
|
263
264
|
- ruby_speech.gemspec
|
265
|
+
- spec/ruby_speech/grxml/builtins_spec.rb
|
264
266
|
- spec/ruby_speech/grxml/grammar_spec.rb
|
265
267
|
- spec/ruby_speech/grxml/item_spec.rb
|
266
268
|
- spec/ruby_speech/grxml/match_spec.rb
|
@@ -291,10 +293,12 @@ files:
|
|
291
293
|
- spec/ruby_speech/ssml_spec.rb
|
292
294
|
- spec/ruby_speech_spec.rb
|
293
295
|
- spec/spec_helper.rb
|
296
|
+
- spec/support/grammar_matchers.rb
|
294
297
|
- spec/support/match_examples.rb
|
295
298
|
- spec/support/matchers.rb
|
296
299
|
homepage: https://github.com/benlangfeld/ruby_speech
|
297
|
-
licenses:
|
300
|
+
licenses:
|
301
|
+
- MIT
|
298
302
|
metadata: {}
|
299
303
|
post_install_message:
|
300
304
|
rdoc_options: []
|
@@ -317,6 +321,7 @@ signing_key:
|
|
317
321
|
specification_version: 4
|
318
322
|
summary: A Ruby library for TTS & ASR document preparation
|
319
323
|
test_files:
|
324
|
+
- spec/ruby_speech/grxml/builtins_spec.rb
|
320
325
|
- spec/ruby_speech/grxml/grammar_spec.rb
|
321
326
|
- spec/ruby_speech/grxml/item_spec.rb
|
322
327
|
- spec/ruby_speech/grxml/match_spec.rb
|
@@ -347,6 +352,7 @@ test_files:
|
|
347
352
|
- spec/ruby_speech/ssml_spec.rb
|
348
353
|
- spec/ruby_speech_spec.rb
|
349
354
|
- spec/spec_helper.rb
|
355
|
+
- spec/support/grammar_matchers.rb
|
350
356
|
- spec/support/match_examples.rb
|
351
357
|
- spec/support/matchers.rb
|
352
358
|
has_rdoc:
|