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