@kubohiroya/turbowarp-teachable-machine 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/backend-wasm.js +7 -7
- package/dist/backend-webgpu.js +622 -622
- package/dist/composition.js +2684 -2369
- package/dist/posenet.js +418 -451
- package/dist/runtime.js +1405 -1405
- package/dist/tm.js +1987 -1435
- package/dist/types/extension.d.ts +13 -0
- package/package.json +15 -10
package/dist/posenet.js
CHANGED
|
@@ -1,502 +1,469 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
var package_default = {
|
|
2
|
+
name: "@kubohiroya/turbowarp-teachable-machine",
|
|
3
|
+
version: "3.4.0",
|
|
4
|
+
description: "A TurboWarp extension for recognition using Teachable Machine pose, image, and audio models.",
|
|
5
|
+
author: "Hiroya Kubo <hiroya@cuc.ac.jp>",
|
|
6
|
+
license: "MPL-2.0",
|
|
7
|
+
homepage: "https://github.com/kubohiroya/turbowarp-teachable-machine#readme",
|
|
8
|
+
repository: {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kubohiroya/turbowarp-teachable-machine.git"
|
|
11
|
+
},
|
|
12
|
+
bugs: { "url": "https://github.com/kubohiroya/turbowarp-teachable-machine/issues" },
|
|
13
|
+
exports: {
|
|
14
|
+
"./composition": {
|
|
15
|
+
"types": "./dist/types/composition.d.ts",
|
|
16
|
+
"import": "./dist/composition.js"
|
|
17
|
+
},
|
|
18
|
+
"./posenet": {
|
|
19
|
+
"types": "./dist/types/posenet.d.ts",
|
|
20
|
+
"import": "./dist/posenet.js"
|
|
21
|
+
},
|
|
22
|
+
"./posenet-assets/*": "./dist/posenet/mobilenet-v1-075-stride16/*",
|
|
23
|
+
"./wasm-assets/*": "./dist/wasm/*",
|
|
24
|
+
"./dist/backend-wasm.js": "./dist/backend-wasm.js",
|
|
25
|
+
"./dist/backend-webgpu.js": "./dist/backend-webgpu.js",
|
|
26
|
+
"./runtime": "./dist/runtime.js",
|
|
27
|
+
"./dist/tm.js": "./dist/tm.js",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
type: "module",
|
|
31
|
+
files: [
|
|
32
|
+
"dist/",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"THIRD_PARTY_NOTICES.md"
|
|
36
|
+
],
|
|
37
|
+
unpkg: "./dist/tm.js",
|
|
38
|
+
jsdelivr: "./dist/tm.js",
|
|
39
|
+
publishConfig: {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
},
|
|
43
|
+
packageManager: "pnpm@11.11.0",
|
|
44
|
+
engines: { "node": ">=22.18.0" },
|
|
45
|
+
scripts: {
|
|
46
|
+
"dev": "vite build --watch",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"build": "pnpm build:extension && pnpm build:composition && pnpm build:runtime && pnpm build:backends && pnpm build:posenet && pnpm build:notices && pnpm build:types",
|
|
49
|
+
"build:extension": "vite build",
|
|
50
|
+
"build:composition": "vite build --config vite.composition.config.ts",
|
|
51
|
+
"build:runtime": "vite build --config vite.runtime.config.ts",
|
|
52
|
+
"build:backends": "vite build --config vite.backend-wasm.config.ts && vite build --config vite.backend-webgpu.config.ts && node scripts/copy-wasm-assets.ts",
|
|
53
|
+
"build:posenet": "vite build --config vite.posenet.config.ts && node scripts/copy-posenet-assets.ts",
|
|
54
|
+
"build:notices": "node scripts/copy-third-party-notices.ts",
|
|
55
|
+
"build:types": "tsc --project tsconfig.build.json",
|
|
56
|
+
"types:composition:check": "tsc --project tsconfig.composition-consumer.json",
|
|
57
|
+
"types:posenet:check": "tsc --project tsconfig.posenet-consumer.json",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"docs": "node scripts/generate-readme.ts",
|
|
60
|
+
"docs:check": "npm run docs && git diff --exit-code -- README.md",
|
|
61
|
+
"docs:pages:check": "node scripts/check-pages.ts",
|
|
62
|
+
"release:consistency:check": "node scripts/check-release-consistency.ts",
|
|
63
|
+
"check:dist": "npm run build && git diff --exit-code -- dist",
|
|
64
|
+
"archive:check": "node scripts/check-github-archive.ts",
|
|
65
|
+
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
66
|
+
"release:check": "npm publish --dry-run --access public --tag latest",
|
|
67
|
+
"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",
|
|
68
|
+
"prepack": "npm run build && npm run release:consistency:check"
|
|
69
|
+
},
|
|
70
|
+
devDependencies: {
|
|
71
|
+
"@kubohiroya/turbowarp-camera-source": "0.13.0",
|
|
72
|
+
"@kubohiroya/vite-plugin-turbowarp-extension": "0.4.0",
|
|
73
|
+
"@teachablemachine/image": "0.8.5",
|
|
74
|
+
"@teachablemachine/pose": "0.8.6",
|
|
75
|
+
"@tensorflow-models/posenet": "2.2.2",
|
|
76
|
+
"@tensorflow-models/speech-commands": "0.5.4",
|
|
77
|
+
"@tensorflow/tfjs": "4.22.0",
|
|
78
|
+
"@tensorflow/tfjs-backend-wasm": "4.22.0",
|
|
79
|
+
"@tensorflow/tfjs-backend-webgpu": "4.22.0",
|
|
80
|
+
"@types/node": "^22.20.1",
|
|
81
|
+
"typescript": "^5.9.3",
|
|
82
|
+
"vite": "^8.2.2",
|
|
83
|
+
"vitest": "^4.0.14"
|
|
84
|
+
},
|
|
85
|
+
keywords: [
|
|
86
|
+
"turbowarp",
|
|
87
|
+
"scratch",
|
|
88
|
+
"extension",
|
|
89
|
+
"pose",
|
|
90
|
+
"image",
|
|
91
|
+
"audio",
|
|
92
|
+
"camera",
|
|
93
|
+
"teachable-machine"
|
|
94
|
+
]
|
|
6
95
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/posenet.ts
|
|
98
|
+
var poseNetModelDefaults = Object.freeze({
|
|
99
|
+
architecture: "MobileNetV1",
|
|
100
|
+
multiplier: .75,
|
|
101
|
+
outputStride: 16,
|
|
102
|
+
inputResolution: 257
|
|
12
103
|
});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
104
|
+
var poseNetBaseUrl = "https://storage.googleapis.com/tfjs-models/savedmodel/posenet/mobilenet/float/075/";
|
|
105
|
+
var poseNetPackageAssetDirectory = "dist/posenet/mobilenet-v1-075-stride16";
|
|
106
|
+
var expectedFiles = Object.freeze([
|
|
107
|
+
Object.freeze({
|
|
108
|
+
path: "model-stride16.json",
|
|
109
|
+
url: `${poseNetBaseUrl}model-stride16.json`,
|
|
110
|
+
mediaType: "application/json",
|
|
111
|
+
sha256: "dd63bf2d3b983e8c80020749f135164beda00a33374c8a7be230b9598f24f798",
|
|
112
|
+
size: 49720,
|
|
113
|
+
packagePath: `${poseNetPackageAssetDirectory}/model-stride16.json`,
|
|
114
|
+
packageSpecifier: `${package_default.name}/posenet-assets/model-stride16.json`,
|
|
115
|
+
maxBytes: 65536
|
|
116
|
+
}),
|
|
117
|
+
Object.freeze({
|
|
118
|
+
path: "group1-shard1of2.bin",
|
|
119
|
+
url: `${poseNetBaseUrl}group1-shard1of2.bin`,
|
|
120
|
+
mediaType: "application/octet-stream",
|
|
121
|
+
sha256: "ce6afc62f89782d43139fab76c641b281a82dee2cd2759aa036c4b28aea16439",
|
|
122
|
+
size: 4194304,
|
|
123
|
+
packagePath: `${poseNetPackageAssetDirectory}/group1-shard1of2.bin`,
|
|
124
|
+
packageSpecifier: `${package_default.name}/posenet-assets/group1-shard1of2.bin`,
|
|
125
|
+
maxBytes: 4194304
|
|
126
|
+
}),
|
|
127
|
+
Object.freeze({
|
|
128
|
+
path: "group1-shard2of2.bin",
|
|
129
|
+
url: `${poseNetBaseUrl}group1-shard2of2.bin`,
|
|
130
|
+
mediaType: "application/octet-stream",
|
|
131
|
+
sha256: "2a35b8cfb86eb50928931e03dc30c0972fdd375f148b177ee40676b81a17692d",
|
|
132
|
+
size: 838476,
|
|
133
|
+
packagePath: `${poseNetPackageAssetDirectory}/group1-shard2of2.bin`,
|
|
134
|
+
packageSpecifier: `${package_default.name}/posenet-assets/group1-shard2of2.bin`,
|
|
135
|
+
maxBytes: 1048576
|
|
136
|
+
})
|
|
46
137
|
]);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
138
|
+
var poseNetBundleManifest = Object.freeze({
|
|
139
|
+
formatVersion: 1,
|
|
140
|
+
distribution: Object.freeze({
|
|
141
|
+
package: package_default.name,
|
|
142
|
+
version: package_default.version,
|
|
143
|
+
assetDirectory: poseNetPackageAssetDirectory
|
|
144
|
+
}),
|
|
145
|
+
model: poseNetModelDefaults,
|
|
146
|
+
runtime: Object.freeze({
|
|
147
|
+
package: "@tensorflow-models/posenet",
|
|
148
|
+
version: "2.2.2"
|
|
149
|
+
}),
|
|
150
|
+
source: Object.freeze({
|
|
151
|
+
provider: "tensorflow",
|
|
152
|
+
repository: "https://github.com/tensorflow/tfjs-models/tree/v2.2.2/posenet",
|
|
153
|
+
modelUrl: poseNetBaseUrl
|
|
154
|
+
}),
|
|
155
|
+
license: Object.freeze({
|
|
156
|
+
spdx: "Apache-2.0",
|
|
157
|
+
notice: "https://www.apache.org/licenses/LICENSE-2.0"
|
|
158
|
+
}),
|
|
159
|
+
limits: Object.freeze({
|
|
160
|
+
maxFiles: expectedFiles.length,
|
|
161
|
+
maxTotalBytes: 5242880
|
|
162
|
+
}),
|
|
163
|
+
files: expectedFiles
|
|
73
164
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
165
|
+
var PoseNetBundleError = class extends Error {
|
|
166
|
+
code;
|
|
167
|
+
constructor(code, message) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "PoseNetBundleError";
|
|
170
|
+
this.code = code;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
82
173
|
function fail(code, message) {
|
|
83
|
-
|
|
174
|
+
throw new PoseNetBundleError(code, message);
|
|
84
175
|
}
|
|
85
176
|
function abortError() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
177
|
+
const error = /* @__PURE__ */ new Error("TM PoseNet model loading was cancelled.");
|
|
178
|
+
error.name = "AbortError";
|
|
179
|
+
Object.defineProperty(error, "code", { value: "TM-POSENET-ABORTED" });
|
|
180
|
+
return error;
|
|
90
181
|
}
|
|
91
182
|
function throwIfAborted(signal) {
|
|
92
|
-
|
|
183
|
+
if (signal?.aborted) throw abortError();
|
|
93
184
|
}
|
|
94
185
|
function isRecord(value) {
|
|
95
|
-
|
|
186
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
187
|
}
|
|
97
188
|
function requireBytes(value, label) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
189
|
+
if (value instanceof Uint8Array) return value;
|
|
190
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
191
|
+
return fail("TM-POSENET-ASSET-001", `${label} must be a Uint8Array or ArrayBuffer.`);
|
|
101
192
|
}
|
|
102
193
|
function requireSubtleCrypto(value) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
return value;
|
|
194
|
+
if (!isRecord(value) || typeof value.digest !== "function") return fail("TM-POSENET-ASSET-001", "Web Crypto subtle.digest is required.");
|
|
195
|
+
return value;
|
|
107
196
|
}
|
|
108
197
|
async function sha256(bytes, subtleCrypto) {
|
|
109
|
-
|
|
110
|
-
|
|
198
|
+
const digest = await subtleCrypto.digest("SHA-256", bytes);
|
|
199
|
+
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
111
200
|
}
|
|
112
201
|
function encodeBase64(bytes) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
return btoa(chunks.join(""));
|
|
202
|
+
if (typeof btoa !== "function") return fail("TM-POSENET-ASSET-001", "Base64 encoding requires btoa.");
|
|
203
|
+
const chunks = [];
|
|
204
|
+
const chunkSize = 32768;
|
|
205
|
+
for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
|
|
206
|
+
const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength));
|
|
207
|
+
let binary = "";
|
|
208
|
+
for (const value of chunk) binary += String.fromCharCode(value);
|
|
209
|
+
chunks.push(binary);
|
|
210
|
+
}
|
|
211
|
+
return btoa(chunks.join(""));
|
|
125
212
|
}
|
|
126
213
|
function isBase64Character(code) {
|
|
127
|
-
|
|
214
|
+
return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47;
|
|
128
215
|
}
|
|
129
216
|
function hasValidBase64Alphabet(value) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
return dataLength > 0;
|
|
217
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
218
|
+
const dataLength = value.length - padding;
|
|
219
|
+
for (let index = 0; index < dataLength; index += 1) if (!isBase64Character(value.charCodeAt(index))) return false;
|
|
220
|
+
return dataLength > 0;
|
|
136
221
|
}
|
|
137
222
|
function decodeBase64(value, label, maxBytes) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
`${label} is invalid Base64: ${error instanceof Error ? error.message : String(error)}`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
if (decoded.length > maxBytes) {
|
|
155
|
-
return fail("TM-POSENET-ASSET-004", `${label} exceeds its decoded byte limit.`);
|
|
156
|
-
}
|
|
157
|
-
const bytes = new Uint8Array(decoded.length);
|
|
158
|
-
for (let index = 0; index < decoded.length; index += 1) {
|
|
159
|
-
bytes[index] = decoded.charCodeAt(index);
|
|
160
|
-
}
|
|
161
|
-
return bytes;
|
|
223
|
+
const maximumLength = Math.ceil(maxBytes / 3) * 4;
|
|
224
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || value.length % 4 !== 0 || !hasValidBase64Alphabet(value)) return fail("TM-POSENET-ASSET-001", `${label} must be bounded padded Base64.`);
|
|
225
|
+
if (typeof atob !== "function") return fail("TM-POSENET-ASSET-001", "Base64 decoding requires atob.");
|
|
226
|
+
let decoded;
|
|
227
|
+
try {
|
|
228
|
+
decoded = atob(value);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return fail("TM-POSENET-ASSET-001", `${label} is invalid Base64: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
}
|
|
232
|
+
if (decoded.length > maxBytes) return fail("TM-POSENET-ASSET-004", `${label} exceeds its decoded byte limit.`);
|
|
233
|
+
const bytes = new Uint8Array(decoded.length);
|
|
234
|
+
for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
|
|
235
|
+
return bytes;
|
|
162
236
|
}
|
|
163
237
|
function canonicalProjectBundle(descriptor) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
sha256: expected.sha256,
|
|
182
|
-
data: candidate.data
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
return Object.freeze({ formatVersion: 1, encoding: "base64", files: Object.freeze(files) });
|
|
238
|
+
if (!isRecord(descriptor) || descriptor.formatVersion !== 1 || descriptor.encoding !== "base64" || !Array.isArray(descriptor.files) || descriptor.files.length !== expectedFiles.length) return fail("TM-POSENET-ASSET-001", "PoseNet project bundle format is invalid.");
|
|
239
|
+
const files = expectedFiles.map((expected) => {
|
|
240
|
+
const candidate = descriptor.files.find((file) => isRecord(file) && file.path === expected.path);
|
|
241
|
+
if (!isRecord(candidate) || candidate.mediaType !== expected.mediaType || candidate.sha256 !== expected.sha256 || !Number.isSafeInteger(candidate.size) || Number(candidate.size) !== expected.size || typeof candidate.data !== "string") return fail("TM-POSENET-ASSET-001", `PoseNet project file metadata is invalid: ${expected.path}.`);
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
path: expected.path,
|
|
244
|
+
mediaType: expected.mediaType,
|
|
245
|
+
size: Number(candidate.size),
|
|
246
|
+
sha256: expected.sha256,
|
|
247
|
+
data: candidate.data
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
formatVersion: 1,
|
|
252
|
+
encoding: "base64",
|
|
253
|
+
files: Object.freeze(files)
|
|
254
|
+
});
|
|
186
255
|
}
|
|
187
256
|
function decodeProjectBundle(descriptor, signal) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"TM-POSENET-ASSET-004",
|
|
202
|
-
`PoseNet project file size does not match: ${expected.path}.`
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
return Object.freeze({ path: expected.path, mediaType: expected.mediaType, bytes });
|
|
206
|
-
})
|
|
207
|
-
);
|
|
257
|
+
throwIfAborted(signal);
|
|
258
|
+
const canonical = canonicalProjectBundle(descriptor);
|
|
259
|
+
return Object.freeze(canonical.files.map((candidate, index) => {
|
|
260
|
+
throwIfAborted(signal);
|
|
261
|
+
const expected = expectedFiles[index];
|
|
262
|
+
const bytes = decodeBase64(candidate.data, `PoseNet project file ${expected.path}`, expected.size);
|
|
263
|
+
if (bytes.byteLength !== candidate.size) return fail("TM-POSENET-ASSET-004", `PoseNet project file size does not match: ${expected.path}.`);
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
path: expected.path,
|
|
266
|
+
mediaType: expected.mediaType,
|
|
267
|
+
bytes
|
|
268
|
+
});
|
|
269
|
+
}));
|
|
208
270
|
}
|
|
209
|
-
async function verifyPoseNetBundle(files, {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
bytes: new Uint8Array(bytes)
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
const jsonFile = verifiedFiles.find(({ path }) => path === "model-stride16.json");
|
|
259
|
-
if (!jsonFile) {
|
|
260
|
-
return fail("TM-POSENET-ASSET-002", "PoseNet model JSON is missing.");
|
|
261
|
-
}
|
|
262
|
-
let modelJson;
|
|
263
|
-
try {
|
|
264
|
-
modelJson = JSON.parse(new TextDecoder().decode(jsonFile.bytes));
|
|
265
|
-
} catch (error) {
|
|
266
|
-
return fail(
|
|
267
|
-
"TM-POSENET-ASSET-001",
|
|
268
|
-
`PoseNet model JSON is invalid: ${error instanceof Error ? error.message : String(error)}`
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
const weightsManifest = isRecord(modelJson) && Array.isArray(modelJson.weightsManifest) ? modelJson.weightsManifest : [];
|
|
272
|
-
const manifestPaths = weightsManifest.flatMap(
|
|
273
|
-
(entry) => isRecord(entry) && Array.isArray(entry.paths) ? entry.paths : []
|
|
274
|
-
);
|
|
275
|
-
const expectedShardPaths = expectedFiles.filter(({ path }) => path.endsWith(".bin")).map(({ path }) => path);
|
|
276
|
-
if (manifestPaths.length !== expectedShardPaths.length || expectedShardPaths.some((path) => !manifestPaths.includes(path))) {
|
|
277
|
-
return fail(
|
|
278
|
-
"TM-POSENET-ASSET-001",
|
|
279
|
-
"PoseNet model JSON references unexpected weight shards."
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
return Object.freeze({
|
|
283
|
-
manifest: poseNetBundleManifest,
|
|
284
|
-
files: Object.freeze(verifiedFiles)
|
|
285
|
-
});
|
|
271
|
+
async function verifyPoseNetBundle(files, { subtleCrypto = globalThis.crypto?.subtle, signal } = {}) {
|
|
272
|
+
throwIfAborted(signal);
|
|
273
|
+
const digestRuntime = requireSubtleCrypto(subtleCrypto);
|
|
274
|
+
if (!Array.isArray(files) || files.length !== expectedFiles.length) return fail("TM-POSENET-ASSET-004", "PoseNet bundle must contain exactly three files.");
|
|
275
|
+
let totalBytes = 0;
|
|
276
|
+
const candidates = expectedFiles.map((expected) => {
|
|
277
|
+
const candidate = files.find((file) => isRecord(file) && file.path === expected.path);
|
|
278
|
+
if (!candidate) return fail("TM-POSENET-ASSET-002", `PoseNet file is missing: ${expected.path}.`);
|
|
279
|
+
const bytes = requireBytes(candidate.bytes, `PoseNet file ${expected.path}`);
|
|
280
|
+
if (bytes.byteLength !== expected.size) return fail("TM-POSENET-ASSET-004", `PoseNet file size is invalid: ${expected.path}.`);
|
|
281
|
+
totalBytes += bytes.byteLength;
|
|
282
|
+
return {
|
|
283
|
+
expected,
|
|
284
|
+
bytes
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
if (totalBytes > poseNetBundleManifest.limits.maxTotalBytes) return fail("TM-POSENET-ASSET-004", "PoseNet bundle exceeds its total byte limit.");
|
|
288
|
+
const digests = await Promise.all(candidates.map(async ({ bytes }) => {
|
|
289
|
+
throwIfAborted(signal);
|
|
290
|
+
return sha256(bytes, digestRuntime);
|
|
291
|
+
}));
|
|
292
|
+
throwIfAborted(signal);
|
|
293
|
+
const verifiedFiles = candidates.map(({ expected, bytes }, index) => {
|
|
294
|
+
if (digests[index] !== expected.sha256) return fail("TM-POSENET-ASSET-003", `PoseNet file integrity mismatch: ${expected.path}.`);
|
|
295
|
+
return Object.freeze({
|
|
296
|
+
path: expected.path,
|
|
297
|
+
url: expected.url,
|
|
298
|
+
mediaType: expected.mediaType,
|
|
299
|
+
bytes: new Uint8Array(bytes)
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
const jsonFile = verifiedFiles.find(({ path }) => path === "model-stride16.json");
|
|
303
|
+
if (!jsonFile) return fail("TM-POSENET-ASSET-002", "PoseNet model JSON is missing.");
|
|
304
|
+
let modelJson;
|
|
305
|
+
try {
|
|
306
|
+
modelJson = JSON.parse(new TextDecoder().decode(jsonFile.bytes));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return fail("TM-POSENET-ASSET-001", `PoseNet model JSON is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
|
+
}
|
|
310
|
+
const manifestPaths = (isRecord(modelJson) && Array.isArray(modelJson.weightsManifest) ? modelJson.weightsManifest : []).flatMap((entry) => isRecord(entry) && Array.isArray(entry.paths) ? entry.paths : []);
|
|
311
|
+
const expectedShardPaths = expectedFiles.filter(({ path }) => path.endsWith(".bin")).map(({ path }) => path);
|
|
312
|
+
if (manifestPaths.length !== expectedShardPaths.length || expectedShardPaths.some((path) => !manifestPaths.includes(path))) return fail("TM-POSENET-ASSET-001", "PoseNet model JSON references unexpected weight shards.");
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
manifest: poseNetBundleManifest,
|
|
315
|
+
files: Object.freeze(verifiedFiles)
|
|
316
|
+
});
|
|
286
317
|
}
|
|
287
318
|
async function loadPoseNetBundle(loadFile, options = {}) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
})
|
|
300
|
-
);
|
|
301
|
-
return verifyPoseNetBundle(files, options);
|
|
319
|
+
if (typeof loadFile !== "function") throw new TypeError("PoseNet file loader is required.");
|
|
320
|
+
throwIfAborted(options.signal);
|
|
321
|
+
return verifyPoseNetBundle(await Promise.all(expectedFiles.map(async (file) => {
|
|
322
|
+
const bytes = requireBytes(await loadFile(file), `PoseNet file ${file.path}`);
|
|
323
|
+
throwIfAborted(options.signal);
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
path: file.path,
|
|
326
|
+
mediaType: file.mediaType,
|
|
327
|
+
bytes
|
|
328
|
+
});
|
|
329
|
+
})), options);
|
|
302
330
|
}
|
|
303
331
|
async function createPoseNetProjectBundle(files, options = {}) {
|
|
304
|
-
|
|
305
|
-
return createProjectBundleFromVerified(verified);
|
|
332
|
+
return createProjectBundleFromVerified(await verifyPoseNetBundle(files, options));
|
|
306
333
|
}
|
|
307
334
|
function createProjectBundleFromVerified(verified) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
})
|
|
320
|
-
)
|
|
321
|
-
)
|
|
322
|
-
});
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
formatVersion: 1,
|
|
337
|
+
encoding: "base64",
|
|
338
|
+
files: Object.freeze(verified.files.map((file, index) => Object.freeze({
|
|
339
|
+
path: file.path,
|
|
340
|
+
mediaType: file.mediaType,
|
|
341
|
+
size: file.bytes.byteLength,
|
|
342
|
+
sha256: expectedFiles[index].sha256,
|
|
343
|
+
data: encodeBase64(file.bytes)
|
|
344
|
+
})))
|
|
345
|
+
});
|
|
323
346
|
}
|
|
324
347
|
async function createPoseNetProjectBundleFromLoader(loadFile, options = {}) {
|
|
325
|
-
|
|
348
|
+
return createProjectBundleFromVerified(await loadPoseNetBundle(loadFile, options));
|
|
326
349
|
}
|
|
327
350
|
async function loadPoseNetProjectBundle(descriptor, options = {}) {
|
|
328
|
-
|
|
351
|
+
return verifyPoseNetBundle(decodeProjectBundle(descriptor, options.signal), options);
|
|
329
352
|
}
|
|
330
353
|
async function validatePoseNetProjectBundle(descriptor, options = {}) {
|
|
331
|
-
|
|
332
|
-
|
|
354
|
+
await loadPoseNetProjectBundle(descriptor, options);
|
|
355
|
+
return canonicalProjectBundle(descriptor);
|
|
333
356
|
}
|
|
334
357
|
function validateRuntime(value) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
"TM-POSENET-RUNTIME-001",
|
|
338
|
-
"TM runtime must provide Webcam and loadFromFiles."
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
return value;
|
|
358
|
+
if (!isRecord(value) || typeof value.Webcam !== "function" || typeof value.loadFromFiles !== "function") return fail("TM-POSENET-RUNTIME-001", "TM runtime must provide Webcam and loadFromFiles.");
|
|
359
|
+
return value;
|
|
342
360
|
}
|
|
343
361
|
function requestUrl(input, baseUrl) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
362
|
+
const value = typeof input === "string" ? input : isRecord(input) ? input.url : void 0;
|
|
363
|
+
if (typeof value !== "string") return null;
|
|
364
|
+
try {
|
|
365
|
+
return new URL(value, baseUrl).href;
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
351
369
|
}
|
|
352
370
|
async function disposeRuntimeModel(value) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
const errors = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
361
|
-
if (errors.length > 0) {
|
|
362
|
-
throw new AggregateError(errors, "TM could not dispose a cancelled runtime model.");
|
|
363
|
-
}
|
|
371
|
+
if (!isRecord(value)) return;
|
|
372
|
+
const classifier = isRecord(value.model) && typeof value.model.dispose === "function" ? value.model : null;
|
|
373
|
+
const poseNet = isRecord(value.posenetModel) && typeof value.posenetModel.dispose === "function" ? value.posenetModel : null;
|
|
374
|
+
const resources = classifier && poseNet && classifier !== poseNet ? [classifier, poseNet] : typeof value.dispose === "function" ? [value] : [...new Set([classifier, poseNet].filter((resource) => resource !== null))];
|
|
375
|
+
const errors = (await Promise.allSettled(resources.map((resource) => Promise.resolve().then(() => resource.dispose())))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
376
|
+
if (errors.length > 0) throw new AggregateError(errors, "TM could not dispose a cancelled runtime model.");
|
|
364
377
|
}
|
|
365
378
|
function createBundledTMRuntime(options) {
|
|
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
|
-
throw new AggregateError(
|
|
455
|
-
[cancellation, runtimeError, disposalError].filter((error) => error !== void 0),
|
|
456
|
-
"TM cancellation encountered a runtime or disposal failure."
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
throw cancellation;
|
|
460
|
-
}
|
|
461
|
-
if (supplyResult.status === "rejected") {
|
|
462
|
-
if (disposalError) {
|
|
463
|
-
throw new AggregateError(
|
|
464
|
-
[supplyResult.reason, disposalError],
|
|
465
|
-
"PoseNet verification failed and model cleanup was incomplete."
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
throw supplyResult.reason;
|
|
469
|
-
}
|
|
470
|
-
if (runtimeResult.status === "rejected") throw runtimeResult.reason;
|
|
471
|
-
return runtimeResult.value;
|
|
472
|
-
} finally {
|
|
473
|
-
if (globalObject.fetch === localFetch) {
|
|
474
|
-
try {
|
|
475
|
-
globalObject.fetch = previousFetch;
|
|
476
|
-
} catch {
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
const current = previousLoad.then(task, task);
|
|
482
|
-
previousLoad = current.catch(() => void 0);
|
|
483
|
-
return current;
|
|
484
|
-
}
|
|
485
|
-
return Object.freeze({
|
|
486
|
-
Webcam: runtime.Webcam,
|
|
487
|
-
loadFromFiles,
|
|
488
|
-
poseNetManifest: poseNetBundleManifest
|
|
489
|
-
});
|
|
379
|
+
if (!isRecord(options)) throw new TypeError("PoseNet runtime options are required.");
|
|
380
|
+
if (options.parallelModelInitialization !== void 0 && typeof options.parallelModelInitialization !== "boolean") throw new TypeError("parallelModelInitialization must be a boolean.");
|
|
381
|
+
const runtime = validateRuntime(options.runtime);
|
|
382
|
+
const globalObject = options.globalObject ?? globalThis;
|
|
383
|
+
const ResponseConstructor = globalObject.Response ?? globalThis.Response;
|
|
384
|
+
if (typeof ResponseConstructor !== "function") return fail("TM-POSENET-RUNTIME-001", "Response constructor is required.");
|
|
385
|
+
if ([
|
|
386
|
+
options.files,
|
|
387
|
+
options.loadFiles,
|
|
388
|
+
options.projectBundle
|
|
389
|
+
].filter((value) => value !== void 0).length !== 1) throw new TypeError("Provide exactly one of files, loadFiles, or projectBundle.");
|
|
390
|
+
let verification;
|
|
391
|
+
const verifySupply = () => {
|
|
392
|
+
verification ??= options.projectBundle ? loadPoseNetProjectBundle(options.projectBundle, { subtleCrypto: options.subtleCrypto ?? globalObject.crypto?.subtle }) : Promise.resolve(options.loadFiles ? options.loadFiles() : options.files).then((files) => verifyPoseNetBundle(files, { subtleCrypto: options.subtleCrypto ?? globalObject.crypto?.subtle }));
|
|
393
|
+
return verification;
|
|
394
|
+
};
|
|
395
|
+
let previousLoad = Promise.resolve();
|
|
396
|
+
async function loadFromFiles(model, weights, metadata, loadOptions = {}) {
|
|
397
|
+
const task = async () => {
|
|
398
|
+
throwIfAborted(loadOptions.signal);
|
|
399
|
+
const previousFetch = globalObject.fetch;
|
|
400
|
+
if (typeof previousFetch !== "function") return fail("TM-POSENET-RUNTIME-001", "Browser fetch is required.");
|
|
401
|
+
const baseUrl = globalObject.location?.href ?? "http://localhost/";
|
|
402
|
+
const localFetch = async (input) => {
|
|
403
|
+
const bundle = await verifySupply();
|
|
404
|
+
const byUrl = new Map(bundle.files.map((file) => [file.url, file]));
|
|
405
|
+
const url = requestUrl(input, baseUrl);
|
|
406
|
+
const file = url ? byUrl.get(url) : void 0;
|
|
407
|
+
if (!file) return fail("TM-POSENET-FETCH-001", `Unexpected PoseNet request: ${url ?? "(invalid)"}.`);
|
|
408
|
+
return new ResponseConstructor(file.bytes.slice().buffer, {
|
|
409
|
+
status: 200,
|
|
410
|
+
headers: { "content-type": file.mediaType }
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
globalObject.fetch = localFetch;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return fail("TM-POSENET-FETCH-001", `PoseNet fetch interception is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
let runtimeLoad;
|
|
420
|
+
try {
|
|
421
|
+
runtimeLoad = Promise.resolve(runtime.loadFromFiles(model, weights, metadata, {
|
|
422
|
+
...loadOptions,
|
|
423
|
+
...options.parallelModelInitialization ? { parallelModelInitialization: true } : {}
|
|
424
|
+
}));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
runtimeLoad = Promise.reject(error);
|
|
427
|
+
}
|
|
428
|
+
const supply = verifySupply();
|
|
429
|
+
const [runtimeResult, supplyResult] = await Promise.allSettled([runtimeLoad, supply]);
|
|
430
|
+
let disposalError;
|
|
431
|
+
if (runtimeResult.status === "fulfilled" && (loadOptions.signal?.aborted || supplyResult.status === "rejected")) try {
|
|
432
|
+
await disposeRuntimeModel(runtimeResult.value);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
disposalError = error;
|
|
435
|
+
}
|
|
436
|
+
if (loadOptions.signal?.aborted) {
|
|
437
|
+
const cancellation = abortError();
|
|
438
|
+
const runtimeError = runtimeResult.status === "rejected" && runtimeResult.reason?.name !== "AbortError" ? runtimeResult.reason : void 0;
|
|
439
|
+
if (runtimeError || disposalError) throw new AggregateError([
|
|
440
|
+
cancellation,
|
|
441
|
+
runtimeError,
|
|
442
|
+
disposalError
|
|
443
|
+
].filter((error) => error !== void 0), "TM cancellation encountered a runtime or disposal failure.");
|
|
444
|
+
throw cancellation;
|
|
445
|
+
}
|
|
446
|
+
if (supplyResult.status === "rejected") {
|
|
447
|
+
if (disposalError) throw new AggregateError([supplyResult.reason, disposalError], "PoseNet verification failed and model cleanup was incomplete.");
|
|
448
|
+
throw supplyResult.reason;
|
|
449
|
+
}
|
|
450
|
+
if (runtimeResult.status === "rejected") throw runtimeResult.reason;
|
|
451
|
+
return runtimeResult.value;
|
|
452
|
+
} finally {
|
|
453
|
+
if (globalObject.fetch === localFetch) try {
|
|
454
|
+
globalObject.fetch = previousFetch;
|
|
455
|
+
} catch {}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const current = previousLoad.then(task, task);
|
|
459
|
+
previousLoad = current.catch(() => void 0);
|
|
460
|
+
return current;
|
|
461
|
+
}
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
Webcam: runtime.Webcam,
|
|
464
|
+
loadFromFiles,
|
|
465
|
+
poseNetManifest: poseNetBundleManifest
|
|
466
|
+
});
|
|
490
467
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
createBundledTMRuntime,
|
|
494
|
-
createPoseNetProjectBundle,
|
|
495
|
-
createPoseNetProjectBundleFromLoader,
|
|
496
|
-
loadPoseNetBundle,
|
|
497
|
-
loadPoseNetProjectBundle,
|
|
498
|
-
poseNetBundleManifest,
|
|
499
|
-
poseNetModelDefaults,
|
|
500
|
-
validatePoseNetProjectBundle,
|
|
501
|
-
verifyPoseNetBundle
|
|
502
|
-
};
|
|
468
|
+
//#endregion
|
|
469
|
+
export { PoseNetBundleError, createBundledTMRuntime, createPoseNetProjectBundle, createPoseNetProjectBundleFromLoader, loadPoseNetBundle, loadPoseNetProjectBundle, poseNetBundleManifest, poseNetModelDefaults, validatePoseNetProjectBundle, verifyPoseNetBundle };
|