@camstack/addon-pipeline 1.0.6 → 1.0.7

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.
Files changed (34) hide show
  1. package/dist/audio-analyzer/index.js +6 -7
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-nodeav/index.js +1 -1
  4. package/dist/audio-codec-nodeav/index.mjs +1 -1
  5. package/dist/decoder-nodeav/index.js +1 -1
  6. package/dist/decoder-nodeav/index.mjs +1 -1
  7. package/dist/detection-pipeline/index.js +14 -15
  8. package/dist/detection-pipeline/index.mjs +2 -2
  9. package/dist/{dist-DsDFrG0I.mjs → dist-CjrjeaDd.mjs} +1681 -1644
  10. package/dist/{dist-BiUtYscO.js → dist-G45MVm6i.js} +1680 -1643
  11. package/dist/model-download-service-C7AjBsX9-B0ekM6dF.mjs +301 -0
  12. package/dist/model-download-service-C7AjBsX9-rXY-VFDk.js +358 -0
  13. package/dist/motion-wasm/index.js +1 -1
  14. package/dist/motion-wasm/index.mjs +1 -1
  15. package/dist/pipeline-runner/index.js +1 -1
  16. package/dist/pipeline-runner/index.mjs +1 -1
  17. package/dist/recorder/index.js +4 -5
  18. package/dist/recorder/index.mjs +2 -2
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbTGCEnd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Tbqpu0v3.mjs} +3 -3
  21. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-3STWM0yI.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DCsgcqTa.mjs} +1 -1
  22. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dsz9DmNr.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CHcXI1Wf.mjs} +1 -1
  23. package/dist/stream-broker/{hostInit-BJ3QDdFs.mjs → hostInit-tIev5Gd9.mjs} +3 -3
  24. package/dist/stream-broker/index.js +9 -10
  25. package/dist/stream-broker/index.mjs +3 -3
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B4oJIlgF.js → MaskShapeCanvas-DI4BY7W2-C0kKwNX_.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-C1EEbk2V-CUopGB1R.js → MotionZonesSettings-C1EEbk2V-CYtJc892.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CyTsHaor.js → PrivacyMaskSettings-APgPLF7p-C2SRtNe6.js} +1 -1
  30. package/embed-dist/assets/index-B2LRyXWh.js +80 -0
  31. package/embed-dist/index.html +1 -1
  32. package/package.json +1 -1
  33. package/dist/chunk-D6vf50IK.js +0 -28
  34. package/embed-dist/assets/index-hwJEVIPM.js +0 -80
