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