@kubohiroya/turbowarp-teachable-machine 3.5.0 → 3.5.2

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,435 +7,1110 @@
7
7
  (function (Scratch) {
8
8
  'use strict';
9
9
 
10
+ //#region node_modules/.pnpm/@kubohiroya+turbowarp-extension-data-editor@0.2.1_react@18.3.1/node_modules/@kubohiroya/turbowarp-extension-data-editor/dist/fixed-input-schema.js
11
+ var numberToken = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
12
+ var invalidNumber = Object.freeze({
13
+ kind: "invalid",
14
+ code: "fixed-number-input-invalid"
15
+ });
16
+ /**
17
+ * Parse a NEW numeric draft according to NP-finite-new-input-v1.
18
+ * Never apply this to opening/no-op raw values or use it as an owner validator.
19
+ * Keep the validated token so negative zero, precision and exponent spelling survive.
20
+ */
21
+ function parseFixedNumberInput(input) {
22
+ if (typeof input === "number") {
23
+ if (!Number.isFinite(input)) return invalidNumber;
24
+ return Object.freeze({
25
+ kind: "valid",
26
+ token: Object.is(input, -0) ? "-0" : String(input),
27
+ value: input
28
+ });
29
+ }
30
+ if (typeof input !== "string") return invalidNumber;
31
+ const token = input.trim();
32
+ if (!numberToken.test(token)) return invalidNumber;
33
+ const value = Number(token);
34
+ return Number.isFinite(value) ? Object.freeze({
35
+ kind: "valid",
36
+ token,
37
+ value
38
+ }) : invalidNumber;
39
+ }
40
+ var block_editor_fixed_inputs_schema_default = {
41
+ $schema: "https://json-schema.org/draft/2020-12/schema",
42
+ $id: "https://kubohiroya.github.io/turbowarp-teachable-machine/schemas/block-editor-fixed-inputs.schema.json",
43
+ title: "turbowarp-teachable-machine fixed input registration schema",
44
+ $defs: {
45
+ "setRecognitionMode": {
46
+ "type": "object",
47
+ "properties": { "MODE": {
48
+ "type": "string",
49
+ "enum": [
50
+ "pose",
51
+ "image",
52
+ "audio"
53
+ ]
54
+ } }
55
+ },
56
+ "setComputeMode": {
57
+ "type": "object",
58
+ "properties": { "MODE": {
59
+ "type": "string",
60
+ "enum": [
61
+ "auto",
62
+ "webgpu",
63
+ "webgl",
64
+ "wasm",
65
+ "cpu"
66
+ ]
67
+ } }
68
+ },
69
+ "setPreviewOpacity": {
70
+ "type": "object",
71
+ "properties": { "OPACITY": {
72
+ "type": "number",
73
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
74
+ } }
75
+ },
76
+ "setPreviewPosition": {
77
+ "type": "object",
78
+ "properties": { "POSITION": {
79
+ "type": "string",
80
+ "enum": [
81
+ "top-left",
82
+ "top-right",
83
+ "bottom-left",
84
+ "bottom-right",
85
+ "center",
86
+ "full-stage"
87
+ ]
88
+ } }
89
+ },
90
+ "setPreviewMirroring": {
91
+ "type": "object",
92
+ "properties": { "MIRRORING": {
93
+ "type": "string",
94
+ "enum": ["mirrored", "unmirrored"]
95
+ } }
96
+ },
97
+ "setPoseOverlayVisibility": {
98
+ "type": "object",
99
+ "properties": { "VISIBILITY": {
100
+ "type": "string",
101
+ "enum": ["on", "off"]
102
+ } }
103
+ },
104
+ "setPoseJointStyle": {
105
+ "type": "object",
106
+ "properties": {
107
+ "PART": {
108
+ "type": "string",
109
+ "enum": [
110
+ "nose",
111
+ "leftEye",
112
+ "rightEye",
113
+ "leftEar",
114
+ "rightEar",
115
+ "leftShoulder",
116
+ "rightShoulder",
117
+ "leftElbow",
118
+ "rightElbow",
119
+ "leftWrist",
120
+ "rightWrist",
121
+ "leftHip",
122
+ "rightHip",
123
+ "leftKnee",
124
+ "rightKnee",
125
+ "leftAnkle",
126
+ "rightAnkle"
127
+ ]
128
+ },
129
+ "OPACITY": {
130
+ "type": "number",
131
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
132
+ },
133
+ "RADIUS": {
134
+ "type": "number",
135
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
136
+ }
137
+ }
138
+ },
139
+ "setPoseBoneStyle": {
140
+ "type": "object",
141
+ "properties": {
142
+ "OPACITY": {
143
+ "type": "number",
144
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
145
+ },
146
+ "WIDTH": {
147
+ "type": "number",
148
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
149
+ }
150
+ }
151
+ },
152
+ "setPoseOverlayMinimumConfidence": {
153
+ "type": "object",
154
+ "properties": { "CONFIDENCE": {
155
+ "type": "number",
156
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
157
+ } }
158
+ },
159
+ "setPoseConfidenceScaling": {
160
+ "type": "object",
161
+ "properties": {
162
+ "PROPERTY": {
163
+ "type": "string",
164
+ "enum": [
165
+ "joint-opacity",
166
+ "joint-radius",
167
+ "bone-opacity",
168
+ "bone-width"
169
+ ]
170
+ },
171
+ "STATE": {
172
+ "type": "string",
173
+ "enum": ["on", "off"]
174
+ }
175
+ }
176
+ },
177
+ "setAccumulatedPoseParameters": {
178
+ "type": "object",
179
+ "properties": {
180
+ "ACCUMULATION": {
181
+ "type": "number",
182
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
183
+ },
184
+ "DECAY": {
185
+ "type": "number",
186
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
187
+ }
188
+ }
189
+ },
190
+ "setAccumulatedPoseThreshold": {
191
+ "type": "object",
192
+ "properties": { "THRESHOLD": {
193
+ "type": "number",
194
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
195
+ } }
196
+ },
197
+ "isPoseWithThreshold": {
198
+ "type": "object",
199
+ "properties": { "THRESHOLD": {
200
+ "type": "number",
201
+ "$comment": "New finite numeric input uses NP-finite-new-input-v1. Existing raw is preserved on no-op; runtime clamp/floor is not applied to stored raw."
202
+ } }
203
+ }
204
+ }
205
+ };
206
+ //#endregion
207
+ //#region src/fixed-input-schema.ts
208
+ /** Owner canonical fragments, distinct from older SOURCE/editor schemas. */
209
+ function fixedInputBundle(opcode, argument) {
210
+ const source = {
211
+ repository: "turbowarp-teachable-machine",
212
+ path: "schemas/block-editor-fixed-inputs.schema.json",
213
+ symbol: `#/$defs/${opcode}/properties/${argument}`
214
+ };
215
+ const fragment = block_editor_fixed_inputs_schema_default.$defs[opcode].properties[argument];
216
+ return {
217
+ identity: {
218
+ ownerId: "turbowarp-teachable-machine",
219
+ contractId: "fixed-inputs",
220
+ revision: "1",
221
+ variantId: `${opcode}.${argument}`
222
+ },
223
+ dialect: "2020-12",
224
+ root: {
225
+ retrievalUri: block_editor_fixed_inputs_schema_default.$id,
226
+ document: fragment,
227
+ artifact: {
228
+ role: "canonical-schema",
229
+ source
230
+ }
231
+ },
232
+ resources: [],
233
+ provenance: {
234
+ canonicalSources: [source, {
235
+ repository: "turbowarp-teachable-machine",
236
+ path: "src/extension.ts",
237
+ symbol: opcode
238
+ }],
239
+ generatedFiles: [],
240
+ referenceSchemas: [],
241
+ generationSources: []
242
+ }
243
+ };
244
+ }
245
+ block_editor_fixed_inputs_schema_default.$defs;
246
+ //#endregion
247
+ //#region src/fixed-input-editors.ts
248
+ var existingNumber = (value) => {
249
+ if (typeof value !== "number" && typeof value !== "string" || typeof value === "string" && value.trim() === "") throw new TypeError("Expected a numeric literal.");
250
+ const number = Number(value);
251
+ if (!Number.isFinite(number)) throw new TypeError("Expected a finite number.");
252
+ return number;
253
+ };
254
+ var newNumber = (value) => {
255
+ const parsed = parseFixedNumberInput(value);
256
+ if (parsed.kind !== "valid") throw new TypeError("Invalid new numeric input.");
257
+ return parsed.token;
258
+ };
259
+ var aliases = {
260
+ "setRecognitionMode.MODE": {
261
+ "pose": "pose",
262
+ "poses": "pose",
263
+ "body": "pose",
264
+ "image": "image",
265
+ "images": "image",
266
+ "object": "image",
267
+ "card": "image",
268
+ "marker": "image",
269
+ "audio": "audio",
270
+ "sound": "audio",
271
+ "voice": "audio",
272
+ "microphone": "audio",
273
+ "mic": "audio",
274
+ "ポーズ": "pose",
275
+ "姿勢": "pose",
276
+ "画像": "image",
277
+ "物体": "image",
278
+ "カード": "image",
279
+ "記号": "image",
280
+ "音声": "audio",
281
+ "音": "audio",
282
+ "声": "audio",
283
+ "マイク": "audio"
284
+ },
285
+ "setPreviewPosition.POSITION": {
286
+ "左上": "top-left",
287
+ "右上": "top-right",
288
+ "左下": "bottom-left",
289
+ "右下": "bottom-right",
290
+ "中央": "center",
291
+ "ステージ全体": "full-stage",
292
+ "top-left": "top-left",
293
+ "top-right": "top-right",
294
+ "bottom-left": "bottom-left",
295
+ "bottom-right": "bottom-right",
296
+ "center": "center",
297
+ "full-stage": "full-stage"
298
+ },
299
+ "setPreviewMirroring.MIRRORING": {
300
+ "mirrored": "mirrored",
301
+ "unmirrored": "unmirrored",
302
+ "mirror": "mirrored",
303
+ "normal": "unmirrored",
304
+ "左右反転": "mirrored",
305
+ "そのまま": "unmirrored"
306
+ },
307
+ "setPoseOverlayVisibility.VISIBILITY": {
308
+ "on": "on",
309
+ "off": "off",
310
+ "show": "on",
311
+ "hide": "off",
312
+ "true": "on",
313
+ "false": "off",
314
+ "表示": "on",
315
+ "非表示": "off"
316
+ },
317
+ "setPoseConfidenceScaling.STATE": {
318
+ "on": "on",
319
+ "off": "off",
320
+ "show": "on",
321
+ "hide": "off",
322
+ "true": "on",
323
+ "false": "off",
324
+ "表示": "on",
325
+ "非表示": "off"
326
+ },
327
+ "setComputeMode.MODE": {
328
+ "auto": "auto",
329
+ "automatic": "auto",
330
+ "default": "auto",
331
+ "fastest": "auto",
332
+ "自動": "auto",
333
+ "おまかせ": "auto",
334
+ "webgpu": "webgpu",
335
+ "gpu": "webgpu",
336
+ "webgl": "webgl",
337
+ "wasm": "wasm",
338
+ "webassembly": "wasm",
339
+ "simd": "wasm",
340
+ "cpu": "cpu",
341
+ "javascript": "cpu"
342
+ }
343
+ };
344
+ function binding(opcode, argument, values) {
345
+ return {
346
+ argument,
347
+ schemaBundle: fixedInputBundle(opcode, argument),
348
+ control: values ? {
349
+ kind: "enum",
350
+ choices: values.map((value) => ({ value }))
351
+ } : { kind: "number" },
352
+ read(value) {
353
+ if (!values) return existingNumber(value);
354
+ if (typeof value !== "string") throw new TypeError("Expected a string.");
355
+ return projectEnum(opcode, argument, value);
356
+ },
357
+ encode(value) {
358
+ if (!values) return newNumber(value);
359
+ if (typeof value !== "string" || !values.includes(value)) throw new TypeError("Invalid selection.");
360
+ return value;
361
+ },
362
+ validateCandidate(value) {
363
+ try {
364
+ if (!values) existingNumber(value);
365
+ else if (typeof value !== "string" || !values.includes(value)) throw new TypeError("Invalid selection.");
366
+ return [];
367
+ } catch {
368
+ return [{ message: "Invalid fixed input." }];
369
+ }
370
+ }
371
+ };
372
+ }
373
+ function projectEnum(opcode, argument, value) {
374
+ const key = `${opcode}.${argument}`;
375
+ const token = argument === "POSITION" ? value : value.trim().toLowerCase();
376
+ return aliases[key]?.[token] ?? value;
377
+ }
378
+ var fixedInputEditors = [
379
+ {
380
+ apiVersion: 1,
381
+ extensionId: "kubohiroyatm",
382
+ editorId: "fixed-setRecognitionMode",
383
+ opcodes: ["kubohiroyatm_setRecognitionMode"],
384
+ contractId: "turbowarp-teachable-machine/setRecognitionMode",
385
+ contractRevision: "1",
386
+ validationContext: () => ({
387
+ consumerId: "turbowarp-teachable-machine",
388
+ nowMs: 0,
389
+ settings: null
390
+ }),
391
+ bindings: [binding("setRecognitionMode", "MODE", [
392
+ "pose",
393
+ "image",
394
+ "audio"
395
+ ])]
396
+ },
397
+ {
398
+ apiVersion: 1,
399
+ extensionId: "kubohiroyatm",
400
+ editorId: "fixed-setComputeMode",
401
+ opcodes: ["kubohiroyatm_setComputeMode"],
402
+ contractId: "turbowarp-teachable-machine/setComputeMode",
403
+ contractRevision: "1",
404
+ validationContext: () => ({
405
+ consumerId: "turbowarp-teachable-machine",
406
+ nowMs: 0,
407
+ settings: null
408
+ }),
409
+ bindings: [binding("setComputeMode", "MODE", [
410
+ "auto",
411
+ "webgpu",
412
+ "webgl",
413
+ "wasm",
414
+ "cpu"
415
+ ])]
416
+ },
417
+ {
418
+ apiVersion: 1,
419
+ extensionId: "kubohiroyatm",
420
+ editorId: "fixed-setPreviewOpacity",
421
+ opcodes: ["kubohiroyatm_setPreviewOpacity"],
422
+ contractId: "turbowarp-teachable-machine/setPreviewOpacity",
423
+ contractRevision: "1",
424
+ validationContext: () => ({
425
+ consumerId: "turbowarp-teachable-machine",
426
+ nowMs: 0,
427
+ settings: null
428
+ }),
429
+ bindings: [binding("setPreviewOpacity", "OPACITY")]
430
+ },
431
+ {
432
+ apiVersion: 1,
433
+ extensionId: "kubohiroyatm",
434
+ editorId: "fixed-setPreviewPosition",
435
+ opcodes: ["kubohiroyatm_setPreviewPosition"],
436
+ contractId: "turbowarp-teachable-machine/setPreviewPosition",
437
+ contractRevision: "1",
438
+ validationContext: () => ({
439
+ consumerId: "turbowarp-teachable-machine",
440
+ nowMs: 0,
441
+ settings: null
442
+ }),
443
+ bindings: [binding("setPreviewPosition", "POSITION", [
444
+ "top-left",
445
+ "top-right",
446
+ "bottom-left",
447
+ "bottom-right",
448
+ "center",
449
+ "full-stage"
450
+ ])]
451
+ },
452
+ {
453
+ apiVersion: 1,
454
+ extensionId: "kubohiroyatm",
455
+ editorId: "fixed-setPreviewMirroring",
456
+ opcodes: ["kubohiroyatm_setPreviewMirroring"],
457
+ contractId: "turbowarp-teachable-machine/setPreviewMirroring",
458
+ contractRevision: "1",
459
+ validationContext: () => ({
460
+ consumerId: "turbowarp-teachable-machine",
461
+ nowMs: 0,
462
+ settings: null
463
+ }),
464
+ bindings: [binding("setPreviewMirroring", "MIRRORING", ["mirrored", "unmirrored"])]
465
+ },
466
+ {
467
+ apiVersion: 1,
468
+ extensionId: "kubohiroyatm",
469
+ editorId: "fixed-setPoseOverlayVisibility",
470
+ opcodes: ["kubohiroyatm_setPoseOverlayVisibility"],
471
+ contractId: "turbowarp-teachable-machine/setPoseOverlayVisibility",
472
+ contractRevision: "1",
473
+ validationContext: () => ({
474
+ consumerId: "turbowarp-teachable-machine",
475
+ nowMs: 0,
476
+ settings: null
477
+ }),
478
+ bindings: [binding("setPoseOverlayVisibility", "VISIBILITY", ["on", "off"])]
479
+ },
480
+ {
481
+ apiVersion: 1,
482
+ extensionId: "kubohiroyatm",
483
+ editorId: "fixed-setPoseJointStyle",
484
+ opcodes: ["kubohiroyatm_setPoseJointStyle"],
485
+ contractId: "turbowarp-teachable-machine/setPoseJointStyle",
486
+ contractRevision: "1",
487
+ validationContext: () => ({
488
+ consumerId: "turbowarp-teachable-machine",
489
+ nowMs: 0,
490
+ settings: null
491
+ }),
492
+ bindings: [
493
+ binding("setPoseJointStyle", "PART", [
494
+ "nose",
495
+ "leftEye",
496
+ "rightEye",
497
+ "leftEar",
498
+ "rightEar",
499
+ "leftShoulder",
500
+ "rightShoulder",
501
+ "leftElbow",
502
+ "rightElbow",
503
+ "leftWrist",
504
+ "rightWrist",
505
+ "leftHip",
506
+ "rightHip",
507
+ "leftKnee",
508
+ "rightKnee",
509
+ "leftAnkle",
510
+ "rightAnkle"
511
+ ]),
512
+ binding("setPoseJointStyle", "OPACITY"),
513
+ binding("setPoseJointStyle", "RADIUS")
514
+ ]
515
+ },
516
+ {
517
+ apiVersion: 1,
518
+ extensionId: "kubohiroyatm",
519
+ editorId: "fixed-setPoseBoneStyle",
520
+ opcodes: ["kubohiroyatm_setPoseBoneStyle"],
521
+ contractId: "turbowarp-teachable-machine/setPoseBoneStyle",
522
+ contractRevision: "1",
523
+ validationContext: () => ({
524
+ consumerId: "turbowarp-teachable-machine",
525
+ nowMs: 0,
526
+ settings: null
527
+ }),
528
+ bindings: [binding("setPoseBoneStyle", "OPACITY"), binding("setPoseBoneStyle", "WIDTH")]
529
+ },
530
+ {
531
+ apiVersion: 1,
532
+ extensionId: "kubohiroyatm",
533
+ editorId: "fixed-setPoseOverlayMinimumConfidence",
534
+ opcodes: ["kubohiroyatm_setPoseOverlayMinimumConfidence"],
535
+ contractId: "turbowarp-teachable-machine/setPoseOverlayMinimumConfidence",
536
+ contractRevision: "1",
537
+ validationContext: () => ({
538
+ consumerId: "turbowarp-teachable-machine",
539
+ nowMs: 0,
540
+ settings: null
541
+ }),
542
+ bindings: [binding("setPoseOverlayMinimumConfidence", "CONFIDENCE")]
543
+ },
544
+ {
545
+ apiVersion: 1,
546
+ extensionId: "kubohiroyatm",
547
+ editorId: "fixed-setPoseConfidenceScaling",
548
+ opcodes: ["kubohiroyatm_setPoseConfidenceScaling"],
549
+ contractId: "turbowarp-teachable-machine/setPoseConfidenceScaling",
550
+ contractRevision: "1",
551
+ validationContext: () => ({
552
+ consumerId: "turbowarp-teachable-machine",
553
+ nowMs: 0,
554
+ settings: null
555
+ }),
556
+ bindings: [binding("setPoseConfidenceScaling", "PROPERTY", [
557
+ "joint-opacity",
558
+ "joint-radius",
559
+ "bone-opacity",
560
+ "bone-width"
561
+ ]), binding("setPoseConfidenceScaling", "STATE", ["on", "off"])]
562
+ },
563
+ {
564
+ apiVersion: 1,
565
+ extensionId: "kubohiroyatm",
566
+ editorId: "fixed-setAccumulatedPoseParameters",
567
+ opcodes: ["kubohiroyatm_setAccumulatedPoseParameters"],
568
+ contractId: "turbowarp-teachable-machine/setAccumulatedPoseParameters",
569
+ contractRevision: "1",
570
+ validationContext: () => ({
571
+ consumerId: "turbowarp-teachable-machine",
572
+ nowMs: 0,
573
+ settings: null
574
+ }),
575
+ bindings: [binding("setAccumulatedPoseParameters", "ACCUMULATION"), binding("setAccumulatedPoseParameters", "DECAY")]
576
+ },
577
+ {
578
+ apiVersion: 1,
579
+ extensionId: "kubohiroyatm",
580
+ editorId: "fixed-setAccumulatedPoseThreshold",
581
+ opcodes: ["kubohiroyatm_setAccumulatedPoseThreshold"],
582
+ contractId: "turbowarp-teachable-machine/setAccumulatedPoseThreshold",
583
+ contractRevision: "1",
584
+ validationContext: () => ({
585
+ consumerId: "turbowarp-teachable-machine",
586
+ nowMs: 0,
587
+ settings: null
588
+ }),
589
+ bindings: [binding("setAccumulatedPoseThreshold", "THRESHOLD")]
590
+ },
591
+ {
592
+ apiVersion: 1,
593
+ extensionId: "kubohiroyatm",
594
+ editorId: "fixed-isPoseWithThreshold",
595
+ opcodes: ["kubohiroyatm_isPoseWithThreshold"],
596
+ contractId: "turbowarp-teachable-machine/isPoseWithThreshold",
597
+ contractRevision: "1",
598
+ validationContext: () => ({
599
+ consumerId: "turbowarp-teachable-machine",
600
+ nowMs: 0,
601
+ settings: null
602
+ }),
603
+ bindings: [binding("isPoseWithThreshold", "THRESHOLD")]
604
+ }
605
+ ];
606
+ /** Register only currently declared opcodes; release obsolete feature-flag scopes. */
607
+ function fixedInputRegistration(scratch) {
608
+ const host = typeof scratch === "object" && scratch !== null ? scratch : {};
609
+ let active;
610
+ let activeOpcodes = "";
611
+ const removers = [];
612
+ let disposed = false;
613
+ const dispose = () => {
614
+ host.vm?.runtime?.off?.("RUNTIME_DISPOSED", shutdown);
615
+ for (const remove of removers.splice(0).reverse()) remove();
616
+ active = void 0;
617
+ activeOpcodes = "";
618
+ };
619
+ const shutdown = () => {
620
+ disposed = true;
621
+ dispose();
622
+ };
623
+ return {
624
+ ensure(opcodes) {
625
+ const api = host.gui?.blockEditors;
626
+ const signature = [...new Set(opcodes)].sort().join("\0");
627
+ if (disposed || api === active && signature === activeOpcodes) return;
628
+ dispose();
629
+ if (api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== "function") return;
630
+ try {
631
+ for (const definition of fixedInputEditors) if (definition.opcodes.every((opcode) => opcodes.includes(opcode))) {
632
+ const remove = api.registerFixedInputEditor.call(api, definition);
633
+ if (typeof remove !== "function") throw new TypeError("Invalid fixed registration cleanup.");
634
+ removers.push(remove);
635
+ }
636
+ active = api;
637
+ activeOpcodes = signature;
638
+ host.vm?.runtime?.on?.("RUNTIME_DISPOSED", shutdown);
639
+ } catch (error) {
640
+ dispose();
641
+ throw error;
642
+ }
643
+ },
644
+ dispose: shutdown
645
+ };
646
+ }
10
647
  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
- ]
648
+ extensionName: "Teachable Machine",
649
+ blocks: [
650
+ {
651
+ "opcode": "versionReporter",
652
+ "blockType": "REPORTER",
653
+ "text": "Teachable Machine version",
654
+ "description": "Returns the extension version."
655
+ },
656
+ {
657
+ "opcode": "setRecognitionMode",
658
+ "blockType": "COMMAND",
659
+ "text": "set recognition mode to [MODE]",
660
+ "description": "Selects whether the current Teachable Machine model recognizes poses, images, or audio.",
661
+ "arguments": { "MODE": {
662
+ "type": "STRING",
663
+ "menu": "recognitionModeMenu",
664
+ "defaultValue": "pose"
665
+ } }
666
+ },
667
+ {
668
+ "opcode": "recognitionModeReporter",
669
+ "blockType": "REPORTER",
670
+ "text": "recognition mode",
671
+ "description": "Returns the current recognition mode."
672
+ },
673
+ {
674
+ "opcode": "setComputeMode",
675
+ "blockType": "COMMAND",
676
+ "text": "set compute mode to [MODE]",
677
+ "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.",
678
+ "arguments": { "MODE": {
679
+ "type": "STRING",
680
+ "menu": "computeModeMenu",
681
+ "defaultValue": "auto"
682
+ } }
683
+ },
684
+ {
685
+ "opcode": "computeModeReporter",
686
+ "blockType": "REPORTER",
687
+ "text": "compute mode",
688
+ "description": "Returns the requested compute mode."
689
+ },
690
+ {
691
+ "opcode": "computeBackendReporter",
692
+ "blockType": "REPORTER",
693
+ "text": "active compute backend",
694
+ "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."
695
+ },
696
+ {
697
+ "opcode": "setModelURL",
698
+ "blockType": "COMMAND",
699
+ "text": "set model URL to [URL]",
700
+ "description": "Sets the Teachable Machine model URL.",
701
+ "arguments": { "URL": {
702
+ "type": "STRING",
703
+ "defaultValue": "https://teachablemachine.withgoogle.com/models/XXXX/"
704
+ } }
705
+ },
706
+ {
707
+ "opcode": "startCamera",
708
+ "blockType": "COMMAND",
709
+ "text": "start camera",
710
+ "description": "Starts the camera and attaches the preview."
711
+ },
712
+ {
713
+ "opcode": "stopCamera",
714
+ "blockType": "COMMAND",
715
+ "text": "stop camera",
716
+ "description": "Stops the camera and recognition loop."
717
+ },
718
+ {
719
+ "opcode": "isCameraRunning",
720
+ "blockType": "BOOLEAN",
721
+ "text": "camera is running?",
722
+ "description": "Reports whether the camera is running."
723
+ },
724
+ {
725
+ "opcode": "refreshCameraList",
726
+ "blockType": "COMMAND",
727
+ "text": "refresh camera list",
728
+ "description": "Refreshes the list of available video input devices."
729
+ },
730
+ {
731
+ "opcode": "setCameraSelection",
732
+ "blockType": "COMMAND",
733
+ "text": "set camera to [CAMERA]",
734
+ "description": "Selects the default, front, back, or a detected camera and switches a running camera.",
735
+ "arguments": { "CAMERA": {
736
+ "type": "STRING",
737
+ "menu": "cameraMenu",
738
+ "defaultValue": "default"
739
+ } }
740
+ },
741
+ {
742
+ "opcode": "cameraCountReporter",
743
+ "blockType": "REPORTER",
744
+ "text": "camera count",
745
+ "description": "Returns the number of video input devices found by the latest refresh."
746
+ },
747
+ {
748
+ "opcode": "cameraDeviceIdReporter",
749
+ "blockType": "REPORTER",
750
+ "text": "camera device ID",
751
+ "description": "Returns the active camera device ID when available."
752
+ },
753
+ {
754
+ "opcode": "cameraDeviceNameReporter",
755
+ "blockType": "REPORTER",
756
+ "text": "camera device name",
757
+ "description": "Returns the active camera device name when available."
758
+ },
759
+ {
760
+ "opcode": "showPreview",
761
+ "blockType": "COMMAND",
762
+ "text": "show camera preview",
763
+ "description": "Shows the camera preview."
764
+ },
765
+ {
766
+ "opcode": "hidePreview",
767
+ "blockType": "COMMAND",
768
+ "text": "hide camera preview",
769
+ "description": "Hides the camera preview."
770
+ },
771
+ {
772
+ "opcode": "isPreviewVisible",
773
+ "blockType": "BOOLEAN",
774
+ "text": "camera preview is visible?",
775
+ "description": "Reports whether the preview is configured as visible."
776
+ },
777
+ {
778
+ "opcode": "setPreviewOpacity",
779
+ "blockType": "COMMAND",
780
+ "text": "set camera preview opacity to [OPACITY]",
781
+ "description": "Sets preview opacity from 0 to 1.",
782
+ "arguments": { "OPACITY": {
783
+ "type": "NUMBER",
784
+ "defaultValue": .6
785
+ } }
786
+ },
787
+ {
788
+ "opcode": "setPreviewPosition",
789
+ "blockType": "COMMAND",
790
+ "text": "set camera preview position to [POSITION]",
791
+ "description": "Sets the preview position on the stage.",
792
+ "arguments": { "POSITION": {
793
+ "type": "STRING",
794
+ "menu": "positionMenu",
795
+ "defaultValue": "bottom-right"
796
+ } }
797
+ },
798
+ {
799
+ "opcode": "setPreviewMirroring",
800
+ "blockType": "COMMAND",
801
+ "text": "set camera preview to [MIRRORING]",
802
+ "description": "Sets whether the preview is mirrored without changing the recognition input.",
803
+ "arguments": { "MIRRORING": {
804
+ "type": "STRING",
805
+ "menu": "previewMirroringMenu",
806
+ "defaultValue": "mirrored"
807
+ } }
808
+ },
809
+ {
810
+ "opcode": "previewMirroringReporter",
811
+ "blockType": "REPORTER",
812
+ "text": "camera preview mirroring",
813
+ "description": "Returns mirrored or unmirrored for the current preview setting."
814
+ },
815
+ {
816
+ "opcode": "setPoseOverlayVisibility",
817
+ "blockType": "COMMAND",
818
+ "text": "set pose overlay [VISIBILITY]",
819
+ "description": "Shows or hides the SVG pose overlay without stopping recognition.",
820
+ "featureFlag": "poseOverlay",
821
+ "arguments": { "VISIBILITY": {
822
+ "type": "STRING",
823
+ "menu": "poseOverlayVisibilityMenu",
824
+ "defaultValue": "on"
825
+ } }
826
+ },
827
+ {
828
+ "opcode": "isPoseOverlayVisible",
829
+ "blockType": "BOOLEAN",
830
+ "text": "pose overlay is visible?",
831
+ "description": "Reports whether the SVG pose overlay is configured as visible.",
832
+ "featureFlag": "poseOverlay"
833
+ },
834
+ {
835
+ "opcode": "setPoseJointStyle",
836
+ "blockType": "COMMAND",
837
+ "text": "set [PART] joint color [COLOR] opacity [OPACITY] radius [RADIUS]",
838
+ "description": "Sets the SVG circle style for one PoseNet joint.",
839
+ "featureFlag": "poseOverlay",
840
+ "arguments": {
841
+ "PART": {
842
+ "type": "STRING",
843
+ "menu": "poseKeypointMenu",
844
+ "defaultValue": "nose"
845
+ },
846
+ "COLOR": {
847
+ "type": "STRING",
848
+ "defaultValue": "#00e5ff"
849
+ },
850
+ "OPACITY": {
851
+ "type": "NUMBER",
852
+ "defaultValue": 1
853
+ },
854
+ "RADIUS": {
855
+ "type": "NUMBER",
856
+ "defaultValue": 4
857
+ }
858
+ }
859
+ },
860
+ {
861
+ "opcode": "setPoseBoneStyle",
862
+ "blockType": "COMMAND",
863
+ "text": "set pose bone color [COLOR] opacity [OPACITY] width [WIDTH]",
864
+ "description": "Sets the color, opacity, and line width for all SVG pose bones.",
865
+ "featureFlag": "poseOverlay",
866
+ "arguments": {
867
+ "COLOR": {
868
+ "type": "STRING",
869
+ "defaultValue": "#00e5ff"
870
+ },
871
+ "OPACITY": {
872
+ "type": "NUMBER",
873
+ "defaultValue": .9
874
+ },
875
+ "WIDTH": {
876
+ "type": "NUMBER",
877
+ "defaultValue": 3
878
+ }
879
+ }
880
+ },
881
+ {
882
+ "opcode": "setPoseOverlayMinimumConfidence",
883
+ "blockType": "COMMAND",
884
+ "text": "set pose overlay minimum confidence to [CONFIDENCE]",
885
+ "description": "Hides joints and bones whose keypoint confidence is below the given value.",
886
+ "featureFlag": "poseOverlay",
887
+ "arguments": { "CONFIDENCE": {
888
+ "type": "NUMBER",
889
+ "defaultValue": .5
890
+ } }
891
+ },
892
+ {
893
+ "opcode": "setPoseConfidenceScaling",
894
+ "blockType": "COMMAND",
895
+ "text": "set pose [PROPERTY] confidence scaling [STATE]",
896
+ "description": "Scales the selected SVG style property from zero to its configured value using keypoint confidence.",
897
+ "featureFlag": "poseOverlay",
898
+ "arguments": {
899
+ "PROPERTY": {
900
+ "type": "STRING",
901
+ "menu": "poseConfidencePropertyMenu",
902
+ "defaultValue": "joint-opacity"
903
+ },
904
+ "STATE": {
905
+ "type": "STRING",
906
+ "menu": "poseOverlayVisibilityMenu",
907
+ "defaultValue": "off"
908
+ }
909
+ }
910
+ },
911
+ {
912
+ "opcode": "loadModel",
913
+ "blockType": "COMMAND",
914
+ "text": "load model",
915
+ "description": "Loads the configured Teachable Machine model."
916
+ },
917
+ {
918
+ "opcode": "isModelLoaded",
919
+ "blockType": "BOOLEAN",
920
+ "text": "model is loaded?",
921
+ "description": "Reports whether the model is loaded."
922
+ },
923
+ {
924
+ "opcode": "startRecognition",
925
+ "blockType": "COMMAND",
926
+ "text": "start recognition",
927
+ "description": "Starts recognition."
928
+ },
929
+ {
930
+ "opcode": "stopRecognition",
931
+ "blockType": "COMMAND",
932
+ "text": "stop recognition",
933
+ "description": "Stops recognition."
934
+ },
935
+ {
936
+ "opcode": "isRecognizing",
937
+ "blockType": "BOOLEAN",
938
+ "text": "recognition is running?",
939
+ "description": "Reports whether recognition is running."
940
+ },
941
+ {
942
+ "opcode": "currentPoseReporter",
943
+ "blockType": "REPORTER",
944
+ "text": "current label",
945
+ "description": "Returns the highest-scoring label."
946
+ },
947
+ {
948
+ "opcode": "scoreReporter",
949
+ "blockType": "REPORTER",
950
+ "text": "confidence",
951
+ "description": "Returns the confidence of the current label."
952
+ },
953
+ {
954
+ "opcode": "poseScoreReporter",
955
+ "blockType": "REPORTER",
956
+ "text": "confidence of [NAME]",
957
+ "description": "Returns the confidence for a named label.",
958
+ "arguments": { "NAME": {
959
+ "type": "STRING",
960
+ "defaultValue": "jump"
961
+ } }
962
+ },
963
+ {
964
+ "opcode": "setAccumulatedPoseParameters",
965
+ "blockType": "COMMAND",
966
+ "text": "set accumulated pose accumulation [ACCUMULATION] decay [DECAY]",
967
+ "description": "Sets the accumulation rate per second and the decay retained per second; decay changes apply to the next recognition session.",
968
+ "featureFlag": "temporalPoseScoring",
969
+ "arguments": {
970
+ "ACCUMULATION": {
971
+ "type": "NUMBER",
972
+ "defaultValue": 1
973
+ },
974
+ "DECAY": {
975
+ "type": "NUMBER",
976
+ "defaultValue": .9
977
+ }
978
+ }
979
+ },
980
+ {
981
+ "opcode": "setAccumulatedPoseThreshold",
982
+ "blockType": "COMMAND",
983
+ "text": "set accumulated pose threshold [THRESHOLD]",
984
+ "description": "Sets the minimum accumulated score required to report a pose; values below the threshold report an empty string.",
985
+ "featureFlag": "temporalPoseScoring",
986
+ "arguments": { "THRESHOLD": {
987
+ "type": "NUMBER",
988
+ "defaultValue": 0
989
+ } }
990
+ },
991
+ {
992
+ "opcode": "resetAccumulatedPose",
993
+ "blockType": "COMMAND",
994
+ "text": "reset accumulated pose scores",
995
+ "description": "Clears all accumulated pose scores.",
996
+ "featureFlag": "temporalPoseScoring"
997
+ },
998
+ {
999
+ "opcode": "accumulatedPoseReporter",
1000
+ "blockType": "REPORTER",
1001
+ "text": "accumulated pose",
1002
+ "description": "Returns the pose label whose accumulated score is highest and meets the threshold, or an empty string otherwise.",
1003
+ "featureFlag": "temporalPoseScoring"
1004
+ },
1005
+ {
1006
+ "opcode": "accumulatedScoreReporter",
1007
+ "blockType": "REPORTER",
1008
+ "text": "accumulated score",
1009
+ "description": "Returns the highest accumulated pose score without rounding.",
1010
+ "featureFlag": "temporalPoseScoring"
1011
+ },
1012
+ {
1013
+ "opcode": "accumulatedPoseScoreReporter",
1014
+ "blockType": "REPORTER",
1015
+ "text": "accumulated score of [NAME]",
1016
+ "description": "Returns the accumulated score for a named pose without rounding.",
1017
+ "featureFlag": "temporalPoseScoring",
1018
+ "arguments": { "NAME": {
1019
+ "type": "STRING",
1020
+ "defaultValue": "jump"
1021
+ } }
1022
+ },
1023
+ {
1024
+ "opcode": "isPose",
1025
+ "blockType": "BOOLEAN",
1026
+ "text": "label is [NAME]?",
1027
+ "description": "Reports whether the named label has at least 0.75 confidence.",
1028
+ "arguments": { "NAME": {
1029
+ "type": "STRING",
1030
+ "defaultValue": "jump"
1031
+ } }
1032
+ },
1033
+ {
1034
+ "opcode": "isPoseWithThreshold",
1035
+ "blockType": "BOOLEAN",
1036
+ "text": "label is [NAME] with confidence at least [THRESHOLD]?",
1037
+ "description": "Reports whether the named label meets the given threshold.",
1038
+ "arguments": {
1039
+ "NAME": {
1040
+ "type": "STRING",
1041
+ "defaultValue": "jump"
1042
+ },
1043
+ "THRESHOLD": {
1044
+ "type": "NUMBER",
1045
+ "defaultValue": .75
1046
+ }
1047
+ }
1048
+ },
1049
+ {
1050
+ "opcode": "cameraMsReporter",
1051
+ "blockType": "REPORTER",
1052
+ "text": "camera startup time (ms)",
1053
+ "description": "Returns camera startup time in milliseconds."
1054
+ },
1055
+ {
1056
+ "opcode": "modelLoadMsReporter",
1057
+ "blockType": "REPORTER",
1058
+ "text": "model load time (ms)",
1059
+ "description": "Returns model load time in milliseconds."
1060
+ },
1061
+ {
1062
+ "opcode": "firstRecognitionMsReporter",
1063
+ "blockType": "REPORTER",
1064
+ "text": "first recognition time (ms)",
1065
+ "description": "Returns first recognition time in milliseconds."
1066
+ },
1067
+ {
1068
+ "opcode": "lastErrorReporter",
1069
+ "blockType": "REPORTER",
1070
+ "text": "last error",
1071
+ "description": "Returns the latest recorded error message."
1072
+ }
1073
+ ]
437
1074
  };
