@scratch/scratch-vm 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
const ArgumentType = require('../../extension-support/argument-type');
|
|
2
|
+
const Cast = require('../../util/cast');
|
|
3
|
+
const BlockType = require('../../extension-support/block-type');
|
|
4
|
+
const formatMessage = require('format-message');
|
|
5
|
+
const log = require('../../util/log');
|
|
6
|
+
const DiffMatchPatch = require('diff-match-patch');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Url of icon to be displayed at the left edge of each extension block.
|
|
11
|
+
* @type {string}
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line max-len
|
|
14
|
+
const iconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSIjRkZGRkZGIj48cGF0aCBkPSJNMTIgMTRjMS42NiAwIDIuOTktMS4zNCAyLjk5LTNMMTUgNWMwLTEuNjYtMS4zNC0zLTMtM1M5IDMuMzQgOSA1djZjMCAxLjY2IDEuMzQgMyAzIDN6bTUuMy0zYzAgMy0yLjU0IDUuMS01LjMgNS4xUzYuNyAxNCA2LjcgMTFINWMwIDMuNDEgMi43MiA2LjIzIDYgNi43MlYyMWgydi0zLjI4YzMuMjgtLjQ4IDYtMy4zIDYtNi43MmgtMS43eiIvPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48L3N2Zz4K';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Url of icon to be displayed in the toolbox menu for the extension category.
|
|
19
|
+
* @type {string}
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line max-len
|
|
22
|
+
const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzc1NzU3NSI+CiAgICA8cGF0aCBkPSJNMTIgMTRjMS42NiAwIDIuOTktMS4zNCAyLjk5LTNMMTUgNWMwLTEuNjYtMS4zNC0zLTMtM1M5IDMuMzQgOSA1djZjMCAxLjY2IDEuMzQgMyAzIDN6bTUuMy0zYzAgMy0yLjU0IDUuMS01LjMgNS4xUzYuNyAxNCA2LjcgMTFINWMwIDMuNDEgMi43MiA2LjIzIDYgNi43MlYyMWgydi0zLjI4YzMuMjgtLjQ4IDYtMy4zIDYtNi43MmgtMS43eiIvPgogICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgo8L3N2Zz4K';
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The url of the speech server.
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
const serverURL = 'wss://speech.scratch.mit.edu';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The amount of time to wait between when we stop sending speech data to the server and when
|
|
33
|
+
* we expect the transcription result marked with isFinal: true to come back from the server.
|
|
34
|
+
* @type {int}
|
|
35
|
+
*/
|
|
36
|
+
const finalResponseTimeoutDurationMs = 3000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The max amount of time the Listen And Wait block will listen for. It may listen for less time
|
|
40
|
+
* if we get back results that are good and think the user is done talking.
|
|
41
|
+
* Currently set to 10sec. This should not exceed the speech api limit (60sec) without redoing how
|
|
42
|
+
* we stream the microphone data data.
|
|
43
|
+
* @type {int}
|
|
44
|
+
*/
|
|
45
|
+
const listenAndWaitBlockTimeoutMs = 10000;
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Scratch3Speech2TextBlocks {
|
|
49
|
+
constructor (runtime) {
|
|
50
|
+
/**
|
|
51
|
+
* The runtime instantiating this block package.
|
|
52
|
+
* @type {Runtime}
|
|
53
|
+
*/
|
|
54
|
+
this.runtime = runtime;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An array of phrases from the [when I hear] hat blocks.
|
|
58
|
+
* The list of phrases in the when I hear hat blocks. This list is sent
|
|
59
|
+
* to the speech api to seed the recognition engine and for deciding
|
|
60
|
+
* whether the transcription results match.
|
|
61
|
+
* @type {Array}
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
this._phraseList = [];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The most recent transcription result received from the speech API that we decided to keep.
|
|
68
|
+
* This is the value returned by the reporter block.
|
|
69
|
+
* @type {String}
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
this._currentUtterance = '';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Similar to _currentUtterance, but set back to '' at the beginning of listening block
|
|
76
|
+
* and on green flag.
|
|
77
|
+
* Used to get the hat blocks to edge trigger. In order to detect someone saying
|
|
78
|
+
* the same thing twice in two subsequent listen and wait blocks
|
|
79
|
+
* and still trigger the hat, we need this to go from
|
|
80
|
+
* '' at the beginning of the listen block to '<transcription value>' at the end.
|
|
81
|
+
* @type {string}
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
this._utteranceForEdgeTrigger = null;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The list of queued `resolve` callbacks for 'Listen and Wait' blocks.
|
|
88
|
+
* We only listen to for one utterance at a time. We may encounter multiple
|
|
89
|
+
* 'Listen and wait' blocks that tell us to start listening. If one starts
|
|
90
|
+
* and hasn't receieved results back yet, when we encounter more, any further ones
|
|
91
|
+
* will all resolve when we get the next acceptable transcription result back.
|
|
92
|
+
* @type {!Array}
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
this._speechPromises = [];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The id of the timeout that will run if we start listening and don't get any
|
|
99
|
+
* transcription results back. e.g. because we didn't hear anything.
|
|
100
|
+
* @type {number}
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
this._speechTimeoutId = null;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The id of the timeout that will run to wait for after we're done listening but
|
|
107
|
+
* are still waiting for a potential isFinal:true transcription result to come back.
|
|
108
|
+
* @type {number}
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
this._speechFinalResponseTimeout = null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The ScriptProcessorNode hooked up to the audio context.
|
|
115
|
+
* @type {ScriptProcessorNode}
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
this._scriptNode = null;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The socket used to communicate with the speech server to send microphone data
|
|
122
|
+
* and recieve transcription results.
|
|
123
|
+
* @type {WebSocket}
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
this._socket = null;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The AudioContext used to manage the microphone.
|
|
130
|
+
* @type {AudioContext}
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
this._context = null;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* MediaStreamAudioSourceNode to handle microphone data.
|
|
137
|
+
* @type {MediaStreamAudioSourceNode}
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
this._sourceNode = null;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A Promise whose fulfillment handler receives a MediaStream object when the microphone has been obtained.
|
|
144
|
+
* @type {Promise}
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
this._audioPromise = null;
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Diff Match Patch is used to do some fuzzy matching of the transcription results
|
|
152
|
+
* with what is in the hat blocks.
|
|
153
|
+
*/
|
|
154
|
+
this._dmp = new DiffMatchPatch();
|
|
155
|
+
// Threshold for diff match patch to use: (0.0 = perfection, 1.0 = very loose).
|
|
156
|
+
this._dmp.Match_Threshold = 0.3;
|
|
157
|
+
|
|
158
|
+
this._newSocketCallback = this._newSocketCallback.bind(this);
|
|
159
|
+
this._setupSocketCallback = this._setupSocketCallback.bind(this);
|
|
160
|
+
this._socketMessageCallback = this._socketMessageCallback.bind(this);
|
|
161
|
+
this._processAudioCallback = this._processAudioCallback.bind(this);
|
|
162
|
+
this._onTranscriptionFromServer = this._onTranscriptionFromServer.bind(this);
|
|
163
|
+
this._resetListening = this._resetListening.bind(this);
|
|
164
|
+
this._stopTranscription = this._stopTranscription.bind(this);
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
this.runtime.on('PROJECT_STOP_ALL', this._resetListening.bind(this));
|
|
168
|
+
this.runtime.on('PROJECT_START', this._resetEdgeTriggerUtterance.bind(this));
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Scans all the 'When I hear' hat blocks for each sprite and pulls out the text. The list
|
|
174
|
+
* is sent off to the speech recognition server as hints. This *only* reads the value out of
|
|
175
|
+
* the hat block shadow. If a block is dropped on top of the shadow, it is skipped.
|
|
176
|
+
* @returns {Array} list of strings from the hat blocks in the project.
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
_scanBlocksForPhraseList () {
|
|
180
|
+
const words = [];
|
|
181
|
+
// For each each target, walk through the top level blocks and check whether
|
|
182
|
+
// they are speech hat/when I hear blocks.
|
|
183
|
+
this.runtime.targets.forEach(target => {
|
|
184
|
+
target.blocks._scripts.forEach(id => {
|
|
185
|
+
const b = target.blocks.getBlock(id);
|
|
186
|
+
if (b.opcode === 'speech_whenIHearHat') {
|
|
187
|
+
// Grab the text from the hat block's shadow.
|
|
188
|
+
const inputId = b.inputs.PHRASE.block;
|
|
189
|
+
const inputBlock = target.blocks.getBlock(inputId);
|
|
190
|
+
// Only grab the value from text blocks. This means we'll
|
|
191
|
+
// miss some. e.g. values in variables or other reporters.
|
|
192
|
+
if (inputBlock.opcode === 'text') {
|
|
193
|
+
const word = target.blocks.getBlock(inputId).fields.TEXT.value;
|
|
194
|
+
words.push(word);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
return words;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get the viewer's language code.
|
|
204
|
+
* @return {string} the language code.
|
|
205
|
+
*/
|
|
206
|
+
_getViewerLanguageCode () {
|
|
207
|
+
return formatMessage.setup().locale || navigator.language || navigator.userLanguage || 'en-US';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resets all things related to listening. Called on Red Stop sign button.
|
|
212
|
+
* - suspends audio processing
|
|
213
|
+
* - closes socket with speech socket server
|
|
214
|
+
* - clears out any remaining speech blocks that are waiting.
|
|
215
|
+
* @private.
|
|
216
|
+
*/
|
|
217
|
+
_resetListening () {
|
|
218
|
+
this.runtime.emitMicListening(false);
|
|
219
|
+
this._stopListening();
|
|
220
|
+
this._closeWebsocket();
|
|
221
|
+
this._resolveSpeechPromises();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Reset the utterance we look for in the when I hear hat block back to
|
|
226
|
+
* the empty string.
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
_resetEdgeTriggerUtterance () {
|
|
230
|
+
this._utteranceForEdgeTrigger = '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Close the connection to the socket server if it is open.
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
_closeWebsocket () {
|
|
238
|
+
if (this._socket && this._socket.readyState === this._socket.OPEN) {
|
|
239
|
+
this._socket.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Call to suspend getting data from the microphone.
|
|
245
|
+
* @private
|
|
246
|
+
*/
|
|
247
|
+
_stopListening () {
|
|
248
|
+
// Note that this can be called before any Listen And Wait block did setup,
|
|
249
|
+
// so check that things exist before disconnecting them.
|
|
250
|
+
if (this._context) {
|
|
251
|
+
this._context.suspend.bind(this._context);
|
|
252
|
+
}
|
|
253
|
+
// This is called on green flag to reset things that may never have existed
|
|
254
|
+
// in the first place. Do a bunch of checks.
|
|
255
|
+
if (this._scriptNode) {
|
|
256
|
+
this._scriptNode.removeEventListener('audioprocess', this._processAudioCallback);
|
|
257
|
+
this._scriptNode.disconnect();
|
|
258
|
+
}
|
|
259
|
+
if (this._sourceNode) {
|
|
260
|
+
this._sourceNode.disconnect();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Resolves all the speech promises we've accumulated so far and empties out the list.
|
|
266
|
+
* @private
|
|
267
|
+
*/
|
|
268
|
+
_resolveSpeechPromises () {
|
|
269
|
+
for (let i = 0; i < this._speechPromises.length; i++) {
|
|
270
|
+
const resFn = this._speechPromises[i];
|
|
271
|
+
resFn();
|
|
272
|
+
}
|
|
273
|
+
this._speechPromises = [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Called when we want to stop listening (e.g. when a listen block times out)
|
|
278
|
+
* but we still want to wait a little to see if we get any transcription results
|
|
279
|
+
* back before yielding the block execution.
|
|
280
|
+
* @private
|
|
281
|
+
*/
|
|
282
|
+
_stopTranscription () {
|
|
283
|
+
this._stopListening();
|
|
284
|
+
if (this._socket && this._socket.readyState === this._socket.OPEN) {
|
|
285
|
+
this._socket.send('stopTranscription');
|
|
286
|
+
}
|
|
287
|
+
// Give it a couple seconds to response before giving up and assuming nothing else will come back.
|
|
288
|
+
this._speechFinalResponseTimeout = setTimeout(this._resetListening, finalResponseTimeoutDurationMs);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Decides whether to keep a given transcirption result.
|
|
293
|
+
* @param {number} fuzzyMatchIndex Index of the fuzzy match or -1 if there is no match.
|
|
294
|
+
* @param {object} result The json object representing the transcription result.
|
|
295
|
+
* @param {string} normalizedTranscript The transcription text used for matching (i.e. lowercased, no punctuation).
|
|
296
|
+
* @returns {boolean} true If a result is good enough to be kept.
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
_shouldKeepResult (fuzzyMatchIndex, result, normalizedTranscript) {
|
|
300
|
+
// The threshold above which we decide transcription results are unlikely to change again.
|
|
301
|
+
// See https://cloud.google.com/speech-to-text/docs/basics#streaming_responses.
|
|
302
|
+
const stabilityThreshold = .85;
|
|
303
|
+
|
|
304
|
+
// For responsiveness of the When I Hear hat blocks, sometimes we want to keep results that are not
|
|
305
|
+
// yet marked 'isFinal' by the speech api. Here are some signals we use.
|
|
306
|
+
|
|
307
|
+
// If the result from the speech api isn't very stable and we only had a fuzzy match, we don't want to use it.
|
|
308
|
+
const shouldKeepFuzzyMatch = fuzzyMatchIndex !== -1 && result.stability > stabilityThreshold;
|
|
309
|
+
|
|
310
|
+
// TODO: This is for debugging. Remove when this function is finalized.
|
|
311
|
+
if (shouldKeepFuzzyMatch) {
|
|
312
|
+
log.info(`Fuzzy match with high stability.`);
|
|
313
|
+
log.info(`match index is ${fuzzyMatchIndex}`);
|
|
314
|
+
const phrases = this._phraseList.join(' ');
|
|
315
|
+
const matchPhrase = phrases.substring(fuzzyMatchIndex, fuzzyMatchIndex + normalizedTranscript.length);
|
|
316
|
+
log.info(`fuzzy match: ${matchPhrase} in ${normalizedTranscript}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If the result is in the phraseList (i.e. it matches one of the 'When I Hear' blocks), we keep it.
|
|
320
|
+
// This might be aggressive... but so far seems to be a good thing.
|
|
321
|
+
const shouldKeepPhraseListMatch = this._phraseList.includes(normalizedTranscript);
|
|
322
|
+
// TODO: This is just for debugging. Remove when this function is finalized.
|
|
323
|
+
if (shouldKeepPhraseListMatch) {
|
|
324
|
+
log.info(`phrase list ${this._phraseList} includes ${normalizedTranscript}`);
|
|
325
|
+
}
|
|
326
|
+
// TODO: This is for debugging. Remove when this function is finalized.
|
|
327
|
+
if (result.isFinal) {
|
|
328
|
+
log.info(`result is final`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!result.isFinal && !shouldKeepPhraseListMatch && !shouldKeepFuzzyMatch) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Normalizes text a bit to facilitate matching. Lowercases, removes some punctuation and whitespace.
|
|
339
|
+
* @param {string} text The text to normalzie
|
|
340
|
+
* @returns {string} The normalized text.
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
_normalizeText (text) {
|
|
344
|
+
text = Cast.toString(text).toLowerCase();
|
|
345
|
+
text = text.replace(/[.?!]/g, '');
|
|
346
|
+
text = text.trim();
|
|
347
|
+
return text;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Call into diff match patch library to compute whether there is a fuzzy match.
|
|
352
|
+
* @param {string} text The text to search in.
|
|
353
|
+
* @param {string} pattern The pattern to look for in text.
|
|
354
|
+
* @returns {number} The index of the match or -1 if there isn't one.
|
|
355
|
+
*/
|
|
356
|
+
_computeFuzzyMatch (text, pattern) {
|
|
357
|
+
// Don't bother matching if any are null.
|
|
358
|
+
if (!pattern || !text) {
|
|
359
|
+
return -1;
|
|
360
|
+
}
|
|
361
|
+
let match = -1;
|
|
362
|
+
try {
|
|
363
|
+
// Look for the text in the pattern starting at position 0.
|
|
364
|
+
match = this._dmp.match_main(text, pattern, 0);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
// This can happen inf the text or pattern gets too long. If so just substring match.
|
|
367
|
+
return pattern.indexOf(text);
|
|
368
|
+
}
|
|
369
|
+
return match;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Processes the results we get back from the speech server. Decides whether the results
|
|
374
|
+
* are good enough to keep. If they are, resolves the 'Listen and Wait' blocks promise and cleans up.
|
|
375
|
+
* @param {object} result The transcription result.
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
_processTranscriptionResult (result) {
|
|
379
|
+
log.info(`Got result: ${JSON.stringify(result)}`);
|
|
380
|
+
const transcriptionResult = this._normalizeText(result.alternatives[0].transcript);
|
|
381
|
+
|
|
382
|
+
// Waiting for an exact match is not satisfying. It makes it hard to catch
|
|
383
|
+
// things like homonyms or things that sound similar "let us" vs "lettuce". Using the fuzzy matching helps
|
|
384
|
+
// more aggressively match the phrases that are in the "When I hear" hat blocks.
|
|
385
|
+
const phrases = this._phraseList.join(' ');
|
|
386
|
+
const fuzzyMatchIndex = this._computeFuzzyMatch(phrases, transcriptionResult);
|
|
387
|
+
|
|
388
|
+
// If the result isn't good enough yet, return without saving and resolving the promises.
|
|
389
|
+
if (!this._shouldKeepResult(fuzzyMatchIndex, result, transcriptionResult)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this._currentUtterance = transcriptionResult;
|
|
394
|
+
log.info(`Keeing result: ${this._currentUtterance}`);
|
|
395
|
+
this._utteranceForEdgeTrigger = transcriptionResult;
|
|
396
|
+
|
|
397
|
+
// We're done listening so resolove all the promises and reset everying so we're ready for next time.
|
|
398
|
+
this._resetListening();
|
|
399
|
+
|
|
400
|
+
// We got results so clear out the timeouts.
|
|
401
|
+
if (this._speechTimeoutId) {
|
|
402
|
+
clearTimeout(this._speechTimeoutId);
|
|
403
|
+
this._speechTimeoutId = null;
|
|
404
|
+
}
|
|
405
|
+
if (this._speechFinalResponseTimeout) {
|
|
406
|
+
clearTimeout(this._speechFinalResponseTimeout);
|
|
407
|
+
this._speechFinalResponseTimeout = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Handle a message from the socket. It contains transcription results.
|
|
413
|
+
* @param {MessageEvent} e The message event containing data from speech server.
|
|
414
|
+
* @private
|
|
415
|
+
*/
|
|
416
|
+
_onTranscriptionFromServer (e) {
|
|
417
|
+
let result = null;
|
|
418
|
+
try {
|
|
419
|
+
result = JSON.parse(e.data);
|
|
420
|
+
} catch (ex) {
|
|
421
|
+
log.error(`Problem parsing json. continuing: ${ex}`);
|
|
422
|
+
// TODO: Question - Should we kill listening and continue?
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
this._processTranscriptionResult(result);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Decide whether the pattern given matches the text. Uses fuzzy matching
|
|
431
|
+
* @param {string} pattern The pattern to look for. Usually this is the transcription result
|
|
432
|
+
* @param {string} text The text to look in. Usually this is the set of phrases from the when I hear blocks
|
|
433
|
+
* @returns {boolean} true if there is a fuzzy match.
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
_speechMatches (pattern, text) {
|
|
437
|
+
pattern = this._normalizeText(pattern);
|
|
438
|
+
text = this._normalizeText(text);
|
|
439
|
+
const match = this._computeFuzzyMatch(text, pattern);
|
|
440
|
+
return match !== -1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Kick off the listening process.
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
_startListening () {
|
|
448
|
+
this.runtime.emitMicListening(true);
|
|
449
|
+
this._initListening();
|
|
450
|
+
// Force the block to timeout if we don't get any results back/the user didn't say anything.
|
|
451
|
+
this._speechTimeoutId = setTimeout(this._stopTranscription, listenAndWaitBlockTimeoutMs);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Resume listening for audio and re-open the socket to send data.
|
|
456
|
+
* @private
|
|
457
|
+
*/
|
|
458
|
+
_resumeListening () {
|
|
459
|
+
this._context.resume.bind(this._context);
|
|
460
|
+
this._newWebsocket();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Does all setup to get microphone data and initializes the web socket.
|
|
465
|
+
* that data to the speech server.
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
_initListening () {
|
|
469
|
+
this._initializeMicrophone();
|
|
470
|
+
this._initScriptNode();
|
|
471
|
+
this._newWebsocket();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Initialize the audio context and connect the microphone.
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
_initializeMicrophone () {
|
|
479
|
+
// Don't make a new context if we already made one.
|
|
480
|
+
if (!this._context) {
|
|
481
|
+
// Safari still needs a webkit prefix for audio context
|
|
482
|
+
this._context = new (window.AudioContext || window.webkitAudioContext)();
|
|
483
|
+
}
|
|
484
|
+
// In safari we have to call getUserMedia every time we want to listen. Other browsers allow
|
|
485
|
+
// you to reuse the mediaStream. See #1202 for more context.
|
|
486
|
+
this._audioPromise = navigator.mediaDevices.getUserMedia({
|
|
487
|
+
audio: true
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
this._audioPromise.then().catch(e => {
|
|
491
|
+
log.error(`Problem connecting to microphone: ${e}`);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Sets up the script processor and the web socket.
|
|
497
|
+
* @private
|
|
498
|
+
*
|
|
499
|
+
*/
|
|
500
|
+
_initScriptNode () {
|
|
501
|
+
// Create a node that sends raw bytes across the websocket
|
|
502
|
+
this._scriptNode = this._context.createScriptProcessor(4096, 1, 1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Callback called when it is time to setup the new web socket.
|
|
507
|
+
* @param {Function} resolve - function to call when the web socket opens succesfully.
|
|
508
|
+
* @param {Function} reject - function to call if opening the web socket fails.
|
|
509
|
+
*/
|
|
510
|
+
_newSocketCallback (resolve, reject) {
|
|
511
|
+
this._socket = new WebSocket(serverURL);
|
|
512
|
+
this._socket.addEventListener('open', resolve);
|
|
513
|
+
this._socket.addEventListener('error', reject);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Callback called once we've initially established the web socket is open and working.
|
|
518
|
+
* Sets up the callback for subsequent messages (i.e. transcription results) and
|
|
519
|
+
* connects to the script node to get data.
|
|
520
|
+
* @private
|
|
521
|
+
*/
|
|
522
|
+
_socketMessageCallback () {
|
|
523
|
+
this._socket.addEventListener('message', this._onTranscriptionFromServer);
|
|
524
|
+
this._startByteStream();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Sets up callback for when socket and audio are initialized.
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
_newWebsocket () {
|
|
532
|
+
const websocketPromise = new Promise(this._newSocketCallback);
|
|
533
|
+
Promise.all([this._audioPromise, websocketPromise]).then(
|
|
534
|
+
this._setupSocketCallback)
|
|
535
|
+
.catch(e => {
|
|
536
|
+
log.error(`Problem with setup: ${e}`);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Callback to handle initial setting up of a socket.
|
|
542
|
+
* Currently we send a setup message (only contains sample rate) but might
|
|
543
|
+
* be useful to send more data so we can do quota stuff.
|
|
544
|
+
* @param {Array} values The
|
|
545
|
+
*/
|
|
546
|
+
_setupSocketCallback (values) {
|
|
547
|
+
this._micStream = values[0];
|
|
548
|
+
this._socket = values[1].target;
|
|
549
|
+
|
|
550
|
+
this._socket.addEventListener('error', e => {
|
|
551
|
+
log.error(`Error from web socket: ${e}`);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Send the initial configuration message. When the server acknowledges
|
|
555
|
+
// it, start streaming the audio bytes to the server and listening for
|
|
556
|
+
// transcriptions.
|
|
557
|
+
this._socket.addEventListener('message', this._socketMessageCallback, {once: true});
|
|
558
|
+
const langCode = this._getViewerLanguageCode();
|
|
559
|
+
this._socket.send(JSON.stringify(
|
|
560
|
+
{
|
|
561
|
+
sampleRate: this._context.sampleRate,
|
|
562
|
+
phrases: this._phraseList,
|
|
563
|
+
locale: langCode
|
|
564
|
+
}
|
|
565
|
+
));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Do setup so we can start streaming mic data.
|
|
570
|
+
* @private
|
|
571
|
+
*/
|
|
572
|
+
_startByteStream () {
|
|
573
|
+
// Hook up the scriptNode to the mic
|
|
574
|
+
this._sourceNode = this._context.createMediaStreamSource(this._micStream);
|
|
575
|
+
this._sourceNode.connect(this._scriptNode);
|
|
576
|
+
this._scriptNode.addEventListener('audioprocess', this._processAudioCallback);
|
|
577
|
+
this._scriptNode.connect(this._context.destination);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Called when we have data from the microphone. Takes that data and ships
|
|
582
|
+
* it off to the speech server for transcription.
|
|
583
|
+
* @param {audioProcessingEvent} e The event with audio data in it.
|
|
584
|
+
* @private
|
|
585
|
+
*/
|
|
586
|
+
_processAudioCallback (e) {
|
|
587
|
+
if (this._socket.readyState === WebSocket.CLOSED ||
|
|
588
|
+
this._socket.readyState === WebSocket.CLOSING) {
|
|
589
|
+
log.error(`Not sending data because not in ready state. State: ${this._socket.readyState}`);
|
|
590
|
+
// TODO: should we stop trying and reset state so it might work next time?
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const MAX_INT = Math.pow(2, 16 - 1) - 1;
|
|
594
|
+
const floatSamples = e.inputBuffer.getChannelData(0);
|
|
595
|
+
// The samples are floats in range [-1, 1]. Convert to 16-bit signed
|
|
596
|
+
// integer.
|
|
597
|
+
this._socket.send(Int16Array.from(floatSamples.map(n => n * MAX_INT)));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The key to load & store a target's speech-related state.
|
|
602
|
+
* @type {string}
|
|
603
|
+
*/
|
|
604
|
+
static get STATE_KEY () {
|
|
605
|
+
return 'Scratch.speech';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @returns {object} Metadata for this extension and its blocks.
|
|
610
|
+
*/
|
|
611
|
+
getInfo () {
|
|
612
|
+
return {
|
|
613
|
+
id: 'speech2text',
|
|
614
|
+
name: formatMessage({
|
|
615
|
+
id: 'speech.extensionName',
|
|
616
|
+
default: 'Speech to Text',
|
|
617
|
+
description: 'Name of extension that adds speech recognition blocks.'
|
|
618
|
+
}),
|
|
619
|
+
menuIconURI: menuIconURI,
|
|
620
|
+
blockIconURI: iconURI,
|
|
621
|
+
blocks: [
|
|
622
|
+
{
|
|
623
|
+
opcode: 'listenAndWait',
|
|
624
|
+
text: formatMessage({
|
|
625
|
+
id: 'speech.listenAndWait',
|
|
626
|
+
default: 'listen and wait',
|
|
627
|
+
// eslint-disable-next-line max-len
|
|
628
|
+
description: 'Start listening to the microphone and wait for a result from the speech recognition system.'
|
|
629
|
+
}),
|
|
630
|
+
blockType: BlockType.COMMAND
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
opcode: 'whenIHearHat',
|
|
634
|
+
text: formatMessage({
|
|
635
|
+
id: 'speech.whenIHear',
|
|
636
|
+
default: 'when I hear [PHRASE]',
|
|
637
|
+
// eslint-disable-next-line max-len
|
|
638
|
+
description: 'Event that triggers when the text entered on the block is recognized by the speech recognition system.'
|
|
639
|
+
}),
|
|
640
|
+
blockType: BlockType.HAT,
|
|
641
|
+
arguments: {
|
|
642
|
+
PHRASE: {
|
|
643
|
+
type: ArgumentType.STRING,
|
|
644
|
+
defaultValue: formatMessage({
|
|
645
|
+
id: 'speech.defaultWhenIHearValue',
|
|
646
|
+
default: 'let\'s go',
|
|
647
|
+
description: 'The default phrase/word that, when heard, triggers the event.'
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
opcode: 'getSpeech',
|
|
654
|
+
text: formatMessage({
|
|
655
|
+
id: 'speech.speechReporter',
|
|
656
|
+
default: 'speech',
|
|
657
|
+
description: 'Get the text of spoken words transcribed by the speech recognition system.'
|
|
658
|
+
}),
|
|
659
|
+
blockType: BlockType.REPORTER
|
|
660
|
+
}
|
|
661
|
+
]
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Start the listening process if it isn't already in progress.
|
|
667
|
+
* @return {Promise} A promise that will resolve when listening is complete.
|
|
668
|
+
*/
|
|
669
|
+
listenAndWait () {
|
|
670
|
+
this._phraseList = this._scanBlocksForPhraseList();
|
|
671
|
+
this._resetEdgeTriggerUtterance();
|
|
672
|
+
|
|
673
|
+
const speechPromise = new Promise(resolve => {
|
|
674
|
+
const listeningInProgress = this._speechPromises.length > 0;
|
|
675
|
+
this._speechPromises.push(resolve);
|
|
676
|
+
if (!listeningInProgress) {
|
|
677
|
+
this._startListening();
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return speechPromise;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* An edge triggered hat block to listen for a specific phrase.
|
|
685
|
+
* @param {object} args - the block arguments.
|
|
686
|
+
* @return {boolean} true if the phrase matches what was transcribed.
|
|
687
|
+
*/
|
|
688
|
+
whenIHearHat (args) {
|
|
689
|
+
return this._speechMatches(args.PHRASE, this._utteranceForEdgeTrigger);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Reporter for the last heard phrase/utterance.
|
|
694
|
+
* @return {string} The lastest thing we heard from a listen and wait block.
|
|
695
|
+
*/
|
|
696
|
+
getSpeech () {
|
|
697
|
+
return this._currentUtterance;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
module.exports = Scratch3Speech2TextBlocks;
|