-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathVSpeechRecognition.hx
More file actions
302 lines (275 loc) · 8.63 KB
/
Copy pathVSpeechRecognition.hx
File metadata and controls
302 lines (275 loc) · 8.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
package vocally;
import haxe.ds.Either;
import haxe.extern.EitherType;
import js.Browser.window;
import js.html.*;
import haxe.ds.Option;
import js.Promise;
using tink.core.Signal;
using tink.core.Callback;
#if compile_library
@:keep
#end
class VSpeechRecognition {
var speechRecognition: Option<Class<SpeechRecognition>>;
var allRecognizers: Array<Recognizer>;
public function new() {
this.allRecognizers = [];
this.speechRecognition = None;
if (Reflect.hasField(window, 'SpeechRecognition')) {
this.speechRecognition = Some(Reflect.field(window, 'SpeechRecognition'));
} else if (Reflect.hasField(window, 'webkitSpeechRecognition')) {
this.speechRecognition = Some(Reflect.field(window, 'webkitSpeechRecognition'));
}
}
public function listenOnce(): Recognizer {
return newRecogniser().start(false);
}
public function listen(): Recognizer {
return newRecogniser().start(true);
}
public function listenFor(commandOrCommands: Either<ListenForCommand, Array<ListenForCommand>>): Recognizer {
var commands: Array<ListenForCommand> =
(Std.is(commandOrCommands, Array))
? cast commandOrCommands
: [cast commandOrCommands];
var draftCommands = commands.filter(c -> c.respondOnDraft);
var sureCommands = commands.filter(c -> !c.respondOnDraft);
// Keep track of which draft commands have already run, so they aren't triggered again for each updated draft.
var draftCommandsTriggered = [];
function match(commandAlts: Array<String>, transcriptAlts: Array<String>) {
// TODO: allow repeating a command within a draft by saying it twice (but not by the draft triggering twice)
for (cmd in commandAlts) {
var cmdRegex = new EReg(cmd, 'i');
for (transcript in transcriptAlts) {
if (cmdRegex.match(transcript)) {
var wildcards = [];
var i = 0;
while (true) {
try {
wildcards.push(cmdRegex.matched(i++));
} catch (e: Dynamic) {
break;
}
}
return Some({
transcript: transcript,
command: cmd,
wildcards: wildcards
});
}
}
}
return None;
}
function checkCommands(result: SpeechRecognitionResult, commands: Array<ListenForCommand>, commandsToIgnore: Array<ListenForCommand>) {
for (cmd in commands) {
if (commandsToIgnore.indexOf(cmd) > -1) {
continue;
}
var cmdAlts = [cmd.command].concat(cmd.alternatives != null ? cmd.alternatives : []);
var transcriptAlts = [for (alt in result) alt.transcript];
switch match(cmdAlts, transcriptAlts) {
case Some(match):
draftCommandsTriggered.push(cmd);
cmd.handler.invoke(match);
case None:
}
}
}
return newRecogniser()
.onDraftAlternatives(draft -> checkCommands(draft, draftCommands, draftCommandsTriggered))
.onResultAlternatives(result -> {
draftCommandsTriggered = [];
checkCommands(result, sureCommands, []);
})
.start(true);
}
/** Abort all current SpeechRecognition listeners. **/
public function stopListening(): VSpeechRecognition {
for (r in allRecognizers) {
r.abort();
}
return this;
}
public function usePolyfill(polyfill: Class<SpeechRecognition>): VSpeechRecognition {
this.speechRecognition = Some(polyfill);
return this;
}
function newRecogniser(): Recognizer {
switch speechRecognition {
case Some(cls):
var recognizer = new Recognizer(cls);
allRecognizers.push(recognizer);
return recognizer;
case None:
throw 'SpeechRecognition is not supported in this browser and a polyfill was not found';
}
}
}
#if compile_library
@:keep
#end
class Recognizer {
public var recognizer: SpeechRecognition;
var results: Option<SpeechRecognitionResultList>;
var resultSignal: SignalTrigger<SpeechRecognitionAlternative>;
var resultAlternativesSignal: SignalTrigger<SpeechRecognitionResult>;
var draftSignal: SignalTrigger<SpeechRecognitionAlternative>;
var draftAlternativesSignal: SignalTrigger<SpeechRecognitionResult>;
var errorSignal: SignalTrigger<SpeechRecognitionError>;
var promise: Option<{
promise: Promise<FinalSpeechRecognitionResult>,
resolve: FinalSpeechRecognitionResult -> Void,
reject: SpeechRecognitionError -> Void
}>;
public function new(cls: Class<SpeechRecognition>) {
recognizer = Type.createInstance(cls, []);
resultSignal = new SignalTrigger();
resultAlternativesSignal = new SignalTrigger();
draftSignal = new SignalTrigger();
draftAlternativesSignal = new SignalTrigger();
errorSignal = new SignalTrigger();
promise = None;
results = None;
recognizer.continuous = true;
recognizer.lang = "en-US";
recognizer.interimResults = true;
recognizer.maxAlternatives = 3;
recognizer.addEventListener("result", (e: SpeechRecognitionEvent) -> {
var results = e.results;
this.results = Some(results);
var lastResult = results[results.length - 1];
if (lastResult.length > 0) {
var alternative = lastResult[0];
var signal = lastResult.isFinal ? resultSignal : draftSignal;
var alternativesSignal =
lastResult.isFinal
? resultAlternativesSignal
: draftAlternativesSignal;
signal.trigger(alternative);
alternativesSignal.trigger(lastResult);
}
});
recognizer.addEventListener("speechend", () -> stop());
recognizer.addEventListener("nomatch", (e: SpeechRecognitionEvent) -> {
// Try again until we get some speech.
start();
});
recognizer.addEventListener("end", (e: Event) -> {
if (recognizer.continuous) {
start();
return;
}
switch [promise, results] {
case [Some(p), Some(resultList)]:
var finalResult: FinalSpeechRecognitionResult = {
allResults: resultList,
transcript: '',
confidence: 0,
};
for (result in resultList) {
finalResult.transcript += " " + result[0].transcript;
finalResult.confidence += result[0].confidence / resultList.length;
}
p.resolve(finalResult);
case _:
}
});
recognizer.addEventListener("error", (e: {error: SpeechRecognitionError}) -> {
var err = e.error;
if (err.message == "no-speech") {
stop();
return;
}
errorSignal.trigger(err);
switch promise {
case Some(p): p.reject(err);
case _:
}
});
}
/** Start listening for a new result. **/
public function start(?keepRestarting: Bool = true): Recognizer {
recognizer.continuous = keepRestarting;
recognizer.start();
return this;
}
/** Stop listening, and try to get a final result. **/
public function stop(): Recognizer {
recognizer.continuous = false;
recognizer.stop();
return this;
}
/** Stop listening, and don't attempt to get a final result. **/
public function abort(): Recognizer {
recognizer.abort();
return this;
}
public function onResult(cb: Callback<SpeechRecognitionAlternative>): Recognizer {
resultSignal.handle(cb);
return this;
}
public function onResultAlternatives(cb: Callback<SpeechRecognitionResult>): Recognizer {
resultAlternativesSignal.handle(cb);
return this;
}
public function onDraft(cb: Callback<SpeechRecognitionAlternative>): Recognizer {
draftSignal.handle(cb);
return this;
}
public function onDraftAlternatives(cb: Callback<SpeechRecognitionResult>): Recognizer {
draftAlternativesSignal.handle(cb);
return this;
}
public function onError(cb: Callback<SpeechRecognitionError>): Recognizer {
errorSignal.handle(cb);
return this;
}
public function finalResult(): Promise<FinalSpeechRecognitionResult> {
switch promise {
case Some(p):
return p.promise;
case None:
var resolve = null;
var reject = null;
var p = new Promise(function (resolveFn, rejectFn) {
resolve = resolveFn;
reject = rejectFn;
});
promise = Some({
promise: p,
resolve: resolve,
reject: reject
});
return p;
}
}
}
/**
An object for receiving multiple recognition results.
It is similar to SpeechRecognitionAlternative, with a confidence and transcript for the combined result.
It gives access to each of the result lists that were used to generate the combined result.
**/
typedef FinalSpeechRecognitionResult = {
allResults: SpeechRecognitionResultList,
confidence: Float,
transcript: String,
}
typedef ListenForCommand = {
/** The command we are listening for **/
var command: String;
/** A function to execute when the command is recognised. **/
var handler: Callback<{
/** The transcript of the result that matched. **/
var transcript: String;
/** The command text (or alternative command text) that matched. **/
var command: String;
/** Any wildcards found. **/
var wildcards: Array<String>;
}>;
/** Whether to wait for a final result or execute as soon as it occurs in a draft. **/
@:optional var respondOnDraft: Bool;
/** Alternative versions of the **/
@:optional var alternatives: Array<String>;
}