438
1075
  //#endregion
1076
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/typeof.js
1077
+ function _typeof(o) {
1078
+ "@babel/helpers - typeof";
1079
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
1080
+ return typeof o;
1081
+ } : function(o) {
1082
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
1083
+ }, _typeof(o);
1084
+ }
1085
+ //#endregion
1086
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/toPrimitive.js
1087
+ function toPrimitive(t, r) {
1088
+ if ("object" != _typeof(t) || !t) return t;
1089
+ var e = t[Symbol.toPrimitive];
1090
+ if (void 0 !== e) {
1091
+ var i = e.call(t, r || "default");
1092
+ if ("object" != _typeof(i)) return i;
1093
+ throw new TypeError("@@toPrimitive must return a primitive value.");
1094
+ }
1095
+ return ("string" === r ? String : Number)(t);
1096
+ }
1097
+ //#endregion
1098
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/toPropertyKey.js
1099
+ function toPropertyKey(t) {
1100
+ var i = toPrimitive(t, "string");
1101
+ return "symbol" == _typeof(i) ? i : i + "";
1102
+ }
1103
+ //#endregion
1104
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/defineProperty.js
1105
+ function _defineProperty(e, r, t) {
1106
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
1107
+ value: t,
1108
+ enumerable: !0,
1109
+ configurable: !0,
1110
+ writable: !0
1111
+ }) : e[r] = t, e;
1112
+ }
1113
+ //#endregion
439
1114
  //#region src/compute-backend.ts
440
1115
  /**
441
1116
  * Compute backend negotiation for the reviewed TensorFlow.js runtime.
@@ -446,205 +1121,214 @@
446
1121
  * free of any TensorFlow.js or DOM dependency so they stay testable.
447
1122
  */
448
1123
  var COMPUTE_MODES = [
449
- "auto",
450
- "webgpu",
451
- "webgl",
452
- "wasm",
453
- "cpu"
1124
+ "auto",
1125
+ "webgpu",
1126
+ "webgl",
1127
+ "wasm",
1128
+ "cpu"
454
1129
  ];
455
1130
  Object.freeze([
456
- "webgpu",
457
- "webgl",
458
- "wasm",
459
- "cpu"
1131
+ "webgpu",
1132
+ "webgl",
1133
+ "wasm",
1134
+ "cpu"
460
1135
  ]);
461
1136
  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"
1137
+ auto: "auto",
1138
+ automatic: "auto",
1139
+ default: "auto",
1140
+ fastest: "auto",
1141
+ 自動: "auto",
1142
+ おまかせ: "auto",
1143
+ webgpu: "webgpu",
1144
+ gpu: "webgpu",
1145
+ webgl: "webgl",
1146
+ wasm: "wasm",
1147
+ webassembly: "wasm",
1148
+ simd: "wasm",
1149
+ cpu: "cpu",
1150
+ javascript: "cpu"
476
1151
  });
477
1152
  function normalizeComputeMode(value, fallback = "auto") {
478
- const text = String(value ?? "").trim().toLowerCase();
479
- if (text === "") return fallback;
480
- return COMPUTE_MODE_ALIASES[text] ?? fallback;
1153
+ const text = String(value ?? "").trim().toLowerCase();
1154
+ if (text === "") return fallback;
1155
+ return COMPUTE_MODE_ALIASES[text] ?? fallback;
481
1156
  }
482
1157
  //#endregion
483
1158
  //#region src/config/feature-flags.ts
484
1159
  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
1160
+ /**
1161
+ * Per-frame confidence flickers near a decision boundary, so the accumulated
1162
+ * score blocks — which integrate confidence over time and decay it — are the
1163
+ * built-in answer to a noisy recognition result. They are on by default; the
1164
+ * event bridge that republishes those changes to other extensions is not.
1165
+ */
1166
+ temporalPoseScoring: true,
1167
+ accumulatedPoseEvents: false,
1168
+ poseOverlay: true
494
1169
  };
495
1170
  //#endregion
496
1171
  //#region src/pose-overlay.ts
497
1172
  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"
1173
+ "nose",
1174
+ "leftEye",
1175
+ "rightEye",
1176
+ "leftEar",
1177
+ "rightEar",
1178
+ "leftShoulder",
1179
+ "rightShoulder",
1180
+ "leftElbow",
1181
+ "rightElbow",
1182
+ "leftWrist",
1183
+ "rightWrist",
1184
+ "leftHip",
1185
+ "rightHip",
1186
+ "leftKnee",
1187
+ "rightKnee",
1188
+ "leftAnkle",
1189
+ "rightAnkle"
515
1190
  ];
516
1191
  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"]
1192
+ ["leftHip", "leftShoulder"],
1193
+ ["leftElbow", "leftShoulder"],
1194
+ ["leftElbow", "leftWrist"],
1195
+ ["leftHip", "leftKnee"],
1196
+ ["leftKnee", "leftAnkle"],
1197
+ ["rightHip", "rightShoulder"],
1198
+ ["rightElbow", "rightShoulder"],
1199
+ ["rightElbow", "rightWrist"],
1200
+ ["rightHip", "rightKnee"],
1201
+ ["rightKnee", "rightAnkle"],
1202
+ ["leftShoulder", "rightShoulder"],
1203
+ ["leftHip", "rightHip"]
529
1204
  ]);
530
1205
  var DEFAULT_POSE_JOINT_STYLE = Object.freeze({
531
- color: "#00e5ff",
532
- opacity: 1,
533
- radius: 4
1206
+ color: "#00e5ff",
1207
+ opacity: 1,
1208
+ radius: 4
534
1209
  });
535
1210
  var DEFAULT_POSE_BONE_STYLE = Object.freeze({
536
- color: "#00e5ff",
537
- opacity: .9,
538
- width: 3
1211
+ color: "#00e5ff",
1212
+ opacity: .9,
1213
+ width: 3
539
1214
  });
540
1215
  var DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING = Object.freeze({
541
- jointOpacity: false,
542
- jointRadius: false,
543
- boneOpacity: false,
544
- boneWidth: false
1216
+ jointOpacity: false,
1217
+ jointRadius: false,
1218
+ boneOpacity: false,
1219
+ boneWidth: false
545
1220
  });
546
1221
  function isPoseKeypointName(value) {
547
- return POSE_KEYPOINT_NAMES.includes(value);
1222
+ return POSE_KEYPOINT_NAMES.includes(value);
548
1223
  }
549
1224
  function confidenceMultiplier(value) {
550
- const confidence = Number(value);
551
- if (!Number.isFinite(confidence)) return 0;
552
- return Math.max(0, Math.min(1, confidence));
1225
+ const confidence = Number(value);
1226
+ if (!Number.isFinite(confidence)) return 0;
1227
+ return Math.max(0, Math.min(1, confidence));
553
1228
  }
