@kubohiroya/turbowarp-teachable-machine 3.2.1 → 3.4.0

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/dist/tm.js CHANGED
@@ -7,1493 +7,2045 @@
7
7
  (function (Scratch) {
8
8
  'use strict';
9
9
 
10
- const extensionName = "Teachable Machine";
11
- const blocks = /* @__PURE__ */ JSON.parse('[{"opcode":"versionReporter","blockType":"REPORTER","text":"Teachable Machine version","description":"Returns the extension version."},{"opcode":"setRecognitionMode","blockType":"COMMAND","text":"set recognition mode to [MODE]","description":"Selects whether the current Teachable Machine model recognizes poses, images, or audio.","arguments":{"MODE":{"type":"STRING","menu":"recognitionModeMenu","defaultValue":"pose"}}},{"opcode":"recognitionModeReporter","blockType":"REPORTER","text":"recognition mode","description":"Returns the current recognition mode."},{"opcode":"setComputeMode","blockType":"COMMAND","text":"set compute mode to [MODE]","description":"Selects the TensorFlow.js compute backend. \\"auto\\" negotiates WebGPU, WebGL, WASM, and CPU in that order and falls back automatically when a backend is unusable. Changing the mode releases a model that was loaded from a URL so it can be reloaded on the new backend.","arguments":{"MODE":{"type":"STRING","menu":"computeModeMenu","defaultValue":"auto"}}},{"opcode":"computeModeReporter","blockType":"REPORTER","text":"compute mode","description":"Returns the requested compute mode."},{"opcode":"computeBackendReporter","blockType":"REPORTER","text":"active compute backend","description":"Returns the TensorFlow.js backend recognition is actually running on, which can differ from the requested mode when the browser could not provide it."},{"opcode":"setModelURL","blockType":"COMMAND","text":"set model URL to [URL]","description":"Sets the Teachable Machine model URL.","arguments":{"URL":{"type":"STRING","defaultValue":"https://teachablemachine.withgoogle.com/models/XXXX/"}}},{"opcode":"startCamera","blockType":"COMMAND","text":"start camera","description":"Starts the camera and attaches the preview."},{"opcode":"stopCamera","blockType":"COMMAND","text":"stop camera","description":"Stops the camera and recognition loop."},{"opcode":"isCameraRunning","blockType":"BOOLEAN","text":"camera is running?","description":"Reports whether the camera is running."},{"opcode":"refreshCameraList","blockType":"COMMAND","text":"refresh camera list","description":"Refreshes the list of available video input devices."},{"opcode":"setCameraSelection","blockType":"COMMAND","text":"set camera to [CAMERA]","description":"Selects the default, front, back, or a detected camera and switches a running camera.","arguments":{"CAMERA":{"type":"STRING","menu":"cameraMenu","defaultValue":"default"}}},{"opcode":"cameraCountReporter","blockType":"REPORTER","text":"camera count","description":"Returns the number of video input devices found by the latest refresh."},{"opcode":"cameraDeviceIdReporter","blockType":"REPORTER","text":"camera device ID","description":"Returns the active camera device ID when available."},{"opcode":"cameraDeviceNameReporter","blockType":"REPORTER","text":"camera device name","description":"Returns the active camera device name when available."},{"opcode":"showPreview","blockType":"COMMAND","text":"show camera preview","description":"Shows the camera preview."},{"opcode":"hidePreview","blockType":"COMMAND","text":"hide camera preview","description":"Hides the camera preview."},{"opcode":"isPreviewVisible","blockType":"BOOLEAN","text":"camera preview is visible?","description":"Reports whether the preview is configured as visible."},{"opcode":"setPreviewOpacity","blockType":"COMMAND","text":"set camera preview opacity to [OPACITY]","description":"Sets preview opacity from 0 to 1.","arguments":{"OPACITY":{"type":"NUMBER","defaultValue":0.6}}},{"opcode":"setPreviewPosition","blockType":"COMMAND","text":"set camera preview position to [POSITION]","description":"Sets the preview position on the stage.","arguments":{"POSITION":{"type":"STRING","menu":"positionMenu","defaultValue":"bottom-right"}}},{"opcode":"setPreviewMirroring","blockType":"COMMAND","text":"set camera preview to [MIRRORING]","description":"Sets whether the preview is mirrored without changing the recognition input.","arguments":{"MIRRORING":{"type":"STRING","menu":"previewMirroringMenu","defaultValue":"mirrored"}}},{"opcode":"previewMirroringReporter","blockType":"REPORTER","text":"camera preview mirroring","description":"Returns mirrored or unmirrored for the current preview setting."},{"opcode":"setPoseOverlayVisibility","blockType":"COMMAND","text":"set pose overlay [VISIBILITY]","description":"Shows or hides the SVG pose overlay without stopping recognition.","featureFlag":"poseOverlay","arguments":{"VISIBILITY":{"type":"STRING","menu":"poseOverlayVisibilityMenu","defaultValue":"on"}}},{"opcode":"isPoseOverlayVisible","blockType":"BOOLEAN","text":"pose overlay is visible?","description":"Reports whether the SVG pose overlay is configured as visible.","featureFlag":"poseOverlay"},{"opcode":"setPoseJointStyle","blockType":"COMMAND","text":"set [PART] joint color [COLOR] opacity [OPACITY] radius [RADIUS]","description":"Sets the SVG circle style for one PoseNet joint.","featureFlag":"poseOverlay","arguments":{"PART":{"type":"STRING","menu":"poseKeypointMenu","defaultValue":"nose"},"COLOR":{"type":"STRING","defaultValue":"#00e5ff"},"OPACITY":{"type":"NUMBER","defaultValue":1},"RADIUS":{"type":"NUMBER","defaultValue":4}}},{"opcode":"setPoseBoneStyle","blockType":"COMMAND","text":"set pose bone color [COLOR] opacity [OPACITY] width [WIDTH]","description":"Sets the color, opacity, and line width for all SVG pose bones.","featureFlag":"poseOverlay","arguments":{"COLOR":{"type":"STRING","defaultValue":"#00e5ff"},"OPACITY":{"type":"NUMBER","defaultValue":0.9},"WIDTH":{"type":"NUMBER","defaultValue":3}}},{"opcode":"setPoseOverlayMinimumConfidence","blockType":"COMMAND","text":"set pose overlay minimum confidence to [CONFIDENCE]","description":"Hides joints and bones whose keypoint confidence is below the given value.","featureFlag":"poseOverlay","arguments":{"CONFIDENCE":{"type":"NUMBER","defaultValue":0.5}}},{"opcode":"setPoseConfidenceScaling","blockType":"COMMAND","text":"set pose [PROPERTY] confidence scaling [STATE]","description":"Scales the selected SVG style property from zero to its configured value using keypoint confidence.","featureFlag":"poseOverlay","arguments":{"PROPERTY":{"type":"STRING","menu":"poseConfidencePropertyMenu","defaultValue":"joint-opacity"},"STATE":{"type":"STRING","menu":"poseOverlayVisibilityMenu","defaultValue":"off"}}},{"opcode":"loadModel","blockType":"COMMAND","text":"load model","description":"Loads the configured Teachable Machine model."},{"opcode":"isModelLoaded","blockType":"BOOLEAN","text":"model is loaded?","description":"Reports whether the model is loaded."},{"opcode":"startRecognition","blockType":"COMMAND","text":"start recognition","description":"Starts recognition."},{"opcode":"stopRecognition","blockType":"COMMAND","text":"stop recognition","description":"Stops recognition."},{"opcode":"isRecognizing","blockType":"BOOLEAN","text":"recognition is running?","description":"Reports whether recognition is running."},{"opcode":"currentPoseReporter","blockType":"REPORTER","text":"current label","description":"Returns the highest-scoring label."},{"opcode":"scoreReporter","blockType":"REPORTER","text":"confidence","description":"Returns the confidence of the current label."},{"opcode":"poseScoreReporter","blockType":"REPORTER","text":"confidence of [NAME]","description":"Returns the confidence for a named label.","arguments":{"NAME":{"type":"STRING","defaultValue":"jump"}}},{"opcode":"setAccumulatedPoseParameters","blockType":"COMMAND","text":"set accumulated pose accumulation [ACCUMULATION] decay [DECAY]","description":"Sets the accumulation rate per second and the decay retained per second; decay changes apply to the next recognition session.","featureFlag":"temporalPoseScoring","arguments":{"ACCUMULATION":{"type":"NUMBER","defaultValue":1},"DECAY":{"type":"NUMBER","defaultValue":0.9}}},{"opcode":"setAccumulatedPoseThreshold","blockType":"COMMAND","text":"set accumulated pose threshold [THRESHOLD]","description":"Sets the minimum accumulated score required to report a pose; values below the threshold report an empty string.","featureFlag":"temporalPoseScoring","arguments":{"THRESHOLD":{"type":"NUMBER","defaultValue":0}}},{"opcode":"resetAccumulatedPose","blockType":"COMMAND","text":"reset accumulated pose scores","description":"Clears all accumulated pose scores.","featureFlag":"temporalPoseScoring"},{"opcode":"accumulatedPoseReporter","blockType":"REPORTER","text":"accumulated pose","description":"Returns the pose label whose accumulated score is highest and meets the threshold, or an empty string otherwise.","featureFlag":"temporalPoseScoring"},{"opcode":"accumulatedScoreReporter","blockType":"REPORTER","text":"accumulated score","description":"Returns the highest accumulated pose score without rounding.","featureFlag":"temporalPoseScoring"},{"opcode":"accumulatedPoseScoreReporter","blockType":"REPORTER","text":"accumulated score of [NAME]","description":"Returns the accumulated score for a named pose without rounding.","featureFlag":"temporalPoseScoring","arguments":{"NAME":{"type":"STRING","defaultValue":"jump"}}},{"opcode":"isPose","blockType":"BOOLEAN","text":"label is [NAME]?","description":"Reports whether the named label has at least 0.75 confidence.","arguments":{"NAME":{"type":"STRING","defaultValue":"jump"}}},{"opcode":"isPoseWithThreshold","blockType":"BOOLEAN","text":"label is [NAME] with confidence at least [THRESHOLD]?","description":"Reports whether the named label meets the given threshold.","arguments":{"NAME":{"type":"STRING","defaultValue":"jump"},"THRESHOLD":{"type":"NUMBER","defaultValue":0.75}}},{"opcode":"cameraMsReporter","blockType":"REPORTER","text":"camera startup time (ms)","description":"Returns camera startup time in milliseconds."},{"opcode":"modelLoadMsReporter","blockType":"REPORTER","text":"model load time (ms)","description":"Returns model load time in milliseconds."},{"opcode":"firstRecognitionMsReporter","blockType":"REPORTER","text":"first recognition time (ms)","description":"Returns first recognition time in milliseconds."},{"opcode":"lastErrorReporter","blockType":"REPORTER","text":"last error","description":"Returns the latest recorded error message."}]');
12
- const definitions = {
13
- extensionName,
14
- blocks
10
+ var block_definitions_default = {
11
+ extensionName: "Teachable Machine",
12
+ blocks: [
13
+ {
14
+ "opcode": "versionReporter",
15
+ "blockType": "REPORTER",
16
+ "text": "Teachable Machine version",
17
+ "description": "Returns the extension version."
18
+ },
19
+ {
20
+ "opcode": "setRecognitionMode",
21
+ "blockType": "COMMAND",
22
+ "text": "set recognition mode to [MODE]",
23
+ "description": "Selects whether the current Teachable Machine model recognizes poses, images, or audio.",
24
+ "arguments": { "MODE": {
25
+ "type": "STRING",
26
+ "menu": "recognitionModeMenu",
27
+ "defaultValue": "pose"
28
+ } }
29
+ },
30
+ {
31
+ "opcode": "recognitionModeReporter",
32
+ "blockType": "REPORTER",
33
+ "text": "recognition mode",
34
+ "description": "Returns the current recognition mode."
35
+ },
36
+ {
37
+ "opcode": "setComputeMode",
38
+ "blockType": "COMMAND",
39
+ "text": "set compute mode to [MODE]",
40
+ "description": "Selects the TensorFlow.js compute backend. \"auto\" negotiates WebGPU, WebGL, WASM, and CPU in that order and falls back automatically when a backend is unusable. Changing the mode releases a model that was loaded from a URL so it can be reloaded on the new backend.",
41
+ "arguments": { "MODE": {
42
+ "type": "STRING",
43
+ "menu": "computeModeMenu",
44
+ "defaultValue": "auto"
45
+ } }
46
+ },
47
+ {
48
+ "opcode": "computeModeReporter",
49
+ "blockType": "REPORTER",
50
+ "text": "compute mode",
51
+ "description": "Returns the requested compute mode."
52
+ },
53
+ {
54
+ "opcode": "computeBackendReporter",
55
+ "blockType": "REPORTER",
56
+ "text": "active compute backend",
57
+ "description": "Returns the TensorFlow.js backend recognition is actually running on, which can differ from the requested mode when the browser could not provide it."
58
+ },
59
+ {
60
+ "opcode": "setModelURL",
61
+ "blockType": "COMMAND",
62
+ "text": "set model URL to [URL]",
63
+ "description": "Sets the Teachable Machine model URL.",
64
+ "arguments": { "URL": {
65
+ "type": "STRING",
66
+ "defaultValue": "https://teachablemachine.withgoogle.com/models/XXXX/"
67
+ } }
68
+ },
69
+ {
70
+ "opcode": "startCamera",
71
+ "blockType": "COMMAND",
72
+ "text": "start camera",
73
+ "description": "Starts the camera and attaches the preview."
74
+ },
75
+ {
76
+ "opcode": "stopCamera",
77
+ "blockType": "COMMAND",
78
+ "text": "stop camera",
79
+ "description": "Stops the camera and recognition loop."
80
+ },
81
+ {
82
+ "opcode": "isCameraRunning",
83
+ "blockType": "BOOLEAN",
84
+ "text": "camera is running?",
85
+ "description": "Reports whether the camera is running."
86
+ },
87
+ {
88
+ "opcode": "refreshCameraList",
89
+ "blockType": "COMMAND",
90
+ "text": "refresh camera list",
91
+ "description": "Refreshes the list of available video input devices."
92
+ },
93
+ {
94
+ "opcode": "setCameraSelection",
95
+ "blockType": "COMMAND",
96
+ "text": "set camera to [CAMERA]",
97
+ "description": "Selects the default, front, back, or a detected camera and switches a running camera.",
98
+ "arguments": { "CAMERA": {
99
+ "type": "STRING",
100
+ "menu": "cameraMenu",
101
+ "defaultValue": "default"
102
+ } }
103
+ },
104
+ {
105
+ "opcode": "cameraCountReporter",
106
+ "blockType": "REPORTER",
107
+ "text": "camera count",
108
+ "description": "Returns the number of video input devices found by the latest refresh."
109
+ },
110
+ {
111
+ "opcode": "cameraDeviceIdReporter",
112
+ "blockType": "REPORTER",
113
+ "text": "camera device ID",
114
+ "description": "Returns the active camera device ID when available."
115
+ },
116
+ {
117
+ "opcode": "cameraDeviceNameReporter",
118
+ "blockType": "REPORTER",
119
+ "text": "camera device name",
120
+ "description": "Returns the active camera device name when available."
121
+ },
122
+ {
123
+ "opcode": "showPreview",
124
+ "blockType": "COMMAND",
125
+ "text": "show camera preview",
126
+ "description": "Shows the camera preview."
127
+ },
128
+ {
129
+ "opcode": "hidePreview",
130
+ "blockType": "COMMAND",
131
+ "text": "hide camera preview",
132
+ "description": "Hides the camera preview."
133
+ },
134
+ {
135
+ "opcode": "isPreviewVisible",
136
+ "blockType": "BOOLEAN",
137
+ "text": "camera preview is visible?",
138
+ "description": "Reports whether the preview is configured as visible."
139
+ },
140
+ {
141
+ "opcode": "setPreviewOpacity",
142
+ "blockType": "COMMAND",
143
+ "text": "set camera preview opacity to [OPACITY]",
144
+ "description": "Sets preview opacity from 0 to 1.",
145
+ "arguments": { "OPACITY": {
146
+ "type": "NUMBER",
147
+ "defaultValue": .6
148
+ } }
149
+ },
150
+ {
151
+ "opcode": "setPreviewPosition",
152
+ "blockType": "COMMAND",
153
+ "text": "set camera preview position to [POSITION]",
154
+ "description": "Sets the preview position on the stage.",
155
+ "arguments": { "POSITION": {
156
+ "type": "STRING",
157
+ "menu": "positionMenu",
158
+ "defaultValue": "bottom-right"
159
+ } }
160
+ },
161
+ {
162
+ "opcode": "setPreviewMirroring",
163
+ "blockType": "COMMAND",
164
+ "text": "set camera preview to [MIRRORING]",
165
+ "description": "Sets whether the preview is mirrored without changing the recognition input.",
166
+ "arguments": { "MIRRORING": {
167
+ "type": "STRING",
168
+ "menu": "previewMirroringMenu",
169
+ "defaultValue": "mirrored"
170
+ } }
171
+ },
172
+ {
173
+ "opcode": "previewMirroringReporter",
174
+ "blockType": "REPORTER",
175
+ "text": "camera preview mirroring",
176
+ "description": "Returns mirrored or unmirrored for the current preview setting."
177
+ },
178
+ {
179
+ "opcode": "setPoseOverlayVisibility",
180
+ "blockType": "COMMAND",
181
+ "text": "set pose overlay [VISIBILITY]",
182
+ "description": "Shows or hides the SVG pose overlay without stopping recognition.",
183
+ "featureFlag": "poseOverlay",
184
+ "arguments": { "VISIBILITY": {
185
+ "type": "STRING",
186
+ "menu": "poseOverlayVisibilityMenu",
187
+ "defaultValue": "on"
188
+ } }
189
+ },
190
+ {
191
+ "opcode": "isPoseOverlayVisible",
192
+ "blockType": "BOOLEAN",
193
+ "text": "pose overlay is visible?",
194
+ "description": "Reports whether the SVG pose overlay is configured as visible.",
195
+ "featureFlag": "poseOverlay"
196
+ },
197
+ {
198
+ "opcode": "setPoseJointStyle",
199
+ "blockType": "COMMAND",
200
+ "text": "set [PART] joint color [COLOR] opacity [OPACITY] radius [RADIUS]",
201
+ "description": "Sets the SVG circle style for one PoseNet joint.",
202
+ "featureFlag": "poseOverlay",
203
+ "arguments": {
204
+ "PART": {
205
+ "type": "STRING",
206
+ "menu": "poseKeypointMenu",
207
+ "defaultValue": "nose"
208
+ },
209
+ "COLOR": {
210
+ "type": "STRING",
211
+ "defaultValue": "#00e5ff"
212
+ },
213
+ "OPACITY": {
214
+ "type": "NUMBER",
215
+ "defaultValue": 1
216
+ },
217
+ "RADIUS": {
218
+ "type": "NUMBER",
219
+ "defaultValue": 4
220
+ }
221
+ }
222
+ },
223
+ {
224
+ "opcode": "setPoseBoneStyle",
225
+ "blockType": "COMMAND",
226
+ "text": "set pose bone color [COLOR] opacity [OPACITY] width [WIDTH]",
227
+ "description": "Sets the color, opacity, and line width for all SVG pose bones.",
228
+ "featureFlag": "poseOverlay",
229
+ "arguments": {
230
+ "COLOR": {
231
+ "type": "STRING",
232
+ "defaultValue": "#00e5ff"
233
+ },
234
+ "OPACITY": {
235
+ "type": "NUMBER",
236
+ "defaultValue": .9
237
+ },
238
+ "WIDTH": {
239
+ "type": "NUMBER",
240
+ "defaultValue": 3
241
+ }
242
+ }
243
+ },
244
+ {
245
+ "opcode": "setPoseOverlayMinimumConfidence",
246
+ "blockType": "COMMAND",
247
+ "text": "set pose overlay minimum confidence to [CONFIDENCE]",
248
+ "description": "Hides joints and bones whose keypoint confidence is below the given value.",
249
+ "featureFlag": "poseOverlay",
250
+ "arguments": { "CONFIDENCE": {
251
+ "type": "NUMBER",
252
+ "defaultValue": .5
253
+ } }
254
+ },
255
+ {
256
+ "opcode": "setPoseConfidenceScaling",
257
+ "blockType": "COMMAND",
258
+ "text": "set pose [PROPERTY] confidence scaling [STATE]",
259
+ "description": "Scales the selected SVG style property from zero to its configured value using keypoint confidence.",
260
+ "featureFlag": "poseOverlay",
261
+ "arguments": {
262
+ "PROPERTY": {
263
+ "type": "STRING",
264
+ "menu": "poseConfidencePropertyMenu",
265
+ "defaultValue": "joint-opacity"
266
+ },
267
+ "STATE": {
268
+ "type": "STRING",
269
+ "menu": "poseOverlayVisibilityMenu",
270
+ "defaultValue": "off"
271
+ }
272
+ }
273
+ },
274
+ {
275
+ "opcode": "loadModel",
276
+ "blockType": "COMMAND",
277
+ "text": "load model",
278
+ "description": "Loads the configured Teachable Machine model."
279
+ },
280
+ {
281
+ "opcode": "isModelLoaded",
282
+ "blockType": "BOOLEAN",
283
+ "text": "model is loaded?",
284
+ "description": "Reports whether the model is loaded."
285
+ },
286
+ {
287
+ "opcode": "startRecognition",
288
+ "blockType": "COMMAND",
289
+ "text": "start recognition",
290
+ "description": "Starts recognition."
291
+ },
292
+ {
293
+ "opcode": "stopRecognition",
294
+ "blockType": "COMMAND",
295
+ "text": "stop recognition",
296
+ "description": "Stops recognition."
297
+ },
298
+ {
299
+ "opcode": "isRecognizing",
300
+ "blockType": "BOOLEAN",
301
+ "text": "recognition is running?",
302
+ "description": "Reports whether recognition is running."
303
+ },
304
+ {
305
+ "opcode": "currentPoseReporter",
306
+ "blockType": "REPORTER",
307
+ "text": "current label",
308
+ "description": "Returns the highest-scoring label."
309
+ },
310
+ {
311
+ "opcode": "scoreReporter",
312
+ "blockType": "REPORTER",
313
+ "text": "confidence",
314
+ "description": "Returns the confidence of the current label."
315
+ },
316
+ {
317
+ "opcode": "poseScoreReporter",
318
+ "blockType": "REPORTER",
319
+ "text": "confidence of [NAME]",
320
+ "description": "Returns the confidence for a named label.",
321
+ "arguments": { "NAME": {
322
+ "type": "STRING",
323
+ "defaultValue": "jump"
324
+ } }
325
+ },
326
+ {
327
+ "opcode": "setAccumulatedPoseParameters",
328
+ "blockType": "COMMAND",
329
+ "text": "set accumulated pose accumulation [ACCUMULATION] decay [DECAY]",
330
+ "description": "Sets the accumulation rate per second and the decay retained per second; decay changes apply to the next recognition session.",
331
+ "featureFlag": "temporalPoseScoring",
332
+ "arguments": {
333
+ "ACCUMULATION": {
334
+ "type": "NUMBER",
335
+ "defaultValue": 1
336
+ },
337
+ "DECAY": {
338
+ "type": "NUMBER",
339
+ "defaultValue": .9
340
+ }
341
+ }
342
+ },
343
+ {
344
+ "opcode": "setAccumulatedPoseThreshold",
345
+ "blockType": "COMMAND",
346
+ "text": "set accumulated pose threshold [THRESHOLD]",
347
+ "description": "Sets the minimum accumulated score required to report a pose; values below the threshold report an empty string.",
348
+ "featureFlag": "temporalPoseScoring",
349
+ "arguments": { "THRESHOLD": {
350
+ "type": "NUMBER",
351
+ "defaultValue": 0
352
+ } }
353
+ },
354
+ {
355
+ "opcode": "resetAccumulatedPose",
356
+ "blockType": "COMMAND",
357
+ "text": "reset accumulated pose scores",
358
+ "description": "Clears all accumulated pose scores.",
359
+ "featureFlag": "temporalPoseScoring"
360
+ },
361
+ {
362
+ "opcode": "accumulatedPoseReporter",
363
+ "blockType": "REPORTER",
364
+ "text": "accumulated pose",
365
+ "description": "Returns the pose label whose accumulated score is highest and meets the threshold, or an empty string otherwise.",
366
+ "featureFlag": "temporalPoseScoring"
367
+ },
368
+ {
369
+ "opcode": "accumulatedScoreReporter",
370
+ "blockType": "REPORTER",
371
+ "text": "accumulated score",
372
+ "description": "Returns the highest accumulated pose score without rounding.",
373
+ "featureFlag": "temporalPoseScoring"
374
+ },
375
+ {
376
+ "opcode": "accumulatedPoseScoreReporter",
377
+ "blockType": "REPORTER",
378
+ "text": "accumulated score of [NAME]",
379
+ "description": "Returns the accumulated score for a named pose without rounding.",
380
+ "featureFlag": "temporalPoseScoring",
381
+ "arguments": { "NAME": {
382
+ "type": "STRING",
383
+ "defaultValue": "jump"
384
+ } }
385
+ },
386
+ {
387
+ "opcode": "isPose",
388
+ "blockType": "BOOLEAN",
389
+ "text": "label is [NAME]?",
390
+ "description": "Reports whether the named label has at least 0.75 confidence.",
391
+ "arguments": { "NAME": {
392
+ "type": "STRING",
393
+ "defaultValue": "jump"
394
+ } }
395
+ },
396
+ {
397
+ "opcode": "isPoseWithThreshold",
398
+ "blockType": "BOOLEAN",
399
+ "text": "label is [NAME] with confidence at least [THRESHOLD]?",
400
+ "description": "Reports whether the named label meets the given threshold.",
401
+ "arguments": {
402
+ "NAME": {
403
+ "type": "STRING",
404
+ "defaultValue": "jump"
405
+ },
406
+ "THRESHOLD": {
407
+ "type": "NUMBER",
408
+ "defaultValue": .75
409
+ }
410
+ }
411
+ },
412
+ {
413
+ "opcode": "cameraMsReporter",
414
+ "blockType": "REPORTER",
415
+ "text": "camera startup time (ms)",
416
+ "description": "Returns camera startup time in milliseconds."
417
+ },
418
+ {
419
+ "opcode": "modelLoadMsReporter",
420
+ "blockType": "REPORTER",
421
+ "text": "model load time (ms)",
422
+ "description": "Returns model load time in milliseconds."
423
+ },
424
+ {
425
+ "opcode": "firstRecognitionMsReporter",
426
+ "blockType": "REPORTER",
427
+ "text": "first recognition time (ms)",
428
+ "description": "Returns first recognition time in milliseconds."
429
+ },
430
+ {
431
+ "opcode": "lastErrorReporter",
432
+ "blockType": "REPORTER",
433
+ "text": "last error",
434
+ "description": "Returns the latest recorded error message."
435
+ }
436
+ ]
15
437
  };
16
- const COMPUTE_MODES = ["auto", "webgpu", "webgl", "wasm", "cpu"];
17
- const COMPUTE_MODE_ALIASES = Object.freeze({
18
- auto: "auto",
19
- automatic: "auto",
20
- default: "auto",
21
- fastest: "auto",
22
- 自動: "auto",
23
- おまかせ: "auto",
24
- webgpu: "webgpu",
25
- gpu: "webgpu",
26
- webgl: "webgl",
27
- wasm: "wasm",
28
- webassembly: "wasm",
29
- simd: "wasm",
30
- cpu: "cpu",
31
- javascript: "cpu"
438
+ //#endregion
439
+ //#region src/compute-backend.ts
440
+ /**
441
+ * Compute backend negotiation for the reviewed TensorFlow.js runtime.
442
+ *
443
+ * TensorFlow.js binds every tensor to the backend that was active when the
444
+ * tensor was created, so the backend has to be settled before a Teachable
445
+ * Machine model is loaded. This module keeps the ordering and fallback rules
446
+ * free of any TensorFlow.js or DOM dependency so they stay testable.
447
+ */
448
+ var COMPUTE_MODES = [
449
+ "auto",
450
+ "webgpu",
451
+ "webgl",
452
+ "wasm",
453
+ "cpu"
454
+ ];
455
+ Object.freeze([
456
+ "webgpu",
457
+ "webgl",
458
+ "wasm",
459
+ "cpu"
460
+ ]);
461
+ var COMPUTE_MODE_ALIASES = Object.freeze({
462
+ auto: "auto",
463
+ automatic: "auto",
464
+ default: "auto",
465
+ fastest: "auto",
466
+ 自動: "auto",
467
+ おまかせ: "auto",
468
+ webgpu: "webgpu",
469
+ gpu: "webgpu",
470
+ webgl: "webgl",
471
+ wasm: "wasm",
472
+ webassembly: "wasm",
473
+ simd: "wasm",
474
+ cpu: "cpu",
475
+ javascript: "cpu"
32
476
  });
33
477
  function normalizeComputeMode(value, fallback = "auto") {
34
- const text = String(value ?? "").trim().toLowerCase();
35
- if (text === "") return fallback;
36
- return COMPUTE_MODE_ALIASES[text] ?? fallback;
478
+ const text = String(value ?? "").trim().toLowerCase();
479
+ if (text === "") return fallback;
480
+ return COMPUTE_MODE_ALIASES[text] ?? fallback;
37
481
  }
38
- const FEATURE_FLAGS = {
39
- /**
40
- * Per-frame confidence flickers near a decision boundary, so the accumulated
41
- * score blocks — which integrate confidence over time and decay it — are the
42
- * built-in answer to a noisy recognition result. They are on by default; the
43
- * event bridge that republishes those changes to other extensions is not.
44
- */
45
- temporalPoseScoring: true,
46
- accumulatedPoseEvents: false,
47
- poseOverlay: true
482
+ //#endregion
483
+ //#region src/config/feature-flags.ts
484
+ var FEATURE_FLAGS = {
485
+ /**
486
+ * Per-frame confidence flickers near a decision boundary, so the accumulated
487
+ * score blocks — which integrate confidence over time and decay it — are the
488
+ * built-in answer to a noisy recognition result. They are on by default; the
489
+ * event bridge that republishes those changes to other extensions is not.
490
+ */
491
+ temporalPoseScoring: true,
492
+ accumulatedPoseEvents: false,
493
+ poseOverlay: true
48
494
  };
49
- const POSE_KEYPOINT_NAMES = [
50
- "nose",
51
- "leftEye",
52
- "rightEye",
53
- "leftEar",
54
- "rightEar",
55
- "leftShoulder",
56
- "rightShoulder",
57
- "leftElbow",
58
- "rightElbow",
59
- "leftWrist",
60
- "rightWrist",
61
- "leftHip",
62
- "rightHip",
63
- "leftKnee",
64
- "rightKnee",
65
- "leftAnkle",
66
- "rightAnkle"
495
+ //#endregion
496
+ //#region src/pose-overlay.ts
497
+ var POSE_KEYPOINT_NAMES = [
498
+ "nose",
499
+ "leftEye",
500
+ "rightEye",
501
+ "leftEar",
502
+ "rightEar",
503
+ "leftShoulder",
504
+ "rightShoulder",
505
+ "leftElbow",
506
+ "rightElbow",
507
+ "leftWrist",
508
+ "rightWrist",
509
+ "leftHip",
510
+ "rightHip",
511
+ "leftKnee",
512
+ "rightKnee",
513
+ "leftAnkle",
514
+ "rightAnkle"
67
515
  ];
68
- const POSE_BONE_CONNECTIONS = Object.freeze([
69
- ["leftHip", "leftShoulder"],
70
- ["leftElbow", "leftShoulder"],
71
- ["leftElbow", "leftWrist"],
72
- ["leftHip", "leftKnee"],
73
- ["leftKnee", "leftAnkle"],
74
- ["rightHip", "rightShoulder"],
75
- ["rightElbow", "rightShoulder"],
76
- ["rightElbow", "rightWrist"],
77
- ["rightHip", "rightKnee"],
78
- ["rightKnee", "rightAnkle"],
79
- ["leftShoulder", "rightShoulder"],
80
- ["leftHip", "rightHip"]
516
+ var POSE_BONE_CONNECTIONS = Object.freeze([
517
+ ["leftHip", "leftShoulder"],
518
+ ["leftElbow", "leftShoulder"],
519
+ ["leftElbow", "leftWrist"],
520
+ ["leftHip", "leftKnee"],
521
+ ["leftKnee", "leftAnkle"],
522
+ ["rightHip", "rightShoulder"],
523
+ ["rightElbow", "rightShoulder"],
524
+ ["rightElbow", "rightWrist"],
525
+ ["rightHip", "rightKnee"],
526
+ ["rightKnee", "rightAnkle"],
527
+ ["leftShoulder", "rightShoulder"],
528
+ ["leftHip", "rightHip"]
81
529
  ]);
82
- const DEFAULT_POSE_JOINT_STYLE = Object.freeze({
83
- color: "#00e5ff",
84
- opacity: 1,
85
- radius: 4
530
+ var DEFAULT_POSE_JOINT_STYLE = Object.freeze({
531
+ color: "#00e5ff",
532
+ opacity: 1,
533
+ radius: 4
86
534
  });
87
- const DEFAULT_POSE_BONE_STYLE = Object.freeze({
88
- color: "#00e5ff",
89
- opacity: 0.9,
90
- width: 3
535
+ var DEFAULT_POSE_BONE_STYLE = Object.freeze({
536
+ color: "#00e5ff",
537
+ opacity: .9,
538
+ width: 3
91
539
  });
92
- const DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING = Object.freeze({
93
- jointOpacity: false,
94
- jointRadius: false,
95
- boneOpacity: false,
96
- boneWidth: false
540
+ var DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING = Object.freeze({
541
+ jointOpacity: false,
542
+ jointRadius: false,
543
+ boneOpacity: false,
544
+ boneWidth: false
97
545
  });
98
546
  function isPoseKeypointName(value) {
99
- return POSE_KEYPOINT_NAMES.includes(value);
547
+ return POSE_KEYPOINT_NAMES.includes(value);
100
548
  }
101
549
  function confidenceMultiplier(value) {
102
- const confidence = Number(value);
103
- if (!Number.isFinite(confidence)) return 0;
104
- return Math.max(0, Math.min(1, confidence));
550
+ const confidence = Number(value);
551
+ if (!Number.isFinite(confidence)) return 0;
552
+ return Math.max(0, Math.min(1, confidence));
105
553
  }
106
- const name = "@kubohiroya/turbowarp-teachable-machine";
107
- const version = "3.2.1";
108
- const packageMetadata = {
109
- name,
110
- version
554
+ var package_default = {
555
+ name: "@kubohiroya/turbowarp-teachable-machine",
556
+ version: "3.4.0",
557
+ description: "A TurboWarp extension for recognition using Teachable Machine pose, image, and audio models.",
558
+ author: "Hiroya Kubo <hiroya@cuc.ac.jp>",
559
+ license: "MPL-2.0",
560
+ homepage: "https://github.com/kubohiroya/turbowarp-teachable-machine#readme",
561
+ repository: {
562
+ "type": "git",
563
+ "url": "git+https://github.com/kubohiroya/turbowarp-teachable-machine.git"
564
+ },
565
+ bugs: { "url": "https://github.com/kubohiroya/turbowarp-teachable-machine/issues" },
566
+ exports: {
567
+ "./composition": {
568
+ "types": "./dist/types/composition.d.ts",
569
+ "import": "./dist/composition.js"
570
+ },
571
+ "./posenet": {
572
+ "types": "./dist/types/posenet.d.ts",
573
+ "import": "./dist/posenet.js"
574
+ },
575
+ "./posenet-assets/*": "./dist/posenet/mobilenet-v1-075-stride16/*",
576
+ "./wasm-assets/*": "./dist/wasm/*",
577
+ "./dist/backend-wasm.js": "./dist/backend-wasm.js",
578
+ "./dist/backend-webgpu.js": "./dist/backend-webgpu.js",
579
+ "./runtime": "./dist/runtime.js",
580
+ "./dist/tm.js": "./dist/tm.js",
581
+ "./package.json": "./package.json"
582
+ },
583
+ type: "module",
584
+ files: [
585
+ "dist/",
586
+ "README.md",
587
+ "LICENSE",
588
+ "THIRD_PARTY_NOTICES.md"
589
+ ],
590
+ unpkg: "./dist/tm.js",
591
+ jsdelivr: "./dist/tm.js",
592
+ publishConfig: {
593
+ "access": "public",
594
+ "registry": "https://registry.npmjs.org/"
595
+ },
596
+ packageManager: "pnpm@11.11.0",
597
+ engines: { "node": ">=22.18.0" },
598
+ scripts: {
599
+ "dev": "vite build --watch",
600
+ "typecheck": "tsc --noEmit",
601
+ "build": "pnpm build:extension && pnpm build:composition && pnpm build:runtime && pnpm build:backends && pnpm build:posenet && pnpm build:notices && pnpm build:types",
602
+ "build:extension": "vite build",
603
+ "build:composition": "vite build --config vite.composition.config.ts",
604
+ "build:runtime": "vite build --config vite.runtime.config.ts",
605
+ "build:backends": "vite build --config vite.backend-wasm.config.ts && vite build --config vite.backend-webgpu.config.ts && node scripts/copy-wasm-assets.ts",
606
+ "build:posenet": "vite build --config vite.posenet.config.ts && node scripts/copy-posenet-assets.ts",
607
+ "build:notices": "node scripts/copy-third-party-notices.ts",
608
+ "build:types": "tsc --project tsconfig.build.json",
609
+ "types:composition:check": "tsc --project tsconfig.composition-consumer.json",
610
+ "types:posenet:check": "tsc --project tsconfig.posenet-consumer.json",
611
+ "test": "vitest run",
612
+ "docs": "node scripts/generate-readme.ts",
613
+ "docs:check": "npm run docs && git diff --exit-code -- README.md",
614
+ "docs:pages:check": "node scripts/check-pages.ts",
615
+ "release:consistency:check": "node scripts/check-release-consistency.ts",
616
+ "check:dist": "npm run build && git diff --exit-code -- dist",
617
+ "archive:check": "node scripts/check-github-archive.ts",
618
+ "pack:check": "npm pack --dry-run --ignore-scripts",
619
+ "release:check": "npm publish --dry-run --access public --tag latest",
620
+ "check": "npm run typecheck && npm run test && npm run build && npm run types:composition:check && npm run types:posenet:check && npm run docs:check && npm run docs:pages:check && npm run release:consistency:check && npm run check:dist && npm run archive:check && npm run pack:check",
621
+ "prepack": "npm run build && npm run release:consistency:check"
622
+ },
623
+ devDependencies: {
624
+ "@kubohiroya/turbowarp-camera-source": "0.13.0",
625
+ "@kubohiroya/vite-plugin-turbowarp-extension": "0.4.0",
626
+ "@teachablemachine/image": "0.8.5",
627
+ "@teachablemachine/pose": "0.8.6",
628
+ "@tensorflow-models/posenet": "2.2.2",
629
+ "@tensorflow-models/speech-commands": "0.5.4",
630
+ "@tensorflow/tfjs": "4.22.0",
631
+ "@tensorflow/tfjs-backend-wasm": "4.22.0",
632
+ "@tensorflow/tfjs-backend-webgpu": "4.22.0",
633
+ "@types/node": "^22.20.1",
634
+ "typescript": "^5.9.3",
635
+ "vite": "^8.2.2",
636
+ "vitest": "^4.0.14"
637
+ },
638
+ keywords: [
639
+ "turbowarp",
640
+ "scratch",
641
+ "extension",
642
+ "pose",
643
+ "image",
644
+ "audio",
645
+ "camera",
646
+ "teachable-machine"
647
+ ]
111
648
  };
112
- const EXTENSION_ID = "kubohiroyatm";
113
- const VERSION = `${packageMetadata.version}-typescript`;
114
- const DOCS_URI = "https://kubohiroya.github.io/turbowarp-teachable-machine/";
115
- const ACCUMULATED_POSE_CHANGED_EVENT = "TM_ACCUMULATED_POSE_CHANGED";
116
- const BLOCK_ICON_URI = `data:image/svg+xml,${encodeURIComponent(
117
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 21V8h13M43 8h13v13M8 43v13h13M43 56h13V43M32 25v15M20 31l12 5 12-5M32 40 23 52M32 40l9 12"/><circle cx="32" cy="18" r="5"/></g></svg>'
118
- )}`;
119
- const BROWSER_RUNTIME_URL = `https://cdn.jsdelivr.net/npm/${packageMetadata.name}@${packageMetadata.version}/dist/runtime.js`;
120
- const POSITION_ITEMS = [
121
- { text: "top left", value: "top-left" },
122
- { text: "top right", value: "top-right" },
123
- { text: "bottom left", value: "bottom-left" },
124
- { text: "bottom right", value: "bottom-right" },
125
- { text: "center", value: "center" },
126
- { text: "full stage", value: "full-stage" }
649
+ //#endregion
650
+ //#region src/extension.ts
651
+ /**
652
+ * Where Camera Source puts itself on the VM runtime.
653
+ *
654
+ * Declared here rather than imported as a value, because this file is bundled into a single-file
655
+ * extension and no dependency's code has ever been inlined into it. The key and the member read
656
+ * below are pinned against Camera Source's own published constant and narrowing function in the
657
+ * tests, which import both and are not shipped, so a rename upstream fails here rather than
658
+ * silently reading an absent extension as "not loaded".
659
+ */
660
+ var CAMERA_SOURCE_RUNTIME_KEY = "ext_kubohiroyacamerasource";
661
+ function readCameraSource(runtime) {
662
+ if (typeof runtime !== "object" || runtime === null) return void 0;
663
+ const candidate = runtime[CAMERA_SOURCE_RUNTIME_KEY];
664
+ if (typeof candidate !== "object" || candidate === null) return void 0;
665
+ const { acquireCamera } = candidate;
666
+ return typeof acquireCamera === "function" ? candidate : void 0;
667
+ }
668
+ var EXTENSION_ID = "kubohiroyatm";
669
+ var VERSION = `${package_default.version}-typescript`;
670
+ var DOCS_URI = "https://kubohiroya.github.io/turbowarp-teachable-machine/";
671
+ var ACCUMULATED_POSE_CHANGED_EVENT = "TM_ACCUMULATED_POSE_CHANGED";
672
+ var BLOCK_ICON_URI = `data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><g fill=\"none\" stroke=\"#fff\" stroke-width=\"5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 21V8h13M43 8h13v13M8 43v13h13M43 56h13V43M32 25v15M20 31l12 5 12-5M32 40 23 52M32 40l9 12\"/><circle cx=\"32\" cy=\"18\" r=\"5\"/></g></svg>")}`;
673
+ var BROWSER_RUNTIME_URL = `https://cdn.jsdelivr.net/npm/${package_default.name}@${package_default.version}/dist/runtime.js`;
674
+ var POSITION_ITEMS = [
675
+ {
676
+ text: "top left",
677
+ value: "top-left"
678
+ },
679
+ {
680
+ text: "top right",
681
+ value: "top-right"
682
+ },
683
+ {
684
+ text: "bottom left",
685
+ value: "bottom-left"
686
+ },
687
+ {
688
+ text: "bottom right",
689
+ value: "bottom-right"
690
+ },
691
+ {
692
+ text: "center",
693
+ value: "center"
694
+ },
695
+ {
696
+ text: "full stage",
697
+ value: "full-stage"
698
+ }
127
699
  ];
128
- const POSITION_ALIASES = {
129
- 左上: "top-left",
130
- 右上: "top-right",
131
- 左下: "bottom-left",
132
- 右下: "bottom-right",
133
- 中央: "center",
134
- ステージ全体: "full-stage",
135
- "top-left": "top-left",
136
- "top-right": "top-right",
137
- "bottom-left": "bottom-left",
138
- "bottom-right": "bottom-right",
139
- center: "center",
140
- "full-stage": "full-stage"
700
+ var POSITION_ALIASES = {
701
+ 左上: "top-left",
702
+ 右上: "top-right",
703
+ 左下: "bottom-left",
704
+ 右下: "bottom-right",
705
+ 中央: "center",
706
+ ステージ全体: "full-stage",
707
+ "top-left": "top-left",
708
+ "top-right": "top-right",
709
+ "bottom-left": "bottom-left",
710
+ "bottom-right": "bottom-right",
711
+ center: "center",
712
+ "full-stage": "full-stage"
141
713
  };
142
- const PREVIEW_MIRRORING_ITEMS = [
143
- { text: "mirrored", value: "mirrored" },
144
- { text: "unmirrored", value: "unmirrored" }
145
- ];
146
- const PREVIEW_MIRRORING_ALIASES = {
147
- mirrored: true,
148
- unmirrored: false,
149
- mirror: true,
150
- normal: false,
151
- "左右反転": true,
152
- "そのまま": false
714
+ var PREVIEW_MIRRORING_ITEMS = [{
715
+ text: "mirrored",
716
+ value: "mirrored"
717
+ }, {
718
+ text: "unmirrored",
719
+ value: "unmirrored"
720
+ }];
721
+ var PREVIEW_MIRRORING_ALIASES = {
722
+ mirrored: true,
723
+ unmirrored: false,
724
+ mirror: true,
725
+ normal: false,
726
+ "左右反転": true,
727
+ "そのまま": false
153
728
  };
154
- const COMPUTE_MODE_ITEMS = COMPUTE_MODES.map(
155
- (value) => ({ text: value, value })
156
- );
157
- const RECOGNITION_MODE_ITEMS = [
158
- { text: "pose", value: "pose" },
159
- { text: "image", value: "image" },
160
- { text: "audio", value: "audio" }
729
+ var COMPUTE_MODE_ITEMS = COMPUTE_MODES.map((value) => ({
730
+ text: value,
731
+ value
732
+ }));
733
+ var RECOGNITION_MODE_ITEMS = [
734
+ {
735
+ text: "pose",
736
+ value: "pose"
737
+ },
738
+ {
739
+ text: "image",
740
+ value: "image"
741
+ },
742
+ {
743
+ text: "audio",
744
+ value: "audio"
745
+ }
161
746
  ];
162
- const RECOGNITION_MODE_ALIASES = {
163
- pose: "pose",
164
- poses: "pose",
165
- body: "pose",
166
- image: "image",
167
- images: "image",
168
- object: "image",
169
- card: "image",
170
- marker: "image",
171
- audio: "audio",
172
- sound: "audio",
173
- voice: "audio",
174
- microphone: "audio",
175
- mic: "audio",
176
- "ポーズ": "pose",
177
- "姿勢": "pose",
178
- "画像": "image",
179
- "物体": "image",
180
- "カード": "image",
181
- "記号": "image",
182
- "音声": "audio",
183
- "音": "audio",
184
- "声": "audio",
185
- "マイク": "audio"
747
+ var RECOGNITION_MODE_ALIASES = {
748
+ pose: "pose",
749
+ poses: "pose",
750
+ body: "pose",
751
+ image: "image",
752
+ images: "image",
753
+ object: "image",
754
+ card: "image",
755
+ marker: "image",
756
+ audio: "audio",
757
+ sound: "audio",
758
+ voice: "audio",
759
+ microphone: "audio",
760
+ mic: "audio",
761
+ "ポーズ": "pose",
762
+ "姿勢": "pose",
763
+ "画像": "image",
764
+ "物体": "image",
765
+ "カード": "image",
766
+ "記号": "image",
767
+ "音声": "audio",
768
+ "音": "audio",
769
+ "声": "audio",
770
+ "マイク": "audio"
186
771
  };
187
- const POSE_OVERLAY_VISIBILITY_ITEMS = [
188
- { text: "on", value: "on" },
189
- { text: "off", value: "off" }
190
- ];
191
- const POSE_OVERLAY_VISIBILITY_ALIASES = {
192
- on: true,
193
- off: false,
194
- show: true,
195
- hide: false,
196
- true: true,
197
- false: false,
198
- 表示: true,
199
- 非表示: false
772
+ var POSE_OVERLAY_VISIBILITY_ITEMS = [{
773
+ text: "on",
774
+ value: "on"
775
+ }, {
776
+ text: "off",
777
+ value: "off"
778
+ }];
779
+ var POSE_OVERLAY_VISIBILITY_ALIASES = {
780
+ on: true,
781
+ off: false,
782
+ show: true,
783
+ hide: false,
784
+ true: true,
785
+ false: false,
786
+ 表示: true,
787
+ 非表示: false
200
788
  };
201
- const POSE_CONFIDENCE_PROPERTY_ITEMS = [
202
- { text: "joint opacity", value: "joint-opacity" },
203
- { text: "joint radius", value: "joint-radius" },
204
- { text: "bone opacity", value: "bone-opacity" },
205
- { text: "bone width", value: "bone-width" }
789
+ var POSE_CONFIDENCE_PROPERTY_ITEMS = [
790
+ {
791
+ text: "joint opacity",
792
+ value: "joint-opacity"
793
+ },
794
+ {
795
+ text: "joint radius",
796
+ value: "joint-radius"
797
+ },
798
+ {
799
+ text: "bone opacity",
800
+ value: "bone-opacity"
801
+ },
802
+ {
803
+ text: "bone width",
804
+ value: "bone-width"
805
+ }
206
806
  ];
207
- const CAMERA_SELECTION_ITEMS = [
208
- { text: "default camera", value: "default" },
209
- { text: "front camera", value: "front" },
210
- { text: "back camera", value: "back" }
807
+ var CAMERA_SELECTION_ITEMS = [
808
+ {
809
+ text: "default camera",
810
+ value: "default"
811
+ },
812
+ {
813
+ text: "front camera",
814
+ value: "front"
815
+ },
816
+ {
817
+ text: "back camera",
818
+ value: "back"
819
+ }
211
820
  ];
212
- const CAMERA_SELECTION_ALIASES = {
213
- default: "default",
214
- front: "front",
215
- back: "back",
216
- user: "front",
217
- environment: "back",
218
- "既定": "default",
219
- "インカメラ": "front",
220
- "前面カメラ": "front",
221
- "背面カメラ": "back"
821
+ var CAMERA_SELECTION_ALIASES = {
822
+ default: "default",
823
+ front: "front",
824
+ back: "back",
825
+ user: "front",
826
+ environment: "back",
827
+ "既定": "default",
828
+ "インカメラ": "front",
829
+ "前面カメラ": "front",
830
+ "背面カメラ": "back"
222
831
  };
223
- const loadingPromises = /* @__PURE__ */ new Map();
832
+ var loadingPromises = /* @__PURE__ */ new Map();
224
833
  function normalizePosition(value) {
225
- return POSITION_ALIASES[String(value ?? "bottom-right")] ?? "bottom-right";
834
+ return POSITION_ALIASES[String(value ?? "bottom-right")] ?? "bottom-right";
226
835
  }
227
836
  function normalizePreviewMirroring(value) {
228
- return PREVIEW_MIRRORING_ALIASES[String(value ?? "mirrored").trim().toLowerCase()] ?? true;
837
+ return PREVIEW_MIRRORING_ALIASES[String(value ?? "mirrored").trim().toLowerCase()] ?? true;
229
838
  }
230
839
  function normalizeRecognitionMode(value) {
231
- return RECOGNITION_MODE_ALIASES[String(value ?? "pose").trim().toLowerCase()] ?? "pose";
840
+ return RECOGNITION_MODE_ALIASES[String(value ?? "pose").trim().toLowerCase()] ?? "pose";
232
841
  }
233
842
  function normalizeCameraSelection(value) {
234
- const selection = String(value ?? "default").trim();
235
- const normalized = CAMERA_SELECTION_ALIASES[selection.toLowerCase() || "default"];
236
- if (normalized === "default" || normalized === "front" || normalized === "back") {
237
- return { kind: "preference", value: normalized };
238
- }
239
- return { kind: "device", value: selection };
843
+ const selection = String(value ?? "default").trim();
844
+ const normalized = CAMERA_SELECTION_ALIASES[selection.toLowerCase() || "default"];
845
+ if (normalized === "default" || normalized === "front" || normalized === "back") return {
846
+ kind: "preference",
847
+ value: normalized
848
+ };
849
+ return {
850
+ kind: "device",
851
+ value: selection
852
+ };
240
853
  }
241
854
  function normalizePoseOverlayVisibility(value) {
242
- return POSE_OVERLAY_VISIBILITY_ALIASES[String(value ?? "on").trim().toLowerCase()] ?? true;
855
+ return POSE_OVERLAY_VISIBILITY_ALIASES[String(value ?? "on").trim().toLowerCase()] ?? true;
243
856
  }
244
857
  function normalizePoseStyleNumber(value, fallback, maximum) {
245
- const number = value === "" ? fallback : Number(value);
246
- if (!Number.isFinite(number)) return fallback;
247
- return Math.max(0, maximum === void 0 ? number : Math.min(maximum, number));
858
+ const number = value === "" ? fallback : Number(value);
859
+ if (!Number.isFinite(number)) return fallback;
860
+ return Math.max(0, maximum === void 0 ? number : Math.min(maximum, number));
248
861
  }
249
862
  function normalizePoseColor(value, fallback) {
250
- const color = String(value ?? "").trim();
251
- return color || fallback;
863
+ return String(value ?? "").trim() || fallback;
252
864
  }
253
865
  function cameraConstraints(selection) {
254
- if (selection.kind === "device") return { deviceId: { exact: selection.value } };
255
- if (selection.value === "front") return { facingMode: { ideal: "user" } };
256
- if (selection.value === "back") return { facingMode: { ideal: "environment" } };
257
- return void 0;
866
+ if (selection.kind === "device") return { deviceId: { exact: selection.value } };
867
+ if (selection.value === "front") return { facingMode: { ideal: "user" } };
868
+ if (selection.value === "back") return { facingMode: { ideal: "environment" } };
258
869
  }
259
870
  function scriptLoadedFor(src) {
260
- if (src === BROWSER_RUNTIME_URL) {
261
- return typeof globalThis.tf !== "undefined" && typeof globalThis.tmPose !== "undefined" && typeof globalThis.tmImage !== "undefined" && typeof globalThis.tmAudio !== "undefined";
262
- }
263
- return false;
871
+ if (src === BROWSER_RUNTIME_URL) return typeof globalThis.tf !== "undefined" && typeof globalThis.tmPose !== "undefined" && typeof globalThis.tmImage !== "undefined" && typeof globalThis.tmAudio !== "undefined";
872
+ return false;
264
873
  }
265
874
  function loadScript(src) {
266
- if (scriptLoadedFor(src)) return Promise.resolve();
267
- const active = loadingPromises.get(src);
268
- if (active) return active;
269
- const existing = Array.from(document.scripts).find((script) => script.src === src);
270
- if (existing?.dataset.kubohiroyatmLoaded === "true") return Promise.resolve();
271
- const promise = new Promise((resolve, reject) => {
272
- const script = existing ?? document.createElement("script");
273
- const cleanup = () => {
274
- script.removeEventListener("load", handleLoad);
275
- script.removeEventListener("error", handleError);
276
- };
277
- const handleLoad = () => {
278
- cleanup();
279
- script.dataset.kubohiroyatmLoaded = "true";
280
- resolve();
281
- };
282
- const handleError = () => {
283
- cleanup();
284
- loadingPromises.delete(src);
285
- reject(new Error("TM: Failed to load script: " + src));
286
- };
287
- script.addEventListener("load", handleLoad, { once: true });
288
- script.addEventListener("error", handleError, { once: true });
289
- if (!existing) {
290
- script.src = src;
291
- document.head.appendChild(script);
292
- } else {
293
- queueMicrotask(() => {
294
- if (scriptLoadedFor(src)) handleLoad();
295
- });
296
- }
297
- });
298
- loadingPromises.set(src, promise);
299
- return promise;
875
+ if (scriptLoadedFor(src)) return Promise.resolve();
876
+ const active = loadingPromises.get(src);
877
+ if (active) return active;
878
+ const existing = Array.from(document.scripts).find((script) => script.src === src);
879
+ if (existing?.dataset.kubohiroyatmLoaded === "true") return Promise.resolve();
880
+ const promise = new Promise((resolve, reject) => {
881
+ const script = existing ?? document.createElement("script");
882
+ const cleanup = () => {
883
+ script.removeEventListener("load", handleLoad);
884
+ script.removeEventListener("error", handleError);
885
+ };
886
+ const handleLoad = () => {
887
+ cleanup();
888
+ script.dataset.kubohiroyatmLoaded = "true";
889
+ resolve();
890
+ };
891
+ const handleError = () => {
892
+ cleanup();
893
+ loadingPromises.delete(src);
894
+ reject(/* @__PURE__ */ new Error("TM: Failed to load script: " + src));
895
+ };
896
+ script.addEventListener("load", handleLoad, { once: true });
897
+ script.addEventListener("error", handleError, { once: true });
898
+ if (!existing) {
899
+ script.src = src;
900
+ document.head.appendChild(script);
901
+ } else queueMicrotask(() => {
902
+ if (scriptLoadedFor(src)) handleLoad();
903
+ });
904
+ });
905
+ loadingPromises.set(src, promise);
906
+ return promise;
300
907
  }
301
908
  function rectanglesIntersect(left, right) {
302
- return left.right > right.left && left.left < right.right && left.bottom > right.top && left.top < right.bottom;
909
+ return left.right > right.left && left.left < right.right && left.bottom > right.top && left.top < right.bottom;
303
910
  }
304
911
  function canvasScore(width, height) {
305
- if (width <= 0 || height <= 0) return Number.NEGATIVE_INFINITY;
306
- const area = width * height;
307
- const aspectPenalty = Math.abs(width / height - 4 / 3);
308
- return area / (1 + aspectPenalty * 4);
912
+ if (width <= 0 || height <= 0) return Number.NEGATIVE_INFINITY;
913
+ return width * height / (1 + Math.abs(width / height - 4 / 3) * 4);
309
914
  }
310
915
  function isDocumentHidden() {
311
- return typeof document !== "undefined" && document.visibilityState === "hidden";
916
+ return typeof document !== "undefined" && document.visibilityState === "hidden";
312
917
  }
918
+ /**
919
+ * Initialize the camera canvas before Teachable Machine or TensorFlow.js requests its context.
920
+ * The camera path is independent of the selected compute mode: whichever backend recognition runs
921
+ * on, TM intentionally uses the browser's normal Canvas2D context here. Its one-draw/one-read
922
+ * camera path does not demonstrate a repeatable end-to-end benefit from forcing a
923
+ * readback-optimized context, and the legacy backend parameter remains accepted for compatibility.
924
+ */
313
925
  function initializeCameraReadbackContext(canvas, _tensorflowBackend) {
314
- if (typeof canvas !== "object" || canvas === null || typeof canvas.getContext !== "function") {
315
- throw new Error("TM: Webcam canvas does not provide a 2D context.");
316
- }
317
- const context = canvas.getContext("2d");
318
- if (!context) throw new Error("TM: Webcam canvas 2D context is unavailable.");
319
- return context;
926
+ if (typeof canvas !== "object" || canvas === null || typeof canvas.getContext !== "function") throw new Error("TM: Webcam canvas does not provide a 2D context.");
927
+ const context = canvas.getContext("2d");
928
+ if (!context) throw new Error("TM: Webcam canvas 2D context is unavailable.");
929
+ return context;
320
930
  }
321
- class TMExtension {
322
- constructor(featureFlags = {}, dependencies = {}) {
323
- this.featureFlags = { ...FEATURE_FLAGS, ...featureFlags };
324
- this.tmPoseRuntime = dependencies.poseRuntime ?? dependencies.runtime ?? null;
325
- this.tmImageRuntime = dependencies.imageRuntime ?? null;
326
- this.tmAudioRuntime = dependencies.audioRuntime ?? null;
327
- this.tmComputeRuntime = dependencies.compute ?? null;
328
- this.computeMode = normalizeComputeMode(dependencies.computeMode);
329
- this.computeSelection = null;
330
- this.modelOwnedByExtension = false;
331
- this.allowRemoteLibraries = dependencies.allowRemoteLibraries ?? true;
332
- this.onAccumulatedPoseChanged = dependencies.onAccumulatedPoseChanged ?? null;
333
- this.recognitionMode = "pose";
334
- this.modelURL = "";
335
- this.model = null;
336
- this.webcam = null;
337
- this.cameraRunning = false;
338
- this.cameraSelection = "default";
339
- this.cameraSelectionIsDeviceId = false;
340
- this.cameraDevices = [];
341
- this.activeCameraDeviceId = "";
342
- this.activeCameraDeviceName = "";
343
- this.cameraSelectionQueue = Promise.resolve();
344
- this.recognizing = false;
345
- this.audioListening = false;
346
- this.loopStarted = false;
347
- this.loopGeneration = 0;
348
- this.activeModelOperations = /* @__PURE__ */ new Map();
349
- this.currentPoseName = "";
350
- this.score = 0;
351
- this.predictions = {};
352
- this.accumulationCoefficient = 1;
353
- this.decayCoefficient = 0.9;
354
- this.activeDecayCoefficient = 0.9;
355
- this.accumulatedPoseThreshold = 0;
356
- this.accumulatedPoseName = "";
357
- this.accumulatedScore = 0;
358
- this.accumulatedPredictions = {};
359
- this.lastAccumulationTime = null;
360
- this.previewOpacity = 0.6;
361
- this.previewPosition = "bottom-right";
362
- this.previewMirrored = true;
363
- this.previewVisible = true;
364
- this.previewCanvas = null;
365
- this.previewStageElement = null;
366
- this.poseOverlayVisible = true;
367
- this.poseOverlayMinimumConfidence = 0.5;
368
- this.poseJointStyles = Object.fromEntries(
369
- POSE_KEYPOINT_NAMES.map((part) => [part, { ...DEFAULT_POSE_JOINT_STYLE }])
370
- );
371
- this.poseBoneStyle = { ...DEFAULT_POSE_BONE_STYLE };
372
- this.poseConfidenceScaling = { ...DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING };
373
- this.poseOverlaySvg = null;
374
- this.poseOverlayJointElements = /* @__PURE__ */ new Map();
375
- this.poseOverlayBoneElements = [];
376
- this.latestPoseKeypoints = [];
377
- this.cameraMs = 0;
378
- this.modelLoadMs = 0;
379
- this.firstRecognitionMs = 0;
380
- this.lastError = "";
381
- this.accumulatedPosePausedForBackground = isDocumentHidden();
382
- this.visibilityChangeListener = () => this.handleDocumentVisibilityChange();
383
- if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") {
384
- document.addEventListener("visibilitychange", this.visibilityChangeListener);
385
- }
386
- }
387
- getInfo() {
388
- return {
389
- id: EXTENSION_ID,
390
- name: Scratch.translate(definitions.extensionName),
391
- docsURI: DOCS_URI,
392
- blockIconURI: BLOCK_ICON_URI,
393
- blocks: definitions.blocks.filter((block) => !block.featureFlag || this.featureFlags[block.featureFlag]).map((block) => ({
394
- opcode: block.opcode,
395
- blockType: Scratch.BlockType[block.blockType],
396
- text: Scratch.translate(block.text),
397
- ...block.disableMonitor ? { disableMonitor: true } : {},
398
- ...block.arguments ? {
399
- arguments: Object.fromEntries(Object.entries(block.arguments).map(([name2, argument]) => [
400
- name2,
401
- {
402
- type: Scratch.ArgumentType[argument.type],
403
- defaultValue: argument.defaultValue,
404
- ...argument.menu ? { menu: argument.menu } : {}
405
- }
406
- ]))
407
- } : {}
408
- })),
409
- menus: {
410
- positionMenu: {
411
- acceptReporters: true,
412
- items: POSITION_ITEMS.map((item) => ({ text: Scratch.translate(item.text), value: item.value }))
413
- },
414
- previewMirroringMenu: {
415
- acceptReporters: true,
416
- items: PREVIEW_MIRRORING_ITEMS.map((item) => ({ text: Scratch.translate(item.text), value: item.value }))
417
- },
418
- recognitionModeMenu: {
419
- acceptReporters: true,
420
- items: RECOGNITION_MODE_ITEMS.map((item) => ({
421
- text: Scratch.translate(item.text),
422
- value: item.value
423
- }))
424
- },
425
- computeModeMenu: {
426
- acceptReporters: true,
427
- items: COMPUTE_MODE_ITEMS.map((item) => ({
428
- text: Scratch.translate(item.text),
429
- value: item.value
430
- }))
431
- },
432
- poseOverlayVisibilityMenu: {
433
- acceptReporters: true,
434
- items: POSE_OVERLAY_VISIBILITY_ITEMS.map((item) => ({
435
- text: Scratch.translate(item.text),
436
- value: item.value
437
- }))
438
- },
439
- poseKeypointMenu: {
440
- acceptReporters: true,
441
- items: POSE_KEYPOINT_NAMES.map((part) => ({ text: part, value: part }))
442
- },
443
- poseConfidencePropertyMenu: {
444
- acceptReporters: true,
445
- items: POSE_CONFIDENCE_PROPERTY_ITEMS.map((item) => ({
446
- text: Scratch.translate(item.text),
447
- value: item.value
448
- }))
449
- },
450
- cameraMenu: {
451
- acceptReporters: true,
452
- items: "getCameraMenuItems"
453
- }
454
- }
455
- };
456
- }
457
- versionReporter() {
458
- return VERSION;
459
- }
460
- setLastError(error) {
461
- this.lastError = String(error?.message ?? error);
462
- }
463
- setRecognitionMode(args) {
464
- const mode = normalizeRecognitionMode(args.MODE);
465
- if (mode === this.recognitionMode) return;
466
- if (this.recognizing) {
467
- throw new Error("Teachable Machine: Stop recognition before changing recognition mode.");
468
- }
469
- if (this.cameraRunning) {
470
- throw new Error("Teachable Machine: Stop the camera before changing recognition mode.");
471
- }
472
- this.recognitionMode = mode;
473
- this.model = null;
474
- this.modelOwnedByExtension = false;
475
- this.modelURL = "";
476
- this.modelLoadMs = 0;
477
- this.firstRecognitionMs = 0;
478
- this.currentPoseName = "";
479
- this.score = 0;
480
- this.predictions = {};
481
- this.clearPoseOverlay();
482
- this.resetAccumulatedPose();
483
- }
484
- recognitionModeReporter() {
485
- return this.recognitionMode;
486
- }
487
- activeComputeRuntime() {
488
- if (this.tmComputeRuntime) return this.tmComputeRuntime;
489
- const runtime = globalThis.tmCompute;
490
- return runtime && typeof runtime.select === "function" ? runtime : null;
491
- }
492
- /**
493
- * TensorFlow.js binds every tensor to the backend that was active when the
494
- * tensor was created, so the backend is negotiated before any model loads. A
495
- * host that preloads a runtime without a compute controller keeps whatever
496
- * backend TensorFlow.js selected for itself.
497
- */
498
- async ensureComputeBackend() {
499
- const compute = this.activeComputeRuntime();
500
- if (!compute) return null;
501
- const selection = await compute.select(this.computeMode);
502
- this.computeSelection = selection;
503
- if (selection.fallback) {
504
- this.setLastError(
505
- new Error(
506
- `Teachable Machine: The ${selection.requested} compute backend was unavailable, so ${selection.backend} is in use.`
507
- )
508
- );
509
- }
510
- return selection;
511
- }
512
- async setComputeMode(args) {
513
- const mode = normalizeComputeMode(args.MODE, this.computeMode);
514
- if (mode === this.computeMode) return;
515
- if (this.recognizing) {
516
- throw new Error("Teachable Machine: Stop recognition before changing the compute mode.");
517
- }
518
- if (this.model && !this.modelOwnedByExtension) {
519
- throw new Error(
520
- "Teachable Machine: Release the prepared model before changing the compute mode."
521
- );
522
- }
523
- const previousModel = this.model;
524
- this.computeMode = mode;
525
- this.computeSelection = null;
526
- this.model = null;
527
- this.modelOwnedByExtension = false;
528
- this.modelLoadMs = 0;
529
- this.firstRecognitionMs = 0;
530
- if (previousModel) await this.releaseOwnedModel(previousModel);
531
- if (this.activeComputeRuntime()) await this.ensureComputeBackend();
532
- }
533
- async releaseOwnedModel(model) {
534
- try {
535
- await this.waitForPreparedModelIdle(model);
536
- await model.dispose?.();
537
- } catch (error) {
538
- this.setLastError(error);
539
- }
540
- }
541
- computeModeReporter() {
542
- return this.computeMode;
543
- }
544
- computeBackendReporter() {
545
- const compute = this.activeComputeRuntime();
546
- if (!compute) return "";
547
- return this.computeSelection?.backend ?? compute.getBackend() ?? "";
548
- }
549
- setModelURL(args) {
550
- this.modelURL = String(args.URL || "").trim();
551
- if (this.modelURL && !this.modelURL.endsWith("/")) this.modelURL += "/";
552
- this.model = null;
553
- this.modelLoadMs = 0;
554
- this.firstRecognitionMs = 0;
555
- }
556
- activeRuntime() {
557
- if (this.recognitionMode === "audio") return this.tmAudioRuntime;
558
- return this.recognitionMode === "image" ? this.tmImageRuntime : this.tmPoseRuntime;
559
- }
560
- async ensureLibrariesLoaded() {
561
- if (this.activeRuntime()) {
562
- if (this.activeComputeRuntime()) await this.ensureComputeBackend();
563
- return;
564
- }
565
- if (!this.allowRemoteLibraries) {
566
- throw new Error("Teachable Machine: A preloaded runtime is required.");
567
- }
568
- if (typeof globalThis.tf === "undefined" || typeof globalThis.tmPose === "undefined" || typeof globalThis.tmImage === "undefined" || typeof globalThis.tmAudio === "undefined") {
569
- await loadScript(BROWSER_RUNTIME_URL);
570
- }
571
- if (typeof globalThis.tf === "undefined" || typeof globalThis.tmPose === "undefined" || typeof globalThis.tmImage === "undefined" || typeof globalThis.tmAudio === "undefined") {
572
- throw new Error("Teachable Machine: The reviewed browser runtime could not be loaded.");
573
- }
574
- this.tmPoseRuntime = globalThis.tmPose;
575
- this.tmImageRuntime = globalThis.tmImage;
576
- this.tmAudioRuntime = globalThis.tmAudio;
577
- await this.ensureComputeBackend();
578
- }
579
- cleanupCameraResources() {
580
- const video = this.webcam?.webcam;
581
- if (video?.srcObject) {
582
- video.srcObject.getTracks().forEach((track) => track.stop());
583
- video.srcObject = null;
584
- }
585
- this.previewCanvas?.parentNode?.removeChild(this.previewCanvas);
586
- this.poseOverlaySvg?.parentNode?.removeChild(this.poseOverlaySvg);
587
- this.previewCanvas = null;
588
- this.previewStageElement = null;
589
- this.poseOverlaySvg = null;
590
- this.poseOverlayJointElements = /* @__PURE__ */ new Map();
591
- this.poseOverlayBoneElements = [];
592
- this.latestPoseKeypoints = [];
593
- this.webcam = null;
594
- this.cameraRunning = false;
595
- this.activeCameraDeviceId = "";
596
- this.activeCameraDeviceName = "";
597
- this.loopStarted = false;
598
- this.loopGeneration += 1;
599
- }
600
- async startCamera() {
601
- if (this.recognitionMode === "audio") {
602
- throw new Error("Teachable Machine: Audio mode uses the microphone through start recognition.");
603
- }
604
- if (this.cameraRunning && this.webcam) {
605
- this.attachPreviewToStage();
606
- return;
607
- }
608
- try {
609
- this.lastError = "";
610
- const startedAt = performance.now();
611
- await this.ensureLibrariesLoaded();
612
- const runtime = this.activeRuntime();
613
- if (!runtime) throw new Error("Teachable Machine: Runtime is unavailable.");
614
- if (!("Webcam" in runtime)) {
615
- throw new Error("Teachable Machine: Camera runtime is unavailable in the current mode.");
616
- }
617
- this.webcam = new runtime.Webcam(320, 240, true);
618
- const constraints = cameraConstraints(this.resolvedCameraSelection());
619
- if (constraints) await this.webcam.setup(constraints);
620
- else await this.webcam.setup();
621
- initializeCameraReadbackContext(this.webcam.canvas);
622
- await this.webcam.play();
623
- this.attachPreviewToStage();
624
- this.cameraRunning = true;
625
- try {
626
- await this.refreshCameraDevices();
627
- } catch {
628
- }
629
- this.updateActiveCameraInfo();
630
- this.cameraMs = Math.round(performance.now() - startedAt);
631
- this.startLoopIfNeeded();
632
- } catch (error) {
633
- this.cleanupCameraResources();
634
- this.setLastError(error);
635
- throw error;
636
- }
637
- }
638
- stopCamera() {
639
- try {
640
- this.stopRecognition();
641
- this.cleanupCameraResources();
642
- this.currentPoseName = "";
643
- this.score = 0;
644
- this.predictions = {};
645
- } catch (error) {
646
- this.setLastError(error);
647
- throw error;
648
- }
649
- }
650
- isCameraRunning() {
651
- return this.cameraRunning;
652
- }
653
- getCameraMenuItems() {
654
- const fixedItems = CAMERA_SELECTION_ITEMS.map((item) => ({
655
- text: Scratch.translate(item.text),
656
- value: item.value
657
- }));
658
- const seen = /* @__PURE__ */ new Set();
659
- const deviceItems = [];
660
- this.cameraDevices.forEach((device, index) => {
661
- if (!device.deviceId || seen.has(device.deviceId)) return;
662
- seen.add(device.deviceId);
663
- deviceItems.push({
664
- text: device.label || `${Scratch.translate("camera")} ${index + 1}`,
665
- value: device.deviceId
666
- });
667
- });
668
- return [...fixedItems, ...deviceItems];
669
- }
670
- async refreshCameraDevices() {
671
- const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
672
- if (!mediaDevices || typeof mediaDevices.enumerateDevices !== "function") {
673
- throw new Error("TM: Camera enumeration is not available in this browser.");
674
- }
675
- const devices = await mediaDevices.enumerateDevices();
676
- this.cameraDevices = devices.filter((device) => device.kind === "videoinput").map((device) => ({ deviceId: device.deviceId, label: device.label }));
677
- return this.cameraDevices;
678
- }
679
- async refreshCameraList() {
680
- try {
681
- this.lastError = "";
682
- await this.refreshCameraDevices();
683
- if (this.cameraRunning) this.updateActiveCameraInfo();
684
- } catch (error) {
685
- this.setLastError(error);
686
- throw error;
687
- }
688
- }
689
- setCameraSelection(args) {
690
- const selection = normalizeCameraSelection(args.CAMERA);
691
- return this.enqueueCameraSelection(selection);
692
- }
693
- setCameraDeviceId(deviceId) {
694
- if (typeof deviceId !== "string" || deviceId.trim().length === 0) {
695
- return Promise.reject(new Error("TM: Camera device ID must be a non-empty string."));
696
- }
697
- return this.enqueueCameraSelection({ kind: "device", value: deviceId });
698
- }
699
- resolvedCameraSelection() {
700
- return this.cameraSelectionIsDeviceId ? { kind: "device", value: this.cameraSelection } : { kind: "preference", value: this.cameraSelection };
701
- }
702
- enqueueCameraSelection(selection) {
703
- const operation = this.cameraSelectionQueue.then(() => this.applyCameraSelection(selection));
704
- this.cameraSelectionQueue = operation.catch(() => void 0);
705
- return operation;
706
- }
707
- async applyCameraSelection(selection) {
708
- const previousSelection = this.cameraSelection;
709
- const previousSelectionIsDeviceId = this.cameraSelectionIsDeviceId;
710
- const wasRunning = this.cameraRunning;
711
- this.cameraSelection = selection.value;
712
- this.cameraSelectionIsDeviceId = selection.kind === "device";
713
- if (!wasRunning) return;
714
- this.cleanupCameraResources();
715
- try {
716
- await this.startCamera();
717
- } catch (switchError) {
718
- this.cameraSelection = previousSelection;
719
- this.cameraSelectionIsDeviceId = previousSelectionIsDeviceId;
720
- try {
721
- await this.startCamera();
722
- } catch (rollbackError) {
723
- const error = new AggregateError(
724
- [switchError, rollbackError],
725
- "TM: Camera switch and rollback both failed."
726
- );
727
- this.setLastError(error);
728
- throw error;
729
- }
730
- this.setLastError(switchError);
731
- throw switchError;
732
- }
733
- }
734
- updateActiveCameraInfo() {
735
- const stream = this.webcam?.webcam?.srcObject;
736
- const videoTrack = stream?.getVideoTracks?.()[0] ?? stream?.getTracks?.().find((track) => track.kind === "video" || track.kind === void 0);
737
- const settings = videoTrack?.getSettings?.() ?? {};
738
- const selectedDeviceId = this.cameraSelectionIsDeviceId ? this.cameraSelection : "";
739
- this.activeCameraDeviceId = String(settings.deviceId || selectedDeviceId);
740
- const device = this.cameraDevices.find((candidate) => candidate.deviceId === this.activeCameraDeviceId);
741
- this.activeCameraDeviceName = String(videoTrack?.label || device?.label || "");
742
- }
743
- cameraCountReporter() {
744
- return this.cameraDevices.length;
745
- }
746
- cameraDeviceIdReporter() {
747
- return this.activeCameraDeviceId;
748
- }
749
- cameraDeviceNameReporter() {
750
- return this.activeCameraDeviceName;
751
- }
752
- showPreview() {
753
- try {
754
- this.previewVisible = true;
755
- if (!this.webcam?.canvas) throw new Error("TM: Start the camera before showing the preview.");
756
- this.attachPreviewToStage();
757
- this.previewCanvas.style.display = "block";
758
- this.updatePoseOverlayVisibility();
759
- this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
760
- } catch (error) {
761
- this.setLastError(error);
762
- throw error;
763
- }
764
- }
765
- hidePreview() {
766
- this.previewVisible = false;
767
- if (this.previewCanvas) this.previewCanvas.style.display = "none";
768
- this.updatePoseOverlayVisibility();
769
- }
770
- isPreviewVisible() {
771
- return this.previewVisible;
772
- }
773
- setPreviewOpacity(args) {
774
- let opacity = args.OPACITY === "" ? 0.6 : Number(args.OPACITY);
775
- if (Number.isNaN(opacity)) opacity = 0.6;
776
- this.previewOpacity = Math.max(0, Math.min(1, opacity));
777
- if (this.previewCanvas) this.previewCanvas.style.opacity = String(this.previewOpacity);
778
- }
779
- setPreviewPosition(args) {
780
- this.previewPosition = normalizePosition(args.POSITION);
781
- if (this.previewCanvas) {
782
- this.updatePreviewStyle();
783
- this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
784
- }
785
- }
786
- setPreviewMirroring(args) {
787
- this.previewMirrored = normalizePreviewMirroring(args.MIRRORING);
788
- if (this.previewCanvas) this.updatePreviewStyle();
789
- }
790
- previewMirroringReporter() {
791
- return this.previewMirrored ? "mirrored" : "unmirrored";
792
- }
793
- setPoseOverlayVisibility(args) {
794
- this.poseOverlayVisible = normalizePoseOverlayVisibility(args.VISIBILITY);
795
- this.updatePoseOverlayVisibility();
796
- }
797
- showPoseOverlay() {
798
- this.poseOverlayVisible = true;
799
- this.updatePoseOverlayVisibility();
800
- }
801
- hidePoseOverlay() {
802
- this.poseOverlayVisible = false;
803
- this.updatePoseOverlayVisibility();
804
- }
805
- isPoseOverlayVisible() {
806
- return this.featureFlags.poseOverlay && this.poseOverlayVisible;
807
- }
808
- setPoseJointStyle(args) {
809
- const part = String(args.PART ?? "");
810
- if (!isPoseKeypointName(part)) throw new Error(`TM: Unknown PoseNet joint: ${part}`);
811
- const previous = this.poseJointStyles[part];
812
- this.poseJointStyles[part] = {
813
- color: normalizePoseColor(args.COLOR, previous.color),
814
- opacity: normalizePoseStyleNumber(args.OPACITY, previous.opacity, 1),
815
- radius: normalizePoseStyleNumber(args.RADIUS, previous.radius)
816
- };
817
- this.redrawPoseOverlay();
818
- }
819
- setPoseBoneStyle(args) {
820
- this.poseBoneStyle = {
821
- color: normalizePoseColor(args.COLOR, this.poseBoneStyle.color),
822
- opacity: normalizePoseStyleNumber(args.OPACITY, this.poseBoneStyle.opacity, 1),
823
- width: normalizePoseStyleNumber(args.WIDTH, this.poseBoneStyle.width)
824
- };
825
- this.redrawPoseOverlay();
826
- }
827
- setPoseOverlayMinimumConfidence(args) {
828
- this.poseOverlayMinimumConfidence = normalizePoseStyleNumber(
829
- args.CONFIDENCE,
830
- this.poseOverlayMinimumConfidence,
831
- 1
832
- );
833
- this.redrawPoseOverlay();
834
- }
835
- setPoseConfidenceScaling(args) {
836
- const property = String(args.PROPERTY ?? "");
837
- const key = {
838
- "joint-opacity": "jointOpacity",
839
- "joint-radius": "jointRadius",
840
- "bone-opacity": "boneOpacity",
841
- "bone-width": "boneWidth"
842
- }[property];
843
- if (!key) throw new Error(`TM: Unknown confidence-scaled property: ${property}`);
844
- this.poseConfidenceScaling[key] = normalizePoseOverlayVisibility(args.STATE);
845
- this.redrawPoseOverlay();
846
- }
847
- async loadModel() {
848
- if (this.model) return;
849
- if (!this.modelURL) throw new Error("Teachable Machine: Set the model URL first.");
850
- try {
851
- this.lastError = "";
852
- const startedAt = performance.now();
853
- await this.ensureLibrariesLoaded();
854
- const runtime = this.activeRuntime();
855
- if (!runtime || typeof runtime.load !== "function") {
856
- throw new Error("Teachable Machine: The URL loader is not available.");
857
- }
858
- this.model = await runtime.load(
859
- this.modelURL + "model.json",
860
- this.modelURL + "metadata.json"
861
- );
862
- this.modelOwnedByExtension = true;
863
- this.modelLoadMs = Math.round(performance.now() - startedAt);
864
- } catch (error) {
865
- this.setLastError(error);
866
- throw error;
867
- }
868
- }
869
- isModelLoaded() {
870
- return Boolean(this.model);
871
- }
872
- usePreparedModel(model) {
873
- if (!model || typeof model !== "object") {
874
- throw new TypeError("TM: Prepared model must be an object.");
875
- }
876
- if (this.recognizing && this.model !== model) {
877
- throw new Error("TM: Stop recognition before changing the active model.");
878
- }
879
- this.model = model;
880
- this.modelOwnedByExtension = false;
881
- this.modelURL = "";
882
- this.modelLoadMs = 0;
883
- this.firstRecognitionMs = 0;
884
- }
885
- clearPreparedModel(model) {
886
- if (model !== void 0 && this.model !== model) return;
887
- this.stopRecognition();
888
- this.model = null;
889
- this.modelOwnedByExtension = false;
890
- this.modelURL = "";
891
- this.modelLoadMs = 0;
892
- this.firstRecognitionMs = 0;
893
- }
894
- async startRecognition() {
895
- try {
896
- this.lastError = "";
897
- const startingNewSession = !this.recognizing;
898
- if (this.recognitionMode !== "audio" && !this.cameraRunning) await this.startCamera();
899
- await this.loadModel();
900
- if (startingNewSession && this.featureFlags.temporalPoseScoring) {
901
- this.startAccumulatedPoseSession();
902
- }
903
- this.recognizing = true;
904
- if (this.recognitionMode === "audio") await this.startAudioRecognition();
905
- else this.startLoopIfNeeded();
906
- } catch (error) {
907
- this.setLastError(error);
908
- throw error;
909
- }
910
- }
911
- stopRecognition() {
912
- this.recognizing = false;
913
- this.stopAudioRecognition();
914
- this.currentPoseName = "";
915
- this.score = 0;
916
- this.predictions = {};
917
- this.clearPoseOverlay();
918
- this.resetAccumulatedPose("stop");
919
- }
920
- isRecognizing() {
921
- return this.recognizing;
922
- }
923
- async startAudioRecognition() {
924
- if (!this.model || typeof this.model.listen !== "function") {
925
- throw new Error("Teachable Machine: The active model does not support audio recognition.");
926
- }
927
- if (typeof this.model.isListening === "function" && this.model.isListening()) {
928
- this.audioListening = true;
929
- return;
930
- }
931
- const first = this.firstRecognitionMs === 0;
932
- const startedAt = first ? performance.now() : 0;
933
- await this.model.listen(
934
- async (result) => {
935
- if (!this.recognizing || this.recognitionMode !== "audio" || this.model === null) return;
936
- if (first && this.firstRecognitionMs === 0) {
937
- this.firstRecognitionMs = Math.round(performance.now() - startedAt);
938
- }
939
- this.applyPredictions(this.audioPredictionFromResult(this.model, result));
940
- },
941
- {
942
- probabilityThreshold: 0,
943
- overlapFactor: 0.5,
944
- invokeCallbackOnNoiseAndUnknown: true
945
- }
946
- );
947
- this.audioListening = true;
948
- }
949
- stopAudioRecognition() {
950
- if (!this.audioListening || !this.model || typeof this.model.stopListening !== "function") {
951
- this.audioListening = false;
952
- return;
953
- }
954
- this.audioListening = false;
955
- void this.model.stopListening().catch((error) => this.setLastError(error));
956
- }
957
- audioPredictionFromResult(model, result) {
958
- const labels = typeof model.wordLabels === "function" ? model.wordLabels() : [];
959
- const scores = Array.isArray(result?.scores) ? result.scores[0] : result?.scores;
960
- if (!labels.length || !scores || typeof scores.length !== "number") {
961
- throw new Error("Teachable Machine: Audio recognition did not return labels and scores.");
962
- }
963
- return labels.map((className, index) => ({
964
- className,
965
- probability: Number(scores[index] ?? 0)
966
- }));
967
- }
968
- applyPredictions(prediction) {
969
- let best = { className: "", probability: 0 };
970
- this.predictions = {};
971
- for (const result of prediction) {
972
- this.predictions[result.className] = result.probability;
973
- if (result.probability > best.probability) best = result;
974
- }
975
- this.currentPoseName = best.className;
976
- this.score = best.probability;
977
- if (this.featureFlags.temporalPoseScoring) {
978
- this.updateAccumulatedPose(prediction);
979
- }
980
- }
981
- findStageElement() {
982
- try {
983
- const stageCanvas = this.findLikelyStageCanvas();
984
- if (stageCanvas.parentElement) return stageCanvas.parentElement;
985
- } catch {
986
- }
987
- const editorStage = document.querySelector(".stage_stage-wrapper_2bejr") || document.querySelector('[class*="stage_stage-wrapper"]') || document.querySelector('[class*="stage-wrapper"]') || document.querySelector('[class*="stage-wrapper_stage-wrapper"]');
988
- if (editorStage) return editorStage;
989
- throw new Error("TM: TurboWarp stage element was not found.");
990
- }
991
- findLikelyStageCanvas() {
992
- const webcamCanvas = this.webcam?.canvas ?? null;
993
- const allCanvases = Array.from(document.querySelectorAll("canvas")).filter((canvas) => canvas !== webcamCanvas && canvas !== this.previewCanvas);
994
- const visibleCandidates = allCanvases.map((canvas) => {
995
- const rect = canvas.getBoundingClientRect();
996
- const style = window.getComputedStyle(canvas);
997
- return { canvas, rect, style, score: canvasScore(rect.width, rect.height) };
998
- }).filter((item) => item.rect.width >= 200 && item.rect.height >= 150).filter((item) => item.style.display !== "none").filter((item) => item.style.visibility !== "hidden").filter((item) => Number(item.style.opacity || 1) !== 0).sort((left, right) => right.score - left.score);
999
- if (visibleCandidates[0]) return visibleCandidates[0].canvas;
1000
- const fallbackCandidates = allCanvases.map((canvas) => ({ canvas, score: canvasScore(canvas.width, canvas.height) })).filter((item) => item.canvas.width >= 200 && item.canvas.height >= 150).sort((left, right) => right.score - left.score);
1001
- if (fallbackCandidates[0]) return fallbackCandidates[0].canvas;
1002
- throw new Error("TM: No likely stage canvas was found. The editor or packager DOM may be unsupported.");
1003
- }
1004
- validatePreviewAttachment(stage, canvas) {
1005
- if (!stage || !canvas || canvas.parentElement !== stage) {
1006
- throw new Error("TM: Preview canvas was not attached to the stage.");
1007
- }
1008
- const canvasStyle = window.getComputedStyle(canvas);
1009
- if (canvasStyle.display === "none" && this.previewVisible) {
1010
- throw new Error("TM: Preview canvas is hidden by display:none.");
1011
- }
1012
- if (canvasStyle.visibility === "hidden" && this.previewVisible) {
1013
- throw new Error("TM: Preview canvas is hidden by visibility:hidden.");
1014
- }
1015
- const stageRect = stage.getBoundingClientRect();
1016
- const canvasRect = canvas.getBoundingClientRect();
1017
- const documentHidden = typeof document.visibilityState === "string" && document.visibilityState === "hidden";
1018
- const layoutUnavailable = stageRect.width === 0 || stageRect.height === 0;
1019
- if (!documentHidden && !layoutUnavailable && this.previewVisible) {
1020
- if (canvasRect.width <= 0 || canvasRect.height <= 0) {
1021
- throw new Error("TM: Preview canvas was attached but has zero size.");
1022
- }
1023
- if (!rectanglesIntersect(stageRect, canvasRect)) {
1024
- throw new Error("TM: Preview canvas does not intersect the stage.");
1025
- }
1026
- }
1027
- }
1028
- createSvgElement(name2) {
1029
- if (typeof document.createElementNS === "function") {
1030
- return document.createElementNS("http://www.w3.org/2000/svg", name2);
1031
- }
1032
- return document.createElement(name2);
1033
- }
1034
- ensurePoseOverlayElement() {
1035
- if (!this.featureFlags.poseOverlay) return null;
1036
- if (this.poseOverlaySvg) return this.poseOverlaySvg;
1037
- const svg = this.createSvgElement("svg");
1038
- svg.setAttribute("viewBox", "0 0 320 240");
1039
- svg.setAttribute("width", "320");
1040
- svg.setAttribute("height", "240");
1041
- svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1042
- svg.setAttribute("aria-hidden", "true");
1043
- svg.setAttribute("focusable", "false");
1044
- const boneGroup = this.createSvgElement("g");
1045
- boneGroup.setAttribute("data-layer", "bones");
1046
- this.poseOverlayBoneElements = POSE_BONE_CONNECTIONS.map(([first, second]) => {
1047
- const line = this.createSvgElement("line");
1048
- line.setAttribute("data-bone", `${first}-${second}`);
1049
- line.setAttribute("stroke-linecap", "round");
1050
- line.style.display = "none";
1051
- boneGroup.appendChild(line);
1052
- return { first, second, line };
1053
- });
1054
- svg.appendChild(boneGroup);
1055
- const jointGroup = this.createSvgElement("g");
1056
- jointGroup.setAttribute("data-layer", "joints");
1057
- this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1058
- for (const part of POSE_KEYPOINT_NAMES) {
1059
- const circle = this.createSvgElement("circle");
1060
- circle.setAttribute("data-joint", part);
1061
- circle.style.display = "none";
1062
- jointGroup.appendChild(circle);
1063
- this.poseOverlayJointElements.set(part, circle);
1064
- }
1065
- svg.appendChild(jointGroup);
1066
- this.poseOverlaySvg = svg;
1067
- return svg;
1068
- }
1069
- updatePoseOverlayVisibility() {
1070
- if (!this.poseOverlaySvg) return;
1071
- this.poseOverlaySvg.style.display = this.previewVisible && this.poseOverlayVisible ? "block" : "none";
1072
- }
1073
- clearPoseOverlay() {
1074
- this.latestPoseKeypoints = [];
1075
- for (const circle of this.poseOverlayJointElements.values()) {
1076
- circle.style.display = "none";
1077
- }
1078
- for (const { line } of this.poseOverlayBoneElements) line.style.display = "none";
1079
- }
1080
- redrawPoseOverlay() {
1081
- if (this.latestPoseKeypoints.length > 0) {
1082
- this.renderPoseOverlay(this.latestPoseKeypoints);
1083
- }
1084
- }
1085
- renderPoseOverlay(keypoints) {
1086
- if (!this.featureFlags.poseOverlay || !this.poseOverlaySvg || !Array.isArray(keypoints)) {
1087
- this.clearPoseOverlay();
1088
- return;
1089
- }
1090
- this.latestPoseKeypoints = keypoints;
1091
- const recognized = /* @__PURE__ */ new Map();
1092
- for (const candidate of keypoints) {
1093
- if (typeof candidate !== "object" || candidate === null || !isPoseKeypointName(candidate.part)) {
1094
- continue;
1095
- }
1096
- const keypoint = candidate;
1097
- const x = Number(keypoint.position?.x);
1098
- const y = Number(keypoint.position?.y);
1099
- if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
1100
- recognized.set(keypoint.part, {
1101
- part: keypoint.part,
1102
- score: confidenceMultiplier(keypoint.score),
1103
- position: { x, y }
1104
- });
1105
- }
1106
- for (const part of POSE_KEYPOINT_NAMES) {
1107
- const circle = this.poseOverlayJointElements.get(part);
1108
- const keypoint = recognized.get(part);
1109
- if (!circle || !keypoint || keypoint.score < this.poseOverlayMinimumConfidence) {
1110
- if (circle) circle.style.display = "none";
1111
- continue;
1112
- }
1113
- const style = this.poseJointStyles[part];
1114
- const opacityMultiplier = this.poseConfidenceScaling.jointOpacity ? keypoint.score : 1;
1115
- const radiusMultiplier = this.poseConfidenceScaling.jointRadius ? keypoint.score : 1;
1116
- circle.setAttribute("cx", String(keypoint.position.x));
1117
- circle.setAttribute("cy", String(keypoint.position.y));
1118
- circle.setAttribute("r", String(style.radius * radiusMultiplier));
1119
- circle.setAttribute("fill", style.color);
1120
- circle.setAttribute("fill-opacity", String(style.opacity * opacityMultiplier));
1121
- circle.style.display = "block";
1122
- }
1123
- for (const { first, second, line } of this.poseOverlayBoneElements) {
1124
- const firstKeypoint = recognized.get(first);
1125
- const secondKeypoint = recognized.get(second);
1126
- if (!firstKeypoint || !secondKeypoint || firstKeypoint.score < this.poseOverlayMinimumConfidence || secondKeypoint.score < this.poseOverlayMinimumConfidence) {
1127
- line.style.display = "none";
1128
- continue;
1129
- }
1130
- const confidence = Math.min(firstKeypoint.score, secondKeypoint.score);
1131
- const opacityMultiplier = this.poseConfidenceScaling.boneOpacity ? confidence : 1;
1132
- const widthMultiplier = this.poseConfidenceScaling.boneWidth ? confidence : 1;
1133
- line.setAttribute("x1", String(firstKeypoint.position.x));
1134
- line.setAttribute("y1", String(firstKeypoint.position.y));
1135
- line.setAttribute("x2", String(secondKeypoint.position.x));
1136
- line.setAttribute("y2", String(secondKeypoint.position.y));
1137
- line.setAttribute("stroke", this.poseBoneStyle.color);
1138
- line.setAttribute("stroke-opacity", String(this.poseBoneStyle.opacity * opacityMultiplier));
1139
- line.setAttribute("stroke-width", String(this.poseBoneStyle.width * widthMultiplier));
1140
- line.style.display = "block";
1141
- }
1142
- }
1143
- attachPreviewToStage() {
1144
- if (!this.webcam) throw new Error("TM: Start the camera before attaching the preview.");
1145
- if (!this.webcam.canvas) throw new Error("TM: webcam.canvas is unavailable.");
1146
- const stage = this.findStageElement();
1147
- const canvas = this.webcam.canvas;
1148
- let stageCanvas = null;
1149
- try {
1150
- const candidate = this.findLikelyStageCanvas();
1151
- if (candidate.parentElement === stage) stageCanvas = candidate;
1152
- } catch {
1153
- }
1154
- this.previewCanvas = canvas;
1155
- this.previewStageElement = stage;
1156
- const overlay = this.ensurePoseOverlayElement();
1157
- const computed = window.getComputedStyle(stage);
1158
- if (computed.position === "static") stage.style.position = "relative";
1159
- stage.style.overflow = "hidden";
1160
- Object.assign(canvas.style, {
1161
- position: "absolute",
1162
- zIndex: "auto",
1163
- pointerEvents: "none",
1164
- border: "2px solid rgba(255, 255, 255, 0.7)",
1165
- borderRadius: "8px",
1166
- background: "#000",
1167
- opacity: String(this.previewOpacity),
1168
- display: this.previewVisible ? "block" : "none",
1169
- boxSizing: "border-box"
1170
- });
1171
- if (overlay) {
1172
- Object.assign(overlay.style, {
1173
- position: "absolute",
1174
- zIndex: "auto",
1175
- pointerEvents: "none",
1176
- border: "2px solid transparent",
1177
- background: "transparent",
1178
- display: this.previewVisible && this.poseOverlayVisible ? "block" : "none",
1179
- boxSizing: "border-box",
1180
- overflow: "hidden"
1181
- });
1182
- }
1183
- let insertionPoint = stageCanvas?.nextSibling ?? null;
1184
- while (insertionPoint && (insertionPoint === canvas || insertionPoint === overlay)) {
1185
- insertionPoint = insertionPoint.nextSibling;
1186
- }
1187
- if (stageCanvas && typeof stage.insertBefore === "function") {
1188
- stage.insertBefore(canvas, insertionPoint);
1189
- } else if (canvas.parentNode !== stage) {
1190
- stage.appendChild(canvas);
1191
- }
1192
- if (overlay) {
1193
- if (stageCanvas && typeof stage.insertBefore === "function") {
1194
- stage.insertBefore(overlay, insertionPoint);
1195
- } else if (overlay.parentNode !== stage) {
1196
- stage.appendChild(overlay);
1197
- }
1198
- }
1199
- this.updatePreviewStyle();
1200
- this.updatePoseOverlayVisibility();
1201
- this.validatePreviewAttachment(stage, canvas);
1202
- }
1203
- updatePreviewStyle() {
1204
- const canvas = this.previewCanvas;
1205
- if (!canvas) throw new Error("TM: Start the camera before positioning the preview.");
1206
- const targets = [canvas, this.poseOverlaySvg].filter(Boolean);
1207
- for (const target of targets) {
1208
- Object.assign(target.style, {
1209
- left: "",
1210
- right: "",
1211
- top: "",
1212
- bottom: "",
1213
- transform: "",
1214
- objectFit: "",
1215
- width: "35%",
1216
- height: "auto",
1217
- borderRadius: "8px"
1218
- });
1219
- }
1220
- let positionTransform = "";
1221
- switch (this.previewPosition) {
1222
- case "top-left":
1223
- for (const target of targets) {
1224
- target.style.left = "8px";
1225
- target.style.top = "8px";
1226
- }
1227
- break;
1228
- case "top-right":
1229
- for (const target of targets) {
1230
- target.style.right = "8px";
1231
- target.style.top = "8px";
1232
- }
1233
- break;
1234
- case "bottom-left":
1235
- for (const target of targets) {
1236
- target.style.left = "8px";
1237
- target.style.bottom = "8px";
1238
- }
1239
- break;
1240
- case "center":
1241
- for (const target of targets) {
1242
- target.style.left = "50%";
1243
- target.style.top = "50%";
1244
- }
1245
- positionTransform = "translate(-50%, -50%)";
1246
- break;
1247
- case "full-stage":
1248
- for (const target of targets) {
1249
- target.style.left = "0";
1250
- target.style.top = "0";
1251
- target.style.width = "100%";
1252
- target.style.height = "100%";
1253
- target.style.objectFit = "cover";
1254
- target.style.borderRadius = "0";
1255
- }
1256
- break;
1257
- default:
1258
- for (const target of targets) {
1259
- target.style.right = "8px";
1260
- target.style.bottom = "8px";
1261
- }
1262
- }
1263
- const mirroringTransform = this.previewMirrored ? "" : "scaleX(-1)";
1264
- for (const target of targets) {
1265
- target.style.transform = [positionTransform, mirroringTransform].filter(Boolean).join(" ");
1266
- }
1267
- this.poseOverlaySvg?.setAttribute(
1268
- "preserveAspectRatio",
1269
- this.previewPosition === "full-stage" ? "xMidYMid slice" : "xMidYMid meet"
1270
- );
1271
- }
1272
- startLoopIfNeeded() {
1273
- if (this.loopStarted || !this.cameraRunning || !this.webcam) return;
1274
- this.loopStarted = true;
1275
- const generation = ++this.loopGeneration;
1276
- void this.loop(generation);
1277
- }
1278
- trackPreparedModelOperation(model, operation) {
1279
- let active = this.activeModelOperations.get(model);
1280
- if (!active) {
1281
- active = /* @__PURE__ */ new Set();
1282
- this.activeModelOperations.set(model, active);
1283
- }
1284
- active.add(operation);
1285
- void operation.then(
1286
- () => {
1287
- active.delete(operation);
1288
- if (active.size === 0) this.activeModelOperations.delete(model);
1289
- },
1290
- () => {
1291
- active.delete(operation);
1292
- if (active.size === 0) this.activeModelOperations.delete(model);
1293
- }
1294
- );
1295
- return operation;
1296
- }
1297
- async waitForPreparedModelIdle(model) {
1298
- await Promise.allSettled([...this.activeModelOperations.get(model) ?? []]);
1299
- }
1300
- async loop(generation = this.loopGeneration) {
1301
- if (generation !== this.loopGeneration || !this.cameraRunning || !this.webcam) {
1302
- if (generation === this.loopGeneration) this.loopStarted = false;
1303
- return;
1304
- }
1305
- try {
1306
- this.webcam.update();
1307
- if (this.recognizing && this.model) {
1308
- const model = this.model;
1309
- const recognitionMode = this.recognitionMode;
1310
- const first = this.firstRecognitionMs === 0;
1311
- const startedAt = first ? performance.now() : 0;
1312
- const recognition = await this.trackPreparedModelOperation(
1313
- model,
1314
- this.recognizeFrame(model, recognitionMode)
1315
- );
1316
- if (generation !== this.loopGeneration || !this.cameraRunning || !this.recognizing || this.model !== model || this.recognitionMode !== recognitionMode) {
1317
- } else {
1318
- if (first) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
1319
- this.renderPoseOverlay(recognition.keypoints);
1320
- this.applyPredictions(recognition.prediction);
1321
- }
1322
- }
1323
- } catch (error) {
1324
- this.setLastError(error);
1325
- }
1326
- if (generation === this.loopGeneration && this.cameraRunning && this.webcam) {
1327
- requestAnimationFrame(() => void this.loop(generation));
1328
- } else if (generation === this.loopGeneration) {
1329
- this.loopStarted = false;
1330
- }
1331
- }
1332
- async recognizeFrame(model, recognitionMode) {
1333
- if (recognitionMode === "pose") {
1334
- if (typeof model.estimatePose !== "function") {
1335
- throw new Error("Teachable Machine: The active model does not support pose recognition.");
1336
- }
1337
- const estimate = await model.estimatePose(this.webcam.canvas);
1338
- const prediction2 = await model.predict(estimate.posenetOutput);
1339
- return { keypoints: estimate.pose?.keypoints, prediction: prediction2 };
1340
- }
1341
- const source = this.webcam.webcam ?? this.webcam.canvas;
1342
- const prediction = await model.predict(source);
1343
- return { keypoints: [], prediction };
1344
- }
1345
- currentPoseReporter() {
1346
- return this.currentPoseName;
1347
- }
1348
- scoreReporter() {
1349
- return Math.round(this.score * 100) / 100;
1350
- }
1351
- poseScoreReporter(args) {
1352
- const value = this.predictions[String(args.NAME || "")] || 0;
1353
- return Math.round(value * 100) / 100;
1354
- }
1355
- setAccumulatedPoseParameters(args) {
1356
- const accumulation = args.ACCUMULATION === "" ? 1 : Number(args.ACCUMULATION);
1357
- const decay = args.DECAY === "" ? 0.9 : Number(args.DECAY);
1358
- this.accumulationCoefficient = Number.isFinite(accumulation) ? Math.max(0, accumulation) : 1;
1359
- this.decayCoefficient = Number.isFinite(decay) ? Math.max(0, Math.min(1, decay)) : 0.9;
1360
- }
1361
- setAccumulatedPoseThreshold(args) {
1362
- const threshold = args.THRESHOLD === "" ? 0 : Number(args.THRESHOLD);
1363
- this.accumulatedPoseThreshold = Number.isFinite(threshold) ? Math.max(0, threshold) : 0;
1364
- this.updateAccumulatedPoseSelection();
1365
- }
1366
- startAccumulatedPoseSession(now = performance.now()) {
1367
- this.activeDecayCoefficient = this.decayCoefficient;
1368
- this.lastAccumulationTime = now;
1369
- }
1370
- supportsAccumulatedPoseEvents() {
1371
- return this.featureFlags.temporalPoseScoring && this.featureFlags.accumulatedPoseEvents;
1372
- }
1373
- emitAccumulatedPoseChanged(previousPoseName, reason) {
1374
- if (!this.supportsAccumulatedPoseEvents() || previousPoseName === this.accumulatedPoseName) return;
1375
- const payload = {
1376
- version: 2,
1377
- poseName: this.accumulatedPoseName,
1378
- previousPoseName,
1379
- score: this.accumulatedScore,
1380
- reason,
1381
- timestamp: performance.now()
1382
- };
1383
- if (this.onAccumulatedPoseChanged) {
1384
- try {
1385
- this.onAccumulatedPoseChanged(payload);
1386
- } catch {
1387
- }
1388
- } else if (typeof Scratch !== "undefined") {
1389
- Scratch.vm?.runtime?.emit(ACCUMULATED_POSE_CHANGED_EVENT, payload);
1390
- }
1391
- }
1392
- dispose() {
1393
- this.stopCamera();
1394
- if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") {
1395
- document.removeEventListener("visibilitychange", this.visibilityChangeListener);
1396
- }
1397
- this.onAccumulatedPoseChanged = null;
1398
- }
1399
- resetAccumulatedPose(reason = "reset") {
1400
- const previousPoseName = this.accumulatedPoseName;
1401
- this.accumulatedPoseName = "";
1402
- this.accumulatedScore = 0;
1403
- this.accumulatedPredictions = {};
1404
- this.lastAccumulationTime = this.recognizing ? performance.now() : null;
1405
- this.emitAccumulatedPoseChanged(previousPoseName, reason);
1406
- }
1407
- handleDocumentVisibilityChange() {
1408
- if (typeof document === "undefined") return;
1409
- if (isDocumentHidden()) {
1410
- this.accumulatedPosePausedForBackground = true;
1411
- return;
1412
- }
1413
- if (this.accumulatedPosePausedForBackground) {
1414
- this.accumulatedPosePausedForBackground = false;
1415
- this.lastAccumulationTime = this.recognizing ? performance.now() : null;
1416
- }
1417
- }
1418
- updateAccumulatedPose(prediction, now = performance.now()) {
1419
- if (isDocumentHidden()) {
1420
- this.accumulatedPosePausedForBackground = true;
1421
- return;
1422
- }
1423
- if (this.accumulatedPosePausedForBackground) return;
1424
- const elapsedSeconds = this.lastAccumulationTime === null ? 0 : Math.max(0, (now - this.lastAccumulationTime) / 1e3);
1425
- const decayMultiplier = elapsedSeconds === 0 ? 1 : Math.pow(this.activeDecayCoefficient, elapsedSeconds);
1426
- for (const name2 of Object.keys(this.accumulatedPredictions)) {
1427
- this.accumulatedPredictions[name2] *= decayMultiplier;
1428
- }
1429
- for (const result of prediction) {
1430
- const name2 = String(result.className || "");
1431
- const probability = Number(result.probability);
1432
- if (!name2 || !Number.isFinite(probability)) continue;
1433
- const contribution = Math.max(0, Math.min(1, probability)) * this.accumulationCoefficient * elapsedSeconds;
1434
- this.accumulatedPredictions[name2] = (this.accumulatedPredictions[name2] || 0) + contribution;
1435
- }
1436
- this.lastAccumulationTime = now;
1437
- this.updateAccumulatedPoseSelection();
1438
- }
1439
- updateAccumulatedPoseSelection() {
1440
- const previousPoseName = this.accumulatedPoseName;
1441
- this.accumulatedPoseName = "";
1442
- this.accumulatedScore = 0;
1443
- let bestPoseName = "";
1444
- for (const [name2, value] of Object.entries(this.accumulatedPredictions)) {
1445
- if (value > this.accumulatedScore) {
1446
- bestPoseName = name2;
1447
- this.accumulatedScore = value;
1448
- }
1449
- }
1450
- if (bestPoseName && this.accumulatedScore >= this.accumulatedPoseThreshold) {
1451
- this.accumulatedPoseName = bestPoseName;
1452
- }
1453
- this.emitAccumulatedPoseChanged(previousPoseName, "recognition");
1454
- }
1455
- accumulatedPoseReporter() {
1456
- return this.accumulatedPoseName;
1457
- }
1458
- accumulatedScoreReporter() {
1459
- return this.accumulatedScore;
1460
- }
1461
- accumulatedPoseScoreReporter(args) {
1462
- const value = this.accumulatedPredictions[String(args.NAME || "")] || 0;
1463
- return value;
1464
- }
1465
- isPose(args) {
1466
- return (this.predictions[String(args.NAME || "")] || 0) >= 0.75;
1467
- }
1468
- isPoseWithThreshold(args) {
1469
- const name2 = String(args.NAME || "");
1470
- let threshold = args.THRESHOLD === "" ? 0.75 : Number(args.THRESHOLD);
1471
- if (Number.isNaN(threshold)) threshold = 0.75;
1472
- threshold = Math.max(0, Math.min(1, threshold));
1473
- return (this.predictions[name2] || 0) >= threshold;
1474
- }
1475
- cameraMsReporter() {
1476
- return this.cameraMs;
1477
- }
1478
- modelLoadMsReporter() {
1479
- return this.modelLoadMs;
1480
- }
1481
- firstRecognitionMsReporter() {
1482
- return this.firstRecognitionMs;
1483
- }
1484
- lastErrorReporter() {
1485
- return this.lastError;
1486
- }
1487
- }
1488
- if (!Scratch.extensions.unsandboxed) {
1489
- throw new Error(
1490
- `TM must run without the extension sandbox. In TurboWarp's custom extension dialog, load the downloaded tm.js from the Files tab with "Run without sandbox" enabled; a URL pasted into the URL tab is always sandboxed.`
1491
- );
1492
- }
1493
- const extension = new TMExtension();
931
+ var TMExtension = class {
932
+ constructor(featureFlags = {}, dependencies = {}) {
933
+ this.featureFlags = {
934
+ ...FEATURE_FLAGS,
935
+ ...featureFlags
936
+ };
937
+ this.tmPoseRuntime = dependencies.poseRuntime ?? dependencies.runtime ?? null;
938
+ this.tmImageRuntime = dependencies.imageRuntime ?? null;
939
+ this.tmAudioRuntime = dependencies.audioRuntime ?? null;
940
+ this.tmComputeRuntime = dependencies.compute ?? null;
941
+ this.computeMode = normalizeComputeMode(dependencies.computeMode);
942
+ this.computeSelection = null;
943
+ this.modelOwnedByExtension = false;
944
+ this.allowRemoteLibraries = dependencies.allowRemoteLibraries ?? true;
945
+ this.onAccumulatedPoseChanged = dependencies.onAccumulatedPoseChanged ?? null;
946
+ this.recognitionMode = "pose";
947
+ this.modelURL = "";
948
+ this.model = null;
949
+ this.webcam = null;
950
+ this.cameraLease = null;
951
+ this.cameraRunning = false;
952
+ this.cameraSelection = "default";
953
+ this.cameraSelectionIsDeviceId = false;
954
+ this.cameraDevices = [];
955
+ this.activeCameraDeviceId = "";
956
+ this.activeCameraDeviceName = "";
957
+ this.cameraSelectionQueue = Promise.resolve();
958
+ this.recognizing = false;
959
+ this.audioListening = false;
960
+ this.loopStarted = false;
961
+ this.loopGeneration = 0;
962
+ this.activeModelOperations = /* @__PURE__ */ new Map();
963
+ this.currentPoseName = "";
964
+ this.score = 0;
965
+ this.predictions = {};
966
+ this.accumulationCoefficient = 1;
967
+ this.decayCoefficient = .9;
968
+ this.activeDecayCoefficient = .9;
969
+ this.accumulatedPoseThreshold = 0;
970
+ this.accumulatedPoseName = "";
971
+ this.accumulatedScore = 0;
972
+ this.accumulatedPredictions = {};
973
+ this.lastAccumulationTime = null;
974
+ this.previewOpacity = .6;
975
+ this.previewPosition = "bottom-right";
976
+ this.previewMirrored = true;
977
+ this.previewVisible = true;
978
+ this.previewCanvas = null;
979
+ this.previewStageElement = null;
980
+ this.poseOverlayVisible = true;
981
+ this.poseOverlayMinimumConfidence = .5;
982
+ this.poseJointStyles = Object.fromEntries(POSE_KEYPOINT_NAMES.map((part) => [part, { ...DEFAULT_POSE_JOINT_STYLE }]));
983
+ this.poseBoneStyle = { ...DEFAULT_POSE_BONE_STYLE };
984
+ this.poseConfidenceScaling = { ...DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING };
985
+ this.poseOverlaySvg = null;
986
+ this.poseOverlayJointElements = /* @__PURE__ */ new Map();
987
+ this.poseOverlayBoneElements = [];
988
+ this.latestPoseKeypoints = [];
989
+ this.cameraMs = 0;
990
+ this.modelLoadMs = 0;
991
+ this.firstRecognitionMs = 0;
992
+ this.lastError = "";
993
+ this.accumulatedPosePausedForBackground = isDocumentHidden();
994
+ this.visibilityChangeListener = () => this.handleDocumentVisibilityChange();
995
+ if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") document.addEventListener("visibilitychange", this.visibilityChangeListener);
996
+ }
997
+ getInfo() {
998
+ return {
999
+ id: EXTENSION_ID,
1000
+ name: Scratch.translate(block_definitions_default.extensionName),
1001
+ docsURI: DOCS_URI,
1002
+ blockIconURI: BLOCK_ICON_URI,
1003
+ blocks: block_definitions_default.blocks.filter((block) => !block.featureFlag || this.featureFlags[block.featureFlag]).map((block) => ({
1004
+ opcode: block.opcode,
1005
+ blockType: Scratch.BlockType[block.blockType],
1006
+ text: Scratch.translate(block.text),
1007
+ ...block.disableMonitor ? { disableMonitor: true } : {},
1008
+ ...block.arguments ? { arguments: Object.fromEntries(Object.entries(block.arguments).map(([name, argument]) => [name, {
1009
+ type: Scratch.ArgumentType[argument.type],
1010
+ defaultValue: argument.defaultValue,
1011
+ ...argument.menu ? { menu: argument.menu } : {}
1012
+ }])) } : {}
1013
+ })),
1014
+ menus: {
1015
+ positionMenu: {
1016
+ acceptReporters: true,
1017
+ items: POSITION_ITEMS.map((item) => ({
1018
+ text: Scratch.translate(item.text),
1019
+ value: item.value
1020
+ }))
1021
+ },
1022
+ previewMirroringMenu: {
1023
+ acceptReporters: true,
1024
+ items: PREVIEW_MIRRORING_ITEMS.map((item) => ({
1025
+ text: Scratch.translate(item.text),
1026
+ value: item.value
1027
+ }))
1028
+ },
1029
+ recognitionModeMenu: {
1030
+ acceptReporters: true,
1031
+ items: RECOGNITION_MODE_ITEMS.map((item) => ({
1032
+ text: Scratch.translate(item.text),
1033
+ value: item.value
1034
+ }))
1035
+ },
1036
+ computeModeMenu: {
1037
+ acceptReporters: true,
1038
+ items: COMPUTE_MODE_ITEMS.map((item) => ({
1039
+ text: Scratch.translate(item.text),
1040
+ value: item.value
1041
+ }))
1042
+ },
1043
+ poseOverlayVisibilityMenu: {
1044
+ acceptReporters: true,
1045
+ items: POSE_OVERLAY_VISIBILITY_ITEMS.map((item) => ({
1046
+ text: Scratch.translate(item.text),
1047
+ value: item.value
1048
+ }))
1049
+ },
1050
+ poseKeypointMenu: {
1051
+ acceptReporters: true,
1052
+ items: POSE_KEYPOINT_NAMES.map((part) => ({
1053
+ text: part,
1054
+ value: part
1055
+ }))
1056
+ },
1057
+ poseConfidencePropertyMenu: {
1058
+ acceptReporters: true,
1059
+ items: POSE_CONFIDENCE_PROPERTY_ITEMS.map((item) => ({
1060
+ text: Scratch.translate(item.text),
1061
+ value: item.value
1062
+ }))
1063
+ },
1064
+ cameraMenu: {
1065
+ acceptReporters: true,
1066
+ items: "getCameraMenuItems"
1067
+ }
1068
+ }
1069
+ };
1070
+ }
1071
+ versionReporter() {
1072
+ return VERSION;
1073
+ }
1074
+ setLastError(error) {
1075
+ this.lastError = String(error?.message ?? error);
1076
+ }
1077
+ setRecognitionMode(args) {
1078
+ const mode = normalizeRecognitionMode(args.MODE);
1079
+ if (mode === this.recognitionMode) return;
1080
+ if (this.recognizing) throw new Error("Teachable Machine: Stop recognition before changing recognition mode.");
1081
+ if (this.cameraRunning) throw new Error("Teachable Machine: Stop the camera before changing recognition mode.");
1082
+ this.recognitionMode = mode;
1083
+ this.model = null;
1084
+ this.modelOwnedByExtension = false;
1085
+ this.modelURL = "";
1086
+ this.modelLoadMs = 0;
1087
+ this.firstRecognitionMs = 0;
1088
+ this.currentPoseName = "";
1089
+ this.score = 0;
1090
+ this.predictions = {};
1091
+ this.clearPoseOverlay();
1092
+ this.resetAccumulatedPose();
1093
+ }
1094
+ recognitionModeReporter() {
1095
+ return this.recognitionMode;
1096
+ }
1097
+ activeComputeRuntime() {
1098
+ if (this.tmComputeRuntime) return this.tmComputeRuntime;
1099
+ const runtime = globalThis.tmCompute;
1100
+ return runtime && typeof runtime.select === "function" ? runtime : null;
1101
+ }
1102
+ /**
1103
+ * TensorFlow.js binds every tensor to the backend that was active when the
1104
+ * tensor was created, so the backend is negotiated before any model loads. A
1105
+ * host that preloads a runtime without a compute controller keeps whatever
1106
+ * backend TensorFlow.js selected for itself.
1107
+ */
1108
+ async ensureComputeBackend() {
1109
+ const compute = this.activeComputeRuntime();
1110
+ if (!compute) return null;
1111
+ const selection = await compute.select(this.computeMode);
1112
+ this.computeSelection = selection;
1113
+ if (selection.fallback) this.setLastError(/* @__PURE__ */ new Error(`Teachable Machine: The ${selection.requested} compute backend was unavailable, so ${selection.backend} is in use.`));
1114
+ return selection;
1115
+ }
1116
+ async setComputeMode(args) {
1117
+ const mode = normalizeComputeMode(args.MODE, this.computeMode);
1118
+ if (mode === this.computeMode) return;
1119
+ if (this.recognizing) throw new Error("Teachable Machine: Stop recognition before changing the compute mode.");
1120
+ if (this.model && !this.modelOwnedByExtension) throw new Error("Teachable Machine: Release the prepared model before changing the compute mode.");
1121
+ const previousModel = this.model;
1122
+ this.computeMode = mode;
1123
+ this.computeSelection = null;
1124
+ this.model = null;
1125
+ this.modelOwnedByExtension = false;
1126
+ this.modelLoadMs = 0;
1127
+ this.firstRecognitionMs = 0;
1128
+ if (previousModel) await this.releaseOwnedModel(previousModel);
1129
+ if (this.activeComputeRuntime()) await this.ensureComputeBackend();
1130
+ }
1131
+ async releaseOwnedModel(model) {
1132
+ try {
1133
+ await this.waitForPreparedModelIdle(model);
1134
+ await model.dispose?.();
1135
+ } catch (error) {
1136
+ this.setLastError(error);
1137
+ }
1138
+ }
1139
+ computeModeReporter() {
1140
+ return this.computeMode;
1141
+ }
1142
+ computeBackendReporter() {
1143
+ const compute = this.activeComputeRuntime();
1144
+ if (!compute) return "";
1145
+ return this.computeSelection?.backend ?? compute.getBackend() ?? "";
1146
+ }
1147
+ setModelURL(args) {
1148
+ this.modelURL = String(args.URL || "").trim();
1149
+ if (this.modelURL && !this.modelURL.endsWith("/")) this.modelURL += "/";
1150
+ this.model = null;
1151
+ this.modelLoadMs = 0;
1152
+ this.firstRecognitionMs = 0;
1153
+ }
1154
+ activeRuntime() {
1155
+ if (this.recognitionMode === "audio") return this.tmAudioRuntime;
1156
+ return this.recognitionMode === "image" ? this.tmImageRuntime : this.tmPoseRuntime;
1157
+ }
1158
+ async ensureLibrariesLoaded() {
1159
+ if (this.activeRuntime()) {
1160
+ if (this.activeComputeRuntime()) await this.ensureComputeBackend();
1161
+ return;
1162
+ }
1163
+ if (!this.allowRemoteLibraries) throw new Error("Teachable Machine: A preloaded runtime is required.");
1164
+ if (typeof globalThis.tf === "undefined" || typeof globalThis.tmPose === "undefined" || typeof globalThis.tmImage === "undefined" || typeof globalThis.tmAudio === "undefined") await loadScript(BROWSER_RUNTIME_URL);
1165
+ if (typeof globalThis.tf === "undefined" || typeof globalThis.tmPose === "undefined" || typeof globalThis.tmImage === "undefined" || typeof globalThis.tmAudio === "undefined") throw new Error("Teachable Machine: The reviewed browser runtime could not be loaded.");
1166
+ this.tmPoseRuntime = globalThis.tmPose;
1167
+ this.tmImageRuntime = globalThis.tmImage;
1168
+ this.tmAudioRuntime = globalThis.tmAudio;
1169
+ await this.ensureComputeBackend();
1170
+ }
1171
+ cleanupCameraResources() {
1172
+ const lease = this.cameraLease;
1173
+ this.cameraLease = null;
1174
+ const video = this.webcam?.webcam;
1175
+ if (lease) lease.release();
1176
+ else if (video?.srcObject) {
1177
+ video.srcObject.getTracks().forEach((track) => track.stop());
1178
+ video.srcObject = null;
1179
+ }
1180
+ this.previewCanvas?.parentNode?.removeChild(this.previewCanvas);
1181
+ this.poseOverlaySvg?.parentNode?.removeChild(this.poseOverlaySvg);
1182
+ this.previewCanvas = null;
1183
+ this.previewStageElement = null;
1184
+ this.poseOverlaySvg = null;
1185
+ this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1186
+ this.poseOverlayBoneElements = [];
1187
+ this.latestPoseKeypoints = [];
1188
+ this.webcam = null;
1189
+ this.cameraRunning = false;
1190
+ this.activeCameraDeviceId = "";
1191
+ this.activeCameraDeviceName = "";
1192
+ this.loopStarted = false;
1193
+ this.loopGeneration += 1;
1194
+ }
1195
+ async startCamera() {
1196
+ if (this.recognitionMode === "audio") throw new Error("Teachable Machine: Audio mode uses the microphone through start recognition.");
1197
+ if (this.cameraRunning && this.webcam) {
1198
+ this.attachPreviewToStage();
1199
+ return;
1200
+ }
1201
+ try {
1202
+ this.lastError = "";
1203
+ const startedAt = performance.now();
1204
+ await this.ensureLibrariesLoaded();
1205
+ const runtime = this.activeRuntime();
1206
+ if (!runtime) throw new Error("Teachable Machine: Runtime is unavailable.");
1207
+ if (!("Webcam" in runtime)) throw new Error("Teachable Machine: Camera runtime is unavailable in the current mode.");
1208
+ this.webcam = new runtime.Webcam(320, 240, true);
1209
+ const lease = await this.acquireSharedCamera();
1210
+ if (lease) {
1211
+ this.cameraLease = lease;
1212
+ this.webcam.webcam = lease.getFrameSource().element;
1213
+ }
1214
+ const constraints = lease ? void 0 : cameraConstraints(this.resolvedCameraSelection());
1215
+ if (constraints) await this.webcam.setup(constraints);
1216
+ else await this.webcam.setup();
1217
+ initializeCameraReadbackContext(this.webcam.canvas);
1218
+ if (!lease) await this.webcam.play();
1219
+ this.attachPreviewToStage();
1220
+ this.cameraRunning = true;
1221
+ try {
1222
+ await this.refreshCameraDevices();
1223
+ } catch {}
1224
+ this.updateActiveCameraInfo();
1225
+ this.cameraMs = Math.round(performance.now() - startedAt);
1226
+ this.startLoopIfNeeded();
1227
+ } catch (error) {
1228
+ this.cleanupCameraResources();
1229
+ this.setLastError(error);
1230
+ throw error;
1231
+ }
1232
+ }
1233
+ /**
1234
+ * A lease on the shared camera, or null when Camera Source is not loaded.
1235
+ *
1236
+ * Camera Source owns the device and decides when a stream stops, which is what lets one camera
1237
+ * serve pose recognition and anything else reading frames at the same time. It is a separate
1238
+ * TurboWarp extension, and this one is also distributed for standalone URL loading, so its
1239
+ * absence is ordinary rather than a failure: the self-acquired path stays for it.
1240
+ *
1241
+ * One camera id for every visual recognition mode. Opening a second device for a work that
1242
+ * recognises both poses and images is not what sharing is for, and `pose` is the role name
1243
+ * Camera Source's own documentation uses for this consumer.
1244
+ */
1245
+ async acquireSharedCamera() {
1246
+ const cameraSource = readCameraSource(Scratch.vm?.runtime);
1247
+ if (!cameraSource) return null;
1248
+ const selection = this.resolvedCameraSelection();
1249
+ const options = {
1250
+ owner: package_default.name,
1251
+ cameraId: "pose"
1252
+ };
1253
+ if (selection.kind === "device") options.deviceId = selection.value;
1254
+ return cameraSource.acquireCamera(options);
1255
+ }
1256
+ stopCamera() {
1257
+ try {
1258
+ this.stopRecognition();
1259
+ this.cleanupCameraResources();
1260
+ this.currentPoseName = "";
1261
+ this.score = 0;
1262
+ this.predictions = {};
1263
+ } catch (error) {
1264
+ this.setLastError(error);
1265
+ throw error;
1266
+ }
1267
+ }
1268
+ isCameraRunning() {
1269
+ return this.cameraRunning;
1270
+ }
1271
+ getCameraMenuItems() {
1272
+ const fixedItems = CAMERA_SELECTION_ITEMS.map((item) => ({
1273
+ text: Scratch.translate(item.text),
1274
+ value: item.value
1275
+ }));
1276
+ const seen = /* @__PURE__ */ new Set();
1277
+ const deviceItems = [];
1278
+ this.cameraDevices.forEach((device, index) => {
1279
+ if (!device.deviceId || seen.has(device.deviceId)) return;
1280
+ seen.add(device.deviceId);
1281
+ deviceItems.push({
1282
+ text: device.label || `${Scratch.translate("camera")} ${index + 1}`,
1283
+ value: device.deviceId
1284
+ });
1285
+ });
1286
+ return [...fixedItems, ...deviceItems];
1287
+ }
1288
+ async refreshCameraDevices() {
1289
+ const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
1290
+ if (!mediaDevices || typeof mediaDevices.enumerateDevices !== "function") throw new Error("TM: Camera enumeration is not available in this browser.");
1291
+ const devices = await mediaDevices.enumerateDevices();
1292
+ this.cameraDevices = devices.filter((device) => device.kind === "videoinput").map((device) => ({
1293
+ deviceId: device.deviceId,
1294
+ label: device.label
1295
+ }));
1296
+ return this.cameraDevices;
1297
+ }
1298
+ async refreshCameraList() {
1299
+ try {
1300
+ this.lastError = "";
1301
+ await this.refreshCameraDevices();
1302
+ if (this.cameraRunning) this.updateActiveCameraInfo();
1303
+ } catch (error) {
1304
+ this.setLastError(error);
1305
+ throw error;
1306
+ }
1307
+ }
1308
+ setCameraSelection(args) {
1309
+ const selection = normalizeCameraSelection(args.CAMERA);
1310
+ return this.enqueueCameraSelection(selection);
1311
+ }
1312
+ setCameraDeviceId(deviceId) {
1313
+ if (typeof deviceId !== "string" || deviceId.trim().length === 0) return Promise.reject(/* @__PURE__ */ new Error("TM: Camera device ID must be a non-empty string."));
1314
+ return this.enqueueCameraSelection({
1315
+ kind: "device",
1316
+ value: deviceId
1317
+ });
1318
+ }
1319
+ resolvedCameraSelection() {
1320
+ return this.cameraSelectionIsDeviceId ? {
1321
+ kind: "device",
1322
+ value: this.cameraSelection
1323
+ } : {
1324
+ kind: "preference",
1325
+ value: this.cameraSelection
1326
+ };
1327
+ }
1328
+ enqueueCameraSelection(selection) {
1329
+ const operation = this.cameraSelectionQueue.then(() => this.applyCameraSelection(selection));
1330
+ this.cameraSelectionQueue = operation.catch(() => void 0);
1331
+ return operation;
1332
+ }
1333
+ async applyCameraSelection(selection) {
1334
+ const previousSelection = this.cameraSelection;
1335
+ const previousSelectionIsDeviceId = this.cameraSelectionIsDeviceId;
1336
+ const wasRunning = this.cameraRunning;
1337
+ this.cameraSelection = selection.value;
1338
+ this.cameraSelectionIsDeviceId = selection.kind === "device";
1339
+ if (!wasRunning) return;
1340
+ this.cleanupCameraResources();
1341
+ try {
1342
+ await this.startCamera();
1343
+ } catch (switchError) {
1344
+ this.cameraSelection = previousSelection;
1345
+ this.cameraSelectionIsDeviceId = previousSelectionIsDeviceId;
1346
+ try {
1347
+ await this.startCamera();
1348
+ } catch (rollbackError) {
1349
+ const error = new AggregateError([switchError, rollbackError], "TM: Camera switch and rollback both failed.");
1350
+ this.setLastError(error);
1351
+ throw error;
1352
+ }
1353
+ this.setLastError(switchError);
1354
+ throw switchError;
1355
+ }
1356
+ }
1357
+ updateActiveCameraInfo() {
1358
+ const stream = this.webcam?.webcam?.srcObject;
1359
+ const videoTrack = stream?.getVideoTracks?.()[0] ?? stream?.getTracks?.().find((track) => track.kind === "video" || track.kind === void 0);
1360
+ const settings = videoTrack?.getSettings?.() ?? {};
1361
+ const selectedDeviceId = this.cameraSelectionIsDeviceId ? this.cameraSelection : "";
1362
+ this.activeCameraDeviceId = String(settings.deviceId || selectedDeviceId);
1363
+ const device = this.cameraDevices.find((candidate) => candidate.deviceId === this.activeCameraDeviceId);
1364
+ this.activeCameraDeviceName = String(videoTrack?.label || device?.label || "");
1365
+ }
1366
+ cameraCountReporter() {
1367
+ return this.cameraDevices.length;
1368
+ }
1369
+ cameraDeviceIdReporter() {
1370
+ return this.activeCameraDeviceId;
1371
+ }
1372
+ cameraDeviceNameReporter() {
1373
+ return this.activeCameraDeviceName;
1374
+ }
1375
+ showPreview() {
1376
+ try {
1377
+ this.previewVisible = true;
1378
+ if (!this.webcam?.canvas) throw new Error("TM: Start the camera before showing the preview.");
1379
+ this.attachPreviewToStage();
1380
+ this.previewCanvas.style.display = "block";
1381
+ this.updatePoseOverlayVisibility();
1382
+ this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
1383
+ } catch (error) {
1384
+ this.setLastError(error);
1385
+ throw error;
1386
+ }
1387
+ }
1388
+ hidePreview() {
1389
+ this.previewVisible = false;
1390
+ if (this.previewCanvas) this.previewCanvas.style.display = "none";
1391
+ this.updatePoseOverlayVisibility();
1392
+ }
1393
+ isPreviewVisible() {
1394
+ return this.previewVisible;
1395
+ }
1396
+ setPreviewOpacity(args) {
1397
+ let opacity = args.OPACITY === "" ? .6 : Number(args.OPACITY);
1398
+ if (Number.isNaN(opacity)) opacity = .6;
1399
+ this.previewOpacity = Math.max(0, Math.min(1, opacity));
1400
+ if (this.previewCanvas) this.previewCanvas.style.opacity = String(this.previewOpacity);
1401
+ }
1402
+ setPreviewPosition(args) {
1403
+ this.previewPosition = normalizePosition(args.POSITION);
1404
+ if (this.previewCanvas) {
1405
+ this.updatePreviewStyle();
1406
+ this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
1407
+ }
1408
+ }
1409
+ setPreviewMirroring(args) {
1410
+ this.previewMirrored = normalizePreviewMirroring(args.MIRRORING);
1411
+ if (this.previewCanvas) this.updatePreviewStyle();
1412
+ }
1413
+ previewMirroringReporter() {
1414
+ return this.previewMirrored ? "mirrored" : "unmirrored";
1415
+ }
1416
+ setPoseOverlayVisibility(args) {
1417
+ this.poseOverlayVisible = normalizePoseOverlayVisibility(args.VISIBILITY);
1418
+ this.updatePoseOverlayVisibility();
1419
+ }
1420
+ showPoseOverlay() {
1421
+ this.poseOverlayVisible = true;
1422
+ this.updatePoseOverlayVisibility();
1423
+ }
1424
+ hidePoseOverlay() {
1425
+ this.poseOverlayVisible = false;
1426
+ this.updatePoseOverlayVisibility();
1427
+ }
1428
+ isPoseOverlayVisible() {
1429
+ return this.featureFlags.poseOverlay && this.poseOverlayVisible;
1430
+ }
1431
+ setPoseJointStyle(args) {
1432
+ const part = String(args.PART ?? "");
1433
+ if (!isPoseKeypointName(part)) throw new Error(`TM: Unknown PoseNet joint: ${part}`);
1434
+ const previous = this.poseJointStyles[part];
1435
+ this.poseJointStyles[part] = {
1436
+ color: normalizePoseColor(args.COLOR, previous.color),
1437
+ opacity: normalizePoseStyleNumber(args.OPACITY, previous.opacity, 1),
1438
+ radius: normalizePoseStyleNumber(args.RADIUS, previous.radius)
1439
+ };
1440
+ this.redrawPoseOverlay();
1441
+ }
1442
+ setPoseBoneStyle(args) {
1443
+ this.poseBoneStyle = {
1444
+ color: normalizePoseColor(args.COLOR, this.poseBoneStyle.color),
1445
+ opacity: normalizePoseStyleNumber(args.OPACITY, this.poseBoneStyle.opacity, 1),
1446
+ width: normalizePoseStyleNumber(args.WIDTH, this.poseBoneStyle.width)
1447
+ };
1448
+ this.redrawPoseOverlay();
1449
+ }
1450
+ setPoseOverlayMinimumConfidence(args) {
1451
+ this.poseOverlayMinimumConfidence = normalizePoseStyleNumber(args.CONFIDENCE, this.poseOverlayMinimumConfidence, 1);
1452
+ this.redrawPoseOverlay();
1453
+ }
1454
+ setPoseConfidenceScaling(args) {
1455
+ const property = String(args.PROPERTY ?? "");
1456
+ const key = {
1457
+ "joint-opacity": "jointOpacity",
1458
+ "joint-radius": "jointRadius",
1459
+ "bone-opacity": "boneOpacity",
1460
+ "bone-width": "boneWidth"
1461
+ }[property];
1462
+ if (!key) throw new Error(`TM: Unknown confidence-scaled property: ${property}`);
1463
+ this.poseConfidenceScaling[key] = normalizePoseOverlayVisibility(args.STATE);
1464
+ this.redrawPoseOverlay();
1465
+ }
1466
+ async loadModel() {
1467
+ if (this.model) return;
1468
+ if (!this.modelURL) throw new Error("Teachable Machine: Set the model URL first.");
1469
+ try {
1470
+ this.lastError = "";
1471
+ const startedAt = performance.now();
1472
+ await this.ensureLibrariesLoaded();
1473
+ const runtime = this.activeRuntime();
1474
+ if (!runtime || typeof runtime.load !== "function") throw new Error("Teachable Machine: The URL loader is not available.");
1475
+ this.model = await runtime.load(this.modelURL + "model.json", this.modelURL + "metadata.json");
1476
+ this.modelOwnedByExtension = true;
1477
+ this.modelLoadMs = Math.round(performance.now() - startedAt);
1478
+ } catch (error) {
1479
+ this.setLastError(error);
1480
+ throw error;
1481
+ }
1482
+ }
1483
+ isModelLoaded() {
1484
+ return Boolean(this.model);
1485
+ }
1486
+ usePreparedModel(model) {
1487
+ if (!model || typeof model !== "object") throw new TypeError("TM: Prepared model must be an object.");
1488
+ if (this.recognizing && this.model !== model) throw new Error("TM: Stop recognition before changing the active model.");
1489
+ this.model = model;
1490
+ this.modelOwnedByExtension = false;
1491
+ this.modelURL = "";
1492
+ this.modelLoadMs = 0;
1493
+ this.firstRecognitionMs = 0;
1494
+ }
1495
+ clearPreparedModel(model) {
1496
+ if (model !== void 0 && this.model !== model) return;
1497
+ this.stopRecognition();
1498
+ this.model = null;
1499
+ this.modelOwnedByExtension = false;
1500
+ this.modelURL = "";
1501
+ this.modelLoadMs = 0;
1502
+ this.firstRecognitionMs = 0;
1503
+ }
1504
+ async startRecognition() {
1505
+ try {
1506
+ this.lastError = "";
1507
+ const startingNewSession = !this.recognizing;
1508
+ if (this.recognitionMode !== "audio" && !this.cameraRunning) await this.startCamera();
1509
+ await this.loadModel();
1510
+ if (startingNewSession && this.featureFlags.temporalPoseScoring) this.startAccumulatedPoseSession();
1511
+ this.recognizing = true;
1512
+ if (this.recognitionMode === "audio") await this.startAudioRecognition();
1513
+ else this.startLoopIfNeeded();
1514
+ } catch (error) {
1515
+ this.setLastError(error);
1516
+ throw error;
1517
+ }
1518
+ }
1519
+ stopRecognition() {
1520
+ this.recognizing = false;
1521
+ this.stopAudioRecognition();
1522
+ this.currentPoseName = "";
1523
+ this.score = 0;
1524
+ this.predictions = {};
1525
+ this.clearPoseOverlay();
1526
+ this.resetAccumulatedPose("stop");
1527
+ }
1528
+ isRecognizing() {
1529
+ return this.recognizing;
1530
+ }
1531
+ async startAudioRecognition() {
1532
+ if (!this.model || typeof this.model.listen !== "function") throw new Error("Teachable Machine: The active model does not support audio recognition.");
1533
+ if (typeof this.model.isListening === "function" && this.model.isListening()) {
1534
+ this.audioListening = true;
1535
+ return;
1536
+ }
1537
+ const first = this.firstRecognitionMs === 0;
1538
+ const startedAt = first ? performance.now() : 0;
1539
+ await this.model.listen(async (result) => {
1540
+ if (!this.recognizing || this.recognitionMode !== "audio" || this.model === null) return;
1541
+ if (first && this.firstRecognitionMs === 0) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
1542
+ this.applyPredictions(this.audioPredictionFromResult(this.model, result));
1543
+ }, {
1544
+ probabilityThreshold: 0,
1545
+ overlapFactor: .5,
1546
+ invokeCallbackOnNoiseAndUnknown: true
1547
+ });
1548
+ this.audioListening = true;
1549
+ }
1550
+ stopAudioRecognition() {
1551
+ if (!this.audioListening || !this.model || typeof this.model.stopListening !== "function") {
1552
+ this.audioListening = false;
1553
+ return;
1554
+ }
1555
+ this.audioListening = false;
1556
+ this.model.stopListening().catch((error) => this.setLastError(error));
1557
+ }
1558
+ audioPredictionFromResult(model, result) {
1559
+ const labels = typeof model.wordLabels === "function" ? model.wordLabels() : [];
1560
+ const scores = Array.isArray(result?.scores) ? result.scores[0] : result?.scores;
1561
+ if (!labels.length || !scores || typeof scores.length !== "number") throw new Error("Teachable Machine: Audio recognition did not return labels and scores.");
1562
+ return labels.map((className, index) => ({
1563
+ className,
1564
+ probability: Number(scores[index] ?? 0)
1565
+ }));
1566
+ }
1567
+ applyPredictions(prediction) {
1568
+ let best = {
1569
+ className: "",
1570
+ probability: 0
1571
+ };
1572
+ this.predictions = {};
1573
+ for (const result of prediction) {
1574
+ this.predictions[result.className] = result.probability;
1575
+ if (result.probability > best.probability) best = result;
1576
+ }
1577
+ this.currentPoseName = best.className;
1578
+ this.score = best.probability;
1579
+ if (this.featureFlags.temporalPoseScoring) this.updateAccumulatedPose(prediction);
1580
+ }
1581
+ findStageElement() {
1582
+ try {
1583
+ const stageCanvas = this.findLikelyStageCanvas();
1584
+ if (stageCanvas.parentElement) return stageCanvas.parentElement;
1585
+ } catch {}
1586
+ const editorStage = document.querySelector(".stage_stage-wrapper_2bejr") || document.querySelector("[class*=\"stage_stage-wrapper\"]") || document.querySelector("[class*=\"stage-wrapper\"]") || document.querySelector("[class*=\"stage-wrapper_stage-wrapper\"]");
1587
+ if (editorStage) return editorStage;
1588
+ throw new Error("TM: TurboWarp stage element was not found.");
1589
+ }
1590
+ findLikelyStageCanvas() {
1591
+ const webcamCanvas = this.webcam?.canvas ?? null;
1592
+ const allCanvases = Array.from(document.querySelectorAll("canvas")).filter((canvas) => canvas !== webcamCanvas && canvas !== this.previewCanvas);
1593
+ const visibleCandidates = allCanvases.map((canvas) => {
1594
+ const rect = canvas.getBoundingClientRect();
1595
+ return {
1596
+ canvas,
1597
+ rect,
1598
+ style: window.getComputedStyle(canvas),
1599
+ score: canvasScore(rect.width, rect.height)
1600
+ };
1601
+ }).filter((item) => item.rect.width >= 200 && item.rect.height >= 150).filter((item) => item.style.display !== "none").filter((item) => item.style.visibility !== "hidden").filter((item) => Number(item.style.opacity || 1) !== 0).sort((left, right) => right.score - left.score);
1602
+ if (visibleCandidates[0]) return visibleCandidates[0].canvas;
1603
+ const fallbackCandidates = allCanvases.map((canvas) => ({
1604
+ canvas,
1605
+ score: canvasScore(canvas.width, canvas.height)
1606
+ })).filter((item) => item.canvas.width >= 200 && item.canvas.height >= 150).sort((left, right) => right.score - left.score);
1607
+ if (fallbackCandidates[0]) return fallbackCandidates[0].canvas;
1608
+ throw new Error("TM: No likely stage canvas was found. The editor or packager DOM may be unsupported.");
1609
+ }
1610
+ validatePreviewAttachment(stage, canvas) {
1611
+ if (!stage || !canvas || canvas.parentElement !== stage) throw new Error("TM: Preview canvas was not attached to the stage.");
1612
+ const canvasStyle = window.getComputedStyle(canvas);
1613
+ if (canvasStyle.display === "none" && this.previewVisible) throw new Error("TM: Preview canvas is hidden by display:none.");
1614
+ if (canvasStyle.visibility === "hidden" && this.previewVisible) throw new Error("TM: Preview canvas is hidden by visibility:hidden.");
1615
+ const stageRect = stage.getBoundingClientRect();
1616
+ const canvasRect = canvas.getBoundingClientRect();
1617
+ const documentHidden = typeof document.visibilityState === "string" && document.visibilityState === "hidden";
1618
+ const layoutUnavailable = stageRect.width === 0 || stageRect.height === 0;
1619
+ if (!documentHidden && !layoutUnavailable && this.previewVisible) {
1620
+ if (canvasRect.width <= 0 || canvasRect.height <= 0) throw new Error("TM: Preview canvas was attached but has zero size.");
1621
+ if (!rectanglesIntersect(stageRect, canvasRect)) throw new Error("TM: Preview canvas does not intersect the stage.");
1622
+ }
1623
+ }
1624
+ createSvgElement(name) {
1625
+ if (typeof document.createElementNS === "function") return document.createElementNS("http://www.w3.org/2000/svg", name);
1626
+ return document.createElement(name);
1627
+ }
1628
+ ensurePoseOverlayElement() {
1629
+ if (!this.featureFlags.poseOverlay) return null;
1630
+ if (this.poseOverlaySvg) return this.poseOverlaySvg;
1631
+ const svg = this.createSvgElement("svg");
1632
+ svg.setAttribute("viewBox", "0 0 320 240");
1633
+ svg.setAttribute("width", "320");
1634
+ svg.setAttribute("height", "240");
1635
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1636
+ svg.setAttribute("aria-hidden", "true");
1637
+ svg.setAttribute("focusable", "false");
1638
+ const boneGroup = this.createSvgElement("g");
1639
+ boneGroup.setAttribute("data-layer", "bones");
1640
+ this.poseOverlayBoneElements = POSE_BONE_CONNECTIONS.map(([first, second]) => {
1641
+ const line = this.createSvgElement("line");
1642
+ line.setAttribute("data-bone", `${first}-${second}`);
1643
+ line.setAttribute("stroke-linecap", "round");
1644
+ line.style.display = "none";
1645
+ boneGroup.appendChild(line);
1646
+ return {
1647
+ first,
1648
+ second,
1649
+ line
1650
+ };
1651
+ });
1652
+ svg.appendChild(boneGroup);
1653
+ const jointGroup = this.createSvgElement("g");
1654
+ jointGroup.setAttribute("data-layer", "joints");
1655
+ this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1656
+ for (const part of POSE_KEYPOINT_NAMES) {
1657
+ const circle = this.createSvgElement("circle");
1658
+ circle.setAttribute("data-joint", part);
1659
+ circle.style.display = "none";
1660
+ jointGroup.appendChild(circle);
1661
+ this.poseOverlayJointElements.set(part, circle);
1662
+ }
1663
+ svg.appendChild(jointGroup);
1664
+ this.poseOverlaySvg = svg;
1665
+ return svg;
1666
+ }
1667
+ updatePoseOverlayVisibility() {
1668
+ if (!this.poseOverlaySvg) return;
1669
+ this.poseOverlaySvg.style.display = this.previewVisible && this.poseOverlayVisible ? "block" : "none";
1670
+ }
1671
+ clearPoseOverlay() {
1672
+ this.latestPoseKeypoints = [];
1673
+ for (const circle of this.poseOverlayJointElements.values()) circle.style.display = "none";
1674
+ for (const { line } of this.poseOverlayBoneElements) line.style.display = "none";
1675
+ }
1676
+ redrawPoseOverlay() {
1677
+ if (this.latestPoseKeypoints.length > 0) this.renderPoseOverlay(this.latestPoseKeypoints);
1678
+ }
1679
+ renderPoseOverlay(keypoints) {
1680
+ if (!this.featureFlags.poseOverlay || !this.poseOverlaySvg || !Array.isArray(keypoints)) {
1681
+ this.clearPoseOverlay();
1682
+ return;
1683
+ }
1684
+ this.latestPoseKeypoints = keypoints;
1685
+ const recognized = /* @__PURE__ */ new Map();
1686
+ for (const candidate of keypoints) {
1687
+ if (typeof candidate !== "object" || candidate === null || !isPoseKeypointName(candidate.part)) continue;
1688
+ const keypoint = candidate;
1689
+ const x = Number(keypoint.position?.x);
1690
+ const y = Number(keypoint.position?.y);
1691
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
1692
+ recognized.set(keypoint.part, {
1693
+ part: keypoint.part,
1694
+ score: confidenceMultiplier(keypoint.score),
1695
+ position: {
1696
+ x,
1697
+ y
1698
+ }
1699
+ });
1700
+ }
1701
+ for (const part of POSE_KEYPOINT_NAMES) {
1702
+ const circle = this.poseOverlayJointElements.get(part);
1703
+ const keypoint = recognized.get(part);
1704
+ if (!circle || !keypoint || keypoint.score < this.poseOverlayMinimumConfidence) {
1705
+ if (circle) circle.style.display = "none";
1706
+ continue;
1707
+ }
1708
+ const style = this.poseJointStyles[part];
1709
+ const opacityMultiplier = this.poseConfidenceScaling.jointOpacity ? keypoint.score : 1;
1710
+ const radiusMultiplier = this.poseConfidenceScaling.jointRadius ? keypoint.score : 1;
1711
+ circle.setAttribute("cx", String(keypoint.position.x));
1712
+ circle.setAttribute("cy", String(keypoint.position.y));
1713
+ circle.setAttribute("r", String(style.radius * radiusMultiplier));
1714
+ circle.setAttribute("fill", style.color);
1715
+ circle.setAttribute("fill-opacity", String(style.opacity * opacityMultiplier));
1716
+ circle.style.display = "block";
1717
+ }
1718
+ for (const { first, second, line } of this.poseOverlayBoneElements) {
1719
+ const firstKeypoint = recognized.get(first);
1720
+ const secondKeypoint = recognized.get(second);
1721
+ if (!firstKeypoint || !secondKeypoint || firstKeypoint.score < this.poseOverlayMinimumConfidence || secondKeypoint.score < this.poseOverlayMinimumConfidence) {
1722
+ line.style.display = "none";
1723
+ continue;
1724
+ }
1725
+ const confidence = Math.min(firstKeypoint.score, secondKeypoint.score);
1726
+ const opacityMultiplier = this.poseConfidenceScaling.boneOpacity ? confidence : 1;
1727
+ const widthMultiplier = this.poseConfidenceScaling.boneWidth ? confidence : 1;
1728
+ line.setAttribute("x1", String(firstKeypoint.position.x));
1729
+ line.setAttribute("y1", String(firstKeypoint.position.y));
1730
+ line.setAttribute("x2", String(secondKeypoint.position.x));
1731
+ line.setAttribute("y2", String(secondKeypoint.position.y));
1732
+ line.setAttribute("stroke", this.poseBoneStyle.color);
1733
+ line.setAttribute("stroke-opacity", String(this.poseBoneStyle.opacity * opacityMultiplier));
1734
+ line.setAttribute("stroke-width", String(this.poseBoneStyle.width * widthMultiplier));
1735
+ line.style.display = "block";
1736
+ }
1737
+ }
1738
+ attachPreviewToStage() {
1739
+ if (!this.webcam) throw new Error("TM: Start the camera before attaching the preview.");
1740
+ if (!this.webcam.canvas) throw new Error("TM: webcam.canvas is unavailable.");
1741
+ const stage = this.findStageElement();
1742
+ const canvas = this.webcam.canvas;
1743
+ let stageCanvas = null;
1744
+ try {
1745
+ const candidate = this.findLikelyStageCanvas();
1746
+ if (candidate.parentElement === stage) stageCanvas = candidate;
1747
+ } catch {}
1748
+ this.previewCanvas = canvas;
1749
+ this.previewStageElement = stage;
1750
+ const overlay = this.ensurePoseOverlayElement();
1751
+ if (window.getComputedStyle(stage).position === "static") stage.style.position = "relative";
1752
+ stage.style.overflow = "hidden";
1753
+ Object.assign(canvas.style, {
1754
+ position: "absolute",
1755
+ zIndex: "auto",
1756
+ pointerEvents: "none",
1757
+ border: "2px solid rgba(255, 255, 255, 0.7)",
1758
+ borderRadius: "8px",
1759
+ background: "#000",
1760
+ opacity: String(this.previewOpacity),
1761
+ display: this.previewVisible ? "block" : "none",
1762
+ boxSizing: "border-box"
1763
+ });
1764
+ if (overlay) Object.assign(overlay.style, {
1765
+ position: "absolute",
1766
+ zIndex: "auto",
1767
+ pointerEvents: "none",
1768
+ border: "2px solid transparent",
1769
+ background: "transparent",
1770
+ display: this.previewVisible && this.poseOverlayVisible ? "block" : "none",
1771
+ boxSizing: "border-box",
1772
+ overflow: "hidden"
1773
+ });
1774
+ let insertionPoint = stageCanvas?.nextSibling ?? null;
1775
+ while (insertionPoint && (insertionPoint === canvas || insertionPoint === overlay)) insertionPoint = insertionPoint.nextSibling;
1776
+ if (stageCanvas && typeof stage.insertBefore === "function") stage.insertBefore(canvas, insertionPoint);
1777
+ else if (canvas.parentNode !== stage) stage.appendChild(canvas);
1778
+ if (overlay) {
1779
+ if (stageCanvas && typeof stage.insertBefore === "function") stage.insertBefore(overlay, insertionPoint);
1780
+ else if (overlay.parentNode !== stage) stage.appendChild(overlay);
1781
+ }
1782
+ this.updatePreviewStyle();
1783
+ this.updatePoseOverlayVisibility();
1784
+ this.validatePreviewAttachment(stage, canvas);
1785
+ }
1786
+ updatePreviewStyle() {
1787
+ const canvas = this.previewCanvas;
1788
+ if (!canvas) throw new Error("TM: Start the camera before positioning the preview.");
1789
+ const targets = [canvas, this.poseOverlaySvg].filter(Boolean);
1790
+ for (const target of targets) Object.assign(target.style, {
1791
+ left: "",
1792
+ right: "",
1793
+ top: "",
1794
+ bottom: "",
1795
+ transform: "",
1796
+ objectFit: "",
1797
+ width: "35%",
1798
+ height: "auto",
1799
+ borderRadius: "8px"
1800
+ });
1801
+ let positionTransform = "";
1802
+ switch (this.previewPosition) {
1803
+ case "top-left":
1804
+ for (const target of targets) {
1805
+ target.style.left = "8px";
1806
+ target.style.top = "8px";
1807
+ }
1808
+ break;
1809
+ case "top-right":
1810
+ for (const target of targets) {
1811
+ target.style.right = "8px";
1812
+ target.style.top = "8px";
1813
+ }
1814
+ break;
1815
+ case "bottom-left":
1816
+ for (const target of targets) {
1817
+ target.style.left = "8px";
1818
+ target.style.bottom = "8px";
1819
+ }
1820
+ break;
1821
+ case "center":
1822
+ for (const target of targets) {
1823
+ target.style.left = "50%";
1824
+ target.style.top = "50%";
1825
+ }
1826
+ positionTransform = "translate(-50%, -50%)";
1827
+ break;
1828
+ case "full-stage":
1829
+ for (const target of targets) {
1830
+ target.style.left = "0";
1831
+ target.style.top = "0";
1832
+ target.style.width = "100%";
1833
+ target.style.height = "100%";
1834
+ target.style.objectFit = "cover";
1835
+ target.style.borderRadius = "0";
1836
+ }
1837
+ break;
1838
+ default: for (const target of targets) {
1839
+ target.style.right = "8px";
1840
+ target.style.bottom = "8px";
1841
+ }
1842
+ }
1843
+ const mirroringTransform = this.previewMirrored ? "" : "scaleX(-1)";
1844
+ for (const target of targets) target.style.transform = [positionTransform, mirroringTransform].filter(Boolean).join(" ");
1845
+ this.poseOverlaySvg?.setAttribute("preserveAspectRatio", this.previewPosition === "full-stage" ? "xMidYMid slice" : "xMidYMid meet");
1846
+ }
1847
+ startLoopIfNeeded() {
1848
+ if (this.loopStarted || !this.cameraRunning || !this.webcam) return;
1849
+ this.loopStarted = true;
1850
+ const generation = ++this.loopGeneration;
1851
+ this.loop(generation);
1852
+ }
1853
+ trackPreparedModelOperation(model, operation) {
1854
+ let active = this.activeModelOperations.get(model);
1855
+ if (!active) {
1856
+ active = /* @__PURE__ */ new Set();
1857
+ this.activeModelOperations.set(model, active);
1858
+ }
1859
+ active.add(operation);
1860
+ operation.then(() => {
1861
+ active.delete(operation);
1862
+ if (active.size === 0) this.activeModelOperations.delete(model);
1863
+ }, () => {
1864
+ active.delete(operation);
1865
+ if (active.size === 0) this.activeModelOperations.delete(model);
1866
+ });
1867
+ return operation;
1868
+ }
1869
+ async waitForPreparedModelIdle(model) {
1870
+ await Promise.allSettled([...this.activeModelOperations.get(model) ?? []]);
1871
+ }
1872
+ async loop(generation = this.loopGeneration) {
1873
+ if (generation !== this.loopGeneration || !this.cameraRunning || !this.webcam) {
1874
+ if (generation === this.loopGeneration) this.loopStarted = false;
1875
+ return;
1876
+ }
1877
+ try {
1878
+ this.webcam.update();
1879
+ if (this.recognizing && this.model) {
1880
+ const model = this.model;
1881
+ const recognitionMode = this.recognitionMode;
1882
+ const first = this.firstRecognitionMs === 0;
1883
+ const startedAt = first ? performance.now() : 0;
1884
+ const recognition = await this.trackPreparedModelOperation(model, this.recognizeFrame(model, recognitionMode));
1885
+ if (generation !== this.loopGeneration || !this.cameraRunning || !this.recognizing || this.model !== model || this.recognitionMode !== recognitionMode) {} else {
1886
+ if (first) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
1887
+ this.renderPoseOverlay(recognition.keypoints);
1888
+ this.applyPredictions(recognition.prediction);
1889
+ }
1890
+ }
1891
+ } catch (error) {
1892
+ this.setLastError(error);
1893
+ }
1894
+ if (generation === this.loopGeneration && this.cameraRunning && this.webcam) requestAnimationFrame(() => void this.loop(generation));
1895
+ else if (generation === this.loopGeneration) this.loopStarted = false;
1896
+ }
1897
+ async recognizeFrame(model, recognitionMode) {
1898
+ if (recognitionMode === "pose") {
1899
+ if (typeof model.estimatePose !== "function") throw new Error("Teachable Machine: The active model does not support pose recognition.");
1900
+ const estimate = await model.estimatePose(this.webcam.canvas);
1901
+ const prediction = await model.predict(estimate.posenetOutput);
1902
+ return {
1903
+ keypoints: estimate.pose?.keypoints,
1904
+ prediction
1905
+ };
1906
+ }
1907
+ const source = this.webcam.webcam ?? this.webcam.canvas;
1908
+ return {
1909
+ keypoints: [],
1910
+ prediction: await model.predict(source)
1911
+ };
1912
+ }
1913
+ currentPoseReporter() {
1914
+ return this.currentPoseName;
1915
+ }
1916
+ scoreReporter() {
1917
+ return Math.round(this.score * 100) / 100;
1918
+ }
1919
+ poseScoreReporter(args) {
1920
+ const value = this.predictions[String(args.NAME || "")] || 0;
1921
+ return Math.round(value * 100) / 100;
1922
+ }
1923
+ setAccumulatedPoseParameters(args) {
1924
+ const accumulation = args.ACCUMULATION === "" ? 1 : Number(args.ACCUMULATION);
1925
+ const decay = args.DECAY === "" ? .9 : Number(args.DECAY);
1926
+ this.accumulationCoefficient = Number.isFinite(accumulation) ? Math.max(0, accumulation) : 1;
1927
+ this.decayCoefficient = Number.isFinite(decay) ? Math.max(0, Math.min(1, decay)) : .9;
1928
+ }
1929
+ setAccumulatedPoseThreshold(args) {
1930
+ const threshold = args.THRESHOLD === "" ? 0 : Number(args.THRESHOLD);
1931
+ this.accumulatedPoseThreshold = Number.isFinite(threshold) ? Math.max(0, threshold) : 0;
1932
+ this.updateAccumulatedPoseSelection();
1933
+ }
1934
+ startAccumulatedPoseSession(now = performance.now()) {
1935
+ this.activeDecayCoefficient = this.decayCoefficient;
1936
+ this.lastAccumulationTime = now;
1937
+ }
1938
+ supportsAccumulatedPoseEvents() {
1939
+ return this.featureFlags.temporalPoseScoring && this.featureFlags.accumulatedPoseEvents;
1940
+ }
1941
+ emitAccumulatedPoseChanged(previousPoseName, reason) {
1942
+ if (!this.supportsAccumulatedPoseEvents() || previousPoseName === this.accumulatedPoseName) return;
1943
+ const payload = {
1944
+ version: 2,
1945
+ poseName: this.accumulatedPoseName,
1946
+ previousPoseName,
1947
+ score: this.accumulatedScore,
1948
+ reason,
1949
+ timestamp: performance.now()
1950
+ };
1951
+ if (this.onAccumulatedPoseChanged) try {
1952
+ this.onAccumulatedPoseChanged(payload);
1953
+ } catch {}
1954
+ else if (typeof Scratch !== "undefined") Scratch.vm?.runtime?.emit(ACCUMULATED_POSE_CHANGED_EVENT, payload);
1955
+ }
1956
+ dispose() {
1957
+ this.stopCamera();
1958
+ if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") document.removeEventListener("visibilitychange", this.visibilityChangeListener);
1959
+ this.onAccumulatedPoseChanged = null;
1960
+ }
1961
+ resetAccumulatedPose(reason = "reset") {
1962
+ const previousPoseName = this.accumulatedPoseName;
1963
+ this.accumulatedPoseName = "";
1964
+ this.accumulatedScore = 0;
1965
+ this.accumulatedPredictions = {};
1966
+ this.lastAccumulationTime = this.recognizing ? performance.now() : null;
1967
+ this.emitAccumulatedPoseChanged(previousPoseName, reason);
1968
+ }
1969
+ handleDocumentVisibilityChange() {
1970
+ if (typeof document === "undefined") return;
1971
+ if (isDocumentHidden()) {
1972
+ this.accumulatedPosePausedForBackground = true;
1973
+ return;
1974
+ }
1975
+ if (this.accumulatedPosePausedForBackground) {
1976
+ this.accumulatedPosePausedForBackground = false;
1977
+ this.lastAccumulationTime = this.recognizing ? performance.now() : null;
1978
+ }
1979
+ }
1980
+ updateAccumulatedPose(prediction, now = performance.now()) {
1981
+ if (isDocumentHidden()) {
1982
+ this.accumulatedPosePausedForBackground = true;
1983
+ return;
1984
+ }
1985
+ if (this.accumulatedPosePausedForBackground) return;
1986
+ const elapsedSeconds = this.lastAccumulationTime === null ? 0 : Math.max(0, (now - this.lastAccumulationTime) / 1e3);
1987
+ const decayMultiplier = elapsedSeconds === 0 ? 1 : Math.pow(this.activeDecayCoefficient, elapsedSeconds);
1988
+ for (const name of Object.keys(this.accumulatedPredictions)) this.accumulatedPredictions[name] *= decayMultiplier;
1989
+ for (const result of prediction) {
1990
+ const name = String(result.className || "");
1991
+ const probability = Number(result.probability);
1992
+ if (!name || !Number.isFinite(probability)) continue;
1993
+ const contribution = Math.max(0, Math.min(1, probability)) * this.accumulationCoefficient * elapsedSeconds;
1994
+ this.accumulatedPredictions[name] = (this.accumulatedPredictions[name] || 0) + contribution;
1995
+ }
1996
+ this.lastAccumulationTime = now;
1997
+ this.updateAccumulatedPoseSelection();
1998
+ }
1999
+ updateAccumulatedPoseSelection() {
2000
+ const previousPoseName = this.accumulatedPoseName;
2001
+ this.accumulatedPoseName = "";
2002
+ this.accumulatedScore = 0;
2003
+ let bestPoseName = "";
2004
+ for (const [name, value] of Object.entries(this.accumulatedPredictions)) if (value > this.accumulatedScore) {
2005
+ bestPoseName = name;
2006
+ this.accumulatedScore = value;
2007
+ }
2008
+ if (bestPoseName && this.accumulatedScore >= this.accumulatedPoseThreshold) this.accumulatedPoseName = bestPoseName;
2009
+ this.emitAccumulatedPoseChanged(previousPoseName, "recognition");
2010
+ }
2011
+ accumulatedPoseReporter() {
2012
+ return this.accumulatedPoseName;
2013
+ }
2014
+ accumulatedScoreReporter() {
2015
+ return this.accumulatedScore;
2016
+ }
2017
+ accumulatedPoseScoreReporter(args) {
2018
+ return this.accumulatedPredictions[String(args.NAME || "")] || 0;
2019
+ }
2020
+ isPose(args) {
2021
+ return (this.predictions[String(args.NAME || "")] || 0) >= .75;
2022
+ }
2023
+ isPoseWithThreshold(args) {
2024
+ const name = String(args.NAME || "");
2025
+ let threshold = args.THRESHOLD === "" ? .75 : Number(args.THRESHOLD);
2026
+ if (Number.isNaN(threshold)) threshold = .75;
2027
+ threshold = Math.max(0, Math.min(1, threshold));
2028
+ return (this.predictions[name] || 0) >= threshold;
2029
+ }
2030
+ cameraMsReporter() {
2031
+ return this.cameraMs;
2032
+ }
2033
+ modelLoadMsReporter() {
2034
+ return this.modelLoadMs;
2035
+ }
2036
+ firstRecognitionMsReporter() {
2037
+ return this.firstRecognitionMs;
2038
+ }
2039
+ lastErrorReporter() {
2040
+ return this.lastError;
2041
+ }
2042
+ };
2043
+ //#endregion
2044
+ //#region src/index.ts
2045
+ if (!Scratch.extensions.unsandboxed) throw new Error("TM must run without the extension sandbox. In TurboWarp's custom extension dialog, load the downloaded tm.js from the Files tab with \"Run without sandbox\" enabled; a URL pasted into the URL tab is always sandboxed.");
2046
+ var extension = new TMExtension();
1494
2047
  Scratch.extensions.register(extension);
1495
- if (Scratch.vm?.runtime) {
1496
- Scratch.vm.runtime.ext_kubohiroyatm = extension;
1497
- }
2048
+ if (Scratch.vm?.runtime) Scratch.vm.runtime.ext_kubohiroyatm = extension;
2049
+ //#endregion
1498
2050
 
1499
2051
  })(Scratch);