@camstack/addon-pipeline 1.2.47 → 1.2.49
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/{model-download-service-D8B-4ktF-7-YxE9Wx.js → addon-utils-DveoBR6G.js} +219 -3
- package/dist/{model-download-service-D8B-4ktF-BB7oZL3y.mjs → addon-utils-QuxtOdK1.mjs} +219 -3
- package/dist/audio-analyzer/index.js +4 -4
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +9 -9
- package/dist/detection-pipeline/index.mjs +3 -3
- package/dist/{dist-CfxC7XoI.mjs → dist-CLJKoYEP.mjs} +1301 -1284
- package/dist/{dist-C9Q1t0nT.js → dist-J2DeRZAN.js} +1301 -1284
- package/dist/{event-loop-stall-monitor-B_XUPTwp.mjs → event-loop-stall-monitor-B6dqQjiX.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-Luq9AX4t.js → event-loop-stall-monitor-Bkbiw20z.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +5 -5
- package/dist/recorder/index.mjs +2 -2
- package/dist/session-decode/decode-worker-child.js +1 -1
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-SKj0Zdz2.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DZikMEgD.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cj2xMupA.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czrsozsg.mjs +26 -0
- package/dist/stream-broker/{hostInit-D8gyxL0j.mjs → hostInit-i24FVz-3.mjs} +2 -2
- package/dist/stream-broker/index.js +198 -132
- package/dist/stream-broker/index.mjs +197 -131
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-rqbyKlm9.mjs → worker-protocol-BxPNiino.mjs} +1 -1
- package/dist/{worker-protocol-BUJpm7l3.js → worker-protocol-CNi5srgV.js} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DwXIyaHw.js → MaskShapeCanvas-DI4BY7W2-m7YpaY98.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Cf8EtWEG.js → MotionZonesSettings-NcxxQN8r-619YTOjy.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BvsVXya9.js → PrivacyMaskSettings-APgPLF7p-B-k8v0O5.js} +1 -1
- package/embed-dist/assets/index-CYO4OY8_.js +112 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-KpixNEma.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-rd7UaqfV.mjs +0 -26
- package/embed-dist/assets/index-BuAeIGv8.js +0 -112
|
@@ -4,7 +4,9 @@ let node_fs = require("node:fs");
|
|
|
4
4
|
node_fs = require_chunk.__toESM(node_fs, 1);
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
node_path = require_chunk.__toESM(node_path, 1);
|
|
7
|
-
|
|
7
|
+
let node_util = require("node:util");
|
|
8
|
+
let node_zlib = require("node:zlib");
|
|
9
|
+
//#region ../system/dist/model-download-service-CTBHzORJ.mjs
|
|
8
10
|
/**
|
|
9
11
|
* Map a rel path to one candidate absolute path PER root, keeping only roots the
|
|
10
12
|
* path stays within (traversal guard). The handler serves the first candidate
|
|
@@ -70,6 +72,186 @@ function parseRangeHeader(header, size) {
|
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
75
|
+
* Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
|
|
76
|
+
* an eligibility rule, and a BOUNDED cache of the compressed bytes.
|
|
77
|
+
*
|
|
78
|
+
* ## Why the ADDON owns the encoding
|
|
79
|
+
*
|
|
80
|
+
* The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
|
|
81
|
+
* is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
|
|
82
|
+
* emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
|
|
83
|
+
* for any compressible body over its 1 KiB threshold once the body had crossed
|
|
84
|
+
* the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
|
|
85
|
+
* icons; the deploy-bundle pull route had already dodged the same class with
|
|
86
|
+
* `compress: false`. The resolution then, and the contract now, is that an addon
|
|
87
|
+
* route owns its content-coding END TO END: it negotiates, it compresses, it
|
|
88
|
+
* stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
|
|
89
|
+
* untouched (`proxyToUpstream` replays the upstream headers verbatim).
|
|
90
|
+
*
|
|
91
|
+
* So compression belongs HERE, in the shared file data-plane, and nowhere above
|
|
92
|
+
* it. Anything that re-compresses on top of this reintroduces the empty-brotli
|
|
93
|
+
* bug rather than doubling the ratio.
|
|
94
|
+
*
|
|
95
|
+
* ## Why the bytes are cached, and why the cache is bounded
|
|
96
|
+
*
|
|
97
|
+
* Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
|
|
98
|
+
* gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
|
|
99
|
+
* 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
|
|
100
|
+
* paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
|
|
101
|
+
* where a real client is waiting.
|
|
102
|
+
*
|
|
103
|
+
* The cache is an LRU bounded by BOTH entry count and total bytes, and it
|
|
104
|
+
* refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
|
|
105
|
+
* `Map` keyed by URL is how a long-lived process walks into an OOM, and this
|
|
106
|
+
* repo has already spent a session on one.
|
|
107
|
+
*
|
|
108
|
+
* The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
|
|
109
|
+
* place under an UNCHANGED name (an SPA shell, a service worker) can never be
|
|
110
|
+
* served from the previous build's bytes.
|
|
111
|
+
*/
|
|
112
|
+
var brotliAsync = (0, node_util.promisify)(node_zlib.brotliCompress);
|
|
113
|
+
var gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
114
|
+
/** LRU bounds. Both are enforced; whichever binds first evicts. */
|
|
115
|
+
var MAX_CACHE_ENTRIES = 64;
|
|
116
|
+
var MAX_CACHE_BYTES = 32 * 1024 * 1024;
|
|
117
|
+
/** Brotli quality — see the file header for the measured trade. */
|
|
118
|
+
var BROTLI_QUALITY = 5;
|
|
119
|
+
/**
|
|
120
|
+
* Content types worth compressing. An ALLOW-list, not a deny-list: an unknown
|
|
121
|
+
* type (`application/octet-stream`) is assumed to be opaque binary. This is what
|
|
122
|
+
* keeps jpeg/png/webp/mp4/m4s/woff2 out — running brotli over them burns CPU to
|
|
123
|
+
* make the body marginally BIGGER.
|
|
124
|
+
*/
|
|
125
|
+
var COMPRESSIBLE_EXACT = new Set([
|
|
126
|
+
"application/javascript",
|
|
127
|
+
"application/ecmascript",
|
|
128
|
+
"application/json",
|
|
129
|
+
"application/manifest+json",
|
|
130
|
+
"application/wasm",
|
|
131
|
+
"application/xml",
|
|
132
|
+
"application/xhtml+xml",
|
|
133
|
+
"application/vnd.apple.mpegurl",
|
|
134
|
+
"image/svg+xml"
|
|
135
|
+
]);
|
|
136
|
+
/** Is a body of this content type worth compressing at all? */
|
|
137
|
+
function isCompressibleContentType(contentType) {
|
|
138
|
+
const base = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
139
|
+
if (base.length === 0) return false;
|
|
140
|
+
if (base.startsWith("text/")) return true;
|
|
141
|
+
if (base.endsWith("+json") || base.endsWith("+xml")) return true;
|
|
142
|
+
return COMPRESSIBLE_EXACT.has(base);
|
|
143
|
+
}
|
|
144
|
+
/** Parse `Accept-Encoding` into tokens with their q-values (default 1). */
|
|
145
|
+
function parseAcceptEncoding(header) {
|
|
146
|
+
if (header === void 0 || header.trim().length === 0) return [];
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const part of header.split(",")) {
|
|
149
|
+
const [rawToken, ...params] = part.split(";");
|
|
150
|
+
const token = rawToken?.trim().toLowerCase() ?? "";
|
|
151
|
+
if (token.length === 0) continue;
|
|
152
|
+
let q = 1;
|
|
153
|
+
for (const param of params) {
|
|
154
|
+
const [name, value] = param.split("=");
|
|
155
|
+
if (name?.trim().toLowerCase() !== "q") continue;
|
|
156
|
+
const parsed = Number(value?.trim());
|
|
157
|
+
q = Number.isFinite(parsed) ? parsed : 1;
|
|
158
|
+
}
|
|
159
|
+
out.push({
|
|
160
|
+
token,
|
|
161
|
+
q
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function accepts(prefs, token) {
|
|
167
|
+
const exact = prefs.find((p) => p.token === token);
|
|
168
|
+
if (exact) return exact.q > 0;
|
|
169
|
+
const wildcard = prefs.find((p) => p.token === "*");
|
|
170
|
+
return wildcard !== void 0 && wildcard.q > 0;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Pick a coding for this request: brotli when offered, else gzip, else identity.
|
|
174
|
+
*
|
|
175
|
+
* Returns `null` for identity — including when the header is ABSENT. A client
|
|
176
|
+
* that offers nothing gets the raw bytes; guessing on its behalf is how a
|
|
177
|
+
* non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
|
|
178
|
+
* a body it cannot read.
|
|
179
|
+
*/
|
|
180
|
+
function negotiateContentEncoding(acceptEncoding) {
|
|
181
|
+
const prefs = parseAcceptEncoding(acceptEncoding);
|
|
182
|
+
if (prefs.length === 0) return null;
|
|
183
|
+
if (accepts(prefs, "br")) return "br";
|
|
184
|
+
if (accepts(prefs, "gzip")) return "gzip";
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
var cache = /* @__PURE__ */ new Map();
|
|
188
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
189
|
+
var compressions = 0;
|
|
190
|
+
var hits = 0;
|
|
191
|
+
var cachedBytes = 0;
|
|
192
|
+
function cacheKey(key) {
|
|
193
|
+
return `${key.encoding}|${String(key.mtimeMs)}|${String(key.size)}|${key.path}`;
|
|
194
|
+
}
|
|
195
|
+
/** Evict least-recently-used entries until both bounds hold. */
|
|
196
|
+
function evictToBounds() {
|
|
197
|
+
for (const [k, buf] of cache) {
|
|
198
|
+
if (cache.size <= MAX_CACHE_ENTRIES && cachedBytes <= MAX_CACHE_BYTES) return;
|
|
199
|
+
cache.delete(k);
|
|
200
|
+
cachedBytes -= buf.byteLength;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function compressBody(raw, encoding) {
|
|
204
|
+
if (encoding === "gzip") return await gzipAsync(raw);
|
|
205
|
+
return await brotliAsync(raw, { params: {
|
|
206
|
+
[node_zlib.constants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY,
|
|
207
|
+
[node_zlib.constants.BROTLI_PARAM_SIZE_HINT]: raw.byteLength
|
|
208
|
+
} });
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Compressed bytes for one asset, compressing at most once per key.
|
|
212
|
+
*
|
|
213
|
+
* `null` means "send the original" — the asset is too large to buffer, the
|
|
214
|
+
* source could not be read, or compression did not actually shrink it. Every
|
|
215
|
+
* failure degrades to identity, so this can only ever be an optimisation.
|
|
216
|
+
*
|
|
217
|
+
* Concurrent callers for the same key share ONE compression: a cold cache hit by
|
|
218
|
+
* a dozen parallel asset requests must not run zlib a dozen times.
|
|
219
|
+
*/
|
|
220
|
+
async function getCompressedAsset(key, readSource) {
|
|
221
|
+
if (key.size > 8388608) return null;
|
|
222
|
+
const id = cacheKey(key);
|
|
223
|
+
const cached = cache.get(id);
|
|
224
|
+
if (cached) {
|
|
225
|
+
cache.delete(id);
|
|
226
|
+
cache.set(id, cached);
|
|
227
|
+
hits += 1;
|
|
228
|
+
return cached;
|
|
229
|
+
}
|
|
230
|
+
const pending = inFlight.get(id) ?? (async () => {
|
|
231
|
+
let raw;
|
|
232
|
+
try {
|
|
233
|
+
raw = await readSource();
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const out = await compressBody(raw, key.encoding);
|
|
238
|
+
compressions += 1;
|
|
239
|
+
if (out.byteLength >= raw.byteLength) return null;
|
|
240
|
+
cache.set(id, out);
|
|
241
|
+
cachedBytes += out.byteLength;
|
|
242
|
+
evictToBounds();
|
|
243
|
+
return out;
|
|
244
|
+
})();
|
|
245
|
+
inFlight.set(id, pending);
|
|
246
|
+
try {
|
|
247
|
+
return await pending;
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
} finally {
|
|
251
|
+
inFlight.delete(id);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
73
255
|
* A ready-made data-plane request handler that serves files from a set of roots
|
|
74
256
|
* with HTTP `Range`, for addons whose data-plane is "stream files off disk"
|
|
75
257
|
* (recording playback, scrub-frame stores, exported clips).
|
|
@@ -80,6 +262,11 @@ function parseRangeHeader(header, size) {
|
|
|
80
262
|
* the hub already authenticated the caller and the data plane is same-origin
|
|
81
263
|
* through the hub's port. Reuses the shared Range/content-type/traversal helpers
|
|
82
264
|
* so there is one implementation across the standalone file-server and this.
|
|
265
|
+
*
|
|
266
|
+
* It also owns its own CONTENT-CODING. The hub bridge is `{ compress: false }`
|
|
267
|
+
* because the global compressor emits an empty brotli stream over the forked-
|
|
268
|
+
* addon route bridge, so nothing above this layer will ever compress an addon
|
|
269
|
+
* body — see `asset-compression.ts` for the incident and the contract.
|
|
83
270
|
*/
|
|
84
271
|
/** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
|
|
85
272
|
function createFileDataPlaneHandler(opts) {
|
|
@@ -107,11 +294,13 @@ function createFileDataPlaneHandler(opts) {
|
|
|
107
294
|
}
|
|
108
295
|
let absPath = null;
|
|
109
296
|
let size = 0;
|
|
297
|
+
let mtimeMs = 0;
|
|
110
298
|
for (const candidate of resolved.candidates) try {
|
|
111
299
|
const st = await node_fs.promises.stat(candidate);
|
|
112
300
|
if (st.isFile()) {
|
|
113
301
|
absPath = candidate;
|
|
114
302
|
size = st.size;
|
|
303
|
+
mtimeMs = st.mtimeMs;
|
|
115
304
|
break;
|
|
116
305
|
}
|
|
117
306
|
} catch {}
|
|
@@ -119,10 +308,13 @@ function createFileDataPlaneHandler(opts) {
|
|
|
119
308
|
res.writeHead(404).end();
|
|
120
309
|
return;
|
|
121
310
|
}
|
|
311
|
+
const contentType = contentTypeFor(absPath, opts.contentTypes);
|
|
312
|
+
const compressible = isCompressibleContentType(contentType);
|
|
122
313
|
const baseHeaders = {
|
|
123
|
-
"content-type":
|
|
314
|
+
"content-type": contentType,
|
|
124
315
|
"accept-ranges": "bytes",
|
|
125
|
-
"cache-control": opts.cacheControl?.(rel) ?? "no-cache"
|
|
316
|
+
"cache-control": opts.cacheControl?.(rel) ?? "no-cache",
|
|
317
|
+
...compressible ? { vary: "accept-encoding" } : {}
|
|
126
318
|
};
|
|
127
319
|
const range = parseRangeHeader(req.headers.range, size);
|
|
128
320
|
if (range) {
|
|
@@ -141,6 +333,30 @@ function createFileDataPlaneHandler(opts) {
|
|
|
141
333
|
}).pipe(res);
|
|
142
334
|
return;
|
|
143
335
|
}
|
|
336
|
+
const encoding = compressible ? negotiateContentEncoding(req.headers["accept-encoding"]) : null;
|
|
337
|
+
if (encoding !== null && size >= 1024) {
|
|
338
|
+
const filePath = absPath;
|
|
339
|
+
const body = await getCompressedAsset({
|
|
340
|
+
path: filePath,
|
|
341
|
+
mtimeMs,
|
|
342
|
+
size,
|
|
343
|
+
encoding
|
|
344
|
+
}, () => node_fs.promises.readFile(filePath));
|
|
345
|
+
if (body !== null) {
|
|
346
|
+
res.writeHead(200, {
|
|
347
|
+
...baseHeaders,
|
|
348
|
+
"content-encoding": encoding,
|
|
349
|
+
"accept-ranges": "none",
|
|
350
|
+
"content-length": String(body.byteLength)
|
|
351
|
+
});
|
|
352
|
+
if (req.method === "HEAD") {
|
|
353
|
+
res.end();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
res.end(body);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
144
360
|
res.writeHead(200, {
|
|
145
361
|
...baseHeaders,
|
|
146
362
|
"content-length": String(size)
|
|
@@ -3,7 +3,9 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { createReadStream, promises } from "node:fs";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { brotliCompress, constants, gzip } from "node:zlib";
|
|
8
|
+
//#region ../system/dist/model-download-service-CTBHzORJ.mjs
|
|
7
9
|
/**
|
|
8
10
|
* Map a rel path to one candidate absolute path PER root, keeping only roots the
|
|
9
11
|
* path stays within (traversal guard). The handler serves the first candidate
|
|
@@ -69,6 +71,186 @@ function parseRangeHeader(header, size) {
|
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
74
|
+
* Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
|
|
75
|
+
* an eligibility rule, and a BOUNDED cache of the compressed bytes.
|
|
76
|
+
*
|
|
77
|
+
* ## Why the ADDON owns the encoding
|
|
78
|
+
*
|
|
79
|
+
* The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
|
|
80
|
+
* is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
|
|
81
|
+
* emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
|
|
82
|
+
* for any compressible body over its 1 KiB threshold once the body had crossed
|
|
83
|
+
* the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
|
|
84
|
+
* icons; the deploy-bundle pull route had already dodged the same class with
|
|
85
|
+
* `compress: false`. The resolution then, and the contract now, is that an addon
|
|
86
|
+
* route owns its content-coding END TO END: it negotiates, it compresses, it
|
|
87
|
+
* stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
|
|
88
|
+
* untouched (`proxyToUpstream` replays the upstream headers verbatim).
|
|
89
|
+
*
|
|
90
|
+
* So compression belongs HERE, in the shared file data-plane, and nowhere above
|
|
91
|
+
* it. Anything that re-compresses on top of this reintroduces the empty-brotli
|
|
92
|
+
* bug rather than doubling the ratio.
|
|
93
|
+
*
|
|
94
|
+
* ## Why the bytes are cached, and why the cache is bounded
|
|
95
|
+
*
|
|
96
|
+
* Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
|
|
97
|
+
* gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
|
|
98
|
+
* 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
|
|
99
|
+
* paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
|
|
100
|
+
* where a real client is waiting.
|
|
101
|
+
*
|
|
102
|
+
* The cache is an LRU bounded by BOTH entry count and total bytes, and it
|
|
103
|
+
* refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
|
|
104
|
+
* `Map` keyed by URL is how a long-lived process walks into an OOM, and this
|
|
105
|
+
* repo has already spent a session on one.
|
|
106
|
+
*
|
|
107
|
+
* The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
|
|
108
|
+
* place under an UNCHANGED name (an SPA shell, a service worker) can never be
|
|
109
|
+
* served from the previous build's bytes.
|
|
110
|
+
*/
|
|
111
|
+
var brotliAsync = promisify(brotliCompress);
|
|
112
|
+
var gzipAsync = promisify(gzip);
|
|
113
|
+
/** LRU bounds. Both are enforced; whichever binds first evicts. */
|
|
114
|
+
var MAX_CACHE_ENTRIES = 64;
|
|
115
|
+
var MAX_CACHE_BYTES = 32 * 1024 * 1024;
|
|
116
|
+
/** Brotli quality — see the file header for the measured trade. */
|
|
117
|
+
var BROTLI_QUALITY = 5;
|
|
118
|
+
/**
|
|
119
|
+
* Content types worth compressing. An ALLOW-list, not a deny-list: an unknown
|
|
120
|
+
* type (`application/octet-stream`) is assumed to be opaque binary. This is what
|
|
121
|
+
* keeps jpeg/png/webp/mp4/m4s/woff2 out — running brotli over them burns CPU to
|
|
122
|
+
* make the body marginally BIGGER.
|
|
123
|
+
*/
|
|
124
|
+
var COMPRESSIBLE_EXACT = new Set([
|
|
125
|
+
"application/javascript",
|
|
126
|
+
"application/ecmascript",
|
|
127
|
+
"application/json",
|
|
128
|
+
"application/manifest+json",
|
|
129
|
+
"application/wasm",
|
|
130
|
+
"application/xml",
|
|
131
|
+
"application/xhtml+xml",
|
|
132
|
+
"application/vnd.apple.mpegurl",
|
|
133
|
+
"image/svg+xml"
|
|
134
|
+
]);
|
|
135
|
+
/** Is a body of this content type worth compressing at all? */
|
|
136
|
+
function isCompressibleContentType(contentType) {
|
|
137
|
+
const base = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
138
|
+
if (base.length === 0) return false;
|
|
139
|
+
if (base.startsWith("text/")) return true;
|
|
140
|
+
if (base.endsWith("+json") || base.endsWith("+xml")) return true;
|
|
141
|
+
return COMPRESSIBLE_EXACT.has(base);
|
|
142
|
+
}
|
|
143
|
+
/** Parse `Accept-Encoding` into tokens with their q-values (default 1). */
|
|
144
|
+
function parseAcceptEncoding(header) {
|
|
145
|
+
if (header === void 0 || header.trim().length === 0) return [];
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const part of header.split(",")) {
|
|
148
|
+
const [rawToken, ...params] = part.split(";");
|
|
149
|
+
const token = rawToken?.trim().toLowerCase() ?? "";
|
|
150
|
+
if (token.length === 0) continue;
|
|
151
|
+
let q = 1;
|
|
152
|
+
for (const param of params) {
|
|
153
|
+
const [name, value] = param.split("=");
|
|
154
|
+
if (name?.trim().toLowerCase() !== "q") continue;
|
|
155
|
+
const parsed = Number(value?.trim());
|
|
156
|
+
q = Number.isFinite(parsed) ? parsed : 1;
|
|
157
|
+
}
|
|
158
|
+
out.push({
|
|
159
|
+
token,
|
|
160
|
+
q
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function accepts(prefs, token) {
|
|
166
|
+
const exact = prefs.find((p) => p.token === token);
|
|
167
|
+
if (exact) return exact.q > 0;
|
|
168
|
+
const wildcard = prefs.find((p) => p.token === "*");
|
|
169
|
+
return wildcard !== void 0 && wildcard.q > 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Pick a coding for this request: brotli when offered, else gzip, else identity.
|
|
173
|
+
*
|
|
174
|
+
* Returns `null` for identity — including when the header is ABSENT. A client
|
|
175
|
+
* that offers nothing gets the raw bytes; guessing on its behalf is how a
|
|
176
|
+
* non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
|
|
177
|
+
* a body it cannot read.
|
|
178
|
+
*/
|
|
179
|
+
function negotiateContentEncoding(acceptEncoding) {
|
|
180
|
+
const prefs = parseAcceptEncoding(acceptEncoding);
|
|
181
|
+
if (prefs.length === 0) return null;
|
|
182
|
+
if (accepts(prefs, "br")) return "br";
|
|
183
|
+
if (accepts(prefs, "gzip")) return "gzip";
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
var cache = /* @__PURE__ */ new Map();
|
|
187
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
188
|
+
var compressions = 0;
|
|
189
|
+
var hits = 0;
|
|
190
|
+
var cachedBytes = 0;
|
|
191
|
+
function cacheKey(key) {
|
|
192
|
+
return `${key.encoding}|${String(key.mtimeMs)}|${String(key.size)}|${key.path}`;
|
|
193
|
+
}
|
|
194
|
+
/** Evict least-recently-used entries until both bounds hold. */
|
|
195
|
+
function evictToBounds() {
|
|
196
|
+
for (const [k, buf] of cache) {
|
|
197
|
+
if (cache.size <= MAX_CACHE_ENTRIES && cachedBytes <= MAX_CACHE_BYTES) return;
|
|
198
|
+
cache.delete(k);
|
|
199
|
+
cachedBytes -= buf.byteLength;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function compressBody(raw, encoding) {
|
|
203
|
+
if (encoding === "gzip") return await gzipAsync(raw);
|
|
204
|
+
return await brotliAsync(raw, { params: {
|
|
205
|
+
[constants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY,
|
|
206
|
+
[constants.BROTLI_PARAM_SIZE_HINT]: raw.byteLength
|
|
207
|
+
} });
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Compressed bytes for one asset, compressing at most once per key.
|
|
211
|
+
*
|
|
212
|
+
* `null` means "send the original" — the asset is too large to buffer, the
|
|
213
|
+
* source could not be read, or compression did not actually shrink it. Every
|
|
214
|
+
* failure degrades to identity, so this can only ever be an optimisation.
|
|
215
|
+
*
|
|
216
|
+
* Concurrent callers for the same key share ONE compression: a cold cache hit by
|
|
217
|
+
* a dozen parallel asset requests must not run zlib a dozen times.
|
|
218
|
+
*/
|
|
219
|
+
async function getCompressedAsset(key, readSource) {
|
|
220
|
+
if (key.size > 8388608) return null;
|
|
221
|
+
const id = cacheKey(key);
|
|
222
|
+
const cached = cache.get(id);
|
|
223
|
+
if (cached) {
|
|
224
|
+
cache.delete(id);
|
|
225
|
+
cache.set(id, cached);
|
|
226
|
+
hits += 1;
|
|
227
|
+
return cached;
|
|
228
|
+
}
|
|
229
|
+
const pending = inFlight.get(id) ?? (async () => {
|
|
230
|
+
let raw;
|
|
231
|
+
try {
|
|
232
|
+
raw = await readSource();
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const out = await compressBody(raw, key.encoding);
|
|
237
|
+
compressions += 1;
|
|
238
|
+
if (out.byteLength >= raw.byteLength) return null;
|
|
239
|
+
cache.set(id, out);
|
|
240
|
+
cachedBytes += out.byteLength;
|
|
241
|
+
evictToBounds();
|
|
242
|
+
return out;
|
|
243
|
+
})();
|
|
244
|
+
inFlight.set(id, pending);
|
|
245
|
+
try {
|
|
246
|
+
return await pending;
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
} finally {
|
|
250
|
+
inFlight.delete(id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
72
254
|
* A ready-made data-plane request handler that serves files from a set of roots
|
|
73
255
|
* with HTTP `Range`, for addons whose data-plane is "stream files off disk"
|
|
74
256
|
* (recording playback, scrub-frame stores, exported clips).
|
|
@@ -79,6 +261,11 @@ function parseRangeHeader(header, size) {
|
|
|
79
261
|
* the hub already authenticated the caller and the data plane is same-origin
|
|
80
262
|
* through the hub's port. Reuses the shared Range/content-type/traversal helpers
|
|
81
263
|
* so there is one implementation across the standalone file-server and this.
|
|
264
|
+
*
|
|
265
|
+
* It also owns its own CONTENT-CODING. The hub bridge is `{ compress: false }`
|
|
266
|
+
* because the global compressor emits an empty brotli stream over the forked-
|
|
267
|
+
* addon route bridge, so nothing above this layer will ever compress an addon
|
|
268
|
+
* body — see `asset-compression.ts` for the incident and the contract.
|
|
82
269
|
*/
|
|
83
270
|
/** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
|
|
84
271
|
function createFileDataPlaneHandler(opts) {
|
|
@@ -106,11 +293,13 @@ function createFileDataPlaneHandler(opts) {
|
|
|
106
293
|
}
|
|
107
294
|
let absPath = null;
|
|
108
295
|
let size = 0;
|
|
296
|
+
let mtimeMs = 0;
|
|
109
297
|
for (const candidate of resolved.candidates) try {
|
|
110
298
|
const st = await promises.stat(candidate);
|
|
111
299
|
if (st.isFile()) {
|
|
112
300
|
absPath = candidate;
|
|
113
301
|
size = st.size;
|
|
302
|
+
mtimeMs = st.mtimeMs;
|
|
114
303
|
break;
|
|
115
304
|
}
|
|
116
305
|
} catch {}
|
|
@@ -118,10 +307,13 @@ function createFileDataPlaneHandler(opts) {
|
|
|
118
307
|
res.writeHead(404).end();
|
|
119
308
|
return;
|
|
120
309
|
}
|
|
310
|
+
const contentType = contentTypeFor(absPath, opts.contentTypes);
|
|
311
|
+
const compressible = isCompressibleContentType(contentType);
|
|
121
312
|
const baseHeaders = {
|
|
122
|
-
"content-type":
|
|
313
|
+
"content-type": contentType,
|
|
123
314
|
"accept-ranges": "bytes",
|
|
124
|
-
"cache-control": opts.cacheControl?.(rel) ?? "no-cache"
|
|
315
|
+
"cache-control": opts.cacheControl?.(rel) ?? "no-cache",
|
|
316
|
+
...compressible ? { vary: "accept-encoding" } : {}
|
|
125
317
|
};
|
|
126
318
|
const range = parseRangeHeader(req.headers.range, size);
|
|
127
319
|
if (range) {
|
|
@@ -140,6 +332,30 @@ function createFileDataPlaneHandler(opts) {
|
|
|
140
332
|
}).pipe(res);
|
|
141
333
|
return;
|
|
142
334
|
}
|
|
335
|
+
const encoding = compressible ? negotiateContentEncoding(req.headers["accept-encoding"]) : null;
|
|
336
|
+
if (encoding !== null && size >= 1024) {
|
|
337
|
+
const filePath = absPath;
|
|
338
|
+
const body = await getCompressedAsset({
|
|
339
|
+
path: filePath,
|
|
340
|
+
mtimeMs,
|
|
341
|
+
size,
|
|
342
|
+
encoding
|
|
343
|
+
}, () => promises.readFile(filePath));
|
|
344
|
+
if (body !== null) {
|
|
345
|
+
res.writeHead(200, {
|
|
346
|
+
...baseHeaders,
|
|
347
|
+
"content-encoding": encoding,
|
|
348
|
+
"accept-ranges": "none",
|
|
349
|
+
"content-length": String(body.byteLength)
|
|
350
|
+
});
|
|
351
|
+
if (req.method === "HEAD") {
|
|
352
|
+
res.end();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
res.end(body);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
143
359
|
res.writeHead(200, {
|
|
144
360
|
...baseHeaders,
|
|
145
361
|
"content-length": String(size)
|
|
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-J2DeRZAN.js");
|
|
7
7
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
8
|
-
const
|
|
8
|
+
const require_addon_utils = require("../addon-utils-DveoBR6G.js");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
10
10
|
node_fs = require_chunk.__toESM(node_fs);
|
|
11
11
|
let node_path = require("node:path");
|
|
@@ -84,7 +84,7 @@ var YamnetPythonPipeline = class {
|
|
|
84
84
|
url: YAMNET_MODEL_URL,
|
|
85
85
|
dest: modelPath
|
|
86
86
|
} });
|
|
87
|
-
await
|
|
87
|
+
await require_addon_utils.downloadFile(YAMNET_MODEL_URL, modelPath);
|
|
88
88
|
this.log.info("YAMNet ONNX model downloaded", { meta: { sizeBytes: node_fs.statSync(modelPath).size } });
|
|
89
89
|
}
|
|
90
90
|
if (!node_fs.existsSync(labelsPath)) {
|
|
@@ -92,7 +92,7 @@ var YamnetPythonPipeline = class {
|
|
|
92
92
|
url: YAMNET_LABELS_URL,
|
|
93
93
|
dest: labelsPath
|
|
94
94
|
} });
|
|
95
|
-
await
|
|
95
|
+
await require_addon_utils.downloadFile(YAMNET_LABELS_URL, labelsPath);
|
|
96
96
|
}
|
|
97
97
|
const pythonDir = resolveAudioPythonDir();
|
|
98
98
|
if (this.installPythonRequirements) await this.installPythonRequirements(node_path.join(pythonDir, "requirements-audio.txt"));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as errMsg, C as audioAnalysisCapability, R as mapAudioLabelToMacro, at as BaseAddon, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, p as HF_BASE_URL, ut as hydrateSchema, w as audioAnalyzerCapability } from "../dist-
|
|
2
|
+
import { $ as errMsg, C as audioAnalysisCapability, R as mapAudioLabelToMacro, at as BaseAddon, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, p as HF_BASE_URL, ut as hydrateSchema, w as audioAnalyzerCapability } from "../dist-CLJKoYEP.mjs";
|
|
3
3
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
4
|
-
import { i as downloadFile } from "../
|
|
4
|
+
import { i as downloadFile } from "../addon-utils-QuxtOdK1.mjs";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path$1 from "node:path";
|
|
7
7
|
//#region src/audio-analyzer/audio-pipeline.ts
|
|
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-
|
|
6
|
+
const require_dist = require("../dist-J2DeRZAN.js");
|
|
7
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Bkbiw20z.js");
|
|
8
8
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
9
9
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
10
|
-
const
|
|
10
|
+
const require_addon_utils = require("../addon-utils-DveoBR6G.js");
|
|
11
11
|
let sharp = require("sharp");
|
|
12
12
|
sharp = require_chunk.__toESM(sharp);
|
|
13
13
|
let node_child_process = require("node:child_process");
|
|
@@ -5141,7 +5141,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5141
5141
|
if (!step.enabled) continue;
|
|
5142
5142
|
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
5143
5143
|
if (!modelEntry) continue;
|
|
5144
|
-
if (
|
|
5144
|
+
if (require_addon_utils.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
|
|
5145
5145
|
await this.downloadWithRetry(modelEntry, format, 3);
|
|
5146
5146
|
}
|
|
5147
5147
|
}
|
|
@@ -5518,7 +5518,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5518
5518
|
const formats = {};
|
|
5519
5519
|
for (const [formatKey, entry] of Object.entries(m.formats)) {
|
|
5520
5520
|
if (!entry) continue;
|
|
5521
|
-
const downloaded =
|
|
5521
|
+
const downloaded = require_addon_utils.isModelDownloaded(this.modelsDir, m, formatKey);
|
|
5522
5522
|
formats[formatKey] = {
|
|
5523
5523
|
url: entry.url,
|
|
5524
5524
|
sizeMB: entry.sizeMB,
|
|
@@ -5641,7 +5641,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5641
5641
|
const { modelId, format, addonId } = input;
|
|
5642
5642
|
const modelEntry = await this.resolveModelEntry(addonId, modelId);
|
|
5643
5643
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
|
|
5644
|
-
if (!
|
|
5644
|
+
if (!require_addon_utils.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
|
|
5645
5645
|
this.log.info("Model deleted from disk", { meta: {
|
|
5646
5646
|
modelId,
|
|
5647
5647
|
format
|
|
@@ -6372,7 +6372,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6372
6372
|
const work = (async () => {
|
|
6373
6373
|
for (const step of needed) {
|
|
6374
6374
|
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
6375
|
-
if (modelEntry && !
|
|
6375
|
+
if (modelEntry && !require_addon_utils.isModelDownloaded(this.modelsDir, modelEntry, format)) {
|
|
6376
6376
|
if (modelEntry.formats[format] === void 0) throw new Error(`Model "${modelEntry.id}" has no ${format} format build (available: ${Object.keys(modelEntry.formats).join(", ") || "none"}) — not retrying a permanent format mismatch`);
|
|
6377
6377
|
this.log.info("Downloading model for step", { meta: {
|
|
6378
6378
|
modelId: step.modelId,
|
|
@@ -6398,7 +6398,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6398
6398
|
/** Download a model with retry + exponential backoff */
|
|
6399
6399
|
async downloadWithRetry(entry, format, maxRetries, onProgress) {
|
|
6400
6400
|
for (let attempt = 1; attempt <= maxRetries; attempt++) try {
|
|
6401
|
-
await
|
|
6401
|
+
await require_addon_utils.ensureModel(this.modelsDir, entry, format, onProgress);
|
|
6402
6402
|
this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
|
|
6403
6403
|
return;
|
|
6404
6404
|
} catch (err) {
|
|
@@ -7240,7 +7240,7 @@ function buildSchemaSlots(formats, modelsDir, customByStep) {
|
|
|
7240
7240
|
id: m.id,
|
|
7241
7241
|
name: m.name,
|
|
7242
7242
|
formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
|
|
7243
|
-
downloaded:
|
|
7243
|
+
downloaded: require_addon_utils.isModelDownloaded(modelsDir, m, f),
|
|
7244
7244
|
sizeMB: entry.sizeMB
|
|
7245
7245
|
}])),
|
|
7246
7246
|
...m.group ? { group: m.group } : {},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as errMsg, At as EventCategory, Et as object, F as enumerateInferenceDevices, I as evaluateZoneRules, J as runtimeDevices$1, M as detectionPipelineCapability, O as defaultDeviceFor$1, Ot as string, U as pipelineExecutorCapability, Z as supportedRuntimes$1, at as BaseAddon, bt as array, ht as parseJsonUnknown, kt as union, l as DEVICE_BACKEND_TO_FORMAT, lt as createEvent, mt as nodePin, t as APPLE_SA_TO_MACRO, ut as hydrateSchema, vt as sleep, x as YAMNET_TO_MACRO } from "../dist-
|
|
3
|
-
import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-
|
|
2
|
+
import { $ as errMsg, At as EventCategory, Et as object, F as enumerateInferenceDevices, I as evaluateZoneRules, J as runtimeDevices$1, M as detectionPipelineCapability, O as defaultDeviceFor$1, Ot as string, U as pipelineExecutorCapability, Z as supportedRuntimes$1, at as BaseAddon, bt as array, ht as parseJsonUnknown, kt as union, l as DEVICE_BACKEND_TO_FORMAT, lt as createEvent, mt as nodePin, t as APPLE_SA_TO_MACRO, ut as hydrateSchema, vt as sleep, x as YAMNET_TO_MACRO } from "../dist-CLJKoYEP.mjs";
|
|
3
|
+
import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-B6dqQjiX.mjs";
|
|
4
4
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
5
5
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
6
|
-
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../
|
|
6
|
+
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-QuxtOdK1.mjs";
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import * as os from "node:os";
|