554
1229
  var package_default = {
555
- name: "@kubohiroya/turbowarp-teachable-machine",
556
- version: "3.5.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
- ]
1230
+ name: "@kubohiroya/turbowarp-teachable-machine",
1231
+ version: "3.5.2",
1232
+ description: "A TurboWarp extension for recognition using Teachable Machine pose, image, and audio models.",
1233
+ author: "Hiroya Kubo <hiroya@cuc.ac.jp>",
1234
+ license: "MPL-2.0",
1235
+ homepage: "https://github.com/kubohiroya/turbowarp-teachable-machine#readme",
1236
+ repository: {
1237
+ "type": "git",
1238
+ "url": "git+https://github.com/kubohiroya/turbowarp-teachable-machine.git"
1239
+ },
1240
+ bugs: { "url": "https://github.com/kubohiroya/turbowarp-teachable-machine/issues" },
1241
+ exports: {
1242
+ "./composition": {
1243
+ "types": "./dist/types/composition.d.ts",
1244
+ "import": "./dist/composition.js"
1245
+ },
1246
+ "./posenet": {
1247
+ "types": "./dist/types/posenet.d.ts",
1248
+ "import": "./dist/posenet.js"
1249
+ },
1250
+ "./posenet-assets/*": "./dist/posenet/mobilenet-v1-075-stride16/*",
1251
+ "./wasm-assets/*": "./dist/wasm/*",
1252
+ "./dist/backend-wasm.js": "./dist/backend-wasm.js",
1253
+ "./dist/backend-webgpu.js": "./dist/backend-webgpu.js",
1254
+ "./runtime": "./dist/runtime.js",
1255
+ "./dist/tm.js": "./dist/tm.js",
1256
+ "./package.json": "./package.json"
1257
+ },
1258
+ type: "module",
1259
+ files: [
1260
+ "dist/",
1261
+ "README.md",
1262
+ "LICENSE",
1263
+ "THIRD_PARTY_NOTICES.md",
1264
+ "schemas/",
1265
+ "docs/fixed-input-editors.ja.md"
1266
+ ],
1267
+ unpkg: "./dist/tm.js",
1268
+ jsdelivr: "./dist/tm.js",
1269
+ publishConfig: {
1270
+ "access": "public",
1271
+ "registry": "https://registry.npmjs.org/"
1272
+ },
1273
+ packageManager: "pnpm@11.11.0",
1274
+ engines: { "node": ">=22.18.0" },
1275
+ scripts: {
1276
+ "dev": "vite build --watch",
1277
+ "typecheck": "tsc --noEmit",
1278
+ "build": "pnpm build:extension && pnpm build:composition && pnpm build:runtime && pnpm build:backends && pnpm build:posenet && pnpm build:notices && pnpm build:types",
1279
+ "build:extension": "vite build",
1280
+ "build:composition": "vite build --config vite.composition.config.ts",
1281
+ "build:runtime": "vite build --config vite.runtime.config.ts",
1282
+ "build:backends": "vite build --config vite.backend-wasm.config.ts && vite build --config vite.backend-webgpu.config.ts && node scripts/copy-wasm-assets.ts",
1283
+ "build:posenet": "vite build --config vite.posenet.config.ts && node scripts/copy-posenet-assets.ts",
1284
+ "build:notices": "node scripts/copy-third-party-notices.ts",
1285
+ "build:types": "tsc --project tsconfig.build.json",
1286
+ "types:composition:check": "tsc --project tsconfig.composition-consumer.json",
1287
+ "types:posenet:check": "tsc --project tsconfig.posenet-consumer.json",
1288
+ "test": "vitest run",
1289
+ "docs": "node scripts/generate-readme.ts",
1290
+ "docs:check": "npm run docs && git diff --exit-code -- README.md",
1291
+ "docs:pages:check": "node scripts/check-pages.ts",
1292
+ "release:consistency:check": "node scripts/check-release-consistency.ts",
1293
+ "check:dist": "npm run build && git diff --exit-code -- dist",
1294
+ "archive:check": "node scripts/check-github-archive.ts",
1295
+ "pack:check": "npm pack --dry-run --ignore-scripts",
1296
+ "release:check": "npm publish --dry-run --access public --tag latest",
1297
+ "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",
1298
+ "prepack": "npm run build && npm run release:consistency:check"
1299
+ },
1300
+ devDependencies: {
1301
+ "@kubohiroya/turbowarp-camera-source": "0.17.1",
1302
+ "@kubohiroya/vite-plugin-turbowarp-extension": "0.6.1",
1303
+ "@teachablemachine/image": "0.8.5",
1304
+ "@teachablemachine/pose": "0.8.6",
1305
+ "@tensorflow-models/posenet": "2.2.2",
1306
+ "@tensorflow-models/speech-commands": "0.5.4",
1307
+ "@tensorflow/tfjs": "4.22.0",
1308
+ "@tensorflow/tfjs-backend-wasm": "4.22.0",
1309
+ "@tensorflow/tfjs-backend-webgpu": "4.22.0",
1310
+ "@types/node": "^22.20.1",
1311
+ "typescript": "^5.9.3",
1312
+ "vite": "^8.2.2",
1313
+ "vitest": "^4.0.14",
1314
+ "@kubohiroya/turbowarp-extension-data-editor": "0.2.1"
1315
+ },
1316
+ keywords: [
1317
+ "turbowarp",
1318
+ "scratch",
1319
+ "extension",
1320
+ "pose",
1321
+ "image",
1322
+ "audio",
1323
+ "camera",
1324
+ "teachable-machine"
1325
+ ],
1326
+ gitHead: "97ef12018789316d6170a7aab29d7c5b0a4c90f4",
1327
+ "x-turbowarp-source": {
1328
+ "revision": "97ef12018789316d6170a7aab29d7c5b0a4c90f4",
1329
+ "revisionRole": "base-commit",
1330
+ "sourceState": "local-release-overlay"
1331
+ }
648
1332
  };
649
1333
  //#endregion
650
1334
  //#region src/extension.ts
@@ -659,11 +1343,11 @@
659
1343
  */
660
1344
  var CAMERA_SOURCE_RUNTIME_KEY = "ext_kubohiroyacamerasource";
661
1345
  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;
1346
+ if (typeof runtime !== "object" || runtime === null) return void 0;
1347
+ const candidate = runtime[CAMERA_SOURCE_RUNTIME_KEY];
1348
+ if (typeof candidate !== "object" || candidate === null) return void 0;
1349
+ const { acquireCamera } = candidate;
1350
+ return typeof acquireCamera === "function" ? candidate : void 0;
667
1351
  }
668
1352
  var EXTENSION_ID = "kubohiroyatm";
669
1353
  var VERSION = `${package_default.version}-typescript`;
@@ -680,267 +1364,267 @@
680
1364
  * coordinates stay in frame space either way.
681
1365
  */
682
1366
  function mapKeypointsToFrame(keypoints, crop) {
683
- if (!Array.isArray(keypoints) || !(crop.size > 0)) return keypoints;
684
- return keypoints.map((keypoint) => {
685
- if (typeof keypoint !== "object" || keypoint === null) return keypoint;
686
- const position = keypoint.position;
687
- const x = Number(position?.x);
688
- const y = Number(position?.y);
689
- if (!Number.isFinite(x) || !Number.isFinite(y)) return keypoint;
690
- return {
691
- ...keypoint,
692
- position: {
693
- ...position,
694
- x: crop.left + (crop.size - x),
695
- y: crop.top + y
696
- }
697
- };
698
- });
1367
+ if (!Array.isArray(keypoints) || !(crop.size > 0)) return keypoints;
1368
+ return keypoints.map((keypoint) => {
1369
+ if (typeof keypoint !== "object" || keypoint === null) return keypoint;
1370
+ const position = keypoint.position;
1371
+ const x = Number(position?.x);
1372
+ const y = Number(position?.y);
1373
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return keypoint;
1374
+ return {
1375
+ ...keypoint,
1376
+ position: {
1377
+ ...position,
1378
+ x: crop.left + (crop.size - x),
1379
+ y: crop.top + y
1380
+ }
1381
+ };
1382
+ });
699
1383
  }
700
1384
  var BROWSER_RUNTIME_URL = `https://cdn.jsdelivr.net/npm/${package_default.name}@${package_default.version}/dist/runtime.js`;
701
1385
  var POSITION_ITEMS = [
702
- {
703
- text: "top left",
704
- value: "top-left"
705
- },
706
- {
707
- text: "top right",
708
- value: "top-right"
709
- },
710
- {
711
- text: "bottom left",
712
- value: "bottom-left"
713
- },
714
- {
715
- text: "bottom right",
716
- value: "bottom-right"
717
- },
718
- {
719
- text: "center",
720
- value: "center"
721
- },
722
- {
723
- text: "full stage",
724
- value: "full-stage"
725
- }
1386
+ {
1387
+ text: "top left",
1388
+ value: "top-left"
1389
+ },
1390
+ {
1391
+ text: "top right",
1392
+ value: "top-right"
1393
+ },
1394
+ {
1395
+ text: "bottom left",
1396
+ value: "bottom-left"
1397
+ },
1398
+ {
1399
+ text: "bottom right",
1400
+ value: "bottom-right"
1401
+ },
1402
+ {
1403
+ text: "center",
1404
+ value: "center"
1405
+ },
1406
+ {
1407
+ text: "full stage",
1408
+ value: "full-stage"
1409
+ }
726
1410
  ];
727
1411
  var POSITION_ALIASES = {
728
- 左上: "top-left",
729
- 右上: "top-right",
730
- 左下: "bottom-left",
731
- 右下: "bottom-right",
732
- 中央: "center",
733
- ステージ全体: "full-stage",
734
- "top-left": "top-left",
735
- "top-right": "top-right",
736
- "bottom-left": "bottom-left",
737
- "bottom-right": "bottom-right",
738
- center: "center",
739
- "full-stage": "full-stage"
1412
+ 左上: "top-left",
1413
+ 右上: "top-right",
1414
+ 左下: "bottom-left",
1415
+ 右下: "bottom-right",
1416
+ 中央: "center",
1417
+ ステージ全体: "full-stage",
1418
+ "top-left": "top-left",
1419
+ "top-right": "top-right",
1420
+ "bottom-left": "bottom-left",
1421
+ "bottom-right": "bottom-right",
1422
+ center: "center",
1423
+ "full-stage": "full-stage"
740
1424
  };
741
1425
  var PREVIEW_MIRRORING_ITEMS = [{
742
- text: "mirrored",
743
- value: "mirrored"
1426
+ text: "mirrored",
1427
+ value: "mirrored"
744
1428
  }, {
745
- text: "unmirrored",
746
- value: "unmirrored"
1429
+ text: "unmirrored",
1430
+ value: "unmirrored"
747
1431
  }];
748
1432
  var PREVIEW_MIRRORING_ALIASES = {
749
- mirrored: true,
750
- unmirrored: false,
751
- mirror: true,
752
- normal: false,
753
- "左右反転": true,
754
- "そのまま": false
1433
+ mirrored: true,
1434
+ unmirrored: false,
1435
+ mirror: true,
1436
+ normal: false,
1437
+ "左右反転": true,
1438
+ "そのまま": false
755
1439
  };
756
1440
  var COMPUTE_MODE_ITEMS = COMPUTE_MODES.map((value) => ({
757
- text: value,
758
- value
1441
+ text: value,
1442
+ value
759
1443
  }));
760
1444
  var RECOGNITION_MODE_ITEMS = [
761
- {
762
- text: "pose",
763
- value: "pose"
764
- },
765
- {
766
- text: "image",
767
- value: "image"
768
- },
769
- {
770
- text: "audio",
771
- value: "audio"
772
- }
1445
+ {
1446
+ text: "pose",
1447
+ value: "pose"
1448
+ },
1449
+ {
1450
+ text: "image",
1451
+ value: "image"
1452
+ },
1453
+ {
1454
+ text: "audio",
1455
+ value: "audio"
1456
+ }
773
1457
  ];
774
1458
  var RECOGNITION_MODE_ALIASES = {
775
- pose: "pose",
776
- poses: "pose",
777
- body: "pose",
778
- image: "image",
779
- images: "image",
780
- object: "image",
781
- card: "image",
782
- marker: "image",
783
- audio: "audio",
784
- sound: "audio",
785
- voice: "audio",
786
- microphone: "audio",
787
- mic: "audio",
788
- "ポーズ": "pose",
789
- "姿勢": "pose",
790
- "画像": "image",
791
- "物体": "image",
792
- "カード": "image",
793
- "記号": "image",
794
- "音声": "audio",
795
- "音": "audio",
796
- "声": "audio",
797
- "マイク": "audio"
1459
+ pose: "pose",
1460
+ poses: "pose",
1461
+ body: "pose",
1462
+ image: "image",
1463
+ images: "image",
1464
+ object: "image",
1465
+ card: "image",
1466
+ marker: "image",
1467
+ audio: "audio",
1468
+ sound: "audio",
1469
+ voice: "audio",
1470
+ microphone: "audio",
1471
+ mic: "audio",
1472
+ "ポーズ": "pose",
1473
+ "姿勢": "pose",
1474
+ "画像": "image",
1475
+ "物体": "image",
1476
+ "カード": "image",
1477
+ "記号": "image",
1478
+ "音声": "audio",
1479
+ "音": "audio",
1480
+ "声": "audio",
1481
+ "マイク": "audio"
798
1482
  };
799
1483
  var POSE_OVERLAY_VISIBILITY_ITEMS = [{
800
- text: "on",
801
- value: "on"
1484
+ text: "on",
1485
+ value: "on"
802
1486
  }, {
803
- text: "off",
804
- value: "off"
1487
+ text: "off",
1488
+ value: "off"
805
1489
  }];
806
1490
  var POSE_OVERLAY_VISIBILITY_ALIASES = {
807
- on: true,
808
- off: false,
809
- show: true,
810
- hide: false,
811
- true: true,
812
- false: false,
813
- 表示: true,
814
- 非表示: false
1491
+ on: true,
1492
+ off: false,
1493
+ show: true,
1494
+ hide: false,
1495
+ true: true,
1496
+ false: false,
1497
+ 表示: true,
1498
+ 非表示: false
815
1499
  };
816
1500
  var POSE_CONFIDENCE_PROPERTY_ITEMS = [
817
- {
818
- text: "joint opacity",
819
- value: "joint-opacity"
820
- },
821
- {
822
- text: "joint radius",
823
- value: "joint-radius"
824
- },
825
- {
826
- text: "bone opacity",
827
- value: "bone-opacity"
828
- },
829
- {
830
- text: "bone width",
831
- value: "bone-width"
832
- }
1501
+ {
1502
+ text: "joint opacity",
1503
+ value: "joint-opacity"
1504
+ },
1505
+ {
1506
+ text: "joint radius",
1507
+ value: "joint-radius"
1508
+ },
1509
+ {
1510
+ text: "bone opacity",
1511
+ value: "bone-opacity"
1512
+ },
1513
+ {
1514
+ text: "bone width",
1515
+ value: "bone-width"
1516
+ }
833
1517
  ];
834
1518
  var CAMERA_SELECTION_ITEMS = [
835
- {
836
- text: "default camera",
837
- value: "default"
838
- },
839
- {
840
- text: "front camera",
841
- value: "front"
842
- },
843
- {
844
- text: "back camera",
845
- value: "back"
846
- }
1519
+ {
1520
+ text: "default camera",
1521
+ value: "default"
1522
+ },
1523
+ {
1524
+ text: "front camera",
1525
+ value: "front"
1526
+ },
1527
+ {
1528
+ text: "back camera",
1529
+ value: "back"
1530
+ }
847
1531
  ];
848
1532
  var CAMERA_SELECTION_ALIASES = {
849
- default: "default",
850
- front: "front",
851
- back: "back",
852
- user: "front",
853
- environment: "back",
854
- "既定": "default",
855
- "インカメラ": "front",
856
- "前面カメラ": "front",
857
- "背面カメラ": "back"
1533
+ default: "default",
1534
+ front: "front",
1535
+ back: "back",
1536
+ user: "front",
1537
+ environment: "back",
1538
+ "既定": "default",
1539
+ "インカメラ": "front",
1540
+ "前面カメラ": "front",
1541
+ "背面カメラ": "back"
858
1542
  };
859
1543
  var loadingPromises = /* @__PURE__ */ new Map();
860
1544
  function normalizePosition(value) {
861
- return POSITION_ALIASES[String(value ?? "bottom-right")] ?? "bottom-right";
1545
+ return POSITION_ALIASES[String(value ?? "bottom-right")] ?? "bottom-right";
862
1546
  }
863
1547
  function normalizePreviewMirroring(value) {
864
- return PREVIEW_MIRRORING_ALIASES[String(value ?? "mirrored").trim().toLowerCase()] ?? true;
1548
+ return PREVIEW_MIRRORING_ALIASES[String(value ?? "mirrored").trim().toLowerCase()] ?? true;
865
1549
  }
866
1550
  function normalizeRecognitionMode(value) {
867
- return RECOGNITION_MODE_ALIASES[String(value ?? "pose").trim().toLowerCase()] ?? "pose";
1551
+ return RECOGNITION_MODE_ALIASES[String(value ?? "pose").trim().toLowerCase()] ?? "pose";
868
1552
  }
869
1553
  function normalizeCameraSelection(value) {
870
- const selection = String(value ?? "default").trim();
871
- const normalized = CAMERA_SELECTION_ALIASES[selection.toLowerCase() || "default"];
872
- if (normalized === "default" || normalized === "front" || normalized === "back") return {
873
- kind: "preference",
874
- value: normalized
875
- };
876
- return {
877
- kind: "device",
878
- value: selection
879
- };
1554
+ const selection = String(value ?? "default").trim();
1555
+ const normalized = CAMERA_SELECTION_ALIASES[selection.toLowerCase() || "default"];
1556
+ if (normalized === "default" || normalized === "front" || normalized === "back") return {
1557
+ kind: "preference",
1558
+ value: normalized
1559
+ };
1560
+ return {
1561
+ kind: "device",
1562
+ value: selection
1563
+ };
880
1564
  }
881
1565
  function normalizePoseOverlayVisibility(value) {
882
- return POSE_OVERLAY_VISIBILITY_ALIASES[String(value ?? "on").trim().toLowerCase()] ?? true;
1566
+ return POSE_OVERLAY_VISIBILITY_ALIASES[String(value ?? "on").trim().toLowerCase()] ?? true;
883
1567
  }
884
1568
  function normalizePoseStyleNumber(value, fallback, maximum) {
885
- const number = value === "" ? fallback : Number(value);
886
- if (!Number.isFinite(number)) return fallback;
887
- return Math.max(0, maximum === void 0 ? number : Math.min(maximum, number));
1569
+ const number = value === "" ? fallback : Number(value);
1570
+ if (!Number.isFinite(number)) return fallback;
1571
+ return Math.max(0, maximum === void 0 ? number : Math.min(maximum, number));
888
1572
  }
889
1573
  function normalizePoseColor(value, fallback) {
890
- return String(value ?? "").trim() || fallback;
1574
+ return String(value ?? "").trim() || fallback;
891
1575
  }
892
1576
  function cameraConstraints(selection) {
893
- if (selection.kind === "device") return { deviceId: { exact: selection.value } };
894
- if (selection.value === "front") return { facingMode: { ideal: "user" } };
895
- if (selection.value === "back") return { facingMode: { ideal: "environment" } };
1577
+ if (selection.kind === "device") return { deviceId: { exact: selection.value } };
1578
+ if (selection.value === "front") return { facingMode: { ideal: "user" } };
1579
+ if (selection.value === "back") return { facingMode: { ideal: "environment" } };
896
1580
  }
897
1581
  function scriptLoadedFor(src) {
898
- if (src === BROWSER_RUNTIME_URL) return typeof globalThis.tf !== "undefined" && typeof globalThis.tmPose !== "undefined" && typeof globalThis.tmImage !== "undefined" && typeof globalThis.tmAudio !== "undefined";
899
- return false;
1582
+ if (src === BROWSER_RUNTIME_URL) return typeof globalThis.tf !== "undefined" && typeof globalThis.tmPose !== "undefined" && typeof globalThis.tmImage !== "undefined" && typeof globalThis.tmAudio !== "undefined";
1583
+ return false;
900
1584
  }
901
1585
  function loadScript(src) {
902
- if (scriptLoadedFor(src)) return Promise.resolve();
903
- const active = loadingPromises.get(src);
904
- if (active) return active;
905
- const existing = Array.from(document.scripts).find((script) => script.src === src);
906
- if (existing?.dataset.kubohiroyatmLoaded === "true") return Promise.resolve();
907
- const promise = new Promise((resolve, reject) => {
908
- const script = existing ?? document.createElement("script");
909
- const cleanup = () => {
910
- script.removeEventListener("load", handleLoad);
911
- script.removeEventListener("error", handleError);
912
- };
913
- const handleLoad = () => {
914
- cleanup();
915
- script.dataset.kubohiroyatmLoaded = "true";
916
- resolve();
917
- };
918
- const handleError = () => {
919
- cleanup();
920
- loadingPromises.delete(src);
921
- reject(/* @__PURE__ */ new Error("TM: Failed to load script: " + src));
922
- };
923
- script.addEventListener("load", handleLoad, { once: true });
924
- script.addEventListener("error", handleError, { once: true });
925
- if (!existing) {
926
- script.src = src;
927
- document.head.appendChild(script);
928
- } else queueMicrotask(() => {
929
- if (scriptLoadedFor(src)) handleLoad();
930
- });
931
- });
932
- loadingPromises.set(src, promise);
933
- return promise;
1586
+ if (scriptLoadedFor(src)) return Promise.resolve();
1587
+ const active = loadingPromises.get(src);
1588
+ if (active) return active;
1589
+ const existing = Array.from(document.scripts).find((script) => script.src === src);
1590
+ if (existing?.dataset.kubohiroyatmLoaded === "true") return Promise.resolve();
1591
+ const promise = new Promise((resolve, reject) => {
1592
+ const script = existing ?? document.createElement("script");
1593
+ const cleanup = () => {
1594
+ script.removeEventListener("load", handleLoad);
1595
+ script.removeEventListener("error", handleError);
1596
+ };
1597
+ const handleLoad = () => {
1598
+ cleanup();
1599
+ script.dataset.kubohiroyatmLoaded = "true";
1600
+ resolve();
1601
+ };
1602
+ const handleError = () => {
1603
+ cleanup();
1604
+ loadingPromises.delete(src);
1605
+ reject(/* @__PURE__ */ new Error("TM: Failed to load script: " + src));
1606
+ };
1607
+ script.addEventListener("load", handleLoad, { once: true });
1608
+ script.addEventListener("error", handleError, { once: true });
1609
+ if (!existing) {
1610
+ script.src = src;
1611
+ document.head.appendChild(script);
1612
+ } else queueMicrotask(() => {
1613
+ if (scriptLoadedFor(src)) handleLoad();
1614
+ });
1615
+ });
1616
+ loadingPromises.set(src, promise);
1617
+ return promise;
934
1618
  }
935
1619
  function rectanglesIntersect(left, right) {
936
- return left.right > right.left && left.left < right.right && left.bottom > right.top && left.top < right.bottom;
1620
+ return left.right > right.left && left.left < right.right && left.bottom > right.top && left.top < right.bottom;
937
1621
  }
938
1622
  function canvasScore(width, height) {
939
- if (width <= 0 || height <= 0) return Number.NEGATIVE_INFINITY;
940
- return width * height / (1 + Math.abs(width / height - 4 / 3) * 4);
1623
+ if (width <= 0 || height <= 0) return Number.NEGATIVE_INFINITY;
1624
+ return width * height / (1 + Math.abs(width / height - 4 / 3) * 4);
941
1625
  }
942
1626
  function isDocumentHidden() {
943
- return typeof document !== "undefined" && document.visibilityState === "hidden";
1627
+ return typeof document !== "undefined" && document.visibilityState === "hidden";
944
1628
  }
945
1629
  /**
946
1630
  * Initialize the camera canvas before Teachable Machine or TensorFlow.js requests its context.
@@ -950,1227 +1634,1238 @@
950
1634
  * readback-optimized context, and the legacy backend parameter remains accepted for compatibility.
951
1635
  */
952
1636
  function initializeCameraReadbackContext(canvas, _tensorflowBackend) {
953
- if (typeof canvas !== "object" || canvas === null || typeof canvas.getContext !== "function") throw new Error("TM: Webcam canvas does not provide a 2D context.");
954
- const context = canvas.getContext("2d");
955
- if (!context) throw new Error("TM: Webcam canvas 2D context is unavailable.");
956
- return context;
1637
+ if (typeof canvas !== "object" || canvas === null || typeof canvas.getContext !== "function") throw new Error("TM: Webcam canvas does not provide a 2D context.");
1638
+ const context = canvas.getContext("2d");
1639
+ if (!context) throw new Error("TM: Webcam canvas 2D context is unavailable.");
1640
+ return context;
957
1641
  }
