Skip to content

Commit 72bff6d

Browse files
authored
New wakeword! (#62)
* Ignore Mac file system rubbish * Add kws library and copy files over from dist directory * Use new recogniser rather than JsSpeechRecogniser * Use the AMD bundle * Tweak threshold * Remove debug * Unused dependency * No need for a closure * Style changes * Remove unsued wakeword model * Style changes * Bump * Rewrite URls * Use https explicitly..
1 parent 45c03e4 commit 72bff6d

File tree

7 files changed

+58
-58
lines changed

7 files changed

+58
-58
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/dist
44
/.publish
55
npm-debug.log
6+
.DS_Store

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ before_install:
1212
- sh -e /etc/init.d/xvfb start
1313
- sleep 3 # give xvfb some time to start
1414
- export CHROME_BIN=google-chrome
15+
# Rewrite ssh URLs that git uses (for SSH clones in submodules of npm
16+
# dependencies
17+
- git config --global url."https://".insteadOf git://

app/data/wakeword_model.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

app/js/lib/speech-controller.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import IntentParser from './intent-parser';
99
const p = Object.freeze({
1010
// Properties
1111
wakewordRecogniser: Symbol('wakewordRecogniser'),
12-
wakewordModelUrl: Symbol('wakewordModelUrl'),
1312
speechRecogniser: Symbol('speechRecogniser'),
1413
speechSynthesis: Symbol('speechSynthesis'),
1514
idle: Symbol('idle'),
1615

1716
// Methods
18-
initialiseSpeechRecognition: Symbol('initialiseSpeechRecognition'),
1917
startListeningForWakeword: Symbol('startListeningForWakeword'),
2018
stopListeningForWakeword: Symbol('stopListeningForWakeword'),
2119
listenForUtterance: Symbol('listenForUtterance'),
@@ -52,7 +50,6 @@ export default class SpeechController extends EventDispatcher {
5250
super(EVENT_INTERFACE);
5351

5452
this[p.idle] = true;
55-
this[p.wakewordModelUrl] = 'data/wakeword_model.json';
5653

5754
this[p.speechRecogniser] = new SpeechRecogniser();
5855
this[p.speechSynthesis] = new SpeechSynthesis();
@@ -73,8 +70,7 @@ export default class SpeechController extends EventDispatcher {
7370
}
7471

7572
start() {
76-
return this[p.initialiseSpeechRecognition]()
77-
.then(this[p.startListeningForWakeword].bind(this));
73+
return this[p.startListeningForWakeword]();
7874
}
7975

8076
startSpeechRecognition() {
@@ -105,14 +101,6 @@ export default class SpeechController extends EventDispatcher {
105101
this[p.speechSynthesis].speak(text);
106102
}
107103

108-
[p.initialiseSpeechRecognition]() {
109-
return fetch(this[p.wakewordModelUrl])
110-
.then((response) => response.json())
111-
.then((model) => {
112-
this[p.wakewordRecogniser].loadModel(model);
113-
});
114-
}
115-
116104
[p.startListeningForWakeword]() {
117105
this.emit(EVENT_INTERFACE[0], { type: EVENT_INTERFACE[0] });
118106
this[p.idle] = true;

app/js/lib/wakeword/recogniser.js

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,65 @@
11
'use strict';
22

3-
import JsSpeechRecognizer from 'components/jsspeechrecognizer';
3+
import PocketSphinx from 'components/webaudiokws';
44

55
export default class WakeWordRecogniser {
6-
constructor(options = {}) {
7-
const minimumConfidence = options.minimumConfidence || 0.35;
8-
const bufferCount = options.bufferCount || 80;
9-
const maxVoiceActivityGap = options.maxVoiceActivityGap || 300;
10-
const numGroups = options.numGroups || 60;
11-
const groupSize = options.groupSize || 5;
6+
constructor() {
7+
this.audioContext = new AudioContext();
128

13-
this.recogniser = new JsSpeechRecognizer();
9+
this.audioSource = navigator.mediaDevices.getUserMedia({
10+
audio: true,
11+
})
12+
.then((stream) => {
13+
return this.audioContext.createMediaStreamSource(stream);
14+
})
15+
.catch((error) => {
16+
console.error(`Could not getUserMedia: ${error}`);
17+
throw error;
18+
});
1419

15-
this.recogniser.keywordSpottingMinimumConfidence = minimumConfidence;
16-
this.recogniser.keywordSpottingBufferCount = bufferCount;
17-
this.recogniser.keywordSpottingMaxVoiceActivityGap = maxVoiceActivityGap;
18-
this.recogniser.numGroups = numGroups;
19-
this.recogniser.groupSize = groupSize;
20+
this.recogniser = new PocketSphinx(this.audioContext, {
21+
pocketSphinxUrl: '/js/components/pocketsphinx.js',
22+
workerUrl: '/js/components/ps-worker.js',
23+
args: [['-kws_threshold', '2']],
24+
});
25+
26+
const dictionary = {
27+
'MAKE': ['M EY K'],
28+
'A': ['AH'],
29+
'NOTE': ['N OW T'],
30+
};
31+
32+
const keywordReady = this.recogniser.addDictionary(dictionary)
33+
.then(() => this.recogniser.addKeyword('MAKE A NOTE'));
2034

35+
this.ready = Promise.all([keywordReady, this.audioSource]);
2136
Object.seal(this);
2237
}
2338

2439
startListening() {
25-
return new Promise((resolve) => {
26-
this.recogniser.closeMic(); // Make sure we don't start another instance
27-
this.recogniser.openMic();
28-
if (!this.recogniser.isRecording()) {
29-
this.recogniser.startKeywordSpottingRecording();
30-
}
31-
32-
resolve();
33-
});
40+
return this.ready
41+
.then(() => {
42+
return this.audioSource;
43+
})
44+
.then((source) => {
45+
source.connect(this.recogniser);
46+
this.recogniser.connect(this.audioContext.destination);
47+
return;
48+
});
3449
}
3550

3651
stopListening() {
37-
return new Promise((resolve) => {
38-
if (this.recogniser.isRecording()) {
39-
this.recogniser.stopRecording();
40-
}
41-
42-
this.recogniser.closeMic();
43-
44-
resolve();
45-
});
46-
}
47-
48-
loadModel(modelData) {
49-
if (this.recogniser.isRecording()) {
50-
throw new Error(
51-
'Load the model data before listening for wakeword');
52-
}
53-
54-
this.recogniser.model = modelData;
52+
return this.ready
53+
.then(() => {
54+
return this.audioSource;
55+
})
56+
.then((source) => {
57+
source.disconnect();
58+
this.recogniser.disconnect();
59+
});
5560
}
5661

5762
setOnKeywordSpottedCallback(fn) {
58-
this.recogniser.keywordSpottedCallback = fn;
63+
this.recogniser.on('keywordspotted', fn);
5964
}
6065
}

gulpfile.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ gulp.task('copy-app-common', () => {
8282
.pipe(rename('js/alameda.js')),
8383

8484
// Components.
85-
gulp.src('./node_modules/jsspeechrecognizer/dist/JsSpeechRecognizer.js')
86-
.pipe(rename('js/components/jsspeechrecognizer.js')),
8785
gulp.src('./node_modules/lodash/lodash.min.js')
8886
.pipe(rename('js/components/lodash.js')),
8987
gulp.src('./node_modules/moment/min/moment-with-locales.min.js')
@@ -94,6 +92,12 @@ gulp.task('copy-app-common', () => {
9492
.pipe(rename('js/components/cldr/core.js')),
9593
gulp.src('./node_modules/twitter_cldr/min/en.min.js')
9694
.pipe(rename('js/components/cldr/en.js')),
95+
gulp.src('./node_modules/webaudio-kws-node/dist/amd-library.js')
96+
.pipe(rename('js/components/webaudiokws.js')),
97+
gulp.src('./node_modules/webaudio-kws-node/dist/ps-worker.js')
98+
.pipe(rename('js/components/ps-worker.js')),
99+
gulp.src('./node_modules/webaudio-kws-node/dist/pocketsphinx.js')
100+
.pipe(rename('js/components/pocketsphinx.js')),
97101

98102
// Polyfills.
99103
gulp.src('./node_modules/whatwg-fetch/fetch.js')

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
"dependencies": {
2323
"alameda": "^1.0.0",
2424
"chrono-node": "^1.2.3",
25-
"jsspeechrecognizer": "fxbox/JsSpeechRecognizer",
2625
"lodash": "^4.13.1",
2726
"moment": "^2.13.0",
2827
"react": "^15.1.0",
2928
"react-dom": "^15.1.0",
3029
"twitter_cldr": "^2.1.1",
3130
"url-search-params": "^0.5.0",
31+
"webaudio-kws-node": "https://github.com/fxbox/webaudio-kws-node",
3232
"webrtc-adapter": "^1.4.0",
3333
"whatwg-fetch": "^1.0.0"
3434
},

0 commit comments

Comments
 (0)