@kubohiroya/turbowarp-teachable-machine 3.2.1 → 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/dist/posenet.js CHANGED
@@ -1,502 +1,468 @@
1
- const name = "@kubohiroya/turbowarp-teachable-machine";
2
- const version = "3.2.1";
3
- const packageMetadata = {
4
- name,
5
- version
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
- const poseNetModelDefaults = Object.freeze({
8
- architecture: "MobileNetV1",
9
- multiplier: 0.75,
10
- outputStride: 16,
11
- inputResolution: 257
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
- const poseNetBaseUrl = "https://storage.googleapis.com/tfjs-models/savedmodel/posenet/mobilenet/float/075/";
14
- const poseNetPackageAssetDirectory = "dist/posenet/mobilenet-v1-075-stride16";
15
- const expectedFiles = Object.freeze([
16
- Object.freeze({
17
- path: "model-stride16.json",
18
- url: `${poseNetBaseUrl}model-stride16.json`,
19
- mediaType: "application/json",
20
- sha256: "dd63bf2d3b983e8c80020749f135164beda00a33374c8a7be230b9598f24f798",
21
- size: 49720,
22
- packagePath: `${poseNetPackageAssetDirectory}/model-stride16.json`,
23
- packageSpecifier: `${packageMetadata.name}/posenet-assets/model-stride16.json`,
24
- maxBytes: 64 * 1024
25
- }),
26
- Object.freeze({
27
- path: "group1-shard1of2.bin",
28
- url: `${poseNetBaseUrl}group1-shard1of2.bin`,
29
- mediaType: "application/octet-stream",
30
- sha256: "ce6afc62f89782d43139fab76c641b281a82dee2cd2759aa036c4b28aea16439",
31
- size: 4194304,
32
- packagePath: `${poseNetPackageAssetDirectory}/group1-shard1of2.bin`,
33
- packageSpecifier: `${packageMetadata.name}/posenet-assets/group1-shard1of2.bin`,
34
- maxBytes: 4 * 1024 * 1024
35
- }),
36
- Object.freeze({
37
- path: "group1-shard2of2.bin",
38
- url: `${poseNetBaseUrl}group1-shard2of2.bin`,
39
- mediaType: "application/octet-stream",
40
- sha256: "2a35b8cfb86eb50928931e03dc30c0972fdd375f148b177ee40676b81a17692d",
41
- size: 838476,
42
- packagePath: `${poseNetPackageAssetDirectory}/group1-shard2of2.bin`,
43
- packageSpecifier: `${packageMetadata.name}/posenet-assets/group1-shard2of2.bin`,
44
- maxBytes: 1024 * 1024
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
- const poseNetBundleManifest = Object.freeze({
48
- formatVersion: 1,
49
- distribution: Object.freeze({
50
- package: packageMetadata.name,
51
- version: packageMetadata.version,
52
- assetDirectory: poseNetPackageAssetDirectory
53
- }),
54
- model: poseNetModelDefaults,
55
- runtime: Object.freeze({
56
- package: "@tensorflow-models/posenet",
57
- version: "2.2.2"
58
- }),
59
- source: Object.freeze({
60
- provider: "tensorflow",
61
- repository: "https://github.com/tensorflow/tfjs-models/tree/v2.2.2/posenet",
62
- modelUrl: poseNetBaseUrl
63
- }),
64
- license: Object.freeze({
65
- spdx: "Apache-2.0",
66
- notice: "https://www.apache.org/licenses/LICENSE-2.0"
67
- }),
68
- limits: Object.freeze({
69
- maxFiles: expectedFiles.length,
70
- maxTotalBytes: 5 * 1024 * 1024
71
- }),
72
- files: expectedFiles
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
- class PoseNetBundleError extends Error {
75
- code;
76
- constructor(code, message) {
77
- super(message);
78
- this.name = "PoseNetBundleError";
79
- this.code = code;
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
- throw new PoseNetBundleError(code, message);
173
+ throw new PoseNetBundleError(code, message);
84
174
  }
85
175
  function abortError() {
86
- const error = new Error("TM PoseNet model loading was cancelled.");
87
- error.name = "AbortError";
88
- Object.defineProperty(error, "code", { value: "TM-POSENET-ABORTED" });
89
- return error;
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
- if (signal?.aborted) throw abortError();
182
+ if (signal?.aborted) throw abortError();
93
183
  }
94
184
  function isRecord(value) {
95
- return typeof value === "object" && value !== null && !Array.isArray(value);
185
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
186
  }
97
187
  function requireBytes(value, label) {
98
- if (value instanceof Uint8Array) return value;
99
- if (value instanceof ArrayBuffer) return new Uint8Array(value);
100
- return fail("TM-POSENET-ASSET-001", `${label} must be a Uint8Array or ArrayBuffer.`);
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
- if (!isRecord(value) || typeof value.digest !== "function") {
104
- return fail("TM-POSENET-ASSET-001", "Web Crypto subtle.digest is required.");
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
- const digest = await subtleCrypto.digest("SHA-256", bytes);
110
- return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
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
- if (typeof btoa !== "function") {
114
- return fail("TM-POSENET-ASSET-001", "Base64 encoding requires btoa.");
115
- }
116
- const chunks = [];
117
- const chunkSize = 32 * 1024;
118
- for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
119
- const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength));
120
- let binary = "";
121
- for (const value of chunk) binary += String.fromCharCode(value);
122
- chunks.push(binary);
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
- return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47;
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
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
131
- const dataLength = value.length - padding;
132
- for (let index = 0; index < dataLength; index += 1) {
133
- if (!isBase64Character(value.charCodeAt(index))) return false;
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
- const maximumLength = Math.ceil(maxBytes / 3) * 4;
139
- if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || value.length % 4 !== 0 || !hasValidBase64Alphabet(value)) {
140
- return fail("TM-POSENET-ASSET-001", `${label} must be bounded padded Base64.`);
141
- }
142
- if (typeof atob !== "function") {
143
- return fail("TM-POSENET-ASSET-001", "Base64 decoding requires atob.");
144
- }
145
- let decoded;
146
- try {
147
- decoded = atob(value);
148
- } catch (error) {
149
- return fail(
150
- "TM-POSENET-ASSET-001",
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
- if (!isRecord(descriptor) || descriptor.formatVersion !== 1 || descriptor.encoding !== "base64" || !Array.isArray(descriptor.files) || descriptor.files.length !== expectedFiles.length) {
165
- return fail("TM-POSENET-ASSET-001", "PoseNet project bundle format is invalid.");
166
- }
167
- const files = expectedFiles.map((expected) => {
168
- const candidate = descriptor.files.find(
169
- (file) => isRecord(file) && file.path === expected.path
170
- );
171
- if (!isRecord(candidate) || candidate.mediaType !== expected.mediaType || candidate.sha256 !== expected.sha256 || !Number.isSafeInteger(candidate.size) || Number(candidate.size) !== expected.size || typeof candidate.data !== "string") {
172
- return fail(
173
- "TM-POSENET-ASSET-001",
174
- `PoseNet project file metadata is invalid: ${expected.path}.`
175
- );
176
- }
177
- return Object.freeze({
178
- path: expected.path,
179
- mediaType: expected.mediaType,
180
- size: Number(candidate.size),
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
- throwIfAborted(signal);
189
- const canonical = canonicalProjectBundle(descriptor);
190
- return Object.freeze(
191
- canonical.files.map((candidate, index) => {
192
- throwIfAborted(signal);
193
- const expected = expectedFiles[index];
194
- const bytes = decodeBase64(
195
- candidate.data,
196
- `PoseNet project file ${expected.path}`,
197
- expected.size
198
- );
199
- if (bytes.byteLength !== candidate.size) {
200
- return fail(
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
- subtleCrypto = globalThis.crypto?.subtle,
211
- signal
212
- } = {}) {
213
- throwIfAborted(signal);
214
- const digestRuntime = requireSubtleCrypto(subtleCrypto);
215
- if (!Array.isArray(files) || files.length !== expectedFiles.length) {
216
- return fail("TM-POSENET-ASSET-004", "PoseNet bundle must contain exactly three files.");
217
- }
218
- let totalBytes = 0;
219
- const candidates = expectedFiles.map((expected) => {
220
- const candidate = files.find((file) => isRecord(file) && file.path === expected.path);
221
- if (!candidate) {
222
- return fail("TM-POSENET-ASSET-002", `PoseNet file is missing: ${expected.path}.`);
223
- }
224
- const bytes = requireBytes(candidate.bytes, `PoseNet file ${expected.path}`);
225
- if (bytes.byteLength !== expected.size) {
226
- return fail(
227
- "TM-POSENET-ASSET-004",
228
- `PoseNet file size is invalid: ${expected.path}.`
229
- );
230
- }
231
- totalBytes += bytes.byteLength;
232
- return { expected, bytes };
233
- });
234
- if (totalBytes > poseNetBundleManifest.limits.maxTotalBytes) {
235
- return fail("TM-POSENET-ASSET-004", "PoseNet bundle exceeds its total byte limit.");
236
- }
237
- const digests = await Promise.all(
238
- candidates.map(async ({ bytes }) => {
239
- throwIfAborted(signal);
240
- return sha256(bytes, digestRuntime);
241
- })
242
- );
243
- throwIfAborted(signal);
244
- const verifiedFiles = candidates.map(({ expected, bytes }, index) => {
245
- if (digests[index] !== expected.sha256) {
246
- return fail(
247
- "TM-POSENET-ASSET-003",
248
- `PoseNet file integrity mismatch: ${expected.path}.`
249
- );
250
- }
251
- return Object.freeze({
252
- path: expected.path,
253
- url: expected.url,
254
- mediaType: expected.mediaType,
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
- if (typeof loadFile !== "function") throw new TypeError("PoseNet file loader is required.");
289
- throwIfAborted(options.signal);
290
- const files = await Promise.all(
291
- expectedFiles.map(async (file) => {
292
- const bytes = requireBytes(await loadFile(file), `PoseNet file ${file.path}`);
293
- throwIfAborted(options.signal);
294
- return Object.freeze({
295
- path: file.path,
296
- mediaType: file.mediaType,
297
- bytes
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
- const verified = await verifyPoseNetBundle(files, options);
305
- return createProjectBundleFromVerified(verified);
331
+ return createProjectBundleFromVerified(await verifyPoseNetBundle(files, options));
306
332
  }
307
333
  function createProjectBundleFromVerified(verified) {
308
- return Object.freeze({
309
- formatVersion: 1,
310
- encoding: "base64",
311
- files: Object.freeze(
312
- verified.files.map(
313
- (file, index) => Object.freeze({
314
- path: file.path,
315
- mediaType: file.mediaType,
316
- size: file.bytes.byteLength,
317
- sha256: expectedFiles[index].sha256,
318
- data: encodeBase64(file.bytes)
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
- return createProjectBundleFromVerified(await loadPoseNetBundle(loadFile, options));
347
+ return createProjectBundleFromVerified(await loadPoseNetBundle(loadFile, options));
326
348
  }
327
349
  async function loadPoseNetProjectBundle(descriptor, options = {}) {
328
- return verifyPoseNetBundle(decodeProjectBundle(descriptor, options.signal), options);
350
+ return verifyPoseNetBundle(decodeProjectBundle(descriptor, options.signal), options);
329
351
  }
330
352
  async function validatePoseNetProjectBundle(descriptor, options = {}) {
331
- await loadPoseNetProjectBundle(descriptor, options);
332
- return canonicalProjectBundle(descriptor);
353
+ await loadPoseNetProjectBundle(descriptor, options);
354
+ return canonicalProjectBundle(descriptor);
333
355
  }
334
356
  function validateRuntime(value) {
335
- if (!isRecord(value) || typeof value.Webcam !== "function" || typeof value.loadFromFiles !== "function") {
336
- return fail(
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
- const value = typeof input === "string" ? input : isRecord(input) ? input.url : void 0;
345
- if (typeof value !== "string") return null;
346
- try {
347
- return new URL(value, baseUrl).href;
348
- } catch {
349
- return null;
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
- if (!isRecord(value)) return;
354
- const classifier = isRecord(value.model) && typeof value.model.dispose === "function" ? value.model : null;
355
- const poseNet = isRecord(value.posenetModel) && typeof value.posenetModel.dispose === "function" ? value.posenetModel : null;
356
- const resources = classifier && poseNet && classifier !== poseNet ? [classifier, poseNet] : typeof value.dispose === "function" ? [value] : [...new Set([classifier, poseNet].filter((resource) => resource !== null))];
357
- const results = await Promise.allSettled(
358
- resources.map((resource) => Promise.resolve().then(() => resource.dispose()))
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
- if (!isRecord(options)) throw new TypeError("PoseNet runtime options are required.");
367
- if (options.parallelModelInitialization !== void 0 && typeof options.parallelModelInitialization !== "boolean") {
368
- throw new TypeError("parallelModelInitialization must be a boolean.");
369
- }
370
- const runtime = validateRuntime(options.runtime);
371
- const globalObject = options.globalObject ?? globalThis;
372
- const ResponseConstructor = globalObject.Response ?? globalThis.Response;
373
- if (typeof ResponseConstructor !== "function") {
374
- return fail("TM-POSENET-RUNTIME-001", "Response constructor is required.");
375
- }
376
- const supplyCount = [options.files, options.loadFiles, options.projectBundle].filter(
377
- (value) => value !== void 0
378
- ).length;
379
- if (supplyCount !== 1) {
380
- throw new TypeError("Provide exactly one of files, loadFiles, or projectBundle.");
381
- }
382
- let verification;
383
- const verifySupply = () => {
384
- verification ??= options.projectBundle ? loadPoseNetProjectBundle(options.projectBundle, {
385
- subtleCrypto: options.subtleCrypto ?? globalObject.crypto?.subtle
386
- }) : Promise.resolve(
387
- options.loadFiles ? options.loadFiles() : options.files
388
- ).then(
389
- (files) => verifyPoseNetBundle(files, {
390
- subtleCrypto: options.subtleCrypto ?? globalObject.crypto?.subtle
391
- })
392
- );
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") {
401
- return fail("TM-POSENET-RUNTIME-001", "Browser fetch is required.");
402
- }
403
- const baseUrl = globalObject.location?.href ?? "http://localhost/";
404
- const localFetch = async (input) => {
405
- const bundle = await verifySupply();
406
- const byUrl = new Map(bundle.files.map((file2) => [file2.url, file2]));
407
- const url = requestUrl(input, baseUrl);
408
- const file = url ? byUrl.get(url) : void 0;
409
- if (!file) {
410
- return fail(
411
- "TM-POSENET-FETCH-001",
412
- `Unexpected PoseNet request: ${url ?? "(invalid)"}.`
413
- );
414
- }
415
- return new ResponseConstructor(file.bytes.slice().buffer, {
416
- status: 200,
417
- headers: { "content-type": file.mediaType }
418
- });
419
- };
420
- try {
421
- globalObject.fetch = localFetch;
422
- } catch (error) {
423
- return fail(
424
- "TM-POSENET-FETCH-001",
425
- `PoseNet fetch interception is unavailable: ${error instanceof Error ? error.message : String(error)}`
426
- );
427
- }
428
- try {
429
- let runtimeLoad;
430
- try {
431
- runtimeLoad = Promise.resolve(
432
- runtime.loadFromFiles(model, weights, metadata, {
433
- ...loadOptions,
434
- ...options.parallelModelInitialization ? { parallelModelInitialization: true } : {}
435
- })
436
- );
437
- } catch (error) {
438
- runtimeLoad = Promise.reject(error);
439
- }
440
- const supply = verifySupply();
441
- const [runtimeResult, supplyResult] = await Promise.allSettled([runtimeLoad, supply]);
442
- let disposalError;
443
- if (runtimeResult.status === "fulfilled" && (loadOptions.signal?.aborted || supplyResult.status === "rejected")) {
444
- try {
445
- await disposeRuntimeModel(runtimeResult.value);
446
- } catch (error) {
447
- disposalError = error;
448
- }
449
- }
450
- if (loadOptions.signal?.aborted) {
451
- const cancellation = abortError();
452
- const runtimeError = runtimeResult.status === "rejected" && runtimeResult.reason?.name !== "AbortError" ? runtimeResult.reason : void 0;
453
- if (runtimeError || disposalError) {
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
- export {
492
- PoseNetBundleError,
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 };