soiz1 commited on
Commit
af7ed6b
·
verified ·
1 Parent(s): 11d7102

Create scratch3_ml2scratch/index.js

Browse files
local-scratch-vm/src/extensions/scratch3_ml2scratch/index.js ADDED
@@ -0,0 +1,1139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const ArgumentType = require('../../extension-support/argument-type');
2
+ const BlockType = require('../../extension-support/block-type');
3
+ const Cast = require('../../util/cast');
4
+ const log = require('../../util/log');
5
+ const ml5 = require('ml5');
6
+
7
+ /**
8
+ * Formatter which is used for translating.
9
+ * When it was loaded as a module, 'formatMessage' will be replaced which is used in the runtime.
10
+ * @type {Function}
11
+ */
12
+ let formatMessage = require('format-message');
13
+
14
+ /**
15
+ * URL to get this extension as a module.
16
+ * When it was loaded as a module, 'extensionURL' will be replaced a URL which is retrieved from.
17
+ * @type {string}
18
+ */
19
+ let extensionURL = 'https://champierre.github.io/ml2scratch/ml2scratch.mjs';
20
+
21
+ const HAT_TIMEOUT = 100;
22
+
23
+ const blockIconURI = 'data:image/png;base64,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';
24
+
25
+ const Message = {
26
+ train_label_1: {
27
+ 'ja': 'ラベル1を学習する',
28
+ 'ja-Hira': 'ラベル1をがくしゅうする',
29
+ 'en': 'train label 1',
30
+ 'zh-cn': '学习标签1',
31
+ 'zh-tw': '學習標籤1'
32
+ },
33
+ train_label_2: {
34
+ 'ja': 'ラベル2を学習する',
35
+ 'ja-Hira': 'ラベル2をがくしゅうする',
36
+ 'en': 'train label 2',
37
+ 'zh-cn': '学习标签2',
38
+ 'zh-tw': '學習標籤2'
39
+ },
40
+ train_label_3: {
41
+ 'ja': 'ラベル3を学習する',
42
+ 'ja-Hira': 'ラベル3をがくしゅうする',
43
+ 'en': 'train label 3',
44
+ 'zh-cn': '学习标签3',
45
+ 'zh-tw': '學習標籤3'
46
+ },
47
+ train: {
48
+ 'ja': 'ラベル[LABEL]を学習する',
49
+ 'ja-Hira': 'ラベル[LABEL]をがくしゅうする',
50
+ 'en': 'train label [LABEL]',
51
+ 'zh-cn': '学习标签[LABEL]',
52
+ 'zh-tw': '學習標籤[LABEL]'
53
+ },
54
+ when_received_block: {
55
+ 'ja': 'ラベル[LABEL]を受け取ったとき',
56
+ 'ja-Hira': 'ラベル[LABEL]をうけとったとき',
57
+ 'en': 'when received label:[LABEL]',
58
+ 'zh-cn': '接收到类别[LABEL]时',
59
+ 'zh-tw': '接收到類別[LABEL]時'
60
+ },
61
+ label_block: {
62
+ 'ja': 'ラベル',
63
+ 'ja-Hira': 'ラベル',
64
+ 'en': 'label',
65
+ 'zh-cn': '标签',
66
+ 'zh-tw': '標籤'
67
+ },
68
+ counts_label_1: {
69
+ 'ja': 'ラベル1の枚数',
70
+ 'ja-Hira': 'ラベル1のまいすう',
71
+ 'en': 'counts of label 1',
72
+ 'zh-cn': '标签数量1',
73
+ 'zh-tw': '標籤數量1'
74
+ },
75
+ counts_label_2: {
76
+ 'ja': 'ラベル2の枚数',
77
+ 'ja-Hira': 'ラベル2のまいすう',
78
+ 'en': 'counts of label 2',
79
+ 'zh-cn': '标签数量2',
80
+ 'zh-tw': '標籤數量2'
81
+ },
82
+ counts_label_3: {
83
+ 'ja': 'ラベル3の枚数',
84
+ 'ja-Hira': 'ラベル3のまいすう',
85
+ 'en': 'counts of label 3',
86
+ 'zh-cn': '标签数量3',
87
+ 'zh-tw': '標籤數量3'
88
+ },
89
+ counts_label_4: {
90
+ 'ja': 'ラベル4の枚数',
91
+ 'ja-Hira': 'ラベル4のまいすう',
92
+ 'en': 'counts of label 4',
93
+ 'zh-cn': '标签数量4',
94
+ 'zh-tw': '標籤數量4'
95
+ },
96
+ counts_label_5: {
97
+ 'ja': 'ラベル5の枚数',
98
+ 'ja-Hira': 'ラベル5のまいすう',
99
+ 'en': 'counts of label 5',
100
+ 'zh-cn': '标签数量5',
101
+ 'zh-tw': '標籤數量5'
102
+ },
103
+ counts_label_6: {
104
+ 'ja': 'ラベル6の枚数',
105
+ 'ja-Hira': 'ラベル6のまいすう',
106
+ 'en': 'counts of label 6',
107
+ 'zh-cn': '标签数量6',
108
+ 'zh-tw': '標籤數量6'
109
+ },
110
+ counts_label_7: {
111
+ 'ja': 'ラベル7の枚数',
112
+ 'ja-Hira': 'ラベル7のまいすう',
113
+ 'en': 'counts of label 7',
114
+ 'zh-cn': '标签数量7',
115
+ 'zh-tw': '標籤數量7'
116
+ },
117
+ counts_label_8: {
118
+ 'ja': 'ラベル8の枚数',
119
+ 'ja-Hira': 'ラベル8のまいすう',
120
+ 'en': 'counts of label 8',
121
+ 'zh-cn': '标签数量8',
122
+ 'zh-tw': '標籤數量8'
123
+ },
124
+ counts_label_9: {
125
+ 'ja': 'ラベル9の枚数',
126
+ 'ja-Hira': 'ラベル9のまいすう',
127
+ 'en': 'counts of label 9',
128
+ 'zh-cn': '标签数量9',
129
+ 'zh-tw': '標籤數量9'
130
+ },
131
+ counts_label_10: {
132
+ 'ja': 'ラベル10の枚数',
133
+ 'ja-Hira': 'ラベル10のまいすう',
134
+ 'en': 'counts of label 10',
135
+ 'zh-cn': '标签数量10',
136
+ 'zh-tw': '標籤數量10'
137
+ },
138
+ counts_label: {
139
+ 'ja': 'ラベル[LABEL]の枚数',
140
+ 'ja-Hira': 'ラベル[LABEL]のまいすう',
141
+ 'en': 'counts of label [LABEL]',
142
+ 'zh-cn': '标签数量[LABEL]',
143
+ 'zh-tw': '標籤數量[LABEL]'
144
+ },
145
+ any: {
146
+ 'ja': 'のどれか',
147
+ 'ja-Hira': 'のどれか',
148
+ 'en': 'any',
149
+ 'zh-cn': '任何',
150
+ 'zh-tw': '任何'
151
+ },
152
+ all: {
153
+ 'ja': 'の全て',
154
+ 'ja-Hira': 'のすべて',
155
+ 'en': 'all',
156
+ 'zh-cn': '所有',
157
+ 'zh-tw': '所有量'
158
+ },
159
+ reset: {
160
+ 'ja': 'ラベル[LABEL]の学習をリセット',
161
+ 'ja-Hira': 'ラベル[LABEL]のがくしゅうをリセット',
162
+ 'en': 'reset label:[LABEL]',
163
+ 'zh-cn': '重置[LABEL]',
164
+ 'zh-tw': '重置[LABEL]'
165
+ },
166
+ download_learning_data: {
167
+ 'ja': '学習データをダウンロード',
168
+ 'ja-Hira': 'がくしゅうデータをダウンロード',
169
+ 'en': 'download learning data',
170
+ 'zh-cn': '下载学习数据',
171
+ 'zh-tw': '下載學習資料'
172
+ },
173
+ upload_learning_data: {
174
+ 'ja': '学習データをアップロード',
175
+ 'ja-Hira': 'がくしゅうデータをアップロード',
176
+ 'en': 'upload learning data',
177
+ 'zh-cn': '上传学习数据',
178
+ 'zh-tw': '上傳學習資料'
179
+ },
180
+ upload: {
181
+ 'ja': 'アップロード',
182
+ 'ja-Hira': 'アップロード',
183
+ 'en': 'upload',
184
+ 'zh-cn': '上传',
185
+ 'zh-tw': '上傳'
186
+ },
187
+ uploaded: {
188
+ 'ja': 'アップロードが完了しました。',
189
+ 'ja-Hira': 'アップロードがかんりょうしました。',
190
+ 'en': 'The upload is complete.',
191
+ 'zh-cn': '上传完成。',
192
+ 'zh-tw': '上傳完成。'
193
+ },
194
+ upload_instruction: {
195
+ 'ja': 'ファイルを選び、アップロードボタンをクリックして下さい。',
196
+ 'ja-Hira': 'ファイルをえらび、アップロードボタンをクリックしてください。',
197
+ 'en': 'Select a file and click the upload button.',
198
+ 'zh-cn': '选择一个文件,然后单击上传按钮。',
199
+ 'zh-tw': '選擇一個檔案,然後點擊上傳按鈕'
200
+ },
201
+ confirm_reset: {
202
+ 'ja': '本当にリセットしてもよろしいですか?',
203
+ 'ja-Hira': 'ほんとうにリセットしてもよろしいですか?',
204
+ 'en': 'Are you sure to reset?',
205
+ 'zh-cn': '你确定要重置吗?',
206
+ 'zh-tw': '您確定要重置嗎?'
207
+ },
208
+ toggle_classification: {
209
+ 'ja': 'ラベル付けを[CLASSIFICATION_STATE]にする',
210
+ 'ja-Hira': 'ラベルづけを[CLASSIFICATION_STATE]にする',
211
+ 'en': 'turn classification [CLASSIFICATION_STATE]',
212
+ 'zh-cn': '[CLASSIFICATION_STATE]分类',
213
+ 'zh-tw': '[CLASSIFICATION_STATE]分類'
214
+ },
215
+ set_classification_interval: {
216
+ 'ja': 'ラベル付けを[CLASSIFICATION_INTERVAL]秒間に1回行う',
217
+ 'ja-Hira': 'ラベルづけを[CLASSIFICATION_INTERVAL]びょうかんに1かいおこなう',
218
+ 'en': 'Label once every [CLASSIFICATION_INTERVAL] seconds',
219
+ 'zh-cn': '每隔[CLASSIFICATION_INTERVAL]秒标记一次',
220
+ 'zh-tw': '每隔[CLASSIFICATION_INTERVAL]秒標記一次'
221
+ },
222
+ video_toggle: {
223
+ 'ja': 'ビデオを[VIDEO_STATE]にする',
224
+ 'ja-Hira': 'ビデオを[VIDEO_STATE]にする',
225
+ 'en': 'turn video [VIDEO_STATE]',
226
+ 'zh-cn': '[VIDEO_STATE]摄像头',
227
+ 'zh-tw': '視訊設為[VIDEO_STATE]'
228
+ },
229
+ set_input: {
230
+ 'ja': '[INPUT]の画像を学習/判定する',
231
+ 'ja-Hira': '[INPUT]のがぞうをがくしゅう/はんていする',
232
+ 'en': 'Learn/Classify [INPUT] image',
233
+ 'zh-cn': '学习/分类[INPUT]图像',
234
+ 'zh-tw': '學習/分類[INPUT]���像'
235
+ },
236
+ on: {
237
+ 'ja': '入',
238
+ 'ja-Hira': 'いり',
239
+ 'en': 'on',
240
+ 'zh-cn': '开启',
241
+ 'zh-tw': '開啟'
242
+ },
243
+ off: {
244
+ 'ja': '切',
245
+ 'ja-Hira': 'きり',
246
+ 'en': 'off',
247
+ 'zh-cn': '关闭',
248
+ 'zh-tw': '關閉'
249
+ },
250
+ video_on_flipped: {
251
+ 'ja': '左右反転',
252
+ 'ja-Hira': 'さゆうはんてん',
253
+ 'en': 'on flipped',
254
+ 'zh-cn': '镜像开启',
255
+ 'zh-tw': '翻轉'
256
+ },
257
+ webcam: {
258
+ 'ja': 'カメラ',
259
+ 'ja-Hira': 'カメラ',
260
+ 'en': 'webcam',
261
+ 'zh-cn': '网络摄像头',
262
+ 'zh-tw': '網路攝影機'
263
+ },
264
+ stage: {
265
+ 'ja': 'ステージ',
266
+ 'ja-Hira': 'ステージ',
267
+ 'en': 'stage',
268
+ 'zh-cn': '舞台',
269
+ 'zh-tw': '舞台'
270
+ },
271
+ first_training_warning: {
272
+ 'ja': '最初の学習にはしばらく時間がかかるので、何度もクリックしないで下さい。',
273
+ 'ja-Hira': 'さいしょのがくしゅうにはしばらくじかんがかかるので、なんどもクリックしないでください。',
274
+ 'en': 'The first training will take a while, so do not click again and again.',
275
+ 'zh-cn': '第一项研究需要一段时间,所以不要一次又一次地点击。',
276
+ 'zh-tw': '第一次訓練需要一段時間,請稍後,不要一直點擊。'
277
+ },
278
+ switch_webcam: {
279
+ 'ja': 'カメラを[DEVICE]に切り替える',
280
+ 'ja-Hira': 'カメラを[DEVICE]にきりかえる',
281
+ 'en': 'switch webcam to [DEVICE]',
282
+ 'zh-cn': '网络摄像头切换到[DEVICE]',
283
+ 'zh-tw': '網路攝影機切換到[DEVICE]'
284
+ }
285
+ }
286
+
287
+ const AvailableLocales = ['en', 'ja', 'ja-Hira', 'zh-cn', 'zh-tw'];
288
+
289
+ class Scratch3ML2ScratchBlocks {
290
+
291
+ /**
292
+ * @return {string} - the name of this extension.
293
+ */
294
+ static get EXTENSION_NAME() {
295
+ return 'ML2Scratch';
296
+ }
297
+
298
+ /**
299
+ * @return {string} - the ID of this extension.
300
+ */
301
+ static get EXTENSION_ID() {
302
+ return 'ml2scratch';
303
+ }
304
+
305
+ /**
306
+ * URL to get this extension.
307
+ * @type {string}
308
+ */
309
+ static get extensionURL() {
310
+ return extensionURL;
311
+ }
312
+
313
+ /**
314
+ * Set URL to get this extension.
315
+ * extensionURL will be reset when the module is loaded from the web.
316
+ * @param {string} url - URL
317
+ */
318
+ static set extensionURL(url) {
319
+ extensionURL = url;
320
+ }
321
+
322
+ constructor(runtime) {
323
+ this.runtime = runtime;
324
+ if (runtime.formatMessage) {
325
+ // Replace 'formatMessage' to a formatter which is used in the runtime.
326
+ formatMessage = runtime.formatMessage;
327
+ }
328
+
329
+ this.when_received = false;
330
+ this.when_received_arr = Array(8).fill(false);
331
+ this.label = null;
332
+ this.locale = this.setLocale();
333
+
334
+ this.blockClickedAt = null;
335
+
336
+ this.counts = null;
337
+ this.firstTraining = true;
338
+
339
+ this.interval = 1000;
340
+ this.globalVideoTransparency = 0;
341
+ this.setVideoTransparency({
342
+ TRANSPARENCY: this.globalVideoTransparency
343
+ });
344
+
345
+ this.canvas = document.querySelector('canvas');
346
+
347
+ this.runtime.ioDevices.video.enableVideo().then(() => { this.input = this.runtime.ioDevices.video.provider.video });
348
+
349
+ this.knnClassifier = ml5.KNNClassifier();
350
+ this.featureExtractor = ml5.featureExtractor('MobileNet', () => {
351
+ console.log('[featureExtractor] Model Loaded!');
352
+ this.timer = setInterval(() => {
353
+ this.classify();
354
+ }, this.interval);
355
+ });
356
+
357
+ this.devices = [{ text: 'default', value: '' }];
358
+
359
+ const dialog = document.createElement("DIALOG");
360
+ dialog.id = "upload-dialog";
361
+ dialog.innerHTML = `
362
+ <html><body>
363
+ <div>${Message.upload_instruction[this.locale]}</p><input type="file" id="upload-files"><input type="button" value="${Message.upload[this.locale]}" id="upload-button"></div>
364
+ <div style="margin-top:10px;display:flex;justify-content:flex-end;"><button id="close" aria-label="close" formnovalidate>閉じる</button></div>
365
+ </body><body>
366
+ `;
367
+ this.uploadDialog = dialog;
368
+ document.body.appendChild(dialog);
369
+
370
+
371
+ document.getElementById("upload-button").onclick = () =>{
372
+ this.uploadButtonClicked();
373
+ }
374
+
375
+ document.getElementById("close").onclick = () =>{
376
+ dialog.close();
377
+ }
378
+
379
+ try {
380
+ navigator.mediaDevices.enumerateDevices().then(media => {
381
+ for (const device of media) {
382
+ if (device.kind === 'videoinput') {
383
+ this.devices.push({
384
+ text: device.label,
385
+ value: device.deviceId
386
+ });
387
+ }
388
+ }
389
+ });
390
+ } catch (e) {
391
+ console.error("failed to load media devices!");
392
+ }
393
+ }
394
+
395
+ getInfo() {
396
+ this.locale = this.setLocale();
397
+
398
+ return {
399
+ id: Scratch3ML2ScratchBlocks.EXTENSION_ID,
400
+ name: Scratch3ML2ScratchBlocks.EXTENSION_NAME,
401
+ extensionURL: Scratch3ML2ScratchBlocks.extensionURL,
402
+ blockIconURI: blockIconURI,
403
+ blocks: [
404
+ {
405
+ opcode: 'addExample1',
406
+ blockType: BlockType.COMMAND,
407
+ text: Message.train_label_1[this.locale]
408
+ },
409
+ {
410
+ opcode: 'addExample2',
411
+ blockType: BlockType.COMMAND,
412
+ text: Message.train_label_2[this.locale]
413
+ },
414
+ {
415
+ opcode: 'addExample3',
416
+ blockType: BlockType.COMMAND,
417
+ text: Message.train_label_3[this.locale]
418
+ },
419
+ {
420
+ opcode: 'train',
421
+ text: Message.train[this.locale],
422
+ blockType: BlockType.COMMAND,
423
+ arguments: {
424
+ LABEL: {
425
+ type: ArgumentType.STRING,
426
+ menu: 'train_menu',
427
+ defaultValue: '4'
428
+ }
429
+ }
430
+ },
431
+ {
432
+ opcode: 'trainAny',
433
+ text: Message.train[this.locale],
434
+ blockType: BlockType.COMMAND,
435
+ arguments: {
436
+ LABEL: {
437
+ type: ArgumentType.STRING,
438
+ defaultValue: '11'
439
+ }
440
+ }
441
+ },
442
+ {
443
+ opcode: 'getLabel',
444
+ text: Message.label_block[this.locale],
445
+ blockType: BlockType.REPORTER
446
+ },
447
+ {
448
+ opcode: 'whenReceived',
449
+ text: Message.when_received_block[this.locale],
450
+ blockType: BlockType.HAT,
451
+ arguments: {
452
+ LABEL: {
453
+ type: ArgumentType.STRING,
454
+ menu: 'received_menu',
455
+ defaultValue: 'any'
456
+ }
457
+ }
458
+ },
459
+ {
460
+ opcode: 'whenReceivedAny',
461
+ text: Message.when_received_block[this.locale],
462
+ blockType: BlockType.HAT,
463
+ arguments: {
464
+ LABEL: {
465
+ type: ArgumentType.STRING,
466
+ defaultValue: '11'
467
+ }
468
+ }
469
+ },
470
+ {
471
+ opcode: 'getCountByLabel1',
472
+ text: Message.counts_label_1[this.locale],
473
+ blockType: BlockType.REPORTER
474
+ },
475
+ {
476
+ opcode: 'getCountByLabel2',
477
+ text: Message.counts_label_2[this.locale],
478
+ blockType: BlockType.REPORTER
479
+ },
480
+ {
481
+ opcode: 'getCountByLabel3',
482
+ text: Message.counts_label_3[this.locale],
483
+ blockType: BlockType.REPORTER
484
+ },
485
+ {
486
+ opcode: 'getCountByLabel4',
487
+ text: Message.counts_label_4[this.locale],
488
+ blockType: BlockType.REPORTER
489
+ },
490
+ {
491
+ opcode: 'getCountByLabel5',
492
+ text: Message.counts_label_5[this.locale],
493
+ blockType: BlockType.REPORTER
494
+ },
495
+ {
496
+ opcode: 'getCountByLabel6',
497
+ text: Message.counts_label_6[this.locale],
498
+ blockType: BlockType.REPORTER
499
+ },
500
+ {
501
+ opcode: 'getCountByLabel7',
502
+ text: Message.counts_label_7[this.locale],
503
+ blockType: BlockType.REPORTER
504
+ },
505
+ {
506
+ opcode: 'getCountByLabel8',
507
+ text: Message.counts_label_8[this.locale],
508
+ blockType: BlockType.REPORTER
509
+ },
510
+ {
511
+ opcode: 'getCountByLabel9',
512
+ text: Message.counts_label_9[this.locale],
513
+ blockType: BlockType.REPORTER
514
+ },
515
+ {
516
+ opcode: 'getCountByLabel10',
517
+ text: Message.counts_label_10[this.locale],
518
+ blockType: BlockType.REPORTER
519
+ },
520
+ {
521
+ opcode: 'getCountByLabel',
522
+ text: Message.counts_label[this.locale],
523
+ blockType: BlockType.REPORTER,
524
+ arguments: {
525
+ LABEL: {
526
+ type: ArgumentType.STRING,
527
+ defaultValue: '11'
528
+ }
529
+ }
530
+ },
531
+ {
532
+ opcode: 'reset',
533
+ blockType: BlockType.COMMAND,
534
+ text: Message.reset[this.locale],
535
+ arguments: {
536
+ LABEL: {
537
+ type: ArgumentType.STRING,
538
+ menu: 'reset_menu',
539
+ defaultValue: 'all'
540
+ }
541
+ }
542
+ },
543
+ {
544
+ opcode: 'resetAny',
545
+ blockType: BlockType.COMMAND,
546
+ text: Message.reset[this.locale],
547
+ arguments: {
548
+ LABEL: {
549
+ type: ArgumentType.STRING,
550
+ defaultValue: '11'
551
+ }
552
+ }
553
+ },
554
+ {
555
+ opcode: 'download',
556
+ text: Message.download_learning_data[this.locale],
557
+ blockType: BlockType.COMMAND
558
+ },
559
+ {
560
+ opcode: 'upload',
561
+ text: Message.upload_learning_data[this.locale],
562
+ blockType: BlockType.COMMAND
563
+ },
564
+ {
565
+ opcode: 'toggleClassification',
566
+ text: Message.toggle_classification[this.locale],
567
+ blockType: BlockType.COMMAND,
568
+ arguments: {
569
+ CLASSIFICATION_STATE: {
570
+ type: ArgumentType.STRING,
571
+ menu: 'classification_menu',
572
+ defaultValue: 'off'
573
+ }
574
+ }
575
+ },
576
+ {
577
+ opcode: 'setClassificationInterval',
578
+ text: Message.set_classification_interval[this.locale],
579
+ blockType: BlockType.COMMAND,
580
+ arguments: {
581
+ CLASSIFICATION_INTERVAL: {
582
+ type: ArgumentType.STRING,
583
+ menu: 'classification_interval_menu',
584
+ defaultValue: '1'
585
+ }
586
+ }
587
+ },
588
+ {
589
+ opcode: 'videoToggle',
590
+ text: Message.video_toggle[this.locale],
591
+ blockType: BlockType.COMMAND,
592
+ arguments: {
593
+ VIDEO_STATE: {
594
+ type: ArgumentType.STRING,
595
+ menu: 'video_menu',
596
+ defaultValue: 'off'
597
+ }
598
+ }
599
+ },
600
+ {
601
+ opcode: 'setVideoTransparency',
602
+ text: formatMessage({
603
+ id: 'videoSensing.setVideoTransparency',
604
+ default: 'set video transparency to [TRANSPARENCY]',
605
+ description: 'Controls transparency of the video preview layer'
606
+ }),
607
+ arguments: {
608
+ TRANSPARENCY: {
609
+ type: ArgumentType.NUMBER,
610
+ defaultValue: 50
611
+ }
612
+ }
613
+ },
614
+ {
615
+ opcode: 'setInput',
616
+ text: Message.set_input[this.locale],
617
+ blockType: BlockType.COMMAND,
618
+ arguments: {
619
+ INPUT: {
620
+ type: ArgumentType.STRING,
621
+ menu: 'input_menu',
622
+ defaultValue: 'webcam'
623
+ }
624
+ }
625
+ },
626
+ {
627
+ opcode: 'switchCamera',
628
+ blockType: BlockType.COMMAND,
629
+ text: Message.switch_webcam[this.locale],
630
+ arguments: {
631
+ DEVICE: {
632
+ type: ArgumentType.STRING,
633
+ defaultValue: '',
634
+ menu: 'mediadevices'
635
+ }
636
+ }
637
+ }
638
+
639
+ ],
640
+ menus: {
641
+ received_menu: {
642
+ items: this.getMenu('received')
643
+ },
644
+ reset_menu: {
645
+ items: this.getMenu('reset')
646
+ },
647
+ train_menu: {
648
+ items: this.getTrainMenu()
649
+ },
650
+ count_menu: {
651
+ items: this.getTrainMenu()
652
+ },
653
+ video_menu: this.getVideoMenu(),
654
+ classification_interval_menu: {
655
+ acceptReporters: true,
656
+ items: this.getClassificationIntervalMenu()
657
+ },
658
+ classification_menu: this.getClassificationMenu(),
659
+ input_menu: this.getInputMenu(),
660
+ mediadevices: {
661
+ acceptReporters: true,
662
+ items: 'getDevices'
663
+ }
664
+ }
665
+ };
666
+ }
667
+
668
+ /**
669
+ * The transparency setting of the video preview stored in a value
670
+ * accessible by any object connected to the virtual machine.
671
+ * @type {number}
672
+ */
673
+ get globalVideoTransparency() {
674
+ const stage = this.runtime.getTargetForStage();
675
+ if (stage) {
676
+ return stage.videoTransparency;
677
+ }
678
+ return 50;
679
+ }
680
+
681
+ set globalVideoTransparency(transparency) {
682
+ const stage = this.runtime.getTargetForStage();
683
+ if (stage) {
684
+ stage.videoTransparency = transparency;
685
+ }
686
+ return transparency;
687
+ }
688
+
689
+ addExample1() {
690
+ this.firstTrainingWarning();
691
+ let features = this.featureExtractor.infer(this.input);
692
+ this.knnClassifier.addExample(features, '1');
693
+ this.updateCounts();
694
+ }
695
+
696
+ addExample2() {
697
+ this.firstTrainingWarning();
698
+ let features = this.featureExtractor.infer(this.input);
699
+ this.knnClassifier.addExample(features, '2');
700
+ this.updateCounts();
701
+ }
702
+
703
+ addExample3() {
704
+ this.firstTrainingWarning();
705
+ let features = this.featureExtractor.infer(this.input);
706
+ this.knnClassifier.addExample(features, '3');
707
+ this.updateCounts();
708
+ }
709
+
710
+ train(args) {
711
+ this.firstTrainingWarning();
712
+ let features = this.featureExtractor.infer(this.input);
713
+ this.knnClassifier.addExample(features, args.LABEL);
714
+ this.updateCounts();
715
+ }
716
+
717
+ trainAny(args) {
718
+ this.train(args);
719
+ }
720
+
721
+ getLabel() {
722
+ return this.label;
723
+ }
724
+
725
+ whenReceived(args) {
726
+ if (args.LABEL === 'any') {
727
+ if (this.when_received) {
728
+ setTimeout(() => {
729
+ this.when_received = false;
730
+ }, HAT_TIMEOUT);
731
+ return true;
732
+ }
733
+ return false;
734
+ } else {
735
+ if (this.when_received_arr[args.LABEL]) {
736
+ setTimeout(() => {
737
+ this.when_received_arr[args.LABEL] = false;
738
+ }, HAT_TIMEOUT);
739
+ return true;
740
+ }
741
+ return false;
742
+ }
743
+ }
744
+
745
+ whenReceivedAny(args) {
746
+ return this.whenReceived(args);
747
+ }
748
+
749
+ getCountByLabel1() {
750
+ if (this.counts) {
751
+ return this.counts['1'];
752
+ } else {
753
+ return 0;
754
+ }
755
+ }
756
+
757
+ getCountByLabel2() {
758
+ if (this.counts) {
759
+ return this.counts['2'];
760
+ } else {
761
+ return 0;
762
+ }
763
+ }
764
+
765
+ getCountByLabel3() {
766
+ if (this.counts) {
767
+ return this.counts['3'];
768
+ } else {
769
+ return 0;
770
+ }
771
+ }
772
+
773
+ getCountByLabel4() {
774
+ if (this.counts) {
775
+ return this.counts['4'];
776
+ } else {
777
+ return 0;
778
+ }
779
+ }
780
+
781
+ getCountByLabel5() {
782
+ if (this.counts) {
783
+ return this.counts['5'];
784
+ } else {
785
+ return 0;
786
+ }
787
+ }
788
+
789
+ getCountByLabel6() {
790
+ if (this.counts) {
791
+ return this.counts['6'];
792
+ } else {
793
+ return 0;
794
+ }
795
+ }
796
+
797
+ getCountByLabel7() {
798
+ if (this.counts) {
799
+ return this.counts['7'];
800
+ } else {
801
+ return 0;
802
+ }
803
+ }
804
+
805
+ getCountByLabel8() {
806
+ if (this.counts) {
807
+ return this.counts['8'];
808
+ } else {
809
+ return 0;
810
+ }
811
+ }
812
+
813
+ getCountByLabel9() {
814
+ if (this.counts) {
815
+ return this.counts['9'];
816
+ } else {
817
+ return 0;
818
+ }
819
+ }
820
+
821
+ getCountByLabel10() {
822
+ if (this.counts) {
823
+ return this.counts['10'];
824
+ } else {
825
+ return 0;
826
+ }
827
+ }
828
+
829
+ getCountByLabel(args) {
830
+ if (this.counts[args.LABEL]) {
831
+ return this.counts[args.LABEL];
832
+ } else {
833
+ return 0;
834
+ }
835
+ }
836
+
837
+ reset(args) {
838
+ if (this.actionRepeated()) { return };
839
+
840
+ setTimeout(() => {
841
+ let result = confirm(Message.confirm_reset[this.locale]);
842
+ if (result) {
843
+ if (args.LABEL == 'all') {
844
+ this.knnClassifier.clearAllLabels();
845
+ for (let label in this.counts) {
846
+ this.counts[label] = 0;
847
+ }
848
+ } else {
849
+ if (this.counts[args.LABEL] > 0) {
850
+ this.knnClassifier.clearLabel(args.LABEL);
851
+ this.counts[args.LABEL] = 0;
852
+ }
853
+ }
854
+ }
855
+ }, 1000);
856
+ }
857
+
858
+ resetAny(args) {
859
+ this.reset(args);
860
+ }
861
+
862
+ download() {
863
+ if (this.actionRepeated()) { return };
864
+ let fileName = String(Date.now());
865
+ this.knnClassifier.save(fileName);
866
+ }
867
+
868
+ upload() {
869
+ if (this.actionRepeated()) { return };
870
+
871
+ document.getElementById('upload-dialog').showModal();
872
+ }
873
+
874
+ toggleClassification(args) {
875
+ let state = args.CLASSIFICATION_STATE;
876
+ if (this.timer) {
877
+ clearTimeout(this.timer);
878
+ }
879
+ if (state === 'on') {
880
+ this.timer = setInterval(() => {
881
+ this.classify();
882
+ }, this.interval);
883
+ }
884
+ }
885
+
886
+ setClassificationInterval(args) {
887
+ if (this.timer) {
888
+ clearTimeout(this.timer);
889
+ }
890
+
891
+ this.interval = args.CLASSIFICATION_INTERVAL * 1000;
892
+ this.timer = setInterval(() => {
893
+ this.classify();
894
+ }, this.interval);
895
+ }
896
+
897
+ videoToggle(args) {
898
+ let state = args.VIDEO_STATE;
899
+ if (state === 'off') {
900
+ this.runtime.ioDevices.video.disableVideo();
901
+ } else {
902
+ this.runtime.ioDevices.video.enableVideo().then(() => { this.input = this.runtime.ioDevices.video.provider.video });
903
+ this.runtime.ioDevices.video.mirror = state === "on";
904
+ }
905
+ }
906
+
907
+ /**
908
+ * A scratch command block handle that configures the video preview's
909
+ * transparency from passed arguments.
910
+ * @param {object} args - the block arguments
911
+ * @param {number} args.TRANSPARENCY - the transparency to set the video
912
+ * preview to
913
+ */
914
+ setVideoTransparency(args) {
915
+ const transparency = Cast.toNumber(args.TRANSPARENCY);
916
+ this.globalVideoTransparency = transparency;
917
+ this.runtime.ioDevices.video.setPreviewGhost(transparency);
918
+ }
919
+
920
+ setInput(args) {
921
+ let input = args.INPUT;
922
+ if (input === 'webcam') {
923
+ this.input = this.runtime.ioDevices.video.provider.video;
924
+ } else {
925
+ this.input = this.canvas;
926
+ }
927
+ }
928
+
929
+ uploadButtonClicked() {
930
+ let files = document.getElementById('upload-files').files;
931
+
932
+ if (files.length <= 0) {
933
+ alert('Please select JSON file.');
934
+ return false;
935
+ }
936
+
937
+ let fr = new FileReader();
938
+
939
+ fr.onload = (e) => {
940
+ let data = JSON.parse(e.target.result);
941
+ this.knnClassifier.load(data, () => {
942
+ console.log('uploaded!');
943
+
944
+ this.updateCounts();
945
+ alert(Message.uploaded[this.locale]);
946
+ });
947
+ }
948
+
949
+ fr.onloadend = (e) => {
950
+ document.getElementById('upload-files').value = "";
951
+ }
952
+
953
+ fr.readAsText(files.item(0));
954
+ this.uploadDialog.close();
955
+ }
956
+
957
+ classify() {
958
+ let numLabels = this.knnClassifier.getNumLabels();
959
+ if (numLabels == 0) return;
960
+
961
+ let features = this.featureExtractor.infer(this.input);
962
+ this.knnClassifier.classify(features, (err, result) => {
963
+ if (err) {
964
+ console.error(err);
965
+ } else {
966
+ this.label = this.getTopConfidenceLabel(result.confidencesByLabel);
967
+ this.when_received = true;
968
+ this.when_received_arr[this.label] = true
969
+ }
970
+ });
971
+ }
972
+
973
+ getTopConfidenceLabel(confidences) {
974
+ let topConfidenceLabel;
975
+ let topConfidence = 0;
976
+
977
+ for (let label in confidences) {
978
+ if (confidences[label] > topConfidence) {
979
+ topConfidenceLabel = label;
980
+ }
981
+ }
982
+
983
+ return topConfidenceLabel;
984
+ }
985
+
986
+ updateCounts() {
987
+ this.counts = this.knnClassifier.getCountByLabel();
988
+ console.debug(this.counts);
989
+ }
990
+
991
+ actionRepeated() {
992
+ let currentTime = Date.now();
993
+ if (this.blockClickedAt && (this.blockClickedAt + 250) > currentTime) {
994
+ console.log('Please do not repeat trigerring this block.');
995
+ this.blockClickedAt = currentTime;
996
+ return true;
997
+ } else {
998
+ this.blockClickedAt = currentTime;
999
+ return false;
1000
+ }
1001
+ }
1002
+
1003
+ getMenu(name) {
1004
+ let arr = [];
1005
+ let defaultValue = 'any';
1006
+ let text = Message.any[this.locale];
1007
+ if (name == 'reset') {
1008
+ defaultValue = 'all';
1009
+ text = Message.all[this.locale];
1010
+ }
1011
+ arr.push({ text: text, value: defaultValue });
1012
+ for (let i = 1; i <= 10; i++) {
1013
+ let obj = {};
1014
+ obj.text = i.toString(10);
1015
+ obj.value = i.toString(10);
1016
+ arr.push(obj);
1017
+ };
1018
+ return arr;
1019
+ }
1020
+
1021
+ getTrainMenu() {
1022
+ let arr = [];
1023
+ for (let i = 4; i <= 10; i++) {
1024
+ let obj = {};
1025
+ obj.text = i.toString(10);
1026
+ obj.value = i.toString(10);
1027
+ arr.push(obj);
1028
+ };
1029
+ return arr;
1030
+ }
1031
+
1032
+ getVideoMenu() {
1033
+ return [
1034
+ {
1035
+ text: Message.off[this.locale],
1036
+ value: 'off'
1037
+ },
1038
+ {
1039
+ text: Message.on[this.locale],
1040
+ value: 'on'
1041
+ },
1042
+ {
1043
+ text: Message.video_on_flipped[this.locale],
1044
+ value: 'on-flipped'
1045
+ }
1046
+ ]
1047
+ }
1048
+
1049
+ getInputMenu() {
1050
+ return [
1051
+ {
1052
+ text: Message.webcam[this.locale],
1053
+ value: 'webcam'
1054
+ },
1055
+ {
1056
+ text: Message.stage[this.locale],
1057
+ value: 'stage'
1058
+ }
1059
+ ]
1060
+ }
1061
+
1062
+ getClassificationIntervalMenu() {
1063
+ return [
1064
+ {
1065
+ text: '1',
1066
+ value: '1'
1067
+ },
1068
+ {
1069
+ text: '0.5',
1070
+ value: '0.5'
1071
+ },
1072
+ {
1073
+ text: '0.2',
1074
+ value: '0.2'
1075
+ },
1076
+ {
1077
+ text: '0.1',
1078
+ value: '0.1'
1079
+ }
1080
+ ]
1081
+ }
1082
+
1083
+ getClassificationMenu() {
1084
+ return [
1085
+ {
1086
+ text: Message.off[this.locale],
1087
+ value: 'off'
1088
+ },
1089
+ {
1090
+ text: Message.on[this.locale],
1091
+ value: 'on'
1092
+ }
1093
+ ]
1094
+ }
1095
+
1096
+ firstTrainingWarning() {
1097
+ if (this.firstTraining) {
1098
+ alert(Message.first_training_warning[this.locale]);
1099
+ this.firstTraining = false;
1100
+ }
1101
+ }
1102
+
1103
+ setLocale() {
1104
+ let locale = formatMessage.setup().locale;
1105
+ if (AvailableLocales.includes(locale)) {
1106
+ return locale;
1107
+ } else {
1108
+ return 'en';
1109
+ }
1110
+ }
1111
+
1112
+ switchCamera(args) {
1113
+ if (args.DEVICE !== '') {
1114
+ if (this.runtime.ioDevices.video.provider._track !== null) {
1115
+ this.runtime.ioDevices.video.provider._track.stop();
1116
+ const deviceId = args.DEVICE;
1117
+ navigator.mediaDevices.getUserMedia({ audio: false, video: { deviceId } }).then(
1118
+ stream => {
1119
+ try {
1120
+ this.runtime.ioDevices.video.provider._video.srcObject = stream;
1121
+ } catch (error) {
1122
+ this.runtime.ioDevices.video.provider._video.src = window.URL.createObjectURL(stream);
1123
+ }
1124
+ // Needed for Safari/Firefox, Chrome auto-plays.
1125
+ this.runtime.ioDevices.video.provider._video.play();
1126
+ this.runtime.ioDevices.video.provider._track = stream.getTracks()[0];
1127
+ }
1128
+ );
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ getDevices() {
1134
+ return this.devices;
1135
+ }
1136
+ }
1137
+
1138
+ exports.blockClass = Scratch3ML2ScratchBlocks; // loadable-extension needs this line.
1139
+ module.exports = Scratch3ML2ScratchBlocks;