@@ -0,0 +1,301 @@
1
+ import * as fs from "node:fs";
2
+ import { createReadStream, promises } from "node:fs";
3
+ import * as path$1 from "node:path";
4
+ import path from "node:path";
5
+ import "node:crypto";
6
+ //#region ../system/dist/model-download-service-C7AjBsX9.mjs
7
+ /**
8
+ * Map a rel path to one candidate absolute path PER root, keeping only roots the
9
+ * path stays within (traversal guard). The handler serves the first candidate
10
+ * that exists. Rejects a path that escapes every root.
11
+ */
12
+ function resolveFilePath(roots, rel) {
13
+ if (rel.length === 0) return { error: "forbidden" };
14
+ const candidates = [];
15
+ for (const rootDir of roots) {
16
+ const root = path.resolve(rootDir);
17
+ const abs = path.resolve(root, rel);
18
+ if (abs === root || abs.startsWith(root + path.sep)) candidates.push(abs);
19
+ }
20
+ if (candidates.length === 0) return { error: "forbidden" };
21
+ return { candidates };
22
+ }
23
+ var DEFAULT_CONTENT_TYPES = {
24
+ ".m3u8": "application/vnd.apple.mpegurl",
25
+ ".m4s": "video/mp4",
26
+ ".mp4": "video/mp4",
27
+ ".idx": "application/octet-stream",
28
+ ".html": "text/html; charset=utf-8",
29
+ ".js": "application/javascript; charset=utf-8",
30
+ ".mjs": "application/javascript; charset=utf-8",
31
+ ".css": "text/css; charset=utf-8",
32
+ ".json": "application/json; charset=utf-8",
33
+ ".map": "application/json; charset=utf-8",
34
+ ".svg": "image/svg+xml",
35
+ ".png": "image/png",
36
+ ".jpg": "image/jpeg",
37
+ ".jpeg": "image/jpeg",
38
+ ".ico": "image/x-icon",
39
+ ".woff2": "font/woff2",
40
+ ".woff": "font/woff",
41
+ ".ttf": "font/ttf"
42
+ };
43
+ function contentTypeFor(filePath, overrides) {
44
+ const ext = path.extname(filePath).toLowerCase();
45
+ return overrides?.[ext] ?? DEFAULT_CONTENT_TYPES[ext] ?? "application/octet-stream";
46
+ }
47
+ /** Parse an HTTP `Range` header against a known size. Null = serve whole file. */
48
+ function parseRangeHeader(header, size) {
49
+ if (!header) return null;
50
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
51
+ if (!m) return null;
52
+ const [, rawStart, rawEnd] = m;
53
+ if (rawStart === "" && rawEnd === "") return null;
54
+ let start;
55
+ let end;
56
+ if (rawStart === "") {
57
+ const n = Number(rawEnd);
58
+ if (n <= 0) return null;
59
+ start = Math.max(0, size - n);
60
+ end = size - 1;
61
+ } else {
62
+ start = Number(rawStart);
63
+ end = rawEnd === "" ? size - 1 : Number(rawEnd);
64
+ }
65
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= size) return null;
66
+ return {
67
+ start,
68
+ end: Math.min(end, size - 1)
69
+ };
70
+ }
71
+ /**
72
+ * A ready-made data-plane request handler that serves files from a set of roots
73
+ * with HTTP `Range`, for addons whose data-plane is "stream files off disk"
74
+ * (recording playback, scrub-frame stores, exported clips).
75
+ *
76
+ * This is the addon-side handler the addon hands to `ctx.dataPlane.serve({ handler })`.
77
+ * It receives the REAL Node `req`/`res`, resolves the request path against the
78
+ * addon's roots (traversal-guarded), and streams the file. NO token and NO CORS:
79
+ * the hub already authenticated the caller and the data plane is same-origin
80
+ * through the hub's port. Reuses the shared Range/content-type/traversal helpers
81
+ * so there is one implementation across the standalone file-server and this.
82
+ */
83
+ /** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
84
+ function createFileDataPlaneHandler(opts) {
85
+ return async (req, res) => {
86
+ if (req.method !== "GET" && req.method !== "HEAD") {
87
+ res.writeHead(405).end();
88
+ return;
89
+ }
90
+ const urlPath = (req.url ?? "/").split("?")[0] ?? "/";
91
+ let rel;
92
+ try {
93
+ rel = decodeURIComponent(urlPath.replace(/^\/+/, ""));
94
+ } catch {
95
+ res.writeHead(400).end();
96
+ return;
97
+ }
98
+ if (rel.length === 0) {
99
+ res.writeHead(404).end();
100
+ return;
101
+ }
102
+ const resolved = resolveFilePath(opts.getRoots(), rel);
103
+ if ("error" in resolved) {
104
+ res.writeHead(403).end();
105
+ return;
106
+ }
107
+ let absPath = null;
108
+ let size = 0;
109
+ for (const candidate of resolved.candidates) try {
110
+ const st = await promises.stat(candidate);
111
+ if (st.isFile()) {
112
+ absPath = candidate;
113
+ size = st.size;
114
+ break;
115
+ }
116
+ } catch {}
117
+ if (absPath === null) {
118
+ res.writeHead(404).end();
119
+ return;
120
+ }
121
+ const baseHeaders = {
122
+ "content-type": contentTypeFor(absPath, opts.contentTypes),
123
+ "accept-ranges": "bytes",
124
+ "cache-control": "no-cache"
125
+ };
126
+ const range = parseRangeHeader(req.headers.range, size);
127
+ if (range) {
128
+ res.writeHead(206, {
129
+ ...baseHeaders,
130
+ "content-range": `bytes ${range.start}-${range.end}/${size}`,
131
+ "content-length": String(range.end - range.start + 1)
132
+ });
133
+ if (req.method === "HEAD") {
134
+ res.end();
135
+ return;
136
+ }
137
+ createReadStream(absPath, {
138
+ start: range.start,
139
+ end: range.end
140
+ }).pipe(res);
141
+ return;
142
+ }
143
+ res.writeHead(200, {
144
+ ...baseHeaders,
145
+ "content-length": String(size)
146
+ });
147
+ if (req.method === "HEAD") {
148
+ res.end();
149
+ return;
150
+ }
151
+ createReadStream(absPath).pipe(res);
152
+ };
153
+ }
154
+ /** Build fetch headers, including HF auth token for huggingface.co URLs */
155
+ function buildHeaders(url) {
156
+ const headers = { "User-Agent": "CamStack/1.0" };
157
+ const hfToken = process.env["HF_TOKEN"] ?? process.env["HUGGING_FACE_HUB_TOKEN"];
158
+ if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
159
+ return headers;
160
+ }
161
+ /**
162
+ * Download a single file from a URL to a destination path.
163
+ * Uses native fetch() (Node 22+) which handles redirects natively.
164
+ * Streams to disk with optional progress callback.
165
+ * Returns the destination path. Skips download if file already exists.
166
+ */
167
+ async function downloadFile(url, destPath, onProgress) {
168
+ if (fs.existsSync(destPath)) return destPath;
169
+ fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
170
+ const tmpPath = destPath + ".downloading";
171
+ try {
172
+ const response = await fetch(url, {
173
+ redirect: "follow",
174
+ headers: buildHeaders(url)
175
+ });
176
+ if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
177
+ if (!response.body) throw new Error(`No response body from ${url}`);
178
+ const total = parseInt(response.headers.get("content-length") ?? "0", 10);
179
+ let downloaded = 0;
180
+ const fileStream = fs.createWriteStream(tmpPath);
181
+ const reader = response.body.getReader();
182
+ try {
183
+ for (;;) {
184
+ const { done, value } = await reader.read();
185
+ if (done || !value) break;
186
+ fileStream.write(value);
187
+ downloaded += value.length;
188
+ onProgress?.(downloaded, total);
189
+ }
190
+ } finally {
191
+ fileStream.end();
192
+ await new Promise((resolve, reject) => {
193
+ fileStream.on("finish", resolve);
194
+ fileStream.on("error", reject);
195
+ });
196
+ }
197
+ fs.renameSync(tmpPath, destPath);
198
+ return destPath;
199
+ } catch (err) {
200
+ try {
201
+ fs.unlinkSync(tmpPath);
202
+ } catch {}
203
+ throw err;
204
+ }
205
+ }
206
+ /**
207
+ * Download every file in a HuggingFace directory bundle (e.g.,
208
+ * `.mlpackage` / OpenVINO IR pair) atomically. `knownFiles` lists the
209
+ * relative paths inside the directory; the function fetches each from
210
+ * `${url}/${file}` and renames the staging directory only on full
211
+ * success. Mirrors `ModelDownloadService.downloadDirectory` but
212
+ * exposed as a standalone for catalog-less callers.
213
+ */
214
+ async function downloadDirectory(url, destDir, knownFiles, onProgress) {
215
+ const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
216
+ if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
217
+ const [, repo, dirPath] = match;
218
+ const files = (knownFiles ?? []).map((f) => ({
219
+ relativePath: f,
220
+ fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
221
+ }));
222
+ if (files.length === 0) throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
223
+ const tmpDir = destDir + ".downloading";
224
+ fs.rmSync(tmpDir, {
225
+ recursive: true,
226
+ force: true
227
+ });
228
+ fs.mkdirSync(tmpDir, { recursive: true });
229
+ let totalDownloaded = 0;
230
+ try {
231
+ for (const file of files) {
232
+ const destPath = path$1.join(tmpDir, file.relativePath);
233
+ fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
234
+ await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
235
+ onProgress?.(totalDownloaded + downloaded, void 0);
236
+ });
237
+ totalDownloaded += fs.statSync(destPath).size;
238
+ }
239
+ fs.rmSync(destDir, {
240
+ recursive: true,
241
+ force: true
242
+ });
243
+ fs.renameSync(tmpDir, destDir);
244
+ } catch (err) {
245
+ fs.rmSync(tmpDir, {
246
+ recursive: true,
247
+ force: true
248
+ });
249
+ throw err;
250
+ }
251
+ }
252
+ /**
253
+ * Resolve a `ModelCatalogEntry` against `modelsDir`: download model file
254
+ * (or directory bundle) + extra files (labels JSON, charset dict, …),
255
+ * skip if already on disk. Returns the local model path.
256
+ */
257
+ async function ensureModel(modelsDir, entry, format, onProgress) {
258
+ const formatEntry = entry.formats[format];
259
+ if (!formatEntry) throw new Error(`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`);
260
+ if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$1.join(modelsDir, extra.filename));
261
+ const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
262
+ const modelPath = path$1.join(modelsDir, filename);
263
+ if (fs.existsSync(modelPath)) if (formatEntry.isDirectory && !fs.existsSync(path$1.join(modelPath, "Manifest.json"))) fs.rmSync(modelPath, {
264
+ recursive: true,
265
+ force: true
266
+ });
267
+ else return modelPath;
268
+ fs.mkdirSync(modelsDir, { recursive: true });
269
+ if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
270
+ else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
271
+ return modelPath;
272
+ }
273
+ /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
274
+ function getModelFilePath(modelsDir, entry, format) {
275
+ const formatEntry = entry.formats[format];
276
+ if (!formatEntry) return null;
277
+ const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
278
+ return path$1.join(modelsDir, filename);
279
+ }
280
+ /** True iff the model file (or `Manifest.json` for directory bundles) exists and is non-empty. */
281
+ function isModelDownloaded(modelsDir, entry, format) {
282
+ const formatEntry = entry.formats[format];
283
+ if (!formatEntry) return false;
284
+ const modelPath = getModelFilePath(modelsDir, entry, format);
285
+ if (!modelPath || !fs.existsSync(modelPath)) return false;
286
+ if (formatEntry.isDirectory) return fs.existsSync(path$1.join(modelPath, "Manifest.json"));
287
+ return fs.statSync(modelPath).size > 0;
288
+ }
289
+ /** Remove the on-disk model file/directory. Returns true if something was deleted. */
290
+ function deleteModelFromDisk(modelsDir, entry, format) {
291
+ const modelPath = getModelFilePath(modelsDir, entry, format);
292
+ if (!modelPath || !fs.existsSync(modelPath)) return false;
293
+ if (entry.formats[format]?.isDirectory) fs.rmSync(modelPath, {
294
+ recursive: true,
295
+ force: true
296
+ });
297
+ else fs.unlinkSync(modelPath);
298
+ return true;
299
+ }
300
+ //#endregion
301
+ export { isModelDownloaded as a, ensureModel as i, deleteModelFromDisk as n, downloadFile as r, createFileDataPlaneHandler as t };
@@ -0,0 +1,358 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_fs = require("node:fs");
24
+ node_fs = __toESM(node_fs, 1);
25
+ let node_path = require("node:path");
26
+ node_path = __toESM(node_path, 1);
27
+ require("node:crypto");
28
+ //#region ../system/dist/model-download-service-C7AjBsX9.mjs
29
+ /**
30
+ * Map a rel path to one candidate absolute path PER root, keeping only roots the
31
+ * path stays within (traversal guard). The handler serves the first candidate
32
+ * that exists. Rejects a path that escapes every root.
33
+ */
34
+ function resolveFilePath(roots, rel) {
35
+ if (rel.length === 0) return { error: "forbidden" };
36
+ const candidates = [];
37
+ for (const rootDir of roots) {
38
+ const root = node_path.default.resolve(rootDir);
39
+ const abs = node_path.default.resolve(root, rel);
40
+ if (abs === root || abs.startsWith(root + node_path.default.sep)) candidates.push(abs);
41
+ }
42
+ if (candidates.length === 0) return { error: "forbidden" };
43
+ return { candidates };
44
+ }
45
+ var DEFAULT_CONTENT_TYPES = {
46
+ ".m3u8": "application/vnd.apple.mpegurl",
47
+ ".m4s": "video/mp4",
48
+ ".mp4": "video/mp4",
49
+ ".idx": "application/octet-stream",
50
+ ".html": "text/html; charset=utf-8",
51
+ ".js": "application/javascript; charset=utf-8",
52
+ ".mjs": "application/javascript; charset=utf-8",
53
+ ".css": "text/css; charset=utf-8",
54
+ ".json": "application/json; charset=utf-8",
55
+ ".map": "application/json; charset=utf-8",
56
+ ".svg": "image/svg+xml",
57
+ ".png": "image/png",
58
+ ".jpg": "image/jpeg",
59
+ ".jpeg": "image/jpeg",
60
+ ".ico": "image/x-icon",
61
+ ".woff2": "font/woff2",
62
+ ".woff": "font/woff",
63
+ ".ttf": "font/ttf"
64
+ };
65
+ function contentTypeFor(filePath, overrides) {
66
+ const ext = node_path.default.extname(filePath).toLowerCase();
67
+ return overrides?.[ext] ?? DEFAULT_CONTENT_TYPES[ext] ?? "application/octet-stream";
68
+ }
69
+ /** Parse an HTTP `Range` header against a known size. Null = serve whole file. */
70
+ function parseRangeHeader(header, size) {
71
+ if (!header) return null;
72
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
73
+ if (!m) return null;
74
+ const [, rawStart, rawEnd] = m;
75
+ if (rawStart === "" && rawEnd === "") return null;
76
+ let start;
77
+ let end;
78
+ if (rawStart === "") {
79
+ const n = Number(rawEnd);
80
+ if (n <= 0) return null;
81
+ start = Math.max(0, size - n);
82
+ end = size - 1;
83
+ } else {
84
+ start = Number(rawStart);
85
+ end = rawEnd === "" ? size - 1 : Number(rawEnd);
86
+ }
87
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= size) return null;
88
+ return {
89
+ start,
90
+ end: Math.min(end, size - 1)
91
+ };
92
+ }
93
+ /**
94
+ * A ready-made data-plane request handler that serves files from a set of roots
95
+ * with HTTP `Range`, for addons whose data-plane is "stream files off disk"
96
+ * (recording playback, scrub-frame stores, exported clips).
97
+ *
98
+ * This is the addon-side handler the addon hands to `ctx.dataPlane.serve({ handler })`.
99
+ * It receives the REAL Node `req`/`res`, resolves the request path against the
100
+ * addon's roots (traversal-guarded), and streams the file. NO token and NO CORS:
101
+ * the hub already authenticated the caller and the data plane is same-origin
102
+ * through the hub's port. Reuses the shared Range/content-type/traversal helpers
103
+ * so there is one implementation across the standalone file-server and this.
104
+ */
105
+ /** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
106
+ function createFileDataPlaneHandler(opts) {
107
+ return async (req, res) => {
108
+ if (req.method !== "GET" && req.method !== "HEAD") {
109
+ res.writeHead(405).end();
110
+ return;
111
+ }
112
+ const urlPath = (req.url ?? "/").split("?")[0] ?? "/";
113
+ let rel;
114
+ try {
115
+ rel = decodeURIComponent(urlPath.replace(/^\/+/, ""));
116
+ } catch {
117
+ res.writeHead(400).end();
118
+ return;
119
+ }
120
+ if (rel.length === 0) {
121
+ res.writeHead(404).end();
122
+ return;
123
+ }
124
+ const resolved = resolveFilePath(opts.getRoots(), rel);
125
+ if ("error" in resolved) {
126
+ res.writeHead(403).end();
127
+ return;
128
+ }
129
+ let absPath = null;
130
+ let size = 0;
131
+ for (const candidate of resolved.candidates) try {
132
+ const st = await node_fs.promises.stat(candidate);
133
+ if (st.isFile()) {
134
+ absPath = candidate;
135
+ size = st.size;
136
+ break;
137
+ }
138
+ } catch {}
139
+ if (absPath === null) {
140
+ res.writeHead(404).end();
141
+ return;
142
+ }
143
+ const baseHeaders = {
144
+ "content-type": contentTypeFor(absPath, opts.contentTypes),
145
+ "accept-ranges": "bytes",
146
+ "cache-control": "no-cache"
147
+ };
148
+ const range = parseRangeHeader(req.headers.range, size);
149
+ if (range) {
150
+ res.writeHead(206, {
151
+ ...baseHeaders,
152
+ "content-range": `bytes ${range.start}-${range.end}/${size}`,
153
+ "content-length": String(range.end - range.start + 1)
154
+ });
155
+ if (req.method === "HEAD") {
156
+ res.end();
157
+ return;
158
+ }
159
+ (0, node_fs.createReadStream)(absPath, {
160
+ start: range.start,
161
+ end: range.end
162
+ }).pipe(res);
163
+ return;
164
+ }
165
+ res.writeHead(200, {
166
+ ...baseHeaders,
167
+ "content-length": String(size)
168
+ });
169
+ if (req.method === "HEAD") {
170
+ res.end();
171
+ return;
172
+ }
173
+ (0, node_fs.createReadStream)(absPath).pipe(res);
174
+ };
175
+ }
176
+ /** Build fetch headers, including HF auth token for huggingface.co URLs */
177
+ function buildHeaders(url) {
178
+ const headers = { "User-Agent": "CamStack/1.0" };
179
+ const hfToken = process.env["HF_TOKEN"] ?? process.env["HUGGING_FACE_HUB_TOKEN"];
180
+ if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
181
+ return headers;
182
+ }
183
+ /**
184
+ * Download a single file from a URL to a destination path.
185
+ * Uses native fetch() (Node 22+) which handles redirects natively.
186
+ * Streams to disk with optional progress callback.
187
+ * Returns the destination path. Skips download if file already exists.
188
+ */
189
+ async function downloadFile(url, destPath, onProgress) {
190
+ if (node_fs.existsSync(destPath)) return destPath;
191
+ node_fs.mkdirSync(node_path.dirname(destPath), { recursive: true });
192
+ const tmpPath = destPath + ".downloading";
193
+ try {
194
+ const response = await fetch(url, {
195
+ redirect: "follow",
196
+ headers: buildHeaders(url)
197
+ });
198
+ if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
199
+ if (!response.body) throw new Error(`No response body from ${url}`);
200
+ const total = parseInt(response.headers.get("content-length") ?? "0", 10);
201
+ let downloaded = 0;
202
+ const fileStream = node_fs.createWriteStream(tmpPath);
203
+ const reader = response.body.getReader();
204
+ try {
205
+ for (;;) {
206
+ const { done, value } = await reader.read();
207
+ if (done || !value) break;
208
+ fileStream.write(value);
209
+ downloaded += value.length;
210
+ onProgress?.(downloaded, total);
211
+ }
212
+ } finally {
213
+ fileStream.end();
214
+ await new Promise((resolve, reject) => {
215
+ fileStream.on("finish", resolve);
216
+ fileStream.on("error", reject);
217
+ });
218
+ }
219
+ node_fs.renameSync(tmpPath, destPath);
220
+ return destPath;
221
+ } catch (err) {
222
+ try {
223
+ node_fs.unlinkSync(tmpPath);
224
+ } catch {}
225
+ throw err;
226
+ }
227
+ }
228
+ /**
229
+ * Download every file in a HuggingFace directory bundle (e.g.,
230
+ * `.mlpackage` / OpenVINO IR pair) atomically. `knownFiles` lists the
231
+ * relative paths inside the directory; the function fetches each from
232
+ * `${url}/${file}` and renames the staging directory only on full
233
+ * success. Mirrors `ModelDownloadService.downloadDirectory` but
234
+ * exposed as a standalone for catalog-less callers.
235
+ */
236
+ async function downloadDirectory(url, destDir, knownFiles, onProgress) {
237
+ const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
238
+ if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
239
+ const [, repo, dirPath] = match;
240
+ const files = (knownFiles ?? []).map((f) => ({
241
+ relativePath: f,
242
+ fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
243
+ }));
244
+ if (files.length === 0) throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
245
+ const tmpDir = destDir + ".downloading";
246
+ node_fs.rmSync(tmpDir, {
247
+ recursive: true,
248
+ force: true
249
+ });
250
+ node_fs.mkdirSync(tmpDir, { recursive: true });
251
+ let totalDownloaded = 0;
252
+ try {
253
+ for (const file of files) {
254
+ const destPath = node_path.join(tmpDir, file.relativePath);
255
+ node_fs.mkdirSync(node_path.dirname(destPath), { recursive: true });
256
+ await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
257
+ onProgress?.(totalDownloaded + downloaded, void 0);
258
+ });
259
+ totalDownloaded += node_fs.statSync(destPath).size;
260
+ }
261
+ node_fs.rmSync(destDir, {
262
+ recursive: true,
263
+ force: true
264
+ });
265
+ node_fs.renameSync(tmpDir, destDir);
266
+ } catch (err) {
267
+ node_fs.rmSync(tmpDir, {
268
+ recursive: true,
269
+ force: true
270
+ });
271
+ throw err;
272
+ }
273
+ }
274
+ /**
275
+ * Resolve a `ModelCatalogEntry` against `modelsDir`: download model file
276
+ * (or directory bundle) + extra files (labels JSON, charset dict, …),
277
+ * skip if already on disk. Returns the local model path.
278
+ */
279
+ async function ensureModel(modelsDir, entry, format, onProgress) {
280
+ const formatEntry = entry.formats[format];
281
+ if (!formatEntry) throw new Error(`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`);
282
+ if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, node_path.join(modelsDir, extra.filename));
283
+ const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
284
+ const modelPath = node_path.join(modelsDir, filename);
285
+ if (node_fs.existsSync(modelPath)) if (formatEntry.isDirectory && !node_fs.existsSync(node_path.join(modelPath, "Manifest.json"))) node_fs.rmSync(modelPath, {
286
+ recursive: true,
287
+ force: true
288
+ });
289
+ else return modelPath;
290
+ node_fs.mkdirSync(modelsDir, { recursive: true });
291
+ if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
292
+ else await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
293
+ return modelPath;
294
+ }
295
+ /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
296
+ function getModelFilePath(modelsDir, entry, format) {
297
+ const formatEntry = entry.formats[format];
298
+ if (!formatEntry) return null;
299
+ const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
300
+ return node_path.join(modelsDir, filename);
301
+ }
302
+ /** True iff the model file (or `Manifest.json` for directory bundles) exists and is non-empty. */
303
+ function isModelDownloaded(modelsDir, entry, format) {
304
+ const formatEntry = entry.formats[format];
305
+ if (!formatEntry) return false;
306
+ const modelPath = getModelFilePath(modelsDir, entry, format);
307
+ if (!modelPath || !node_fs.existsSync(modelPath)) return false;
308
+ if (formatEntry.isDirectory) return node_fs.existsSync(node_path.join(modelPath, "Manifest.json"));
309
+ return node_fs.statSync(modelPath).size > 0;
310
+ }
311
+ /** Remove the on-disk model file/directory. Returns true if something was deleted. */
312
+ function deleteModelFromDisk(modelsDir, entry, format) {
313
+ const modelPath = getModelFilePath(modelsDir, entry, format);
314
+ if (!modelPath || !node_fs.existsSync(modelPath)) return false;
315
+ if (entry.formats[format]?.isDirectory) node_fs.rmSync(modelPath, {
316
+ recursive: true,
317
+ force: true
318
+ });
319
+ else node_fs.unlinkSync(modelPath);
320
+ return true;
321
+ }
322
+ //#endregion
323
+ Object.defineProperty(exports, "__toESM", {
324
+ enumerable: true,
325
+ get: function() {
326
+ return __toESM;
327
+ }
328
+ });
329
+ Object.defineProperty(exports, "createFileDataPlaneHandler", {
330
+ enumerable: true,
331
+ get: function() {
332
+ return createFileDataPlaneHandler;
333
+ }
334
+ });
335
+ Object.defineProperty(exports, "deleteModelFromDisk", {
336
+ enumerable: true,
337
+ get: function() {
338
+ return deleteModelFromDisk;
339
+ }
340
+ });
341
+ Object.defineProperty(exports, "downloadFile", {
342
+ enumerable: true,
343
+ get: function() {
344
+ return downloadFile;
345
+ }
346
+ });
347
+ Object.defineProperty(exports, "ensureModel", {
348
+ enumerable: true,
349
+ get: function() {
350
+ return ensureModel;
351
+ }
352
+ });
353
+ Object.defineProperty(exports, "isModelDownloaded", {
354
+ enumerable: true,
355
+ get: function() {
356
+ return isModelDownloaded;
357
+ }
358
+ });
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BiUtYscO.js");
5
+ const require_dist = require("../dist-G45MVm6i.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { B as motionDetectionCapability, M as evaluateZoneRules, P as hydrateSchema, d as DeviceType, i as BaseAddon } from "../dist-DsDFrG0I.mjs";
1
+ import { C as evaluateZoneRules, I as BaseAddon, O as motionDetectionCapability, W as hydrateSchema, z as DeviceType } from "../dist-CjrjeaDd.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BiUtYscO.js");
5
+ const require_dist = require("../dist-G45MVm6i.js");
6
6
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
7
  //#region src/pipeline-runner/frame-queue.ts
8
8
  /**
@@ -1,4 +1,4 @@
1
- import { $ as boolean, D as customAction, E as createEvent, I as makeSourceBrokerId, Q as array, W as pipelineRunnerCapability, Z as _enum, i as BaseAddon, it as object, j as errMsg, k as defineCustomActions, ot as string, p as EventCategory, rt as number, tt as lazy } from "../dist-DsDFrG0I.mjs";
1
+ import { $ as boolean, A as pipelineRunnerCapability, B as EventCategory, F as errMsg, I as BaseAddon, K as makeSourceBrokerId, Q as array, U as createEvent, Z as _enum, it as object, ot as string, rt as number, tt as lazy, x as defineCustomActions, y as customAction } from "../dist-CjrjeaDd.mjs";
2
2
  import { FrameRingReaderCache } from "@camstack/shm-ring";
3
3
  //#region src/pipeline-runner/frame-queue.ts
4
4
  /**