958
1642
  var TMExtension = class {
959
- constructor(featureFlags = {}, dependencies = {}) {
960
- this.featureFlags = {
961
- ...FEATURE_FLAGS,
962
- ...featureFlags
963
- };
964
- this.tmPoseRuntime = dependencies.poseRuntime ?? dependencies.runtime ?? null;
965
- this.tmImageRuntime = dependencies.imageRuntime ?? null;
966
- this.tmAudioRuntime = dependencies.audioRuntime ?? null;
967
- this.tmComputeRuntime = dependencies.compute ?? null;
968
- this.computeMode = normalizeComputeMode(dependencies.computeMode);
969
- this.computeSelection = null;
970
- this.modelOwnedByExtension = false;
971
- this.allowRemoteLibraries = dependencies.allowRemoteLibraries ?? true;
972
- this.onAccumulatedPoseChanged = dependencies.onAccumulatedPoseChanged ?? null;
973
- this.recognitionMode = "pose";
974
- this.modelURL = "";
975
- this.model = null;
976
- this.webcam = null;
977
- this.cameraLease = null;
978
- this.cameraRunning = false;
979
- this.cameraSelection = "default";
980
- this.cameraSelectionIsDeviceId = false;
981
- this.cameraDevices = [];
982
- this.activeCameraDeviceId = "";
983
- this.activeCameraDeviceName = "";
984
- this.cameraSelectionQueue = Promise.resolve();
985
- this.recognizing = false;
986
- this.audioListening = false;
987
- this.loopStarted = false;
988
- this.loopGeneration = 0;
989
- this.activeModelOperations = /* @__PURE__ */ new Map();
990
- this.currentPoseName = "";
991
- this.score = 0;
992
- this.predictions = {};
993
- this.accumulationCoefficient = 1;
994
- this.decayCoefficient = .9;
995
- this.activeDecayCoefficient = .9;
996
- this.accumulatedPoseThreshold = 0;
997
- this.accumulatedPoseName = "";
998
- this.accumulatedScore = 0;
999
- this.accumulatedPredictions = {};
1000
- this.lastAccumulationTime = null;
1001
- this.previewOpacity = .6;
1002
- this.previewPosition = "bottom-right";
1003
- this.previewMirrored = true;
1004
- this.previewVisible = true;
1005
- this.previewCanvas = null;
1006
- this.previewStageElement = null;
1007
- this.poseOverlayVisible = true;
1008
- this.poseOverlayMinimumConfidence = .5;
1009
- this.poseJointStyles = Object.fromEntries(POSE_KEYPOINT_NAMES.map((part) => [part, { ...DEFAULT_POSE_JOINT_STYLE }]));
1010
- this.poseBoneStyle = { ...DEFAULT_POSE_BONE_STYLE };
1011
- this.poseConfidenceScaling = { ...DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING };
1012
- this.poseOverlaySvg = null;
1013
- this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1014
- this.poseOverlayBoneElements = [];
1015
- this.latestPoseKeypoints = [];
1016
- this.cameraMs = 0;
1017
- this.modelLoadMs = 0;
1018
- this.firstRecognitionMs = 0;
1019
- this.lastError = "";
1020
- this.accumulatedPosePausedForBackground = isDocumentHidden();
1021
- this.visibilityChangeListener = () => this.handleDocumentVisibilityChange();
1022
- if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") document.addEventListener("visibilitychange", this.visibilityChangeListener);
1023
- }
1024
- getInfo() {
1025
- return {
1026
- id: EXTENSION_ID,
1027
- name: Scratch.translate(block_definitions_default.extensionName),
1028
- docsURI: DOCS_URI,
1029
- blockIconURI: BLOCK_ICON_URI,
1030
- blocks: block_definitions_default.blocks.filter((block) => !block.featureFlag || this.featureFlags[block.featureFlag]).map((block) => ({
1031
- opcode: block.opcode,
1032
- blockType: Scratch.BlockType[block.blockType],
1033
- text: Scratch.translate(block.text),
1034
- ...block.disableMonitor ? { disableMonitor: true } : {},
1035
- ...block.arguments ? { arguments: Object.fromEntries(Object.entries(block.arguments).map(([name, argument]) => [name, {
1036
- type: Scratch.ArgumentType[argument.type],
1037
- defaultValue: argument.defaultValue,
1038
- ...argument.menu ? { menu: argument.menu } : {}
1039
- }])) } : {}
1040
- })),
1041
- menus: {
1042
- positionMenu: {
1043
- acceptReporters: true,
1044
- items: POSITION_ITEMS.map((item) => ({
1045
- text: Scratch.translate(item.text),
1046
- value: item.value
1047
- }))
1048
- },
1049
- previewMirroringMenu: {
1050
- acceptReporters: true,
1051
- items: PREVIEW_MIRRORING_ITEMS.map((item) => ({
1052
- text: Scratch.translate(item.text),
1053
- value: item.value
1054
- }))
1055
- },
1056
- recognitionModeMenu: {
1057
- acceptReporters: true,
1058
- items: RECOGNITION_MODE_ITEMS.map((item) => ({
1059
- text: Scratch.translate(item.text),
1060
- value: item.value
1061
- }))
1062
- },
1063
- computeModeMenu: {
1064
- acceptReporters: true,
1065
- items: COMPUTE_MODE_ITEMS.map((item) => ({
1066
- text: Scratch.translate(item.text),
1067
- value: item.value
1068
- }))
1069
- },
1070
- poseOverlayVisibilityMenu: {
1071
- acceptReporters: true,
1072
- items: POSE_OVERLAY_VISIBILITY_ITEMS.map((item) => ({
1073
- text: Scratch.translate(item.text),
1074
- value: item.value
1075
- }))
1076
- },
1077
- poseKeypointMenu: {
1078
- acceptReporters: true,
1079
- items: POSE_KEYPOINT_NAMES.map((part) => ({
1080
- text: part,
1081
- value: part
1082
- }))
1083
- },
1084
- poseConfidencePropertyMenu: {
1085
- acceptReporters: true,
1086
- items: POSE_CONFIDENCE_PROPERTY_ITEMS.map((item) => ({
1087
- text: Scratch.translate(item.text),
1088
- value: item.value
1089
- }))
1090
- },
1091
- cameraMenu: {
1092
- acceptReporters: true,
1093
- items: "getCameraMenuItems"
1094
- }
1095
- }
1096
- };
1097
- }
1098
- versionReporter() {
1099
- return VERSION;
1100
- }
1101
- setLastError(error) {
1102
- this.lastError = String(error?.message ?? error);
1103
- }
1104
- setRecognitionMode(args) {
1105
- const mode = normalizeRecognitionMode(args.MODE);
1106
- if (mode === this.recognitionMode) return;
1107
- if (this.recognizing) throw new Error("Teachable Machine: Stop recognition before changing recognition mode.");
1108
- if (this.cameraRunning) throw new Error("Teachable Machine: Stop the camera before changing recognition mode.");
1109
- this.recognitionMode = mode;
1110
- this.model = null;
1111
- this.modelOwnedByExtension = false;
1112
- this.modelURL = "";
1113
- this.modelLoadMs = 0;
1114
- this.firstRecognitionMs = 0;
1115
- this.currentPoseName = "";
1116
- this.score = 0;
1117
- this.predictions = {};
1118
- this.clearPoseOverlay();
1119
- this.resetAccumulatedPose();
1120
- }
1121
- recognitionModeReporter() {
1122
- return this.recognitionMode;
1123
- }
1124
- activeComputeRuntime() {
1125
- if (this.tmComputeRuntime) return this.tmComputeRuntime;
1126
- const runtime = globalThis.tmCompute;
1127
- return runtime && typeof runtime.select === "function" ? runtime : null;
1128
- }
1129
- /**
1130
- * TensorFlow.js binds every tensor to the backend that was active when the
1131
- * tensor was created, so the backend is negotiated before any model loads. A
1132
- * host that preloads a runtime without a compute controller keeps whatever
1133
- * backend TensorFlow.js selected for itself.
1134
- */
1135
- async ensureComputeBackend() {
1136
- const compute = this.activeComputeRuntime();
1137
- if (!compute) return null;
1138
- const selection = await compute.select(this.computeMode);
1139
- this.computeSelection = selection;
1140
- if (selection.fallback) this.setLastError(/* @__PURE__ */ new Error(`Teachable Machine: The ${selection.requested} compute backend was unavailable, so ${selection.backend} is in use.`));
1141
- return selection;
1142
- }
1143
- async setComputeMode(args) {
1144
- const mode = normalizeComputeMode(args.MODE, this.computeMode);
1145
- if (mode === this.computeMode) return;
1146
- if (this.recognizing) throw new Error("Teachable Machine: Stop recognition before changing the compute mode.");
1147
- if (this.model && !this.modelOwnedByExtension) throw new Error("Teachable Machine: Release the prepared model before changing the compute mode.");
1148
- const previousModel = this.model;
1149
- this.computeMode = mode;
1150
- this.computeSelection = null;
1151
- this.model = null;
1152
- this.modelOwnedByExtension = false;
1153
- this.modelLoadMs = 0;
1154
- this.firstRecognitionMs = 0;
1155
- if (previousModel) await this.releaseOwnedModel(previousModel);
1156
- if (this.activeComputeRuntime()) await this.ensureComputeBackend();
1157
- }
1158
- async releaseOwnedModel(model) {
1159
- try {
1160
- await this.waitForPreparedModelIdle(model);
1161
- await model.dispose?.();
1162
- } catch (error) {
1163
- this.setLastError(error);
1164
- }
1165
- }
1166
- computeModeReporter() {
1167
- return this.computeMode;
1168
- }
1169
- computeBackendReporter() {
1170
- const compute = this.activeComputeRuntime();
1171
- if (!compute) return "";
1172
- return this.computeSelection?.backend ?? compute.getBackend() ?? "";
1173
- }
1174
- setModelURL(args) {
1175
- this.modelURL = String(args.URL || "").trim();
1176
- if (this.modelURL && !this.modelURL.endsWith("/")) this.modelURL += "/";
1177
- this.model = null;
1178
- this.modelLoadMs = 0;
1179
- this.firstRecognitionMs = 0;
1180
- }
1181
- activeRuntime() {
1182
- if (this.recognitionMode === "audio") return this.tmAudioRuntime;
1183
- return this.recognitionMode === "image" ? this.tmImageRuntime : this.tmPoseRuntime;
1184
- }
1185
- async ensureLibrariesLoaded() {
1186
- if (this.activeRuntime()) {
1187
- if (this.activeComputeRuntime()) await this.ensureComputeBackend();
1188
- return;
1189
- }
1190
- if (!this.allowRemoteLibraries) throw new Error("Teachable Machine: A preloaded runtime is required.");
1191
- if (typeof globalThis.tf === "undefined" || typeof globalThis.tmPose === "undefined" || typeof globalThis.tmImage === "undefined" || typeof globalThis.tmAudio === "undefined") await loadScript(BROWSER_RUNTIME_URL);
1192
- 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.");
1193
- this.tmPoseRuntime = globalThis.tmPose;
1194
- this.tmImageRuntime = globalThis.tmImage;
1195
- this.tmAudioRuntime = globalThis.tmAudio;
1196
- await this.ensureComputeBackend();
1197
- }
1198
- cleanupCameraResources() {
1199
- const lease = this.cameraLease;
1200
- this.cameraLease = null;
1201
- const video = this.webcam?.webcam;
1202
- if (lease) lease.release();
1203
- else if (video?.srcObject) {
1204
- video.srcObject.getTracks().forEach((track) => track.stop());
1205
- video.srcObject = null;
1206
- }
1207
- this.previewCanvas?.parentNode?.removeChild(this.previewCanvas);
1208
- this.poseOverlaySvg?.parentNode?.removeChild(this.poseOverlaySvg);
1209
- this.previewCanvas = null;
1210
- this.previewStageElement = null;
1211
- this.poseOverlaySvg = null;
1212
- this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1213
- this.poseOverlayBoneElements = [];
1214
- this.latestPoseKeypoints = [];
1215
- this.webcam = null;
1216
- this.cameraRunning = false;
1217
- this.activeCameraDeviceId = "";
1218
- this.activeCameraDeviceName = "";
1219
- this.loopStarted = false;
1220
- this.loopGeneration += 1;
1221
- }
1222
- async startCamera() {
1223
- if (this.recognitionMode === "audio") throw new Error("Teachable Machine: Audio mode uses the microphone through start recognition.");
1224
- if (this.cameraRunning && this.webcam) {
1225
- this.attachPreviewToStage();
1226
- return;
1227
- }
1228
- try {
1229
- this.lastError = "";
1230
- const startedAt = performance.now();
1231
- await this.ensureLibrariesLoaded();
1232
- const runtime = this.activeRuntime();
1233
- if (!runtime) throw new Error("Teachable Machine: Runtime is unavailable.");
1234
- if (!("Webcam" in runtime)) throw new Error("Teachable Machine: Camera runtime is unavailable in the current mode.");
1235
- this.webcam = new runtime.Webcam(320, 240, true);
1236
- const lease = await this.acquireSharedCamera();
1237
- if (lease) {
1238
- this.cameraLease = lease;
1239
- this.webcam.webcam = lease.getFrameSource().element;
1240
- }
1241
- const constraints = lease ? void 0 : cameraConstraints(this.resolvedCameraSelection());
1242
- if (constraints) await this.webcam.setup(constraints);
1243
- else await this.webcam.setup();
1244
- this.ensureCameraCanvas();
1245
- initializeCameraReadbackContext(this.webcam.canvas);
1246
- if (!lease) await this.webcam.play();
1247
- this.attachPreviewToStage();
1248
- this.cameraRunning = true;
1249
- try {
1250
- await this.refreshCameraDevices();
1251
- } catch {}
1252
- this.updateActiveCameraInfo();
1253
- this.cameraMs = Math.round(performance.now() - startedAt);
1254
- this.startLoopIfNeeded();
1255
- } catch (error) {
1256
- this.cleanupCameraResources();
1257
- this.setLastError(error);
1258
- throw error;
1259
- }
1260
- }
1261
- /**
1262
- * Give the camera the readback canvas `setup()` did not build.
1263
- *
1264
- * Upstream creates that canvas inside the same branch that opens the camera, so an injected
1265
- * element skips the canvas with the `getUserMedia` call. 3.4.0 shipped assuming the two were
1266
- * separate, and every leased start then failed at the first context request with "Webcam canvas
1267
- * does not provide a 2D context" -- which is to say pose recognition did not start at all
1268
- * wherever Camera Source was present. The canvas built here is the one upstream would have
1269
- * built, so the mirrored centre crop every downstream reader expects is unchanged.
1270
- */
1271
- ensureCameraCanvas() {
1272
- if (!this.webcam || this.webcam.canvas) return;
1273
- const canvas = document.createElement("canvas");
1274
- canvas.width = this.webcam.width ?? 320;
1275
- canvas.height = this.webcam.height ?? 240;
1276
- this.webcam.canvas = canvas;
1277
- }
1278
- /**
1279
- * A lease on the shared camera, or null when Camera Source is not loaded.
1280
- *
1281
- * Camera Source owns the device and decides when a stream stops, which is what lets one camera
1282
- * serve pose recognition and anything else reading frames at the same time. It is a separate
1283
- * TurboWarp extension, and this one is also distributed for standalone URL loading, so its
1284
- * absence is ordinary rather than a failure: the self-acquired path stays for it.
1285
- *
1286
- * One camera id for every visual recognition mode. Opening a second device for a work that
1287
- * recognises both poses and images is not what sharing is for, and `pose` is the role name
1288
- * Camera Source's own documentation uses for this consumer.
1289
- */
1290
- async acquireSharedCamera() {
1291
- const cameraSource = readCameraSource(Scratch.vm?.runtime);
1292
- if (!cameraSource) return null;
1293
- const selection = this.resolvedCameraSelection();
1294
- const options = {
1295
- owner: package_default.name,
1296
- cameraId: "pose"
1297
- };
1298
- if (selection.kind === "device") options.deviceId = selection.value;
1299
- return cameraSource.acquireCamera(options);
1300
- }
1301
- stopCamera() {
1302
- try {
1303
- this.stopRecognition();
1304
- this.cleanupCameraResources();
1305
- this.currentPoseName = "";
1306
- this.score = 0;
1307
- this.predictions = {};
1308
- } catch (error) {
1309
- this.setLastError(error);
1310
- throw error;
1311
- }
1312
- }
1313
- isCameraRunning() {
1314
- return this.cameraRunning;
1315
- }
1316
- getCameraMenuItems() {
1317
- const fixedItems = CAMERA_SELECTION_ITEMS.map((item) => ({
1318
- text: Scratch.translate(item.text),
1319
- value: item.value
1320
- }));
1321
- const seen = /* @__PURE__ */ new Set();
1322
- const deviceItems = [];
1323
- this.cameraDevices.forEach((device, index) => {
1324
- if (!device.deviceId || seen.has(device.deviceId)) return;
1325
- seen.add(device.deviceId);
1326
- deviceItems.push({
1327
- text: device.label || `${Scratch.translate("camera")} ${index + 1}`,
1328
- value: device.deviceId
1329
- });
1330
- });
1331
- return [...fixedItems, ...deviceItems];
1332
- }
1333
- async refreshCameraDevices() {
1334
- const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
1335
- if (!mediaDevices || typeof mediaDevices.enumerateDevices !== "function") throw new Error("TM: Camera enumeration is not available in this browser.");
1336
- const devices = await mediaDevices.enumerateDevices();
1337
- this.cameraDevices = devices.filter((device) => device.kind === "videoinput").map((device) => ({
1338
- deviceId: device.deviceId,
1339
- label: device.label
1340
- }));
1341
- return this.cameraDevices;
1342
- }
1343
- async refreshCameraList() {
1344
- try {
1345
- this.lastError = "";
1346
- await this.refreshCameraDevices();
1347
- if (this.cameraRunning) this.updateActiveCameraInfo();
1348
- } catch (error) {
1349
- this.setLastError(error);
1350
- throw error;
1351
- }
1352
- }
1353
- setCameraSelection(args) {
1354
- const selection = normalizeCameraSelection(args.CAMERA);
1355
- return this.enqueueCameraSelection(selection);
1356
- }
1357
- setCameraDeviceId(deviceId) {
1358
- if (typeof deviceId !== "string" || deviceId.trim().length === 0) return Promise.reject(/* @__PURE__ */ new Error("TM: Camera device ID must be a non-empty string."));
1359
- return this.enqueueCameraSelection({
1360
- kind: "device",
1361
- value: deviceId
1362
- });
1363
- }
1364
- resolvedCameraSelection() {
1365
- return this.cameraSelectionIsDeviceId ? {
1366
- kind: "device",
1367
- value: this.cameraSelection
1368
- } : {
1369
- kind: "preference",
1370
- value: this.cameraSelection
1371
- };
1372
- }
1373
- enqueueCameraSelection(selection) {
1374
- const operation = this.cameraSelectionQueue.then(() => this.applyCameraSelection(selection));
1375
- this.cameraSelectionQueue = operation.catch(() => void 0);
1376
- return operation;
1377
- }
1378
- async applyCameraSelection(selection) {
1379
- const previousSelection = this.cameraSelection;
1380
- const previousSelectionIsDeviceId = this.cameraSelectionIsDeviceId;
1381
- const wasRunning = this.cameraRunning;
1382
- this.cameraSelection = selection.value;
1383
- this.cameraSelectionIsDeviceId = selection.kind === "device";
1384
- if (!wasRunning) return;
1385
- this.cleanupCameraResources();
1386
- try {
1387
- await this.startCamera();
1388
- } catch (switchError) {
1389
- this.cameraSelection = previousSelection;
1390
- this.cameraSelectionIsDeviceId = previousSelectionIsDeviceId;
1391
- try {
1392
- await this.startCamera();
1393
- } catch (rollbackError) {
1394
- const error = new AggregateError([switchError, rollbackError], "TM: Camera switch and rollback both failed.");
1395
- this.setLastError(error);
1396
- throw error;
1397
- }
1398
- this.setLastError(switchError);
1399
- throw switchError;
1400
- }
1401
- }
1402
- updateActiveCameraInfo() {
1403
- const stream = this.webcam?.webcam?.srcObject;
1404
- const videoTrack = stream?.getVideoTracks?.()[0] ?? stream?.getTracks?.().find((track) => track.kind === "video" || track.kind === void 0);
1405
- const settings = videoTrack?.getSettings?.() ?? {};
1406
- const selectedDeviceId = this.cameraSelectionIsDeviceId ? this.cameraSelection : "";
1407
- this.activeCameraDeviceId = String(settings.deviceId || selectedDeviceId);
1408
- const device = this.cameraDevices.find((candidate) => candidate.deviceId === this.activeCameraDeviceId);
1409
- this.activeCameraDeviceName = String(videoTrack?.label || device?.label || "");
1410
- }
1411
- cameraCountReporter() {
1412
- return this.cameraDevices.length;
1413
- }
1414
- cameraDeviceIdReporter() {
1415
- return this.activeCameraDeviceId;
1416
- }
1417
- cameraDeviceNameReporter() {
1418
- return this.activeCameraDeviceName;
1419
- }
1420
- showPreview() {
1421
- try {
1422
- this.previewVisible = true;
1423
- if (!this.webcam?.canvas) throw new Error("TM: Start the camera before showing the preview.");
1424
- this.attachPreviewToStage();
1425
- this.previewCanvas.style.display = "block";
1426
- this.updatePoseOverlayVisibility();
1427
- this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
1428
- } catch (error) {
1429
- this.setLastError(error);
1430
- throw error;
1431
- }
1432
- }
1433
- hidePreview() {
1434
- this.previewVisible = false;
1435
- if (this.previewCanvas) this.previewCanvas.style.display = "none";
1436
- this.updatePoseOverlayVisibility();
1437
- }
1438
- isPreviewVisible() {
1439
- return this.previewVisible;
1440
- }
1441
- setPreviewOpacity(args) {
1442
- let opacity = args.OPACITY === "" ? .6 : Number(args.OPACITY);
1443
- if (Number.isNaN(opacity)) opacity = .6;
1444
- this.previewOpacity = Math.max(0, Math.min(1, opacity));
1445
- if (this.previewCanvas) this.previewCanvas.style.opacity = String(this.previewOpacity);
1446
- }
1447
- setPreviewPosition(args) {
1448
- this.previewPosition = normalizePosition(args.POSITION);
1449
- if (this.previewCanvas) {
1450
- this.updatePreviewStyle();
1451
- this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
1452
- }
1453
- }
1454
- setPreviewMirroring(args) {
1455
- this.previewMirrored = normalizePreviewMirroring(args.MIRRORING);
1456
- if (this.previewCanvas) this.updatePreviewStyle();
1457
- }
1458
- previewMirroringReporter() {
1459
- return this.previewMirrored ? "mirrored" : "unmirrored";
1460
- }
1461
- setPoseOverlayVisibility(args) {
1462
- this.poseOverlayVisible = normalizePoseOverlayVisibility(args.VISIBILITY);
1463
- this.updatePoseOverlayVisibility();
1464
- }
1465
- showPoseOverlay() {
1466
- this.poseOverlayVisible = true;
1467
- this.updatePoseOverlayVisibility();
1468
- }
1469
- hidePoseOverlay() {
1470
- this.poseOverlayVisible = false;
1471
- this.updatePoseOverlayVisibility();
1472
- }
1473
- isPoseOverlayVisible() {
1474
- return this.featureFlags.poseOverlay && this.poseOverlayVisible;
1475
- }
1476
- setPoseJointStyle(args) {
1477
- const part = String(args.PART ?? "");
1478
- if (!isPoseKeypointName(part)) throw new Error(`TM: Unknown PoseNet joint: ${part}`);
1479
- const previous = this.poseJointStyles[part];
1480
- this.poseJointStyles[part] = {
1481
- color: normalizePoseColor(args.COLOR, previous.color),
1482
- opacity: normalizePoseStyleNumber(args.OPACITY, previous.opacity, 1),
1483
- radius: normalizePoseStyleNumber(args.RADIUS, previous.radius)
1484
- };
1485
- this.redrawPoseOverlay();
1486
- }
1487
- setPoseBoneStyle(args) {
1488
- this.poseBoneStyle = {
1489
- color: normalizePoseColor(args.COLOR, this.poseBoneStyle.color),
1490
- opacity: normalizePoseStyleNumber(args.OPACITY, this.poseBoneStyle.opacity, 1),
1491
- width: normalizePoseStyleNumber(args.WIDTH, this.poseBoneStyle.width)
1492
- };
1493
- this.redrawPoseOverlay();
1494
- }
1495
- setPoseOverlayMinimumConfidence(args) {
1496
- this.poseOverlayMinimumConfidence = normalizePoseStyleNumber(args.CONFIDENCE, this.poseOverlayMinimumConfidence, 1);
1497
- this.redrawPoseOverlay();
1498
- }
1499
- setPoseConfidenceScaling(args) {
1500
- const property = String(args.PROPERTY ?? "");
1501
- const key = {
1502
- "joint-opacity": "jointOpacity",
1503
- "joint-radius": "jointRadius",
1504
- "bone-opacity": "boneOpacity",
1505
- "bone-width": "boneWidth"
1506
- }[property];
1507
- if (!key) throw new Error(`TM: Unknown confidence-scaled property: ${property}`);
1508
- this.poseConfidenceScaling[key] = normalizePoseOverlayVisibility(args.STATE);
1509
- this.redrawPoseOverlay();
1510
- }
1511
- async loadModel() {
1512
- if (this.model) return;
1513
- if (!this.modelURL) throw new Error("Teachable Machine: Set the model URL first.");
1514
- try {
1515
- this.lastError = "";
1516
- const startedAt = performance.now();
1517
- await this.ensureLibrariesLoaded();
1518
- const runtime = this.activeRuntime();
1519
- if (!runtime || typeof runtime.load !== "function") throw new Error("Teachable Machine: The URL loader is not available.");
1520
- this.model = await runtime.load(this.modelURL + "model.json", this.modelURL + "metadata.json");
1521
- this.modelOwnedByExtension = true;
1522
- this.modelLoadMs = Math.round(performance.now() - startedAt);
1523
- } catch (error) {
1524
- this.setLastError(error);
1525
- throw error;
1526
- }
1527
- }
1528
- isModelLoaded() {
1529
- return Boolean(this.model);
1530
- }
1531
- usePreparedModel(model) {
1532
- if (!model || typeof model !== "object") throw new TypeError("TM: Prepared model must be an object.");
1533
- if (this.recognizing && this.model !== model) throw new Error("TM: Stop recognition before changing the active model.");
1534
- this.model = model;
1535
- this.modelOwnedByExtension = false;
1536
- this.modelURL = "";
1537
- this.modelLoadMs = 0;
1538
- this.firstRecognitionMs = 0;
1539
- }
1540
- clearPreparedModel(model) {
1541
- if (model !== void 0 && this.model !== model) return;
1542
- this.stopRecognition();
1543
- this.model = null;
1544
- this.modelOwnedByExtension = false;
1545
- this.modelURL = "";
1546
- this.modelLoadMs = 0;
1547
- this.firstRecognitionMs = 0;
1548
- }
1549
- async startRecognition() {
1550
- try {
1551
- this.lastError = "";
1552
- const startingNewSession = !this.recognizing;
1553
- if (this.recognitionMode !== "audio" && !this.cameraRunning) await this.startCamera();
1554
- await this.loadModel();
1555
- if (startingNewSession && this.featureFlags.temporalPoseScoring) this.startAccumulatedPoseSession();
1556
- this.recognizing = true;
1557
- if (this.recognitionMode === "audio") await this.startAudioRecognition();
1558
- else this.startLoopIfNeeded();
1559
- } catch (error) {
1560
- this.setLastError(error);
1561
- throw error;
1562
- }
1563
- }
1564
- stopRecognition() {
1565
- this.recognizing = false;
1566
- this.stopAudioRecognition();
1567
- this.currentPoseName = "";
1568
- this.score = 0;
1569
- this.predictions = {};
1570
- this.clearPoseOverlay();
1571
- this.resetAccumulatedPose("stop");
1572
- }
1573
- isRecognizing() {
1574
- return this.recognizing;
1575
- }
1576
- async startAudioRecognition() {
1577
- if (!this.model || typeof this.model.listen !== "function") throw new Error("Teachable Machine: The active model does not support audio recognition.");
1578
- if (typeof this.model.isListening === "function" && this.model.isListening()) {
1579
- this.audioListening = true;
1580
- return;
1581
- }
1582
- const first = this.firstRecognitionMs === 0;
1583
- const startedAt = first ? performance.now() : 0;
1584
- await this.model.listen(async (result) => {
1585
- if (!this.recognizing || this.recognitionMode !== "audio" || this.model === null) return;
1586
- if (first && this.firstRecognitionMs === 0) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
1587
- this.applyPredictions(this.audioPredictionFromResult(this.model, result));
1588
- }, {
1589
- probabilityThreshold: 0,
1590
- overlapFactor: .5,
1591
- invokeCallbackOnNoiseAndUnknown: true
1592
- });
1593
- this.audioListening = true;
1594
- }
1595
- stopAudioRecognition() {
1596
- if (!this.audioListening || !this.model || typeof this.model.stopListening !== "function") {
1597
- this.audioListening = false;
1598
- return;
1599
- }
1600
- this.audioListening = false;
1601
- this.model.stopListening().catch((error) => this.setLastError(error));
1602
- }
1603
- audioPredictionFromResult(model, result) {
1604
- const labels = typeof model.wordLabels === "function" ? model.wordLabels() : [];
1605
- const scores = Array.isArray(result?.scores) ? result.scores[0] : result?.scores;
1606
- if (!labels.length || !scores || typeof scores.length !== "number") throw new Error("Teachable Machine: Audio recognition did not return labels and scores.");
1607
- return labels.map((className, index) => ({
1608
- className,
1609
- probability: Number(scores[index] ?? 0)
1610
- }));
1611
- }
1612
- applyPredictions(prediction) {
1613
- let best = {
1614
- className: "",
1615
- probability: 0
1616
- };
1617
- this.predictions = {};
1618
- for (const result of prediction) {
1619
- this.predictions[result.className] = result.probability;
1620
- if (result.probability > best.probability) best = result;
1621
- }
1622
- this.currentPoseName = best.className;
1623
- this.score = best.probability;
1624
- if (this.featureFlags.temporalPoseScoring) this.updateAccumulatedPose(prediction);
1625
- }
1626
- findStageElement() {
1627
- try {
1628
- const stageCanvas = this.findLikelyStageCanvas();
1629
- if (stageCanvas.parentElement) return stageCanvas.parentElement;
1630
- } catch {}
1631
- 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\"]");
1632
- if (editorStage) return editorStage;
1633
- throw new Error("TM: TurboWarp stage element was not found.");
1634
- }
1635
- findLikelyStageCanvas() {
1636
- const webcamCanvas = this.webcam?.canvas ?? null;
1637
- const allCanvases = Array.from(document.querySelectorAll("canvas")).filter((canvas) => canvas !== webcamCanvas && canvas !== this.previewCanvas);
1638
- const visibleCandidates = allCanvases.map((canvas) => {
1639
- const rect = canvas.getBoundingClientRect();
1640
- return {
1641
- canvas,
1642
- rect,
1643
- style: window.getComputedStyle(canvas),
1644
- score: canvasScore(rect.width, rect.height)
1645
- };
1646
- }).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);
1647
- if (visibleCandidates[0]) return visibleCandidates[0].canvas;
1648
- const fallbackCandidates = allCanvases.map((canvas) => ({
1649
- canvas,
1650
- score: canvasScore(canvas.width, canvas.height)
1651
- })).filter((item) => item.canvas.width >= 200 && item.canvas.height >= 150).sort((left, right) => right.score - left.score);
1652
- if (fallbackCandidates[0]) return fallbackCandidates[0].canvas;
1653
- throw new Error("TM: No likely stage canvas was found. The editor or packager DOM may be unsupported.");
1654
- }
1655
- validatePreviewAttachment(stage, canvas) {
1656
- if (!stage || !canvas || canvas.parentElement !== stage) throw new Error("TM: Preview canvas was not attached to the stage.");
1657
- const canvasStyle = window.getComputedStyle(canvas);
1658
- if (canvasStyle.display === "none" && this.previewVisible) throw new Error("TM: Preview canvas is hidden by display:none.");
1659
- if (canvasStyle.visibility === "hidden" && this.previewVisible) throw new Error("TM: Preview canvas is hidden by visibility:hidden.");
1660
- const stageRect = stage.getBoundingClientRect();
1661
- const canvasRect = canvas.getBoundingClientRect();
1662
- const documentHidden = typeof document.visibilityState === "string" && document.visibilityState === "hidden";
1663
- const layoutUnavailable = stageRect.width === 0 || stageRect.height === 0;
1664
- if (!documentHidden && !layoutUnavailable && this.previewVisible) {
1665
- if (canvasRect.width <= 0 || canvasRect.height <= 0) throw new Error("TM: Preview canvas was attached but has zero size.");
1666
- if (!rectanglesIntersect(stageRect, canvasRect)) throw new Error("TM: Preview canvas does not intersect the stage.");
1667
- }
1668
- }
1669
- createSvgElement(name) {
1670
- if (typeof document.createElementNS === "function") return document.createElementNS("http://www.w3.org/2000/svg", name);
1671
- return document.createElement(name);
1672
- }
1673
- ensurePoseOverlayElement() {
1674
- if (!this.featureFlags.poseOverlay) return null;
1675
- if (this.poseOverlaySvg) return this.poseOverlaySvg;
1676
- const svg = this.createSvgElement("svg");
1677
- svg.setAttribute("viewBox", `0 0 320 240`);
1678
- svg.setAttribute("width", "320");
1679
- svg.setAttribute("height", "240");
1680
- svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
1681
- svg.setAttribute("aria-hidden", "true");
1682
- svg.setAttribute("focusable", "false");
1683
- const boneGroup = this.createSvgElement("g");
1684
- boneGroup.setAttribute("data-layer", "bones");
1685
- this.poseOverlayBoneElements = POSE_BONE_CONNECTIONS.map(([first, second]) => {
1686
- const line = this.createSvgElement("line");
1687
- line.setAttribute("data-bone", `${first}-${second}`);
1688
- line.setAttribute("stroke-linecap", "round");
1689
- line.style.display = "none";
1690
- boneGroup.appendChild(line);
1691
- return {
1692
- first,
1693
- second,
1694
- line
1695
- };
1696
- });
1697
- svg.appendChild(boneGroup);
1698
- const jointGroup = this.createSvgElement("g");
1699
- jointGroup.setAttribute("data-layer", "joints");
1700
- this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1701
- for (const part of POSE_KEYPOINT_NAMES) {
1702
- const circle = this.createSvgElement("circle");
1703
- circle.setAttribute("data-joint", part);
1704
- circle.style.display = "none";
1705
- jointGroup.appendChild(circle);
1706
- this.poseOverlayJointElements.set(part, circle);
1707
- }
1708
- svg.appendChild(jointGroup);
1709
- this.poseOverlaySvg = svg;
1710
- return svg;
1711
- }
1712
- updatePoseOverlayVisibility() {
1713
- if (!this.poseOverlaySvg) return;
1714
- this.poseOverlaySvg.style.display = this.previewVisible && this.poseOverlayVisible ? "block" : "none";
1715
- }
1716
- clearPoseOverlay() {
1717
- this.latestPoseKeypoints = [];
1718
- for (const circle of this.poseOverlayJointElements.values()) circle.style.display = "none";
1719
- for (const { line } of this.poseOverlayBoneElements) line.style.display = "none";
1720
- }
1721
- redrawPoseOverlay() {
1722
- if (this.latestPoseKeypoints.length > 0) this.renderPoseOverlay(this.latestPoseKeypoints);
1723
- }
1724
- renderPoseOverlay(keypoints) {
1725
- if (!this.featureFlags.poseOverlay || !this.poseOverlaySvg || !Array.isArray(keypoints)) {
1726
- this.clearPoseOverlay();
1727
- return;
1728
- }
1729
- this.latestPoseKeypoints = keypoints;
1730
- const recognized = /* @__PURE__ */ new Map();
1731
- for (const candidate of keypoints) {
1732
- if (typeof candidate !== "object" || candidate === null || !isPoseKeypointName(candidate.part)) continue;
1733
- const keypoint = candidate;
1734
- const x = Number(keypoint.position?.x);
1735
- const y = Number(keypoint.position?.y);
1736
- if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
1737
- recognized.set(keypoint.part, {
1738
- part: keypoint.part,
1739
- score: confidenceMultiplier(keypoint.score),
1740
- position: {
1741
- x,
1742
- y
1743
- }
1744
- });
1745
- }
1746
- for (const part of POSE_KEYPOINT_NAMES) {
1747
- const circle = this.poseOverlayJointElements.get(part);
1748
- const keypoint = recognized.get(part);
1749
- if (!circle || !keypoint || keypoint.score < this.poseOverlayMinimumConfidence) {
1750
- if (circle) circle.style.display = "none";
1751
- continue;
1752
- }
1753
- const style = this.poseJointStyles[part];
1754
- const opacityMultiplier = this.poseConfidenceScaling.jointOpacity ? keypoint.score : 1;
1755
- const radiusMultiplier = this.poseConfidenceScaling.jointRadius ? keypoint.score : 1;
1756
- circle.setAttribute("cx", String(keypoint.position.x));
1757
- circle.setAttribute("cy", String(keypoint.position.y));
1758
- circle.setAttribute("r", String(style.radius * radiusMultiplier));
1759
- circle.setAttribute("fill", style.color);
1760
- circle.setAttribute("fill-opacity", String(style.opacity * opacityMultiplier));
1761
- circle.style.display = "block";
1762
- }
1763
- for (const { first, second, line } of this.poseOverlayBoneElements) {
1764
- const firstKeypoint = recognized.get(first);
1765
- const secondKeypoint = recognized.get(second);
1766
- if (!firstKeypoint || !secondKeypoint || firstKeypoint.score < this.poseOverlayMinimumConfidence || secondKeypoint.score < this.poseOverlayMinimumConfidence) {
1767
- line.style.display = "none";
1768
- continue;
1769
- }
1770
- const confidence = Math.min(firstKeypoint.score, secondKeypoint.score);
1771
- const opacityMultiplier = this.poseConfidenceScaling.boneOpacity ? confidence : 1;
1772
- const widthMultiplier = this.poseConfidenceScaling.boneWidth ? confidence : 1;
1773
- line.setAttribute("x1", String(firstKeypoint.position.x));
1774
- line.setAttribute("y1", String(firstKeypoint.position.y));
1775
- line.setAttribute("x2", String(secondKeypoint.position.x));
1776
- line.setAttribute("y2", String(secondKeypoint.position.y));
1777
- line.setAttribute("stroke", this.poseBoneStyle.color);
1778
- line.setAttribute("stroke-opacity", String(this.poseBoneStyle.opacity * opacityMultiplier));
1779
- line.setAttribute("stroke-width", String(this.poseBoneStyle.width * widthMultiplier));
1780
- line.style.display = "block";
1781
- }
1782
- }
1783
- /**
1784
- * The element the stage shows.
1785
- *
1786
- * The camera's own video, when there is one: it carries the camera's resolution and its aspect,
1787
- * so the preview stops being a 320x240 readback canvas stretched to fit. The canvas remains the
1788
- * preview only where there is no video to show, which is the same place inference falls back to
1789
- * it.
1790
- */
1791
- previewSourceElement() {
1792
- const video = this.webcam?.webcam;
1793
- return video && video.videoWidth > 0 ? video : this.webcam?.canvas;
1794
- }
1795
- attachPreviewToStage() {
1796
- if (!this.webcam) throw new Error("TM: Start the camera before attaching the preview.");
1797
- if (!this.webcam.canvas) throw new Error("TM: webcam.canvas is unavailable.");
1798
- const stage = this.findStageElement();
1799
- const canvas = this.previewSourceElement();
1800
- let stageCanvas = null;
1801
- try {
1802
- const candidate = this.findLikelyStageCanvas();
1803
- if (candidate.parentElement === stage) stageCanvas = candidate;
1804
- } catch {}
1805
- this.previewCanvas = canvas;
1806
- this.previewStageElement = stage;
1807
- const overlay = this.ensurePoseOverlayElement();
1808
- if (window.getComputedStyle(stage).position === "static") stage.style.position = "relative";
1809
- stage.style.overflow = "hidden";
1810
- Object.assign(canvas.style, {
1811
- position: "absolute",
1812
- zIndex: "auto",
1813
- pointerEvents: "none",
1814
- border: "2px solid rgba(255, 255, 255, 0.7)",
1815
- borderRadius: "8px",
1816
- background: "#000",
1817
- opacity: String(this.previewOpacity),
1818
- display: this.previewVisible ? "block" : "none",
1819
- boxSizing: "border-box"
1820
- });
1821
- if (overlay) Object.assign(overlay.style, {
1822
- position: "absolute",
1823
- zIndex: "auto",
1824
- pointerEvents: "none",
1825
- border: "2px solid transparent",
1826
- background: "transparent",
1827
- display: this.previewVisible && this.poseOverlayVisible ? "block" : "none",
1828
- boxSizing: "border-box",
1829
- overflow: "hidden"
1830
- });
1831
- let insertionPoint = stageCanvas?.nextSibling ?? null;
1832
- while (insertionPoint && (insertionPoint === canvas || insertionPoint === overlay)) insertionPoint = insertionPoint.nextSibling;
1833
- if (stageCanvas && typeof stage.insertBefore === "function") stage.insertBefore(canvas, insertionPoint);
1834
- else if (canvas.parentNode !== stage) stage.appendChild(canvas);
1835
- if (overlay) {
1836
- if (stageCanvas && typeof stage.insertBefore === "function") stage.insertBefore(overlay, insertionPoint);
1837
- else if (overlay.parentNode !== stage) stage.appendChild(overlay);
1838
- }
1839
- this.updatePreviewStyle();
1840
- this.updatePoseOverlayVisibility();
1841
- this.validatePreviewAttachment(stage, canvas);
1842
- }
1843
- /**
1844
- * Span the overlay over what the preview is showing.
1845
- *
1846
- * Keypoints are in the camera frame's space, so the viewBox has to be the frame -- the video's
1847
- * own width and height, not the readback canvas's fixed 320x240. On the canvas fallback the two
1848
- * are the same thing.
1849
- */
1850
- updatePoseOverlayViewBox() {
1851
- const svg = this.poseOverlaySvg;
1852
- const preview = this.previewCanvas;
1853
- if (!svg || !preview) return;
1854
- const width = preview.videoWidth ?? preview.width;
1855
- const height = preview.videoHeight ?? preview.height;
1856
- if (!(width > 0) || !(height > 0)) return;
1857
- svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1858
- }
1859
- updatePreviewStyle() {
1860
- const canvas = this.previewCanvas;
1861
- if (!canvas) throw new Error("TM: Start the camera before positioning the preview.");
1862
- const targets = [canvas, this.poseOverlaySvg].filter(Boolean);
1863
- for (const target of targets) Object.assign(target.style, {
1864
- left: "",
1865
- right: "",
1866
- top: "",
1867
- bottom: "",
1868
- transform: "",
1869
- objectFit: "",
1870
- width: "35%",
1871
- height: "auto",
1872
- borderRadius: "8px"
1873
- });
1874
- let positionTransform = "";
1875
- switch (this.previewPosition) {
1876
- case "top-left":
1877
- for (const target of targets) {
1878
- target.style.left = "8px";
1879
- target.style.top = "8px";
1880
- }
1881
- break;
1882
- case "top-right":
1883
- for (const target of targets) {
1884
- target.style.right = "8px";
1885
- target.style.top = "8px";
1886
- }
1887
- break;
1888
- case "bottom-left":
1889
- for (const target of targets) {
1890
- target.style.left = "8px";
1891
- target.style.bottom = "8px";
1892
- }
1893
- break;
1894
- case "center":
1895
- for (const target of targets) {
1896
- target.style.left = "50%";
1897
- target.style.top = "50%";
1898
- }
1899
- positionTransform = "translate(-50%, -50%)";
1900
- break;
1901
- case "full-stage":
1902
- for (const target of targets) {
1903
- target.style.left = "0";
1904
- target.style.top = "0";
1905
- target.style.width = "100%";
1906
- target.style.height = "100%";
1907
- target.style.objectFit = "cover";
1908
- target.style.borderRadius = "0";
1909
- }
1910
- break;
1911
- default: for (const target of targets) {
1912
- target.style.right = "8px";
1913
- target.style.bottom = "8px";
1914
- }
1915
- }
1916
- const mirroringTransform = this.previewCanvas === this.webcam?.webcam ? this.previewMirrored ? "scaleX(-1)" : "" : this.previewMirrored ? "" : "scaleX(-1)";
1917
- for (const target of targets) target.style.transform = [positionTransform, mirroringTransform].filter(Boolean).join(" ");
1918
- this.poseOverlaySvg?.setAttribute("preserveAspectRatio", this.previewPosition === "full-stage" ? "xMidYMid slice" : "xMidYMid meet");
1919
- this.updatePoseOverlayViewBox();
1920
- }
1921
- startLoopIfNeeded() {
1922
- if (this.loopStarted || !this.cameraRunning || !this.webcam) return;
1923
- this.loopStarted = true;
1924
- const generation = ++this.loopGeneration;
1925
- this.loop(generation);
1926
- }
1927
- trackPreparedModelOperation(model, operation) {
1928
- let active = this.activeModelOperations.get(model);
1929
- if (!active) {
1930
- active = /* @__PURE__ */ new Set();
1931
- this.activeModelOperations.set(model, active);
1932
- }
1933
- active.add(operation);
1934
- operation.then(() => {
1935
- active.delete(operation);
1936
- if (active.size === 0) this.activeModelOperations.delete(model);
1937
- }, () => {
1938
- active.delete(operation);
1939
- if (active.size === 0) this.activeModelOperations.delete(model);
1940
- });
1941
- return operation;
1942
- }
1943
- async waitForPreparedModelIdle(model) {
1944
- await Promise.allSettled([...this.activeModelOperations.get(model) ?? []]);
1945
- }
1946
- async loop(generation = this.loopGeneration) {
1947
- if (generation !== this.loopGeneration || !this.cameraRunning || !this.webcam) {
1948
- if (generation === this.loopGeneration) this.loopStarted = false;
1949
- return;
1950
- }
1951
- try {
1952
- if (this.previewSourceElement() !== this.webcam.webcam) this.webcam.update();
1953
- if (this.recognizing && this.model) {
1954
- const model = this.model;
1955
- const recognitionMode = this.recognitionMode;
1956
- const first = this.firstRecognitionMs === 0;
1957
- const startedAt = first ? performance.now() : 0;
1958
- const recognition = await this.trackPreparedModelOperation(model, this.recognizeFrame(model, recognitionMode));
1959
- if (generation !== this.loopGeneration || !this.cameraRunning || !this.recognizing || this.model !== model || this.recognitionMode !== recognitionMode) {} else {
1960
- if (first) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
1961
- this.renderPoseOverlay(recognition.keypoints);
1962
- this.applyPredictions(recognition.prediction);
1963
- }
1964
- }
1965
- } catch (error) {
1966
- this.setLastError(error);
1967
- }
1968
- if (generation === this.loopGeneration && this.cameraRunning && this.webcam) requestAnimationFrame(() => void this.loop(generation));
1969
- else if (generation === this.loopGeneration) this.loopStarted = false;
1970
- }
1971
- /**
1972
- * The square the model looks at, as a tensor, taken straight off the camera.
1973
- *
1974
- * The readback canvas is 320x240 and holds a 320x320 centre crop drawn into it, so its bottom
1975
- * quarter -- 25% of the subject's height -- never reaches the model. Cropping the frame itself
1976
- * keeps the whole square. The crop and the mirroring both run as tensor ops, which is where the
1977
- * backend can keep them on the GPU; `fromPixels` uploads a video element directly to a texture
1978
- * on the WebGL and WebGPU backends, so no Canvas2D stage sits in the path.
1979
- *
1980
- * The pixels are mirrored here rather than through `estimatePose(sample, flipHorizontal)`:
1981
- * that flag flips the returned keypoints, not the image, and the classifier head was trained on
1982
- * mirrored frames. Flipping the pixels is what keeps the model's input the same as before.
1983
- *
1984
- * Returns null when there is no frame to take, and the canvas path answers instead.
1985
- */
1986
- captureSquareFrame() {
1987
- const tensorflow = globalThis.tf;
1988
- const video = this.webcam?.webcam;
1989
- if (typeof tensorflow?.browser?.fromPixels !== "function" || typeof tensorflow.tidy !== "function" || !video || !(video.videoWidth > 0) || !(video.videoHeight > 0)) return null;
1990
- const size = Math.min(video.videoWidth, video.videoHeight);
1991
- const frame = tensorflow.browser.fromPixels(video);
1992
- try {
1993
- const top = frame.shape[0] - size >> 1;
1994
- const left = frame.shape[1] - size >> 1;
1995
- return {
1996
- tensor: tensorflow.tidy(() => tensorflow.reverse(tensorflow.slice(frame, [
1997
- top,
1998
- left,
1999
- 0
2000
- ], [
2001
- size,
2002
- size,
2003
- 3
2004
- ]), [1])),
2005
- crop: {
2006
- left,
2007
- top,
2008
- size
2009
- }
2010
- };
2011
- } finally {
2012
- frame.dispose();
2013
- }
2014
- }
2015
- async recognizeFrame(model, recognitionMode) {
2016
- if (recognitionMode === "pose") {
2017
- if (typeof model.estimatePose !== "function") throw new Error("Teachable Machine: The active model does not support pose recognition.");
2018
- const square = this.captureSquareFrame();
2019
- if (!square) {
2020
- this.webcam.update?.();
2021
- const estimate = await model.estimatePose(this.webcam.canvas);
2022
- const prediction = await model.predict(estimate.posenetOutput);
2023
- return {
2024
- keypoints: estimate.pose?.keypoints,
2025
- prediction
2026
- };
2027
- }
2028
- try {
2029
- const estimate = await model.estimatePose(square.tensor);
2030
- const prediction = await model.predict(estimate.posenetOutput);
2031
- return {
2032
- keypoints: mapKeypointsToFrame(estimate.pose?.keypoints, square.crop),
2033
- prediction
2034
- };
2035
- } finally {
2036
- square.tensor.dispose();
2037
- }
2038
- }
2039
- const source = this.webcam.webcam ?? this.webcam.canvas;
2040
- return {
2041
- keypoints: [],
2042
- prediction: await model.predict(source)
2043
- };
2044
- }
2045
- currentPoseReporter() {
2046
- return this.currentPoseName;
2047
- }
2048
- scoreReporter() {
2049
- return Math.round(this.score * 100) / 100;
2050
- }
2051
- poseScoreReporter(args) {
2052
- const value = this.predictions[String(args.NAME || "")] || 0;
2053
- return Math.round(value * 100) / 100;
2054
- }
2055
- setAccumulatedPoseParameters(args) {
2056
- const accumulation = args.ACCUMULATION === "" ? 1 : Number(args.ACCUMULATION);
2057
- const decay = args.DECAY === "" ? .9 : Number(args.DECAY);
2058
- this.accumulationCoefficient = Number.isFinite(accumulation) ? Math.max(0, accumulation) : 1;
2059
- this.decayCoefficient = Number.isFinite(decay) ? Math.max(0, Math.min(1, decay)) : .9;
2060
- }
2061
- setAccumulatedPoseThreshold(args) {
2062
- const threshold = args.THRESHOLD === "" ? 0 : Number(args.THRESHOLD);
2063
- this.accumulatedPoseThreshold = Number.isFinite(threshold) ? Math.max(0, threshold) : 0;
2064
- this.updateAccumulatedPoseSelection();
2065
- }
2066
- startAccumulatedPoseSession(now = performance.now()) {
2067
- this.activeDecayCoefficient = this.decayCoefficient;
2068
- this.lastAccumulationTime = now;
2069
- }
2070
- supportsAccumulatedPoseEvents() {
2071
- return this.featureFlags.temporalPoseScoring && this.featureFlags.accumulatedPoseEvents;
2072
- }
2073
- emitAccumulatedPoseChanged(previousPoseName, reason) {
2074
- if (!this.supportsAccumulatedPoseEvents() || previousPoseName === this.accumulatedPoseName) return;
2075
- const payload = {
2076
- version: 2,
2077
- poseName: this.accumulatedPoseName,
2078
- previousPoseName,
2079
- score: this.accumulatedScore,
2080
- reason,
2081
- timestamp: performance.now()
2082
- };
2083
- if (this.onAccumulatedPoseChanged) try {
2084
- this.onAccumulatedPoseChanged(payload);
2085
- } catch {}
2086
- else if (typeof Scratch !== "undefined") Scratch.vm?.runtime?.emit(ACCUMULATED_POSE_CHANGED_EVENT, payload);
2087
- }
2088
- dispose() {
2089
- this.stopCamera();
2090
- if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") document.removeEventListener("visibilitychange", this.visibilityChangeListener);
2091
- this.onAccumulatedPoseChanged = null;
2092
- }
2093
- resetAccumulatedPose(reason = "reset") {
2094
- const previousPoseName = this.accumulatedPoseName;
2095
- this.accumulatedPoseName = "";
2096
- this.accumulatedScore = 0;
2097
- this.accumulatedPredictions = {};
2098
- this.lastAccumulationTime = this.recognizing ? performance.now() : null;
2099
- this.emitAccumulatedPoseChanged(previousPoseName, reason);
2100
- }
2101
- handleDocumentVisibilityChange() {
2102
- if (typeof document === "undefined") return;
2103
- if (isDocumentHidden()) {
2104
- this.accumulatedPosePausedForBackground = true;
2105
- return;
2106
- }
2107
- if (this.accumulatedPosePausedForBackground) {
2108
- this.accumulatedPosePausedForBackground = false;
2109
- this.lastAccumulationTime = this.recognizing ? performance.now() : null;
2110
- }
2111
- }
2112
- updateAccumulatedPose(prediction, now = performance.now()) {
2113
- if (isDocumentHidden()) {
2114
- this.accumulatedPosePausedForBackground = true;
2115
- return;
2116
- }
2117
- if (this.accumulatedPosePausedForBackground) return;
2118
- const elapsedSeconds = this.lastAccumulationTime === null ? 0 : Math.max(0, (now - this.lastAccumulationTime) / 1e3);
2119
- const decayMultiplier = elapsedSeconds === 0 ? 1 : Math.pow(this.activeDecayCoefficient, elapsedSeconds);
2120
- for (const name of Object.keys(this.accumulatedPredictions)) this.accumulatedPredictions[name] *= decayMultiplier;
2121
- for (const result of prediction) {
2122
- const name = String(result.className || "");
2123
- const probability = Number(result.probability);
2124
- if (!name || !Number.isFinite(probability)) continue;
2125
- const contribution = Math.max(0, Math.min(1, probability)) * this.accumulationCoefficient * elapsedSeconds;
2126
- this.accumulatedPredictions[name] = (this.accumulatedPredictions[name] || 0) + contribution;
2127
- }
2128
- this.lastAccumulationTime = now;
2129
- this.updateAccumulatedPoseSelection();
2130
- }
2131
- updateAccumulatedPoseSelection() {
2132
- const previousPoseName = this.accumulatedPoseName;
2133
- this.accumulatedPoseName = "";
2134
- this.accumulatedScore = 0;
2135
- let bestPoseName = "";
2136
- for (const [name, value] of Object.entries(this.accumulatedPredictions)) if (value > this.accumulatedScore) {
2137
- bestPoseName = name;
2138
- this.accumulatedScore = value;
2139
- }
2140
- if (bestPoseName && this.accumulatedScore >= this.accumulatedPoseThreshold) this.accumulatedPoseName = bestPoseName;
2141
- this.emitAccumulatedPoseChanged(previousPoseName, "recognition");
2142
- }
2143
- accumulatedPoseReporter() {
2144
- return this.accumulatedPoseName;
2145
- }
2146
- accumulatedScoreReporter() {
2147
- return this.accumulatedScore;
2148
- }
2149
- accumulatedPoseScoreReporter(args) {
2150
- return this.accumulatedPredictions[String(args.NAME || "")] || 0;
2151
- }
2152
- isPose(args) {
2153
- return (this.predictions[String(args.NAME || "")] || 0) >= .75;
2154
- }
2155
- isPoseWithThreshold(args) {
2156
- const name = String(args.NAME || "");
2157
- let threshold = args.THRESHOLD === "" ? .75 : Number(args.THRESHOLD);
2158
- if (Number.isNaN(threshold)) threshold = .75;
2159
- threshold = Math.max(0, Math.min(1, threshold));
2160
- return (this.predictions[name] || 0) >= threshold;
2161
- }
2162
- cameraMsReporter() {
2163
- return this.cameraMs;
2164
- }
2165
- modelLoadMsReporter() {
2166
- return this.modelLoadMs;
2167
- }
2168
- firstRecognitionMsReporter() {
2169
- return this.firstRecognitionMs;
2170
- }
2171
- lastErrorReporter() {
2172
- return this.lastError;
2173
- }
1643
+ constructor(featureFlags = {}, dependencies = {}) {
1644
+ _defineProperty(this, "fixedEditors", fixedInputRegistration(typeof Scratch === "undefined" ? void 0 : Scratch));
1645
+ this.featureFlags = {
1646
+ ...FEATURE_FLAGS,
1647
+ ...featureFlags
1648
+ };
1649
+ this.tmPoseRuntime = dependencies.poseRuntime ?? dependencies.runtime ?? null;
1650
+ this.tmImageRuntime = dependencies.imageRuntime ?? null;
1651
+ this.tmAudioRuntime = dependencies.audioRuntime ?? null;
1652
+ this.tmComputeRuntime = dependencies.compute ?? null;
1653
+ this.computeMode = normalizeComputeMode(dependencies.computeMode);
1654
+ this.computeSelection = null;
1655
+ this.modelOwnedByExtension = false;
1656
+ this.allowRemoteLibraries = dependencies.allowRemoteLibraries ?? true;
1657
+ this.onAccumulatedPoseChanged = dependencies.onAccumulatedPoseChanged ?? null;
1658
+ this.recognitionMode = "pose";
1659
+ this.modelURL = "";
1660
+ this.model = null;
1661
+ this.webcam = null;
1662
+ this.cameraLease = null;
1663
+ this.cameraRunning = false;
1664
+ this.cameraSelection = "default";
1665
+ this.cameraSelectionIsDeviceId = false;
1666
+ this.cameraDevices = [];
1667
+ this.activeCameraDeviceId = "";
1668
+ this.activeCameraDeviceName = "";
1669
+ this.cameraSelectionQueue = Promise.resolve();
1670
+ this.recognizing = false;
1671
+ this.audioListening = false;
1672
+ this.loopStarted = false;
1673
+ this.loopGeneration = 0;
1674
+ this.activeModelOperations = /* @__PURE__ */ new Map();
1675
+ this.currentPoseName = "";
1676
+ this.score = 0;
1677
+ this.predictions = {};
1678
+ this.accumulationCoefficient = 1;
1679
+ this.decayCoefficient = .9;
1680
+ this.activeDecayCoefficient = .9;
1681
+ this.accumulatedPoseThreshold = 0;
1682
+ this.accumulatedPoseName = "";
1683
+ this.accumulatedScore = 0;
1684
+ this.accumulatedPredictions = {};
1685
+ this.lastAccumulationTime = null;
1686
+ this.previewOpacity = .6;
1687
+ this.previewPosition = "bottom-right";
1688
+ this.previewMirrored = true;
1689
+ this.previewVisible = true;
1690
+ this.previewCanvas = null;
1691
+ this.previewStageElement = null;
1692
+ this.poseOverlayVisible = true;
1693
+ this.poseOverlayMinimumConfidence = .5;
1694
+ this.poseJointStyles = Object.fromEntries(POSE_KEYPOINT_NAMES.map((part) => [part, { ...DEFAULT_POSE_JOINT_STYLE }]));
1695
+ this.poseBoneStyle = { ...DEFAULT_POSE_BONE_STYLE };
1696
+ this.poseConfidenceScaling = { ...DEFAULT_POSE_OVERLAY_CONFIDENCE_SCALING };
1697
+ this.poseOverlaySvg = null;
1698
+ this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1699
+ this.poseOverlayBoneElements = [];
1700
+ this.latestPoseKeypoints = [];
1701
+ this.cameraMs = 0;
1702
+ this.modelLoadMs = 0;
1703
+ this.firstRecognitionMs = 0;
1704
+ this.lastError = "";
1705
+ this.accumulatedPosePausedForBackground = isDocumentHidden();
1706
+ this.visibilityChangeListener = () => this.handleDocumentVisibilityChange();
1707
+ if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") document.addEventListener("visibilitychange", this.visibilityChangeListener);
1708
+ }
1709
+ getInfo() {
1710
+ const visibleOpcodes = block_definitions_default.blocks.filter((block) => !block.featureFlag || this.featureFlags[block.featureFlag]).map((block) => `${EXTENSION_ID}_${block.opcode}`);
1711
+ this.fixedEditors.ensure(visibleOpcodes);
1712
+ return {
1713
+ id: EXTENSION_ID,
1714
+ name: Scratch.translate(block_definitions_default.extensionName),
1715
+ docsURI: DOCS_URI,
1716
+ blockIconURI: BLOCK_ICON_URI,
1717
+ blocks: block_definitions_default.blocks.filter((block) => !block.featureFlag || this.featureFlags[block.featureFlag]).map((block) => ({
1718
+ opcode: block.opcode,
1719
+ blockType: Scratch.BlockType[block.blockType],
1720
+ text: Scratch.translate(block.text),
1721
+ ...block.disableMonitor ? { disableMonitor: true } : {},
1722
+ ...block.arguments ? { arguments: Object.fromEntries(Object.entries(block.arguments).map(([name, argument]) => [name, {
1723
+ type: Scratch.ArgumentType[argument.type],
1724
+ defaultValue: argument.defaultValue,
1725
+ ...argument.menu ? { menu: argument.menu } : {}
1726
+ }])) } : {}
1727
+ })),
1728
+ menus: {
1729
+ positionMenu: {
1730
+ acceptReporters: true,
1731
+ items: POSITION_ITEMS.map((item) => ({
1732
+ text: Scratch.translate(item.text),
1733
+ value: item.value
1734
+ }))
1735
+ },
1736
+ previewMirroringMenu: {
1737
+ acceptReporters: true,
1738
+ items: PREVIEW_MIRRORING_ITEMS.map((item) => ({
1739
+ text: Scratch.translate(item.text),
1740
+ value: item.value
1741
+ }))
1742
+ },
1743
+ recognitionModeMenu: {
1744
+ acceptReporters: true,
1745
+ items: RECOGNITION_MODE_ITEMS.map((item) => ({
1746
+ text: Scratch.translate(item.text),
1747
+ value: item.value
1748
+ }))
1749
+ },
1750
+ computeModeMenu: {
1751
+ acceptReporters: true,
1752
+ items: COMPUTE_MODE_ITEMS.map((item) => ({
1753
+ text: Scratch.translate(item.text),
1754
+ value: item.value
1755
+ }))
1756
+ },
1757
+ poseOverlayVisibilityMenu: {
1758
+ acceptReporters: true,
1759
+ items: POSE_OVERLAY_VISIBILITY_ITEMS.map((item) => ({
1760
+ text: Scratch.translate(item.text),
1761
+ value: item.value
1762
+ }))
1763
+ },
1764
+ poseKeypointMenu: {
1765
+ acceptReporters: true,
1766
+ items: POSE_KEYPOINT_NAMES.map((part) => ({
1767
+ text: part,
1768
+ value: part
1769
+ }))
1770
+ },
1771
+ poseConfidencePropertyMenu: {
1772
+ acceptReporters: true,
1773
+ items: POSE_CONFIDENCE_PROPERTY_ITEMS.map((item) => ({
1774
+ text: Scratch.translate(item.text),
1775
+ value: item.value
1776
+ }))
1777
+ },
1778
+ cameraMenu: {
1779
+ acceptReporters: true,
1780
+ items: "getCameraMenuItems"
1781
+ }
1782
+ }
1783
+ };
1784
+ }
1785
+ versionReporter() {
1786
+ return VERSION;
1787
+ }
1788
+ setLastError(error) {
1789
+ this.lastError = String(error?.message ?? error);
1790
+ }
1791
+ setRecognitionMode(args) {
1792
+ const mode = normalizeRecognitionMode(args.MODE);
1793
+ if (mode === this.recognitionMode) return;
1794
+ if (this.recognizing) throw new Error("Teachable Machine: Stop recognition before changing recognition mode.");
1795
+ if (this.cameraRunning) throw new Error("Teachable Machine: Stop the camera before changing recognition mode.");
1796
+ this.recognitionMode = mode;
1797
+ this.model = null;
1798
+ this.modelOwnedByExtension = false;
1799
+ this.modelURL = "";
1800
+ this.modelLoadMs = 0;
1801
+ this.firstRecognitionMs = 0;
1802
+ this.currentPoseName = "";
1803
+ this.score = 0;
1804
+ this.predictions = {};
1805
+ this.clearPoseOverlay();
1806
+ this.resetAccumulatedPose();
1807
+ }
1808
+ recognitionModeReporter() {
1809
+ return this.recognitionMode;
1810
+ }
1811
+ activeComputeRuntime() {
1812
+ if (this.tmComputeRuntime) return this.tmComputeRuntime;
1813
+ const runtime = globalThis.tmCompute;
1814
+ return runtime && typeof runtime.select === "function" ? runtime : null;
1815
+ }
1816
+ /**
1817
+ * TensorFlow.js binds every tensor to the backend that was active when the
1818
+ * tensor was created, so the backend is negotiated before any model loads. A
1819
+ * host that preloads a runtime without a compute controller keeps whatever
1820
+ * backend TensorFlow.js selected for itself.
1821
+ */
1822
+ async ensureComputeBackend() {
1823
+ const compute = this.activeComputeRuntime();
1824
+ if (!compute) return null;
1825
+ const selection = await compute.select(this.computeMode);
1826
+ this.computeSelection = selection;
1827
+ if (selection.fallback) this.setLastError(/* @__PURE__ */ new Error(`Teachable Machine: The ${selection.requested} compute backend was unavailable, so ${selection.backend} is in use.`));
1828
+ return selection;
1829
+ }
1830
+ async setComputeMode(args) {
1831
+ const mode = normalizeComputeMode(args.MODE, this.computeMode);
1832
+ if (mode === this.computeMode) return;
1833
+ if (this.recognizing) throw new Error("Teachable Machine: Stop recognition before changing the compute mode.");
1834
+ if (this.model && !this.modelOwnedByExtension) throw new Error("Teachable Machine: Release the prepared model before changing the compute mode.");
1835
+ const previousModel = this.model;
1836
+ this.computeMode = mode;
1837
+ this.computeSelection = null;
1838
+ this.model = null;
1839
+ this.modelOwnedByExtension = false;
1840
+ this.modelLoadMs = 0;
1841
+ this.firstRecognitionMs = 0;
1842
+ if (previousModel) await this.releaseOwnedModel(previousModel);
1843
+ if (this.activeComputeRuntime()) await this.ensureComputeBackend();
1844
+ }
1845
+ async releaseOwnedModel(model) {
1846
+ try {
1847
+ await this.waitForPreparedModelIdle(model);
1848
+ await model.dispose?.();
1849
+ } catch (error) {
1850
+ this.setLastError(error);
1851
+ }
1852
+ }
1853
+ computeModeReporter() {
1854
+ return this.computeMode;
1855
+ }
1856
+ computeBackendReporter() {
1857
+ const compute = this.activeComputeRuntime();
1858
+ if (!compute) return "";
1859
+ return this.computeSelection?.backend ?? compute.getBackend() ?? "";
1860
+ }
1861
+ setModelURL(args) {
1862
+ this.modelURL = String(args.URL || "").trim();
1863
+ if (this.modelURL && !this.modelURL.endsWith("/")) this.modelURL += "/";
1864
+ this.model = null;
1865
+ this.modelLoadMs = 0;
1866
+ this.firstRecognitionMs = 0;
1867
+ }
1868
+ activeRuntime() {
1869
+ if (this.recognitionMode === "audio") return this.tmAudioRuntime;
1870
+ return this.recognitionMode === "image" ? this.tmImageRuntime : this.tmPoseRuntime;
1871
+ }
1872
+ async ensureLibrariesLoaded() {
1873
+ if (this.activeRuntime()) {
1874
+ if (this.activeComputeRuntime()) await this.ensureComputeBackend();
1875
+ return;
1876
+ }
1877
+ if (!this.allowRemoteLibraries) throw new Error("Teachable Machine: A preloaded runtime is required.");
1878
+ if (typeof globalThis.tf === "undefined" || typeof globalThis.tmPose === "undefined" || typeof globalThis.tmImage === "undefined" || typeof globalThis.tmAudio === "undefined") await loadScript(BROWSER_RUNTIME_URL);
1879
+ 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.");
1880
+ this.tmPoseRuntime = globalThis.tmPose;
1881
+ this.tmImageRuntime = globalThis.tmImage;
1882
+ this.tmAudioRuntime = globalThis.tmAudio;
1883
+ await this.ensureComputeBackend();
1884
+ }
1885
+ cleanupCameraResources() {
1886
+ const lease = this.cameraLease;
1887
+ this.cameraLease = null;
1888
+ const video = this.webcam?.webcam;
1889
+ if (lease) lease.release();
1890
+ else if (video?.srcObject) {
1891
+ video.srcObject.getTracks().forEach((track) => track.stop());
1892
+ video.srcObject = null;
1893
+ }
1894
+ this.previewCanvas?.parentNode?.removeChild(this.previewCanvas);
1895
+ this.poseOverlaySvg?.parentNode?.removeChild(this.poseOverlaySvg);
1896
+ this.previewCanvas = null;
1897
+ this.previewStageElement = null;
1898
+ this.poseOverlaySvg = null;
1899
+ this.poseOverlayJointElements = /* @__PURE__ */ new Map();
1900
+ this.poseOverlayBoneElements = [];
1901
+ this.latestPoseKeypoints = [];
1902
+ this.webcam = null;
1903
+ this.cameraRunning = false;
1904
+ this.activeCameraDeviceId = "";
1905
+ this.activeCameraDeviceName = "";
1906
+ this.loopStarted = false;
1907
+ this.loopGeneration += 1;
1908
+ }
1909
+ async startCamera() {
1910
+ if (this.recognitionMode === "audio") throw new Error("Teachable Machine: Audio mode uses the microphone through start recognition.");
1911
+ if (this.cameraRunning && this.webcam) {
1912
+ this.attachPreviewToStage();
1913
+ return;
1914
+ }
1915
+ try {
1916
+ this.lastError = "";
1917
+ const startedAt = performance.now();
1918
+ await this.ensureLibrariesLoaded();
1919
+ const runtime = this.activeRuntime();
1920
+ if (!runtime) throw new Error("Teachable Machine: Runtime is unavailable.");
1921
+ if (!("Webcam" in runtime)) throw new Error("Teachable Machine: Camera runtime is unavailable in the current mode.");
1922
+ this.webcam = new runtime.Webcam(320, 240, true);
1923
+ const lease = await this.acquireSharedCamera();
1924
+ if (lease) {
1925
+ this.cameraLease = lease;
1926
+ this.webcam.webcam = lease.getFrameSource().element;
1927
+ }
1928
+ const constraints = lease ? void 0 : cameraConstraints(this.resolvedCameraSelection());
1929
+ if (constraints) await this.webcam.setup(constraints);
1930
+ else await this.webcam.setup();
1931
+ this.ensureCameraCanvas();
1932
+ initializeCameraReadbackContext(this.webcam.canvas);
1933
+ if (!lease) await this.webcam.play();
1934
+ this.attachPreviewToStage();
1935
+ this.cameraRunning = true;
1936
+ try {
1937
+ await this.refreshCameraDevices();
1938
+ } catch {}
1939
+ this.updateActiveCameraInfo();
1940
+ this.cameraMs = Math.round(performance.now() - startedAt);
1941
+ this.startLoopIfNeeded();
1942
+ } catch (error) {
1943
+ this.cleanupCameraResources();
1944
+ this.setLastError(error);
1945
+ throw error;
1946
+ }
1947
+ }
1948
+ /**
1949
+ * Give the camera the readback canvas `setup()` did not build.
1950
+ *
1951
+ * Upstream creates that canvas inside the same branch that opens the camera, so an injected
1952
+ * element skips the canvas with the `getUserMedia` call. 3.4.0 shipped assuming the two were
1953
+ * separate, and every leased start then failed at the first context request with "Webcam canvas
1954
+ * does not provide a 2D context" -- which is to say pose recognition did not start at all
1955
+ * wherever Camera Source was present. The canvas built here is the one upstream would have
1956
+ * built, so the mirrored centre crop every downstream reader expects is unchanged.
1957
+ */
1958
+ ensureCameraCanvas() {
1959
+ if (!this.webcam || this.webcam.canvas) return;
1960
+ const canvas = document.createElement("canvas");
1961
+ canvas.width = this.webcam.width ?? 320;
1962
+ canvas.height = this.webcam.height ?? 240;
1963
+ this.webcam.canvas = canvas;
1964
+ }
1965
+ /**
1966
+ * A lease on the shared camera, or null when Camera Source is not loaded.
1967
+ *
1968
+ * Camera Source owns the device and decides when a stream stops, which is what lets one camera
1969
+ * serve pose recognition and anything else reading frames at the same time. It is a separate
1970
+ * TurboWarp extension, and this one is also distributed for standalone URL loading, so its
1971
+ * absence is ordinary rather than a failure: the self-acquired path stays for it.
1972
+ *
1973
+ * One camera id for every visual recognition mode. Opening a second device for a work that
1974
+ * recognises both poses and images is not what sharing is for, and `pose` is the role name
1975
+ * Camera Source's own documentation uses for this consumer.
1976
+ */
1977
+ async acquireSharedCamera() {
1978
+ const cameraSource = readCameraSource(Scratch.vm?.runtime);
1979
+ if (!cameraSource) return null;
1980
+ const selection = this.resolvedCameraSelection();
1981
+ const options = {
1982
+ owner: package_default.name,
1983
+ cameraId: "pose"
1984
+ };
1985
+ if (selection.kind === "device") options.deviceId = selection.value;
1986
+ return cameraSource.acquireCamera(options);
1987
+ }
1988
+ stopCamera() {
1989
+ try {
1990
+ this.stopRecognition();
1991
+ this.cleanupCameraResources();
1992
+ this.currentPoseName = "";
1993
+ this.score = 0;
1994
+ this.predictions = {};
1995
+ } catch (error) {
1996
+ this.setLastError(error);
1997
+ throw error;
1998
+ }
1999
+ }
2000
+ isCameraRunning() {
2001
+ return this.cameraRunning;
2002
+ }
2003
+ getCameraMenuItems() {
2004
+ const fixedItems = CAMERA_SELECTION_ITEMS.map((item) => ({
2005
+ text: Scratch.translate(item.text),
2006
+ value: item.value
2007
+ }));
2008
+ const seen = /* @__PURE__ */ new Set();
2009
+ const deviceItems = [];
2010
+ this.cameraDevices.forEach((device, index) => {
2011
+ if (!device.deviceId || seen.has(device.deviceId)) return;
2012
+ seen.add(device.deviceId);
2013
+ deviceItems.push({
2014
+ text: device.label || `${Scratch.translate("camera")} ${index + 1}`,
2015
+ value: device.deviceId
2016
+ });
2017
+ });
2018
+ return [...fixedItems, ...deviceItems];
2019
+ }
2020
+ async refreshCameraDevices() {
2021
+ const mediaDevices = typeof navigator === "undefined" ? void 0 : navigator.mediaDevices;
2022
+ if (!mediaDevices || typeof mediaDevices.enumerateDevices !== "function") throw new Error("TM: Camera enumeration is not available in this browser.");
2023
+ const devices = await mediaDevices.enumerateDevices();
2024
+ this.cameraDevices = devices.filter((device) => device.kind === "videoinput").map((device) => ({
2025
+ deviceId: device.deviceId,
2026
+ label: device.label
2027
+ }));
2028
+ return this.cameraDevices;
2029
+ }
2030
+ async refreshCameraList() {
2031
+ try {
2032
+ this.lastError = "";
2033
+ await this.refreshCameraDevices();
2034
+ if (this.cameraRunning) this.updateActiveCameraInfo();
2035
+ } catch (error) {
2036
+ this.setLastError(error);
2037
+ throw error;
2038
+ }
2039
+ }
2040
+ setCameraSelection(args) {
2041
+ const selection = normalizeCameraSelection(args.CAMERA);
2042
+ return this.enqueueCameraSelection(selection);
2043
+ }
2044
+ setCameraDeviceId(deviceId) {
2045
+ if (typeof deviceId !== "string" || deviceId.trim().length === 0) return Promise.reject(/* @__PURE__ */ new Error("TM: Camera device ID must be a non-empty string."));
2046
+ return this.enqueueCameraSelection({
2047
+ kind: "device",
2048
+ value: deviceId
2049
+ });
2050
+ }
2051
+ resolvedCameraSelection() {
2052
+ return this.cameraSelectionIsDeviceId ? {
2053
+ kind: "device",
2054
+ value: this.cameraSelection
2055
+ } : {
2056
+ kind: "preference",
2057
+ value: this.cameraSelection
2058
+ };
2059
+ }
2060
+ enqueueCameraSelection(selection) {
2061
+ const operation = this.cameraSelectionQueue.then(() => this.applyCameraSelection(selection));
2062
+ this.cameraSelectionQueue = operation.catch(() => void 0);
2063
+ return operation;
2064
+ }
2065
+ async applyCameraSelection(selection) {
2066
+ const previousSelection = this.cameraSelection;
2067
+ const previousSelectionIsDeviceId = this.cameraSelectionIsDeviceId;
2068
+ const wasRunning = this.cameraRunning;
2069
+ this.cameraSelection = selection.value;
2070
+ this.cameraSelectionIsDeviceId = selection.kind === "device";
2071
+ if (!wasRunning) return;
2072
+ this.cleanupCameraResources();
2073
+ try {
2074
+ await this.startCamera();
2075
+ } catch (switchError) {
2076
+ this.cameraSelection = previousSelection;
2077
+ this.cameraSelectionIsDeviceId = previousSelectionIsDeviceId;
2078
+ try {
2079
+ await this.startCamera();
2080
+ } catch (rollbackError) {
2081
+ const error = new AggregateError([switchError, rollbackError], "TM: Camera switch and rollback both failed.");
2082
+ this.setLastError(error);
2083
+ throw error;
2084
+ }
2085
+ this.setLastError(switchError);
2086
+ throw switchError;
2087
+ }
2088
+ }
2089
+ updateActiveCameraInfo() {
2090
+ const stream = this.webcam?.webcam?.srcObject;
2091
+ const videoTrack = stream?.getVideoTracks?.()[0] ?? stream?.getTracks?.().find((track) => track.kind === "video" || track.kind === void 0);
2092
+ const settings = videoTrack?.getSettings?.() ?? {};
2093
+ const selectedDeviceId = this.cameraSelectionIsDeviceId ? this.cameraSelection : "";
2094
+ this.activeCameraDeviceId = String(settings.deviceId || selectedDeviceId);
2095
+ const device = this.cameraDevices.find((candidate) => candidate.deviceId === this.activeCameraDeviceId);
2096
+ this.activeCameraDeviceName = String(videoTrack?.label || device?.label || "");
2097
+ }
2098
+ cameraCountReporter() {
2099
+ return this.cameraDevices.length;
2100
+ }
2101
+ cameraDeviceIdReporter() {
2102
+ return this.activeCameraDeviceId;
2103
+ }
2104
+ cameraDeviceNameReporter() {
2105
+ return this.activeCameraDeviceName;
2106
+ }
2107
+ showPreview() {
2108
+ try {
2109
+ this.previewVisible = true;
2110
+ if (!this.webcam?.canvas) throw new Error("TM: Start the camera before showing the preview.");
2111
+ this.attachPreviewToStage();
2112
+ this.previewCanvas.style.display = "block";
2113
+ this.updatePoseOverlayVisibility();
2114
+ this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
2115
+ } catch (error) {
2116
+ this.setLastError(error);
2117
+ throw error;
2118
+ }
2119
+ }
2120
+ hidePreview() {
2121
+ this.previewVisible = false;
2122
+ if (this.previewCanvas) this.previewCanvas.style.display = "none";
2123
+ this.updatePoseOverlayVisibility();
2124
+ }
2125
+ isPreviewVisible() {
2126
+ return this.previewVisible;
2127
+ }
2128
+ setPreviewOpacity(args) {
2129
+ let opacity = args.OPACITY === "" ? .6 : Number(args.OPACITY);
2130
+ if (Number.isNaN(opacity)) opacity = .6;
2131
+ this.previewOpacity = Math.max(0, Math.min(1, opacity));
2132
+ if (this.previewCanvas) this.previewCanvas.style.opacity = String(this.previewOpacity);
2133
+ }
2134
+ setPreviewPosition(args) {
2135
+ this.previewPosition = normalizePosition(args.POSITION);
2136
+ if (this.previewCanvas) {
2137
+ this.updatePreviewStyle();
2138
+ this.validatePreviewAttachment(this.previewStageElement, this.previewCanvas);
2139
+ }
2140
+ }
2141
+ setPreviewMirroring(args) {
2142
+ this.previewMirrored = normalizePreviewMirroring(args.MIRRORING);
2143
+ if (this.previewCanvas) this.updatePreviewStyle();
2144
+ }
2145
+ previewMirroringReporter() {
2146
+ return this.previewMirrored ? "mirrored" : "unmirrored";
2147
+ }
2148
+ setPoseOverlayVisibility(args) {
2149
+ this.poseOverlayVisible = normalizePoseOverlayVisibility(args.VISIBILITY);
2150
+ this.updatePoseOverlayVisibility();
2151
+ }
2152
+ showPoseOverlay() {
2153
+ this.poseOverlayVisible = true;
2154
+ this.updatePoseOverlayVisibility();
2155
+ }
2156
+ hidePoseOverlay() {
2157
+ this.poseOverlayVisible = false;
2158
+ this.updatePoseOverlayVisibility();
2159
+ }
2160
+ isPoseOverlayVisible() {
2161
+ return this.featureFlags.poseOverlay && this.poseOverlayVisible;
2162
+ }
2163
+ setPoseJointStyle(args) {
2164
+ const part = String(args.PART ?? "");
2165
+ if (!isPoseKeypointName(part)) throw new Error(`TM: Unknown PoseNet joint: ${part}`);
2166
+ const previous = this.poseJointStyles[part];
2167
+ this.poseJointStyles[part] = {
2168
+ color: normalizePoseColor(args.COLOR, previous.color),
2169
+ opacity: normalizePoseStyleNumber(args.OPACITY, previous.opacity, 1),
2170
+ radius: normalizePoseStyleNumber(args.RADIUS, previous.radius)
2171
+ };
2172
+ this.redrawPoseOverlay();
2173
+ }
2174
+ setPoseBoneStyle(args) {
2175
+ this.poseBoneStyle = {
2176
+ color: normalizePoseColor(args.COLOR, this.poseBoneStyle.color),
2177
+ opacity: normalizePoseStyleNumber(args.OPACITY, this.poseBoneStyle.opacity, 1),
2178
+ width: normalizePoseStyleNumber(args.WIDTH, this.poseBoneStyle.width)
2179
+ };
2180
+ this.redrawPoseOverlay();
2181
+ }
2182
+ setPoseOverlayMinimumConfidence(args) {
2183
+ this.poseOverlayMinimumConfidence = normalizePoseStyleNumber(args.CONFIDENCE, this.poseOverlayMinimumConfidence, 1);
2184
+ this.redrawPoseOverlay();
2185
+ }
2186
+ setPoseConfidenceScaling(args) {
2187
+ const property = String(args.PROPERTY ?? "");
2188
+ const key = {
2189
+ "joint-opacity": "jointOpacity",
2190
+ "joint-radius": "jointRadius",
2191
+ "bone-opacity": "boneOpacity",
2192
+ "bone-width": "boneWidth"
2193
+ }[property];
2194
+ if (!key) throw new Error(`TM: Unknown confidence-scaled property: ${property}`);
2195
+ this.poseConfidenceScaling[key] = normalizePoseOverlayVisibility(args.STATE);
2196
+ this.redrawPoseOverlay();
2197
+ }
2198
+ async loadModel() {
2199
+ if (this.model) return;
2200
+ if (!this.modelURL) throw new Error("Teachable Machine: Set the model URL first.");
2201
+ try {
2202
+ this.lastError = "";
2203
+ const startedAt = performance.now();
2204
+ await this.ensureLibrariesLoaded();
2205
+ const runtime = this.activeRuntime();
2206
+ if (!runtime || typeof runtime.load !== "function") throw new Error("Teachable Machine: The URL loader is not available.");
2207
+ this.model = await runtime.load(this.modelURL + "model.json", this.modelURL + "metadata.json");
2208
+ this.modelOwnedByExtension = true;
2209
+ this.modelLoadMs = Math.round(performance.now() - startedAt);
2210
+ } catch (error) {
2211
+ this.setLastError(error);
2212
+ throw error;
2213
+ }
2214
+ }
2215
+ isModelLoaded() {
2216
+ return Boolean(this.model);
2217
+ }
2218
+ usePreparedModel(model) {
2219
+ if (!model || typeof model !== "object") throw new TypeError("TM: Prepared model must be an object.");
2220
+ if (this.recognizing && this.model !== model) throw new Error("TM: Stop recognition before changing the active model.");
2221
+ this.model = model;
2222
+ this.modelOwnedByExtension = false;
2223
+ this.modelURL = "";
2224
+ this.modelLoadMs = 0;
2225
+ this.firstRecognitionMs = 0;
2226
+ }
2227
+ clearPreparedModel(model) {
2228
+ if (model !== void 0 && this.model !== model) return;
2229
+ this.stopRecognition();
2230
+ this.model = null;
2231
+ this.modelOwnedByExtension = false;
2232
+ this.modelURL = "";
2233
+ this.modelLoadMs = 0;
2234
+ this.firstRecognitionMs = 0;
2235
+ }
2236
+ async startRecognition() {
2237
+ try {
2238
+ this.lastError = "";
2239
+ const startingNewSession = !this.recognizing;
2240
+ if (this.recognitionMode !== "audio" && !this.cameraRunning) await this.startCamera();
2241
+ await this.loadModel();
2242
+ if (startingNewSession && this.featureFlags.temporalPoseScoring) this.startAccumulatedPoseSession();
2243
+ this.recognizing = true;
2244
+ if (this.recognitionMode === "audio") await this.startAudioRecognition();
2245
+ else this.startLoopIfNeeded();
2246
+ } catch (error) {
2247
+ this.setLastError(error);
2248
+ throw error;
2249
+ }
2250
+ }
2251
+ stopRecognition() {
2252
+ this.recognizing = false;
2253
+ this.stopAudioRecognition();
2254
+ this.currentPoseName = "";
2255
+ this.score = 0;
2256
+ this.predictions = {};
2257
+ this.clearPoseOverlay();
2258
+ this.resetAccumulatedPose("stop");
2259
+ }
2260
+ isRecognizing() {
2261
+ return this.recognizing;
2262
+ }
2263
+ async startAudioRecognition() {
2264
+ if (!this.model || typeof this.model.listen !== "function") throw new Error("Teachable Machine: The active model does not support audio recognition.");
2265
+ if (typeof this.model.isListening === "function" && this.model.isListening()) {
2266
+ this.audioListening = true;
2267
+ return;
2268
+ }
2269
+ const first = this.firstRecognitionMs === 0;
2270
+ const startedAt = first ? performance.now() : 0;
2271
+ await this.model.listen(async (result) => {
2272
+ if (!this.recognizing || this.recognitionMode !== "audio" || this.model === null) return;
2273
+ if (first && this.firstRecognitionMs === 0) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
2274
+ this.applyPredictions(this.audioPredictionFromResult(this.model, result));
2275
+ }, {
2276
+ probabilityThreshold: 0,
2277
+ overlapFactor: .5,
2278
+ invokeCallbackOnNoiseAndUnknown: true
2279
+ });
2280
+ this.audioListening = true;
2281
+ }
2282
+ stopAudioRecognition() {
2283
+ if (!this.audioListening || !this.model || typeof this.model.stopListening !== "function") {
2284
+ this.audioListening = false;
2285
+ return;
2286
+ }
2287
+ this.audioListening = false;
2288
+ this.model.stopListening().catch((error) => this.setLastError(error));
2289
+ }
2290
+ audioPredictionFromResult(model, result) {
2291
+ const labels = typeof model.wordLabels === "function" ? model.wordLabels() : [];
2292
+ const scores = Array.isArray(result?.scores) ? result.scores[0] : result?.scores;
2293
+ if (!labels.length || !scores || typeof scores.length !== "number") throw new Error("Teachable Machine: Audio recognition did not return labels and scores.");
2294
+ return labels.map((className, index) => ({
2295
+ className,
2296
+ probability: Number(scores[index] ?? 0)
2297
+ }));
2298
+ }
2299
+ applyPredictions(prediction) {
2300
+ let best = {
2301
+ className: "",
2302
+ probability: 0
2303
+ };
2304
+ this.predictions = {};
2305
+ for (const result of prediction) {
2306
+ this.predictions[result.className] = result.probability;
2307
+ if (result.probability > best.probability) best = result;
2308
+ }
2309
+ this.currentPoseName = best.className;
2310
+ this.score = best.probability;
2311
+ if (this.featureFlags.temporalPoseScoring) this.updateAccumulatedPose(prediction);
2312
+ }
2313
+ findStageElement() {
2314
+ try {
2315
+ const stageCanvas = this.findLikelyStageCanvas();
2316
+ if (stageCanvas.parentElement) return stageCanvas.parentElement;
2317
+ } catch {}
2318
+ 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\"]");
2319
+ if (editorStage) return editorStage;
2320
+ throw new Error("TM: TurboWarp stage element was not found.");
2321
+ }
2322
+ findLikelyStageCanvas() {
2323
+ const webcamCanvas = this.webcam?.canvas ?? null;
2324
+ const allCanvases = Array.from(document.querySelectorAll("canvas")).filter((canvas) => canvas !== webcamCanvas && canvas !== this.previewCanvas);
2325
+ const visibleCandidates = allCanvases.map((canvas) => {
2326
+ const rect = canvas.getBoundingClientRect();
2327
+ return {
2328
+ canvas,
2329
+ rect,
2330
+ style: window.getComputedStyle(canvas),
2331
+ score: canvasScore(rect.width, rect.height)
2332
+ };
2333
+ }).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);
2334
+ if (visibleCandidates[0]) return visibleCandidates[0].canvas;
2335
+ const fallbackCandidates = allCanvases.map((canvas) => ({
2336
+ canvas,
2337
+ score: canvasScore(canvas.width, canvas.height)
2338
+ })).filter((item) => item.canvas.width >= 200 && item.canvas.height >= 150).sort((left, right) => right.score - left.score);
2339
+ if (fallbackCandidates[0]) return fallbackCandidates[0].canvas;
2340
+ throw new Error("TM: No likely stage canvas was found. The editor or packager DOM may be unsupported.");
2341
+ }
2342
+ validatePreviewAttachment(stage, canvas) {
2343
+ if (!stage || !canvas || canvas.parentElement !== stage) throw new Error("TM: Preview canvas was not attached to the stage.");
2344
+ const canvasStyle = window.getComputedStyle(canvas);
2345
+ if (canvasStyle.display === "none" && this.previewVisible) throw new Error("TM: Preview canvas is hidden by display:none.");
2346
+ if (canvasStyle.visibility === "hidden" && this.previewVisible) throw new Error("TM: Preview canvas is hidden by visibility:hidden.");
2347
+ const stageRect = stage.getBoundingClientRect();
2348
+ const canvasRect = canvas.getBoundingClientRect();
2349
+ const documentHidden = typeof document.visibilityState === "string" && document.visibilityState === "hidden";
2350
+ const layoutUnavailable = stageRect.width === 0 || stageRect.height === 0;
2351
+ if (!documentHidden && !layoutUnavailable && this.previewVisible) {
2352
+ if (canvasRect.width <= 0 || canvasRect.height <= 0) throw new Error("TM: Preview canvas was attached but has zero size.");
2353
+ if (!rectanglesIntersect(stageRect, canvasRect)) throw new Error("TM: Preview canvas does not intersect the stage.");
2354
+ }
2355
+ }
2356
+ createSvgElement(name) {
2357
+ if (typeof document.createElementNS === "function") return document.createElementNS("http://www.w3.org/2000/svg", name);
2358
+ return document.createElement(name);
2359
+ }
2360
+ ensurePoseOverlayElement() {
2361
+ if (!this.featureFlags.poseOverlay) return null;
2362
+ if (this.poseOverlaySvg) return this.poseOverlaySvg;
2363
+ const svg = this.createSvgElement("svg");
2364
+ svg.setAttribute("viewBox", `0 0 320 240`);
2365
+ svg.setAttribute("width", "320");
2366
+ svg.setAttribute("height", "240");
2367
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
2368
+ svg.setAttribute("aria-hidden", "true");
2369
+ svg.setAttribute("focusable", "false");
2370
+ const boneGroup = this.createSvgElement("g");
2371
+ boneGroup.setAttribute("data-layer", "bones");
2372
+ this.poseOverlayBoneElements = POSE_BONE_CONNECTIONS.map(([first, second]) => {
2373
+ const line = this.createSvgElement("line");
2374
+ line.setAttribute("data-bone", `${first}-${second}`);
2375
+ line.setAttribute("stroke-linecap", "round");
2376
+ line.style.display = "none";
2377
+ boneGroup.appendChild(line);
2378
+ return {
2379
+ first,
2380
+ second,
2381
+ line
2382
+ };
2383
+ });
2384
+ svg.appendChild(boneGroup);
2385
+ const jointGroup = this.createSvgElement("g");
2386
+ jointGroup.setAttribute("data-layer", "joints");
2387
+ this.poseOverlayJointElements = /* @__PURE__ */ new Map();
2388
+ for (const part of POSE_KEYPOINT_NAMES) {
2389
+ const circle = this.createSvgElement("circle");
2390
+ circle.setAttribute("data-joint", part);
2391
+ circle.style.display = "none";
2392
+ jointGroup.appendChild(circle);
2393
+ this.poseOverlayJointElements.set(part, circle);
2394
+ }
2395
+ svg.appendChild(jointGroup);
2396
+ this.poseOverlaySvg = svg;
2397
+ return svg;
2398
+ }
2399
+ updatePoseOverlayVisibility() {
2400
+ if (!this.poseOverlaySvg) return;
2401
+ this.poseOverlaySvg.style.display = this.previewVisible && this.poseOverlayVisible ? "block" : "none";
2402
+ }
2403
+ clearPoseOverlay() {
2404
+ this.latestPoseKeypoints = [];
2405
+ for (const circle of this.poseOverlayJointElements.values()) circle.style.display = "none";
2406
+ for (const { line } of this.poseOverlayBoneElements) line.style.display = "none";
2407
+ }
2408
+ redrawPoseOverlay() {
2409
+ if (this.latestPoseKeypoints.length > 0) this.renderPoseOverlay(this.latestPoseKeypoints);
2410
+ }
2411
+ renderPoseOverlay(keypoints) {
2412
+ if (!this.featureFlags.poseOverlay || !this.poseOverlaySvg || !Array.isArray(keypoints)) {
2413
+ this.clearPoseOverlay();
2414
+ return;
2415
+ }
2416
+ this.latestPoseKeypoints = keypoints;
2417
+ const recognized = /* @__PURE__ */ new Map();
2418
+ for (const candidate of keypoints) {
2419
+ if (typeof candidate !== "object" || candidate === null || !isPoseKeypointName(candidate.part)) continue;
2420
+ const keypoint = candidate;
2421
+ const x = Number(keypoint.position?.x);
2422
+ const y = Number(keypoint.position?.y);
2423
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
2424
+ recognized.set(keypoint.part, {
2425
+ part: keypoint.part,
2426
+ score: confidenceMultiplier(keypoint.score),
2427
+ position: {
2428
+ x,
2429
+ y
2430
+ }
2431
+ });
2432
+ }
2433
+ for (const part of POSE_KEYPOINT_NAMES) {
2434
+ const circle = this.poseOverlayJointElements.get(part);
2435
+ const keypoint = recognized.get(part);
2436
+ if (!circle || !keypoint || keypoint.score < this.poseOverlayMinimumConfidence) {
2437
+ if (circle) circle.style.display = "none";
2438
+ continue;
2439
+ }
2440
+ const style = this.poseJointStyles[part];
2441
+ const opacityMultiplier = this.poseConfidenceScaling.jointOpacity ? keypoint.score : 1;
2442
+ const radiusMultiplier = this.poseConfidenceScaling.jointRadius ? keypoint.score : 1;
2443
+ circle.setAttribute("cx", String(keypoint.position.x));
2444
+ circle.setAttribute("cy", String(keypoint.position.y));
2445
+ circle.setAttribute("r", String(style.radius * radiusMultiplier));
2446
+ circle.setAttribute("fill", style.color);
2447
+ circle.setAttribute("fill-opacity", String(style.opacity * opacityMultiplier));
2448
+ circle.style.display = "block";
2449
+ }
2450
+ for (const { first, second, line } of this.poseOverlayBoneElements) {
2451
+ const firstKeypoint = recognized.get(first);
2452
+ const secondKeypoint = recognized.get(second);
2453
+ if (!firstKeypoint || !secondKeypoint || firstKeypoint.score < this.poseOverlayMinimumConfidence || secondKeypoint.score < this.poseOverlayMinimumConfidence) {
2454
+ line.style.display = "none";
2455
+ continue;
2456
+ }
2457
+ const confidence = Math.min(firstKeypoint.score, secondKeypoint.score);
2458
+ const opacityMultiplier = this.poseConfidenceScaling.boneOpacity ? confidence : 1;
2459
+ const widthMultiplier = this.poseConfidenceScaling.boneWidth ? confidence : 1;
2460
+ line.setAttribute("x1", String(firstKeypoint.position.x));
2461
+ line.setAttribute("y1", String(firstKeypoint.position.y));
2462
+ line.setAttribute("x2", String(secondKeypoint.position.x));
2463
+ line.setAttribute("y2", String(secondKeypoint.position.y));
2464
+ line.setAttribute("stroke", this.poseBoneStyle.color);
2465
+ line.setAttribute("stroke-opacity", String(this.poseBoneStyle.opacity * opacityMultiplier));
2466
+ line.setAttribute("stroke-width", String(this.poseBoneStyle.width * widthMultiplier));
2467
+ line.style.display = "block";
2468
+ }
2469
+ }
2470
+ /**
2471
+ * The element the stage shows.
2472
+ *
2473
+ * The camera's own video, when there is one: it carries the camera's resolution and its aspect,
2474
+ * so the preview stops being a 320x240 readback canvas stretched to fit. The canvas remains the
2475
+ * preview only where there is no video to show, which is the same place inference falls back to
2476
+ * it.
2477
+ */
2478
+ previewSourceElement() {
2479
+ const video = this.webcam?.webcam;
2480
+ return video && video.videoWidth > 0 ? video : this.webcam?.canvas;
2481
+ }
2482
+ attachPreviewToStage() {
2483
+ if (!this.webcam) throw new Error("TM: Start the camera before attaching the preview.");
2484
+ if (!this.webcam.canvas) throw new Error("TM: webcam.canvas is unavailable.");
2485
+ const stage = this.findStageElement();
2486
+ const canvas = this.previewSourceElement();
2487
+ let stageCanvas = null;
2488
+ try {
2489
+ const candidate = this.findLikelyStageCanvas();
2490
+ if (candidate.parentElement === stage) stageCanvas = candidate;
2491
+ } catch {}
2492
+ this.previewCanvas = canvas;
2493
+ this.previewStageElement = stage;
2494
+ const overlay = this.ensurePoseOverlayElement();
2495
+ if (window.getComputedStyle(stage).position === "static") stage.style.position = "relative";
2496
+ stage.style.overflow = "hidden";
2497
+ Object.assign(canvas.style, {
2498
+ position: "absolute",
2499
+ zIndex: "auto",
2500
+ pointerEvents: "none",
2501
+ border: "2px solid rgba(255, 255, 255, 0.7)",
2502
+ borderRadius: "8px",
2503
+ background: "#000",
2504
+ opacity: String(this.previewOpacity),
2505
+ display: this.previewVisible ? "block" : "none",
2506
+ boxSizing: "border-box"
2507
+ });
2508
+ if (overlay) Object.assign(overlay.style, {
2509
+ position: "absolute",
2510
+ zIndex: "auto",
2511
+ pointerEvents: "none",
2512
+ border: "2px solid transparent",
2513
+ background: "transparent",
2514
+ display: this.previewVisible && this.poseOverlayVisible ? "block" : "none",
2515
+ boxSizing: "border-box",
2516
+ overflow: "hidden"
2517
+ });
2518
+ let insertionPoint = stageCanvas?.nextSibling ?? null;
2519
+ while (insertionPoint && (insertionPoint === canvas || insertionPoint === overlay)) insertionPoint = insertionPoint.nextSibling;
2520
+ if (stageCanvas && typeof stage.insertBefore === "function") stage.insertBefore(canvas, insertionPoint);
2521
+ else if (canvas.parentNode !== stage) stage.appendChild(canvas);
2522
+ if (overlay) {
2523
+ if (stageCanvas && typeof stage.insertBefore === "function") stage.insertBefore(overlay, insertionPoint);
2524
+ else if (overlay.parentNode !== stage) stage.appendChild(overlay);
2525
+ }
2526
+ this.updatePreviewStyle();
2527
+ this.updatePoseOverlayVisibility();
2528
+ this.validatePreviewAttachment(stage, canvas);
2529
+ }
2530
+ /**
2531
+ * Span the overlay over what the preview is showing.
2532
+ *
2533
+ * Keypoints are in the camera frame's space, so the viewBox has to be the frame -- the video's
2534
+ * own width and height, not the readback canvas's fixed 320x240. On the canvas fallback the two
2535
+ * are the same thing.
2536
+ */
2537
+ updatePoseOverlayViewBox() {
2538
+ const svg = this.poseOverlaySvg;
2539
+ const preview = this.previewCanvas;
2540
+ if (!svg || !preview) return;
2541
+ const width = preview.videoWidth ?? preview.width;
2542
+ const height = preview.videoHeight ?? preview.height;
2543
+ if (!(width > 0) || !(height > 0)) return;
2544
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
2545
+ }
2546
+ updatePreviewStyle() {
2547
+ const canvas = this.previewCanvas;
2548
+ if (!canvas) throw new Error("TM: Start the camera before positioning the preview.");
2549
+ const targets = [canvas, this.poseOverlaySvg].filter(Boolean);
2550
+ for (const target of targets) Object.assign(target.style, {
2551
+ left: "",
2552
+ right: "",
2553
+ top: "",
2554
+ bottom: "",
2555
+ transform: "",
2556
+ objectFit: "",
2557
+ width: "35%",
2558
+ height: "auto",
2559
+ borderRadius: "8px"
2560
+ });
2561
+ let positionTransform = "";
2562
+ switch (this.previewPosition) {
2563
+ case "top-left":
2564
+ for (const target of targets) {
2565
+ target.style.left = "8px";
2566
+ target.style.top = "8px";
2567
+ }
2568
+ break;
2569
+ case "top-right":
2570
+ for (const target of targets) {
2571
+ target.style.right = "8px";
2572
+ target.style.top = "8px";
2573
+ }
2574
+ break;
2575
+ case "bottom-left":
2576
+ for (const target of targets) {
2577
+ target.style.left = "8px";
2578
+ target.style.bottom = "8px";
2579
+ }
2580
+ break;
2581
+ case "center":
2582
+ for (const target of targets) {
2583
+ target.style.left = "50%";
2584
+ target.style.top = "50%";
2585
+ }
2586
+ positionTransform = "translate(-50%, -50%)";
2587
+ break;
2588
+ case "full-stage":
2589
+ for (const target of targets) {
2590
+ target.style.left = "0";
2591
+ target.style.top = "0";
2592
+ target.style.width = "100%";
2593
+ target.style.height = "100%";
2594
+ target.style.objectFit = "cover";
2595
+ target.style.borderRadius = "0";
2596
+ }
2597
+ break;
2598
+ default: for (const target of targets) {
2599
+ target.style.right = "8px";
2600
+ target.style.bottom = "8px";
2601
+ }
2602
+ }
2603
+ const mirroringTransform = this.previewCanvas === this.webcam?.webcam ? this.previewMirrored ? "scaleX(-1)" : "" : this.previewMirrored ? "" : "scaleX(-1)";
2604
+ for (const target of targets) target.style.transform = [positionTransform, mirroringTransform].filter(Boolean).join(" ");
2605
+ this.poseOverlaySvg?.setAttribute("preserveAspectRatio", this.previewPosition === "full-stage" ? "xMidYMid slice" : "xMidYMid meet");
2606
+ this.updatePoseOverlayViewBox();
2607
+ }
2608
+ startLoopIfNeeded() {
2609
+ if (this.loopStarted || !this.cameraRunning || !this.webcam) return;
2610
+ this.loopStarted = true;
2611
+ const generation = ++this.loopGeneration;
2612
+ this.loop(generation);
2613
+ }
2614
+ trackPreparedModelOperation(model, operation) {
2615
+ let active = this.activeModelOperations.get(model);
2616
+ if (!active) {
2617
+ active = /* @__PURE__ */ new Set();
2618
+ this.activeModelOperations.set(model, active);
2619
+ }
2620
+ active.add(operation);
2621
+ operation.then(() => {
2622
+ active.delete(operation);
2623
+ if (active.size === 0) this.activeModelOperations.delete(model);
2624
+ }, () => {
2625
+ active.delete(operation);
2626
+ if (active.size === 0) this.activeModelOperations.delete(model);
2627
+ });
2628
+ return operation;
2629
+ }
2630
+ async waitForPreparedModelIdle(model) {
2631
+ await Promise.allSettled([...this.activeModelOperations.get(model) ?? []]);
2632
+ }
2633
+ async loop(generation = this.loopGeneration) {
2634
+ if (generation !== this.loopGeneration || !this.cameraRunning || !this.webcam) {
2635
+ if (generation === this.loopGeneration) this.loopStarted = false;
2636
+ return;
2637
+ }
2638
+ try {
2639
+ if (this.previewSourceElement() !== this.webcam.webcam) this.webcam.update();
2640
+ if (this.recognizing && this.model) {
2641
+ const model = this.model;
2642
+ const recognitionMode = this.recognitionMode;
2643
+ const first = this.firstRecognitionMs === 0;
2644
+ const startedAt = first ? performance.now() : 0;
2645
+ const recognition = await this.trackPreparedModelOperation(model, this.recognizeFrame(model, recognitionMode));
2646
+ if (generation !== this.loopGeneration || !this.cameraRunning || !this.recognizing || this.model !== model || this.recognitionMode !== recognitionMode) {} else {
2647
+ if (first) this.firstRecognitionMs = Math.round(performance.now() - startedAt);
2648
+ this.renderPoseOverlay(recognition.keypoints);
2649
+ this.applyPredictions(recognition.prediction);
2650
+ }
2651
+ }
2652
+ } catch (error) {
2653
+ this.setLastError(error);
2654
+ }
2655
+ if (generation === this.loopGeneration && this.cameraRunning && this.webcam) requestAnimationFrame(() => void this.loop(generation));
2656
+ else if (generation === this.loopGeneration) this.loopStarted = false;
2657
+ }
2658
+ /**
2659
+ * The square the model looks at, as a tensor, taken straight off the camera.
2660
+ *
2661
+ * The readback canvas is 320x240 and holds a 320x320 centre crop drawn into it, so its bottom
2662
+ * quarter -- 25% of the subject's height -- never reaches the model. Cropping the frame itself
2663
+ * keeps the whole square. The crop and the mirroring both run as tensor ops, which is where the
2664
+ * backend can keep them on the GPU; `fromPixels` uploads a video element directly to a texture
2665
+ * on the WebGL and WebGPU backends, so no Canvas2D stage sits in the path.
2666
+ *
2667
+ * The pixels are mirrored here rather than through `estimatePose(sample, flipHorizontal)`:
2668
+ * that flag flips the returned keypoints, not the image, and the classifier head was trained on
2669
+ * mirrored frames. Flipping the pixels is what keeps the model's input the same as before.
2670
+ *
2671
+ * The WASM and CPU backends have no texture to upload to, so `fromPixels` reads the frame back
2672
+ * through Canvas2D there and this path loses its advantage -- but not its correctness. Driving
2673
+ * one deterministic frame through every backend returns the same probabilities to two decimals
2674
+ * (webgl / wasm / cpu: 0.08, 0.90, 0.02, 0.00) and `tf.memory().numTensors` stays flat over a
2675
+ * six-second run on each, so nothing here depends on which backend `compute-backend.ts` picked.
2676
+ *
2677
+ * Returns null when there is no frame to take, and the canvas path answers instead.
2678
+ */
2679
+ captureSquareFrame() {
2680
+ const tensorflow = globalThis.tf;
2681
+ const video = this.webcam?.webcam;
2682
+ if (typeof tensorflow?.browser?.fromPixels !== "function" || typeof tensorflow.tidy !== "function" || !video || !(video.videoWidth > 0) || !(video.videoHeight > 0)) return null;
2683
+ const size = Math.min(video.videoWidth, video.videoHeight);
2684
+ const frame = tensorflow.browser.fromPixels(video);
2685
+ try {
2686
+ const top = frame.shape[0] - size >> 1;
2687
+ const left = frame.shape[1] - size >> 1;
2688
+ return {
2689
+ tensor: tensorflow.tidy(() => tensorflow.reverse(tensorflow.slice(frame, [
2690
+ top,
2691
+ left,
2692
+ 0
2693
+ ], [
2694
+ size,
2695
+ size,
2696
+ 3
2697
+ ]), [1])),
2698
+ crop: {
2699
+ left,
2700
+ top,
2701
+ size
2702
+ }
2703
+ };
2704
+ } finally {
2705
+ frame.dispose();
2706
+ }
2707
+ }
2708
+ async recognizeFrame(model, recognitionMode) {
2709
+ if (recognitionMode === "pose") {
2710
+ if (typeof model.estimatePose !== "function") throw new Error("Teachable Machine: The active model does not support pose recognition.");
2711
+ const square = this.captureSquareFrame();
2712
+ if (!square) {
2713
+ this.webcam.update?.();
2714
+ const estimate = await model.estimatePose(this.webcam.canvas);
2715
+ const prediction = await model.predict(estimate.posenetOutput);
2716
+ return {
2717
+ keypoints: estimate.pose?.keypoints,
2718
+ prediction
2719
+ };
2720
+ }
2721
+ try {
2722
+ const estimate = await model.estimatePose(square.tensor);
2723
+ const prediction = await model.predict(estimate.posenetOutput);
2724
+ return {
2725
+ keypoints: mapKeypointsToFrame(estimate.pose?.keypoints, square.crop),
2726
+ prediction
2727
+ };
2728
+ } finally {
2729
+ square.tensor.dispose();
2730
+ }
2731
+ }
2732
+ const video = this.webcam.webcam;
2733
+ const source = video ?? this.webcam.canvas;
2734
+ return {
2735
+ keypoints: [],
2736
+ prediction: await model.predict(source, Boolean(video))
2737
+ };
2738
+ }
2739
+ currentPoseReporter() {
2740
+ return this.currentPoseName;
2741
+ }
2742
+ scoreReporter() {
2743
+ return Math.round(this.score * 100) / 100;
2744
+ }
2745
+ poseScoreReporter(args) {
2746
+ const value = this.predictions[String(args.NAME || "")] || 0;
2747
+ return Math.round(value * 100) / 100;
2748
+ }
2749
+ setAccumulatedPoseParameters(args) {
2750
+ const accumulation = args.ACCUMULATION === "" ? 1 : Number(args.ACCUMULATION);
2751
+ const decay = args.DECAY === "" ? .9 : Number(args.DECAY);
2752
+ this.accumulationCoefficient = Number.isFinite(accumulation) ? Math.max(0, accumulation) : 1;
2753
+ this.decayCoefficient = Number.isFinite(decay) ? Math.max(0, Math.min(1, decay)) : .9;
2754
+ }
2755
+ setAccumulatedPoseThreshold(args) {
2756
+ const threshold = args.THRESHOLD === "" ? 0 : Number(args.THRESHOLD);
2757
+ this.accumulatedPoseThreshold = Number.isFinite(threshold) ? Math.max(0, threshold) : 0;
2758
+ this.updateAccumulatedPoseSelection();
2759
+ }
2760
+ startAccumulatedPoseSession(now = performance.now()) {
2761
+ this.activeDecayCoefficient = this.decayCoefficient;
2762
+ this.lastAccumulationTime = now;
2763
+ }
2764
+ supportsAccumulatedPoseEvents() {
2765
+ return this.featureFlags.temporalPoseScoring && this.featureFlags.accumulatedPoseEvents;
2766
+ }
2767
+ emitAccumulatedPoseChanged(previousPoseName, reason) {
2768
+ if (!this.supportsAccumulatedPoseEvents() || previousPoseName === this.accumulatedPoseName) return;
2769
+ const payload = {
2770
+ version: 2,
2771
+ poseName: this.accumulatedPoseName,
2772
+ previousPoseName,
2773
+ score: this.accumulatedScore,
2774
+ reason,
2775
+ timestamp: performance.now()
2776
+ };
2777
+ if (this.onAccumulatedPoseChanged) try {
2778
+ this.onAccumulatedPoseChanged(payload);
2779
+ } catch {}
2780
+ else if (typeof Scratch !== "undefined") Scratch.vm?.runtime?.emit(ACCUMULATED_POSE_CHANGED_EVENT, payload);
2781
+ }
2782
+ dispose() {
2783
+ this.fixedEditors.dispose();
2784
+ this.stopCamera();
2785
+ if (this.featureFlags.temporalPoseScoring && typeof document !== "undefined") document.removeEventListener("visibilitychange", this.visibilityChangeListener);
2786
+ this.onAccumulatedPoseChanged = null;
2787
+ }
2788
+ resetAccumulatedPose(reason = "reset") {
2789
+ const previousPoseName = this.accumulatedPoseName;
2790
+ this.accumulatedPoseName = "";
2791
+ this.accumulatedScore = 0;
2792
+ this.accumulatedPredictions = {};
2793
+ this.lastAccumulationTime = this.recognizing ? performance.now() : null;
2794
+ this.emitAccumulatedPoseChanged(previousPoseName, reason);
2795
+ }
2796
+ handleDocumentVisibilityChange() {
2797
+ if (typeof document === "undefined") return;
2798
+ if (isDocumentHidden()) {
2799
+ this.accumulatedPosePausedForBackground = true;
2800
+ return;
2801
+ }
2802
+ if (this.accumulatedPosePausedForBackground) {
2803
+ this.accumulatedPosePausedForBackground = false;
2804
+ this.lastAccumulationTime = this.recognizing ? performance.now() : null;
2805
+ }
2806
+ }
2807
+ updateAccumulatedPose(prediction, now = performance.now()) {
2808
+ if (isDocumentHidden()) {
2809
+ this.accumulatedPosePausedForBackground = true;
2810
+ return;
2811
+ }
2812
+ if (this.accumulatedPosePausedForBackground) return;
2813
+ const elapsedSeconds = this.lastAccumulationTime === null ? 0 : Math.max(0, (now - this.lastAccumulationTime) / 1e3);
2814
+ const decayMultiplier = elapsedSeconds === 0 ? 1 : Math.pow(this.activeDecayCoefficient, elapsedSeconds);
2815
+ for (const name of Object.keys(this.accumulatedPredictions)) this.accumulatedPredictions[name] *= decayMultiplier;
2816
+ for (const result of prediction) {
2817
+ const name = String(result.className || "");
2818
+ const probability = Number(result.probability);
2819
+ if (!name || !Number.isFinite(probability)) continue;
2820
+ const contribution = Math.max(0, Math.min(1, probability)) * this.accumulationCoefficient * elapsedSeconds;
2821
+ this.accumulatedPredictions[name] = (this.accumulatedPredictions[name] || 0) + contribution;
2822
+ }
2823
+ this.lastAccumulationTime = now;
2824
+ this.updateAccumulatedPoseSelection();
2825
+ }
2826
+ updateAccumulatedPoseSelection() {
2827
+ const previousPoseName = this.accumulatedPoseName;
2828
+ this.accumulatedPoseName = "";
2829
+ this.accumulatedScore = 0;
2830
+ let bestPoseName = "";
2831
+ for (const [name, value] of Object.entries(this.accumulatedPredictions)) if (value > this.accumulatedScore) {
2832
+ bestPoseName = name;
2833
+ this.accumulatedScore = value;
2834
+ }
2835
+ if (bestPoseName && this.accumulatedScore >= this.accumulatedPoseThreshold) this.accumulatedPoseName = bestPoseName;
2836
+ this.emitAccumulatedPoseChanged(previousPoseName, "recognition");
2837
+ }
2838
+ accumulatedPoseReporter() {
2839
+ return this.accumulatedPoseName;
2840
+ }
2841
+ accumulatedScoreReporter() {
2842
+ return this.accumulatedScore;
2843
+ }
2844
+ accumulatedPoseScoreReporter(args) {
2845
+ return this.accumulatedPredictions[String(args.NAME || "")] || 0;
2846
+ }
2847
+ isPose(args) {
2848
+ return (this.predictions[String(args.NAME || "")] || 0) >= .75;
2849
+ }
2850
+ isPoseWithThreshold(args) {
2851
+ const name = String(args.NAME || "");
2852
+ let threshold = args.THRESHOLD === "" ? .75 : Number(args.THRESHOLD);
2853
+ if (Number.isNaN(threshold)) threshold = .75;
2854
+ threshold = Math.max(0, Math.min(1, threshold));
2855
+ return (this.predictions[name] || 0) >= threshold;
2856
+ }
2857
+ cameraMsReporter() {
2858
+ return this.cameraMs;
2859
+ }
2860
+ modelLoadMsReporter() {
2861
+ return this.modelLoadMs;
2862
+ }
2863
+ firstRecognitionMsReporter() {
2864
+ return this.firstRecognitionMs;
2865
+ }
2866
+ lastErrorReporter() {
2867
+ return this.lastError;
2868
+ }
2174
2869
  };
2175
2870
  //#endregion
2176
2871
  //#region src/index.ts