soiz1 commited on
Commit
84fe9ab
·
verified ·
1 Parent(s): ac24a71

Create scratch3_tm2scratch/index.js

Browse files
local-scratch-vm/src/extensions/scratch3_tm2scratch/index.js ADDED
@@ -0,0 +1,1101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const ArgumentType = require('../../extension-support/argument-type');
2
+ const BlockType = require('../../extension-support/block-type');
3
+ const Cast = require('../../util/cast');
4
+ const MathUtil = require('../../util/math-util');
5
+ const log = require('../../util/log');
6
+ const formatMessage = require('format-message');
7
+ const blockIconURI = 'data:image/png;base64,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';
8
+
9
+ const Message = {
10
+ image_classification_model_url: {
11
+ 'ja': '画像分類モデルURL[URL]',
12
+ 'ja-Hira': 'がぞうぶんるいモデル[URL]',
13
+ 'en': 'image classification model URL [URL]',
14
+ 'ko': '이미지 분류 모델 URL [URL]',
15
+ 'zh-tw': '影像分類模型網址[URL]',
16
+ 'de': 'Bildklassifikationsmodell-URL [URL]'
17
+ },
18
+ image_classification_sample_model_url: {
19
+ 'ja': 'https://teachablemachine.withgoogle.com/models/0rX_3hoH/',
20
+ 'ja-Hira': 'https://teachablemachine.withgoogle.com/models/0rX_3hoH/',
21
+ 'en': 'https://teachablemachine.withgoogle.com/models/0rX_3hoH/',
22
+ 'ko': 'https://teachablemachine.withgoogle.com/models/0rX_3hoH/',
23
+ 'zh-tw': 'https://teachablemachine.withgoogle.com/models/0rX_3hoH/',
24
+ 'de': 'https://teachablemachine.withgoogle.com/models/0rX_3hoH/'
25
+ },
26
+ sound_classification_model_url: {
27
+ 'ja': '音声分類モデルURL[URL]',
28
+ 'ja-Hira': 'おんせいぶんるいモデル[URL]',
29
+ 'en': 'sound classification model URL [URL]',
30
+ 'ko': '소리 분류 모델 URL [URL]',
31
+ 'zh-tw': '聲音分類模型網址[URL]',
32
+ 'de': 'Audioklassifikationsmodell-URL [URL]'
33
+ },
34
+ sound_classification_sample_model_url: {
35
+ 'ja': 'https://teachablemachine.withgoogle.com/models/xP0spGSB/',
36
+ 'ja-Hira': 'https://teachablemachine.withgoogle.com/models/xP0spGSB/',
37
+ 'en': 'https://teachablemachine.withgoogle.com/models/xP0spGSB/',
38
+ 'ko': 'https://teachablemachine.withgoogle.com/models/xP0spGSB/',
39
+ 'zh-tw': 'https://teachablemachine.withgoogle.com/models/xP0spGSB/',
40
+ 'de': 'https://teachablemachine.withgoogle.com/models/xP0spGSB/'
41
+ },
42
+ classify_image: {
43
+ 'ja': '画像を分類する',
44
+ 'ja-Hira': 'がぞうをぶんるいする',
45
+ 'en': 'classify image',
46
+ 'ko': '이미지 분류하기',
47
+ 'zh-tw': '影像分類',
48
+ 'de': 'Bild klassifizieren'
49
+ },
50
+ image_label: {
51
+ 'ja': '画像ラベル',
52
+ 'ja-Hira': 'がぞうラベル',
53
+ 'en': 'image label',
54
+ 'ko': '이미지 라벨',
55
+ 'zh-tw': '影像標籤',
56
+ 'de': 'Bildklasse'
57
+ },
58
+ sound_label: {
59
+ 'ja': '音声ラベル',
60
+ 'ja-Hira': 'おんせいラベル',
61
+ 'en': 'sound label',
62
+ 'ko': '소리 라벨',
63
+ 'zh-tw': '聲音標籤',
64
+ 'de': 'Audioklasse'
65
+ },
66
+ when_received_block: {
67
+ 'ja': '画像ラベル[LABEL]を受け取ったとき',
68
+ 'ja-Hira': 'がぞうラベル[LABEL]をうけとったとき',
69
+ 'en': 'when received image label:[LABEL]',
70
+ 'ko': '[LABEL] 이미지 라벨을 받았을 때:',
71
+ 'zh-cn': '接收到类别[LABEL]时',
72
+ 'zh-tw': '接收到影像標籤:[LABEL]時',
73
+ 'de': 'Wenn ich die Bildklasse [LABEL] erkenne'
74
+ },
75
+ is_image_label_detected: {
76
+ 'ja': '[LABEL]の画像が見つかった',
77
+ 'ja-Hira': '[LABEL]のがぞうがみつかった',
78
+ 'en': 'image [LABEL] detected',
79
+ 'ko': '[LABEL] 이미지가 감지됨',
80
+ 'zh-tw': '影像[LABEL]被偵測?',
81
+ 'de': 'Bildklasse [LABEL] erkannt'
82
+ },
83
+ is_sound_label_detected: {
84
+ 'ja': '[LABEL]の音声が聞こえた',
85
+ 'ja-Hira': '[LABEL]のおんせいがきこえた',
86
+ 'en': 'sound [LABEL] detected',
87
+ 'ko': '[LABEL] 소리가 감지됨',
88
+ 'zh-tw': '聲音[LABEL]被偵測?',
89
+ 'de': 'Audioklasse [LABEL] erkannt'
90
+ },
91
+ image_label_confidence: {
92
+ 'ja': '画像ラベル[LABEL]の確度',
93
+ 'ja-Hira': 'がぞうラベル[LABEL]のかくど',
94
+ 'en': 'confidence of image [LABEL]',
95
+ 'ko': '[LABEL] 이미지 신뢰도',
96
+ 'zh-tw': '影像置信度[LABEL]',
97
+ 'de': 'Konfidenz der Bildklasse [LABEL]'
98
+ },
99
+ sound_label_confidence: {
100
+ 'ja': '音声ラベル[LABEL]の確度',
101
+ 'ja-Hira': 'おんせいラベル[LABEL]のかくど',
102
+ 'en': 'confidence of sound [LABEL]',
103
+ 'ko': '[LABEL] 소리 신뢰도',
104
+ 'zh-tw': '聲音置信度[LABEL]',
105
+ 'de': 'Konfidenz der Audioklasse [LABEL]'
106
+ },
107
+ when_received_sound_label_block: {
108
+ 'ja': '音声ラベル[LABEL]を受け取ったとき',
109
+ 'ja-Hira': '音声ラベル[LABEL]をうけとったとき',
110
+ 'en': 'when received sound label:[LABEL]',
111
+ 'zh-cn': '接收到声音类别[LABEL]时',
112
+ 'ko': '[LABEL] 소리 라벨을 받았을 때:',
113
+ 'zh-tw': '接收到聲音標籤[LABEL]時',
114
+ 'de': 'Wenn ich die Soundklasse [LABEL] erkenne'
115
+ },
116
+ label_block: {
117
+ 'ja': 'ラベル',
118
+ 'ja-Hira': 'ラベル',
119
+ 'en': 'label',
120
+ 'zh-cn': '标签',
121
+ 'ko': '라벨',
122
+ 'zh-tw': '標籤',
123
+ 'de': 'Klasse'
124
+ },
125
+ any: {
126
+ 'ja': 'のどれか',
127
+ 'ja-Hira': 'のどれか',
128
+ 'en': 'any',
129
+ 'zh-cn': '任何',
130
+ 'ko': '어떤',
131
+ 'zh-tw': '任何',
132
+ 'de': 'irgendeine'
133
+ },
134
+ any_without_of: {
135
+ 'ja': 'どれか',
136
+ 'ja-Hira': 'どれか',
137
+ 'en': 'any',
138
+ 'ko': '어떤',
139
+ 'zh-cn': '任何',
140
+ 'zh-tw': '任何',
141
+ 'de': 'irgendeine'
142
+ },
143
+ all: {
144
+ 'ja': 'の全て',
145
+ 'ja-Hira': 'のすべて',
146
+ 'en': 'all',
147
+ 'ko': '모든',
148
+ 'zh-cn': '所有',
149
+ 'zh-tw': '全部',
150
+ 'de': 'Alle'
151
+ },
152
+ toggle_classification: {
153
+ 'ja': 'ラベル付けを[CLASSIFICATION_STATE]にする',
154
+ 'ja-Hira': 'ラベルづけを[CLASSIFICATION_STATE]にする',
155
+ 'en': 'turn classification [CLASSIFICATION_STATE]',
156
+ 'ko': '라벨 분류 [CLASSIFICATION_STATE]',
157
+ 'zh-cn': '[CLASSIFICATION_STATE]分类',
158
+ 'zh-tw': '[CLASSIFICATION_STATE]分類',
159
+ 'de': 'Klassifizierung umschalten: [CLASSIFICATION_STATE]'
160
+ },
161
+ set_confidence_threshold: {
162
+ 'ja': '確度のしきい値を[CONFIDENCE_THRESHOLD]にする',
163
+ 'ja-Hira': 'かくどのしきいちを[CONFIDENCE_THRESHOLD]にする',
164
+ 'en': 'set confidence threshold [CONFIDENCE_THRESHOLD]',
165
+ 'ko': '신뢰도 기준 설정 [CONFIDENCE_THRESHOLD]',
166
+ 'zh-tw': '設定置信度閾值[CONFIDENCE_THRESHOLD]',
167
+ 'de': 'Setze die Konfidenzschwelle auf [CONFIDENCE_THRESHOLD]'
168
+
169
+ },
170
+ get_confidence_threshold: {
171
+ 'ja': '確度のしきい値',
172
+ 'ja-Hira': 'かくどのしきいち',
173
+ 'en': 'confidence threshold',
174
+ 'ko': '신뢰도 기준',
175
+ 'zh-tw': '置信度閾值',
176
+ 'de': 'Konfidenzschwelle'
177
+ },
178
+ set_classification_interval: {
179
+ 'ja': 'ラベル付けを[CLASSIFICATION_INTERVAL]秒間に1回行う',
180
+ 'ja-Hira': 'ラベルづけを[CLASSIFICATION_INTERVAL]びょうかんに1かいおこなう',
181
+ 'en': 'Label once every [CLASSIFICATION_INTERVAL] seconds',
182
+ 'zh-cn': '每隔[CLASSIFICATION_INTERVAL]秒标记一次',
183
+ 'ko': '매 [CLASSIFICATION_INTERVAL]초마다 라벨 분류하기',
184
+ 'zh-tw': '每隔[CLASSIFICATION_INTERVAL]秒標記一次',
185
+ 'de': 'Klassifiziere alle [CLASSIFICATION_INTERVAL] Sekunden'
186
+ },
187
+ video_toggle: {
188
+ 'ja': 'ビデオを[VIDEO_STATE]にする',
189
+ 'ja-Hira': 'ビデオを[VIDEO_STATE]にする',
190
+ 'en': 'turn video [VIDEO_STATE]',
191
+ 'zh-cn': '[VIDEO_STATE]摄像头',
192
+ 'ko': '비디오 화면 [VIDEO_STATE]',
193
+ 'zh-tw': '視訊設為[VIDEO_STATE]',
194
+ 'de': 'Video umschalten: [VIDEO_STATE]'
195
+ },
196
+ on: {
197
+ 'ja': '入',
198
+ 'ja-Hira': 'いり',
199
+ 'en': 'on',
200
+ 'ko': '켜기',
201
+ 'zh-cn': '开启',
202
+ 'zh-tw': '開啟',
203
+ 'de': 'an'
204
+ },
205
+ off: {
206
+ 'ja': '切',
207
+ 'ja-Hira': 'きり',
208
+ 'en': 'off',
209
+ 'ko': '멈추기',
210
+ 'zh-cn': '关闭',
211
+ 'zh-tw': '關閉',
212
+ 'de': 'aus'
213
+ },
214
+ video_on_flipped: {
215
+ 'ja': '左右反転',
216
+ 'ja-Hira': 'さゆうはんてん',
217
+ 'en': 'on flipped',
218
+ 'ko': '좌우 뒤집기',
219
+ 'zh-cn': '镜像开启',
220
+ 'zh-tw': '翻轉',
221
+ 'de': 'an (gespiegelt)'
222
+ },
223
+ switch_webcam: {
224
+ 'ja': 'カメラを[DEVICE]に切り替える',
225
+ 'ja-Hira': 'カメラを[DEVICE]にきりかえる',
226
+ 'en': 'switch webcam to [DEVICE]',
227
+ 'zh-cn': '网络摄像头切换到[DEVICE]',
228
+ 'zh-tw': '網路攝影機切換到[DEVICE]',
229
+ 'de': 'Wechsle Webcam zu [DEVICE]'
230
+ }
231
+ };
232
+
233
+ const AvailableLocales = ['en', 'ja', 'ja-Hira', 'ko', 'zh-cn', 'zh-tw', 'de'];
234
+
235
+ class Scratch3TM2ScratchBlocks {
236
+ constructor(runtime) {
237
+ this.runtime = runtime;
238
+ this.locale = this.setLocale();
239
+
240
+ this.video = document.createElement('video');
241
+ this.video.autoplay = true;
242
+
243
+ this.interval = 1000;
244
+ this.minInterval = 100;
245
+
246
+ const media = navigator.mediaDevices.getUserMedia({
247
+ video: {
248
+ width: 360,
249
+ height: 360
250
+ },
251
+ audio: false
252
+ });
253
+
254
+ media.then(stream => {
255
+ this.video.srcObject = stream;
256
+ });
257
+
258
+ this.timer = setInterval(() => {
259
+ this.classifyVideoImage();
260
+ }, this.minInterval);
261
+
262
+ this.imageModelUrl = null;
263
+ this.imageMetadata = null;
264
+ this.imageClassifier = null;
265
+ this.initImageProbableLabels();
266
+ this.confidenceThreshold = 0.5;
267
+
268
+ this.soundModelUrl = null;
269
+ this.soundMetadata = null;
270
+ this.soundClassifier = null;
271
+ this.soundClassifierEnabled = false;
272
+ this.initSoundProbableLabels();
273
+
274
+ this.runtime.ioDevices.video.enableVideo();
275
+
276
+ this.devices = [{ text: 'default', value: '' }];
277
+ try {
278
+ navigator.mediaDevices.enumerateDevices().then(media => {
279
+ for (const device of media) {
280
+ if (device.kind === 'videoinput') {
281
+ this.devices.push({
282
+ text: device.label,
283
+ value: device.deviceId
284
+ });
285
+ }
286
+ }
287
+ });
288
+ } catch {
289
+ console.error('failed to load media devices!');
290
+ }
291
+
292
+ let script = document.createElement('script');
293
+ script.src = 'https://stretch3.github.io/ml5-library/ml5.min.js';
294
+ document.head.appendChild(script);
295
+ }
296
+
297
+ /**
298
+ * Initialize the result of image classification.
299
+ */
300
+ initImageProbableLabels() {
301
+ this.imageProbableLabels = [];
302
+ }
303
+
304
+ initSoundProbableLabels() {
305
+ this.soundProbableLabels = [];
306
+ }
307
+
308
+ getInfo() {
309
+ this.locale = this.setLocale();
310
+
311
+ return {
312
+ id: 'tm2scratch',
313
+ name: 'TM2Scratch',
314
+ blockIconURI: blockIconURI,
315
+ blocks: [
316
+ {
317
+ opcode: 'whenReceived',
318
+ text: Message.when_received_block[this.locale],
319
+ blockType: BlockType.HAT,
320
+ arguments: {
321
+ LABEL: {
322
+ type: ArgumentType.STRING,
323
+ menu: 'received_menu',
324
+ defaultValue: Message.any[this.locale]
325
+ }
326
+ }
327
+ },
328
+ {
329
+ opcode: 'isImageLabelDetected',
330
+ text: Message.is_image_label_detected[this.locale],
331
+ blockType: BlockType.BOOLEAN,
332
+ arguments: {
333
+ LABEL: {
334
+ type: ArgumentType.STRING,
335
+ menu: 'image_labels_menu',
336
+ defaultValue: Message.any_without_of[this.locale]
337
+ }
338
+ }
339
+ },
340
+ {
341
+ opcode: 'imageLabelConfidence',
342
+ text: Message.image_label_confidence[this.locale],
343
+ blockType: BlockType.REPORTER,
344
+ disableMonitor: true,
345
+ arguments: {
346
+ LABEL: {
347
+ type: ArgumentType.STRING,
348
+ menu: 'image_labels_without_any_menu',
349
+ defaultValue: ''
350
+ }
351
+ }
352
+ },
353
+ {
354
+ opcode: 'setImageClassificationModelURL',
355
+ text: Message.image_classification_model_url[this.locale],
356
+ blockType: BlockType.COMMAND,
357
+ arguments: {
358
+ URL: {
359
+ type: ArgumentType.STRING,
360
+ defaultValue: Message.image_classification_sample_model_url[this.locale]
361
+ }
362
+ }
363
+ },
364
+ {
365
+ opcode: 'classifyVideoImageBlock',
366
+ text: Message.classify_image[this.locale],
367
+ blockType: BlockType.COMMAND
368
+ },
369
+ {
370
+ opcode: 'getImageLabel',
371
+ text: Message.image_label[this.locale],
372
+ blockType: BlockType.REPORTER
373
+ },
374
+ '---',
375
+ {
376
+ opcode: 'whenReceivedSoundLabel',
377
+ text: Message.when_received_sound_label_block[this.locale],
378
+ blockType: BlockType.HAT,
379
+ arguments: {
380
+ LABEL: {
381
+ type: ArgumentType.STRING,
382
+ menu: 'received_sound_label_menu',
383
+ defaultValue: Message.any[this.locale]
384
+ }
385
+ }
386
+ },
387
+ {
388
+ opcode: 'isSoundLabelDetected',
389
+ text: Message.is_sound_label_detected[this.locale],
390
+ blockType: BlockType.BOOLEAN,
391
+ arguments: {
392
+ LABEL: {
393
+ type: ArgumentType.STRING,
394
+ menu: 'sound_labels_menu',
395
+ defaultValue: Message.any_without_of[this.locale]
396
+ }
397
+ }
398
+ },
399
+ {
400
+ opcode: 'soundLabelConfidence',
401
+ text: Message.sound_label_confidence[this.locale],
402
+ blockType: BlockType.REPORTER,
403
+ disableMonitor: true,
404
+ arguments: {
405
+ LABEL: {
406
+ type: ArgumentType.STRING,
407
+ menu: 'sound_labels_without_any_menu',
408
+ defaultValue: ''
409
+ }
410
+ }
411
+ },
412
+ {
413
+ opcode: 'setSoundClassificationModelURL',
414
+ text: Message.sound_classification_model_url[this.locale],
415
+ blockType: BlockType.COMMAND,
416
+ arguments: {
417
+ URL: {
418
+ type: ArgumentType.STRING,
419
+ defaultValue: Message.sound_classification_sample_model_url[this.locale]
420
+ }
421
+ }
422
+ },
423
+ {
424
+ opcode: 'getSoundLabel',
425
+ text: Message.sound_label[this.locale],
426
+ blockType: BlockType.REPORTER
427
+ },
428
+ '---',
429
+ {
430
+ opcode: 'toggleClassification',
431
+ text: Message.toggle_classification[this.locale],
432
+ blockType: BlockType.COMMAND,
433
+ arguments: {
434
+ CLASSIFICATION_STATE: {
435
+ type: ArgumentType.STRING,
436
+ menu: 'classification_menu',
437
+ defaultValue: 'off'
438
+ }
439
+ }
440
+ },
441
+ {
442
+ opcode: 'setClassificationInterval',
443
+ text: Message.set_classification_interval[this.locale],
444
+ blockType: BlockType.COMMAND,
445
+ arguments: {
446
+ CLASSIFICATION_INTERVAL: {
447
+ type: ArgumentType.STRING,
448
+ menu: 'classification_interval_menu',
449
+ defaultValue: '1'
450
+ }
451
+ }
452
+ },
453
+ {
454
+ opcode: 'setConfidenceThreshold',
455
+ text: Message.set_confidence_threshold[this.locale],
456
+ blockType: BlockType.COMMAND,
457
+ arguments: {
458
+ CONFIDENCE_THRESHOLD: {
459
+ type: ArgumentType.NUMBER,
460
+ defaultValue: 0.5
461
+ }
462
+ }
463
+ },
464
+ {
465
+ opcode: 'getConfidenceThreshold',
466
+ text: Message.get_confidence_threshold[this.locale],
467
+ blockType: BlockType.REPORTER,
468
+ disableMonitor: true
469
+ },
470
+ {
471
+ opcode: 'videoToggle',
472
+ text: Message.video_toggle[this.locale],
473
+ blockType: BlockType.COMMAND,
474
+ arguments: {
475
+ VIDEO_STATE: {
476
+ type: ArgumentType.STRING,
477
+ menu: 'video_menu',
478
+ defaultValue: 'off'
479
+ }
480
+ }
481
+ },
482
+ {
483
+ opcode: 'switchCamera',
484
+ blockType: BlockType.COMMAND,
485
+ text: Message.switch_webcam[this.locale],
486
+ arguments: {
487
+ DEVICE: {
488
+ type: ArgumentType.STRING,
489
+ defaultValue: '',
490
+ menu: 'mediadevices'
491
+ }
492
+ }
493
+ }
494
+ ],
495
+ menus: {
496
+ received_menu: {
497
+ acceptReporters: true,
498
+ items: 'getLabelsMenu'
499
+ },
500
+ image_labels_menu: {
501
+ acceptReporters: true,
502
+ items: 'getLabelsWithAnyWithoutOfMenu'
503
+ },
504
+ image_labels_without_any_menu: {
505
+ acceptReporters: true,
506
+ items: 'getLabelsWithoutAnyMenu'
507
+ },
508
+ received_sound_label_menu: {
509
+ acceptReporters: true,
510
+ items: 'getSoundLabelsWithoutBackgroundMenu'
511
+ },
512
+ sound_labels_menu: {
513
+ acceptReporters: true,
514
+ items: 'getSoundLabelsWithoutBackgroundWithAnyWithoutOfMenu'
515
+ },
516
+ sound_labels_without_any_menu: {
517
+ acceptReporters: true,
518
+ items: 'getSoundLabelsWithoutAnyMenu'
519
+ },
520
+ video_menu: this.getVideoMenu(),
521
+ classification_interval_menu: this.getClassificationIntervalMenu(),
522
+ classification_menu: this.getClassificationMenu(),
523
+ mediadevices: {
524
+ acceptReporters: true,
525
+ items: 'getDevices'
526
+ }
527
+ }
528
+ };
529
+ }
530
+
531
+ /**
532
+ * Detect change of the selected image label is the most probable one or not.
533
+ * @param {object} args - The block's arguments.
534
+ * @property {string} LABEL - The label to detect.
535
+ * @return {boolean} - Whether the label is most probable or not.
536
+ */
537
+ whenReceived(args) {
538
+ const label = this.getImageLabel();
539
+ if (args.LABEL === Message.any[this.locale]) {
540
+ return label !== '';
541
+ }
542
+ return label === args.LABEL;
543
+ }
544
+
545
+ /**
546
+ * Detect change of the selected sound label is the most probable one or not.
547
+ * @param {object} args - The block's arguments.
548
+ * @property {string} LABEL - The label to detect.
549
+ * @return {boolean} - Whether the label is most probable or not.
550
+ */
551
+ whenReceivedSoundLabel(args) {
552
+ if (!this.soundClassifierEnabled) {
553
+ return;
554
+ }
555
+
556
+ const label = this.getSoundLabel();
557
+ if (args.LABEL === Message.any[this.locale]) {
558
+ return label !== '';
559
+ }
560
+ return label === args.LABEL;
561
+ }
562
+
563
+ /**
564
+ * Return whether the most probable image label is the selected one or not.
565
+ * @param {object} args - The block's arguments.
566
+ * @property {string} LABEL - The label to detect.
567
+ * @return {boolean} - Whether the label is most probable or not.
568
+ */
569
+ isImageLabelDetected(args) {
570
+ const label = this.getImageLabel();
571
+ if (args.LABEL === Message.any[this.locale]) {
572
+ return label !== '';
573
+ }
574
+ return label === args.LABEL;
575
+ }
576
+
577
+ /**
578
+ * Return whether the most probable sound label is the selected one or not.
579
+ * @param {object} args - The block's arguments.
580
+ * @property {string} LABEL - The label to detect.
581
+ * @return {boolean} - Whether the label is most probable or not.
582
+ */
583
+ isSoundLabelDetected(args) {
584
+ const label = this.getSoundLabel();
585
+ if (args.LABEL === Message.any[this.locale]) {
586
+ return label !== '';
587
+ }
588
+ return label === args.LABEL;
589
+ }
590
+
591
+ /**
592
+ * Return confidence of the image label.
593
+ * @param {object} args - The block's arguments.
594
+ * @property {string} LABEL - Selected label.
595
+ * @return {number} - Confidence of the label.
596
+ */
597
+ imageLabelConfidence(args) {
598
+ if (args.LABEL === '') {
599
+ return 0;
600
+ }
601
+ const entry = this.imageProbableLabels.find(element => element.label === args.LABEL);
602
+ return (entry ? entry.confidence : 0);
603
+ }
604
+
605
+ /**
606
+ * Return confidence of the sound label.
607
+ * @param {object} args - The block's arguments.
608
+ * @property {string} LABEL - Selected label.
609
+ * @return {number} - Confidence of the label.
610
+ */
611
+ soundLabelConfidence(args) {
612
+ if (!this.soundProbableLabels || this.soundProbableLabels.length === 0) return 0;
613
+
614
+ if (args.LABEL === '') {
615
+ return 0;
616
+ }
617
+ const entry = this.soundProbableLabels.find(element => element.label === args.LABEL);
618
+ return (entry ? entry.confidence : 0);
619
+ }
620
+
621
+ /**
622
+ * Set a model for image classification from URL.
623
+ * @param {object} args - the block's arguments.
624
+ * @property {string} URL - URL of model to be loaded.
625
+ * @return {Promise} - A Promise that resolve after loaded.
626
+ */
627
+ setImageClassificationModelURL(args) {
628
+ return this.loadImageClassificationModelFromURL(args.URL);
629
+ }
630
+
631
+ /**
632
+ * Set a model for sound classification from URL.
633
+ * @param {object} args - the block's arguments.
634
+ * @property {string} URL - URL of model to be loaded.
635
+ * @return {Promise} - A Promise that resolve after loaded.
636
+ */
637
+ setSoundClassificationModelURL(args) {
638
+ return this.loadSoundClassificationModelFromURL(args.URL);
639
+ }
640
+
641
+ /**
642
+ * Load a model from URL for image classification.
643
+ * @param {string} url - URL of model to be loaded.
644
+ * @return {Promise} - A Promise that resolves after loaded.
645
+ */
646
+ loadImageClassificationModelFromURL(url) {
647
+ return new Promise(resolve => {
648
+ const timestamp = new Date().getTime();
649
+ fetch(`${url}metadata.json?${timestamp}`)
650
+ .then(res => res.json())
651
+ .then(metadata => {
652
+ if (url === this.imageModelUrl &&
653
+ (new Date(metadata.timeStamp).getTime() === new Date(this.imageMetadata.timeStamp).getTime())) {
654
+ log.info(`image model already loaded: ${url}`);
655
+ resolve();
656
+ } else {
657
+ ml5.imageClassifier(`${url}model.json?${timestamp}`)
658
+ .then(classifier => {
659
+ this.imageModelUrl = url;
660
+ this.imageMetadata = metadata;
661
+ this.imageClassifier = classifier;
662
+ this.initImageProbableLabels();
663
+ log.info(`image model loaded from: ${url}`);
664
+ })
665
+ .catch(error => {
666
+ log.warn(error);
667
+ })
668
+ .finally(() => resolve());
669
+ }
670
+ })
671
+ .catch(error => {
672
+ log.warn(error);
673
+ resolve();
674
+ });
675
+ });
676
+ }
677
+
678
+ /**
679
+ * Load a model from URL for sound classification.
680
+ * @param {string} url - URL of model to be loaded.
681
+ * @return {Promise} - A Promise that resolves after loaded.
682
+ */
683
+ loadSoundClassificationModelFromURL(url) {
684
+ return new Promise(resolve => {
685
+ const timestamp = new Date().getTime();
686
+ fetch(`${url}metadata.json?${timestamp}`)
687
+ .then(res => res.json())
688
+ .then(metadata => {
689
+ if (url === this.soundModelUrl &&
690
+ (new Date(metadata.timeStamp).getTime() === new Date(this.soundMetadata.timeStamp).getTime())) {
691
+ log.info(`sound model already loaded: ${url}`);
692
+ resolve();
693
+ } else {
694
+ ml5.soundClassifier(`${url}model.json`)
695
+ .then(classifier => {
696
+ this.soundModelUrl = url;
697
+ this.soundMetadata = metadata;
698
+ this.soundClassifier = classifier;
699
+ this.initSoundProbableLabels();
700
+ this.soundClassifierEnabled = true;
701
+ this.classifySound();
702
+ log.info(`sound model loaded from: ${url}`);
703
+ })
704
+ .catch(error => {
705
+ log.warn(error);
706
+ })
707
+ .finally(() => resolve());
708
+ }
709
+ })
710
+ .catch(error => {
711
+ log.warn(error);
712
+ resolve();
713
+ });
714
+ });
715
+ }
716
+
717
+ /**
718
+ * Return menu items to detect label in the image.
719
+ * @return {Array} - Menu items with 'any'.
720
+ */
721
+ getLabelsMenu() {
722
+ let items = [Message.any[this.locale]];
723
+ if (!this.imageMetadata) return items;
724
+ items = items.concat(this.imageMetadata.labels);
725
+ return items;
726
+ }
727
+
728
+ /**
729
+ * Return menu items to detect label in the image.
730
+ * @return {Array} - Menu items with 'any without of'.
731
+ */
732
+ getLabelsWithAnyWithoutOfMenu() {
733
+ let items = [Message.any_without_of[this.locale]];
734
+ if (!this.imageMetadata) return items;
735
+ items = items.concat(this.imageMetadata.labels);
736
+ return items;
737
+ }
738
+
739
+ /**
740
+ * Return menu items to detect label in the image.
741
+ * @return {Array} - Menu items with 'any'.
742
+ */
743
+ getSoundLabelsMenu() {
744
+ let items = [Message.any[this.locale]];
745
+ if (!this.soundMetadata) return items;
746
+ items = items.concat(this.soundMetadata.wordLabels);
747
+ return items;
748
+ }
749
+
750
+ /**
751
+ * Return menu itmes to get properties of the image label.
752
+ * @return {Array} - Menu items with ''.
753
+ */
754
+ getLabelsWithoutAnyMenu() {
755
+ let items = [''];
756
+ if (this.imageMetadata) {
757
+ items = items.concat(this.imageMetadata.labels);
758
+ }
759
+ return items;
760
+ }
761
+
762
+ /**
763
+ * Return menu itmes to get properties of the sound label.
764
+ * @return {Array} - Menu items with ''.
765
+ */
766
+ getSoundLabelsWithoutAnyMenu() {
767
+ if (this.soundMetadata) {
768
+ return this.soundMetadata.wordLabels;
769
+ } else {
770
+ return [''];
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Return menu itmes to get properties of the sound label.
776
+ * @return {Array} - Menu items without '_background_noise_'.
777
+ */
778
+ getSoundLabelsWithoutBackgroundMenu() {
779
+ let items = [Message.any[this.locale]];
780
+ if (!this.soundMetadata) return items;
781
+ let arr = this.soundMetadata.wordLabels;
782
+ for (let i = 0; i < arr.length; i++) {
783
+ if (arr[i] !== '_background_noise_') {
784
+ items.push(arr[i]);
785
+ }
786
+ }
787
+ return items;
788
+ }
789
+
790
+ /**
791
+ * Return menu itmes to get properties of the sound label.
792
+ * @return {Array} - Menu items without '_background_noise_' and with 'any without of'.
793
+ */
794
+ getSoundLabelsWithoutBackgroundWithAnyWithoutOfMenu() {
795
+ let items = [Message.any_without_of[this.locale]];
796
+ if (!this.soundMetadata) return items;
797
+ let arr = this.soundMetadata.wordLabels;
798
+ for (let i = 0; i < arr.length; i++) {
799
+ if (arr[i] !== '_background_noise_') {
800
+ items.push(arr[i]);
801
+ }
802
+ }
803
+ return items;
804
+ }
805
+
806
+ /**
807
+ * Pick a probability which has highest confidence.
808
+ * @param {Array} probabilities - An Array of probabilities.
809
+ * @property {number} probabilities.confidence - Probability of the label.
810
+ * @return {object} - One of the highest confidence probability.
811
+ */
812
+ getMostProbableOne(probabilities) {
813
+ if (probabilities.length === 0) return null;
814
+ let mostOne = probabilities[0];
815
+ probabilities.forEach(clss => {
816
+ if (clss.confidence > mostOne.confidence) {
817
+ mostOne = clss;
818
+ }
819
+ });
820
+ return mostOne;
821
+ }
822
+
823
+ /**
824
+ * Classify image from the video input.
825
+ * Call stack will wait until the previous classification was done.
826
+ *
827
+ * @param {object} _args - the block's arguments.
828
+ * @param {object} util - utility object provided by the runtime.
829
+ * @return {Promise} - a Promise that resolves after classification.
830
+ */
831
+ classifyVideoImageBlock(_args, util) {
832
+ if (this._isImageClassifying) {
833
+ if (util) util.yield();
834
+ return;
835
+ }
836
+ return new Promise(resolve => {
837
+ this.classifyImage(this.video)
838
+ .then(result => {
839
+ resolve(JSON.stringify(result));
840
+ });
841
+ });
842
+ }
843
+
844
+ /**
845
+ * Classyfy image from input data source.
846
+ *
847
+ * @param {HTMLImageElement | ImageData | HTMLCanvasElement | HTMLVideoElement} input
848
+ * - Data source for classification.
849
+ * @return {Promise} - A Promise that resolves the result of classification.
850
+ * The result will be empty when the imageClassifier was not set.
851
+ */
852
+ classifyImage(input) {
853
+ if (!this.imageMetadata || !this.imageClassifier) {
854
+ this._isImageClassifying = false;
855
+ return Promise.resolve([]);
856
+ }
857
+ this._isImageClassifying = true;
858
+ return this.imageClassifier.classify(input)
859
+ .then(result => {
860
+ this.imageProbableLabels = result.slice();
861
+ this.imageProbableLabelsUpdated = true;
862
+ return result;
863
+ })
864
+ .finally(() => {
865
+ setTimeout(() => {
866
+ // Initialize probabilities to reset whenReceived blocks.
867
+ this.initImageProbableLabels();
868
+ this._isImageClassifying = false;
869
+ }, this.interval);
870
+ });
871
+ }
872
+
873
+ /**
874
+ * Classify sound.
875
+ */
876
+ classifySound() {
877
+ this.soundClassifier.classify((err, result) => {
878
+ if (this.soundClassifierEnabled && result) {
879
+ this.soundProbableLabels = result.slice();
880
+ setTimeout(() => {
881
+ // Initialize probabilities to reset whenReceivedSoundLabel blocks.
882
+ this.initSoundProbableLabels();
883
+ }, this.interval);
884
+ }
885
+ if (err) {
886
+ console.error(err);
887
+ }
888
+ });
889
+ }
890
+
891
+ /**
892
+ * Get the most probable label in the image.
893
+ * Retrun the last classification result or '' when the first classification was not done.
894
+ * @return {string} label
895
+ */
896
+ getImageLabel() {
897
+ if (!this.imageProbableLabels || this.imageProbableLabels.length === 0) return '';
898
+ const mostOne = this.getMostProbableOne(this.imageProbableLabels);
899
+ return (mostOne.confidence >= this.confidenceThreshold) ? mostOne.label : '';
900
+ }
901
+
902
+ /**
903
+ * Get the most probable label in the sound.
904
+ * Retrun the last classification result or '' when the first classification was not done.
905
+ * @return {string} label
906
+ */
907
+ getSoundLabel() {
908
+ if (!this.soundProbableLabels || this.soundProbableLabels.length === 0) return '';
909
+ const mostOne = this.getMostProbableOne(this.soundProbableLabels);
910
+ return (mostOne.confidence >= this.confidenceThreshold) ? mostOne.label : '';
911
+ }
912
+
913
+ /**
914
+ * Set confidence threshold which should be over for detected label.
915
+ * @param {object} args - the block's arguments.
916
+ * @property {number} CONFIDENCE_THRESHOLD - Value of confidence threshold.
917
+ */
918
+ setConfidenceThreshold(args) {
919
+ let threshold = Cast.toNumber(args.CONFIDENCE_THRESHOLD);
920
+ threshold = MathUtil.clamp(threshold, 0, 1);
921
+ this.confidenceThreshold = threshold;
922
+ }
923
+
924
+ /**
925
+ * Get confidence threshold which should be over for detected label.
926
+ * @param {object} args - the block's arguments.
927
+ * @return {number} - Value of confidence threshold.
928
+ */
929
+ getConfidenceThreshold() {
930
+ return this.confidenceThreshold;
931
+ }
932
+
933
+ /**
934
+ * Set state of the continuous classification.
935
+ * @param {object} args - the block's arguments.
936
+ * @property {string} CLASSIFICATION_STATE - State to be ['on'|'off'].
937
+ */
938
+ toggleClassification(args) {
939
+ const state = args.CLASSIFICATION_STATE;
940
+ if (this.timer) {
941
+ clearTimeout(this.timer);
942
+ }
943
+ this.soundClassifierEnabled = false;
944
+ if (state === 'on') {
945
+ this.timer = setInterval(() => {
946
+ this.classifyVideoImage();
947
+ }, this.minInterval);
948
+ this.soundClassifierEnabled = true;
949
+ }
950
+ }
951
+
952
+ /**
953
+ * Set interval time of the continuous classification.
954
+ * @param {object} args - the block's arguments.
955
+ * @property {number} CLASSIFICATION_INTERVAL - Interval time (seconds).
956
+ */
957
+ setClassificationInterval(args) {
958
+ if (this.timer) {
959
+ clearTimeout(this.timer);
960
+ }
961
+ this.interval = args.CLASSIFICATION_INTERVAL * 1000;
962
+ this.timer = setInterval(() => {
963
+ this.classifyVideoImage();
964
+ }, this.minInterval);
965
+ }
966
+
967
+ /**
968
+ * Show video image on the stage or not.
969
+ * @param {object} args - the block's arguments.
970
+ * @property {string} VIDEO_STATE - Show or not ['on'|'off'].
971
+ */
972
+ videoToggle(args) {
973
+ const state = args.VIDEO_STATE;
974
+ if (state === 'off') {
975
+ this.runtime.ioDevices.video.disableVideo();
976
+ } else {
977
+ this.runtime.ioDevices.video.enableVideo();
978
+ this.runtime.ioDevices.video.mirror = state === 'on';
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Classify video image.
984
+ * @return {Promise} - A Promise that resolves the result of classification.
985
+ * The result will be empty when another classification was under going.
986
+ */
987
+ classifyVideoImage() {
988
+ if (this._isImageClassifying) return Promise.resolve([]);
989
+ return this.classifyImage(this.video);
990
+ }
991
+
992
+ /**
993
+ * Return menu for video showing state.
994
+ * @return {Array} - Menu items.
995
+ */
996
+ getVideoMenu() {
997
+ return [
998
+ {
999
+ text: Message.off[this.locale],
1000
+ value: 'off'
1001
+ },
1002
+ {
1003
+ text: Message.on[this.locale],
1004
+ value: 'on'
1005
+ },
1006
+ {
1007
+ text: Message.video_on_flipped[this.locale],
1008
+ value: 'on-flipped'
1009
+ }
1010
+ ];
1011
+ }
1012
+
1013
+ /**
1014
+ * Return menu for classification interval setting.
1015
+ * @return {object} - Menu.
1016
+ */
1017
+ getClassificationIntervalMenu() {
1018
+ return {
1019
+ acceptReporters: true,
1020
+ items: [
1021
+ {
1022
+ text: '1',
1023
+ value: '1'
1024
+ },
1025
+ {
1026
+ text: '0.5',
1027
+ value: '0.5'
1028
+ },
1029
+ {
1030
+ text: '0.2',
1031
+ value: '0.2'
1032
+ },
1033
+ {
1034
+ text: '0.1',
1035
+ value: '0.1'
1036
+ }
1037
+ ]
1038
+ };
1039
+ }
1040
+
1041
+ /**
1042
+ * Return menu for continuous classification state.
1043
+ * @return {Array} - Menu items.
1044
+ */
1045
+ getClassificationMenu() {
1046
+ return [
1047
+ {
1048
+ text: Message.off[this.locale],
1049
+ value: 'off'
1050
+ },
1051
+ {
1052
+ text: Message.on[this.locale],
1053
+ value: 'on'
1054
+ }
1055
+ ];
1056
+ }
1057
+
1058
+ /**
1059
+ * Get locale for message text.
1060
+ * @return {string} - Locale of this editor.
1061
+ */
1062
+ setLocale() {
1063
+ const locale = formatMessage.setup().locale;
1064
+ if (AvailableLocales.includes(locale)) {
1065
+ return locale;
1066
+ }
1067
+ return 'en';
1068
+
1069
+ }
1070
+ switchCamera(args) {
1071
+ if (args.DEVICE !== '') {
1072
+ if (this.runtime.ioDevices.video.provider._track !== null) {
1073
+ this.runtime.ioDevices.video.provider._track.stop();
1074
+ const deviceId = args.DEVICE;
1075
+ navigator.mediaDevices.getUserMedia({ audio: false, video: { deviceId } }).then(
1076
+ stream => {
1077
+ try {
1078
+ this.runtime.ioDevices.video.provider._video.srcObject = stream;
1079
+ } catch (error) {
1080
+ this.runtime.ioDevices.video.provider._video.src = window.URL.createObjectURL(stream);
1081
+ }
1082
+ // Needed for Safari/Firefox, Chrome auto-plays.
1083
+ this.runtime.ioDevices.video.provider._video.play();
1084
+ this.runtime.ioDevices.video.provider._track = stream.getTracks()[0];
1085
+ }
1086
+ );
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ getDevices() {
1092
+ return this.devices;
1093
+ }
1094
+
1095
+ getBasename (url) {
1096
+ url = url.replace(/\/+$/, '');
1097
+ return url.split('/').pop();
1098
+ }
1099
+ }
1100
+
1101
+ module.exports = Scratch3TM2ScratchBlocks;