@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/dist/posenet.js CHANGED
@@ -1,502 +1,469 @@
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.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
- const poseNetModelDefaults = Object.freeze({
8
- architecture: "MobileNetV1",
9
- multiplier: 0.75,
10
- outputStride: 16,
11
- inputResolution: 257
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
- 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
- })
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
- 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
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
- class PoseNetBundleError extends Error {
75
- code;
76
- constructor(code, message) {
77
- super(message);
78
- this.name = "PoseNetBundleError";
79
- this.code = code;
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
- throw new PoseNetBundleError(code, message);
174
+ throw new PoseNetBundleError(code, message);
84
175
  }
85
176
  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;
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
- if (signal?.aborted) throw abortError();
183
+ if (signal?.aborted) throw abortError();
93
184
  }
94
185
  function isRecord(value) {
95
- return typeof value === "object" && value !== null && !Array.isArray(value);
186
+ return typeof value === "object" && value !== null && !Array.isArray(value);
96
187
  }
97
188
  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.`);
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
- if (!isRecord(value) || typeof value.digest !== "function") {
104
- return fail("TM-POSENET-ASSET-001", "Web Crypto subtle.digest is required.");
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
- const digest = await subtleCrypto.digest("SHA-256", bytes);
110
- return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
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
- 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(""));
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
- return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47;
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
- 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;
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
- 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;
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
- 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) });
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
- 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
- );
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
- 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
- });
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
- 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);
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
- const verified = await verifyPoseNetBundle(files, options);
305
- return createProjectBundleFromVerified(verified);
332
+ return createProjectBundleFromVerified(await verifyPoseNetBundle(files, options));
306
333
  }
307
334
  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
- });
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
- return createProjectBundleFromVerified(await loadPoseNetBundle(loadFile, options));
348
+ return createProjectBundleFromVerified(await loadPoseNetBundle(loadFile, options));
326
349
  }
327
350
  async function loadPoseNetProjectBundle(descriptor, options = {}) {
328
- return verifyPoseNetBundle(decodeProjectBundle(descriptor, options.signal), options);
351
+ return verifyPoseNetBundle(decodeProjectBundle(descriptor, options.signal), options);
329
352
  }
330
353
  async function validatePoseNetProjectBundle(descriptor, options = {}) {
331
- await loadPoseNetProjectBundle(descriptor, options);
332
- return canonicalProjectBundle(descriptor);
354
+ await loadPoseNetProjectBundle(descriptor, options);
355
+ return canonicalProjectBundle(descriptor);
333
356
  }
334
357
  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;
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
- 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
- }
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
- 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
- }
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
- 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
- });
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
- export {
492
- PoseNetBundleError,
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 };