@camstack/core 0.1.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/index.mjs ADDED
@@ -0,0 +1,630 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/events/event-bus.ts
9
+ var EventBus = class {
10
+ listeners = /* @__PURE__ */ new Map();
11
+ on(event, callback) {
12
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
13
+ const set = this.listeners.get(event);
14
+ set.add(callback);
15
+ return () => set.delete(callback);
16
+ }
17
+ emit(event, data) {
18
+ const set = this.listeners.get(event);
19
+ if (!set) return;
20
+ for (const cb of set) {
21
+ try {
22
+ cb(data);
23
+ } catch {
24
+ }
25
+ }
26
+ }
27
+ once(event, callback) {
28
+ const unsub = this.on(event, (data) => {
29
+ unsub();
30
+ callback(data);
31
+ });
32
+ return unsub;
33
+ }
34
+ removeAllListeners(event) {
35
+ if (event) this.listeners.delete(event);
36
+ else this.listeners.clear();
37
+ }
38
+ listenerCount(event) {
39
+ return this.listeners.get(event)?.size ?? 0;
40
+ }
41
+ };
42
+
43
+ // src/download/model-downloader.ts
44
+ import * as fs from "fs";
45
+ import * as path from "path";
46
+ import * as https from "https";
47
+ import * as http from "http";
48
+ import { createHash } from "crypto";
49
+ async function downloadModel(options) {
50
+ const { url, fallbackUrls = [], destDir, filename, expectedSha256, onProgress } = options;
51
+ const fname = filename ?? url.split("/").pop() ?? "model.bin";
52
+ const destPath = path.join(destDir, fname);
53
+ if (fs.existsSync(destPath)) {
54
+ return { filePath: destPath, downloadedBytes: 0, fromCache: true };
55
+ }
56
+ fs.mkdirSync(destDir, { recursive: true });
57
+ const urls = [url, ...fallbackUrls];
58
+ let lastError = null;
59
+ for (const tryUrl of urls) {
60
+ try {
61
+ const bytes = await downloadFile(tryUrl, destPath, onProgress);
62
+ if (expectedSha256) {
63
+ const hash = await computeSha256(destPath);
64
+ if (hash !== expectedSha256) {
65
+ fs.unlinkSync(destPath);
66
+ throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
67
+ }
68
+ }
69
+ return { filePath: destPath, downloadedBytes: bytes, fromCache: false };
70
+ } catch (e) {
71
+ lastError = e;
72
+ if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
73
+ }
74
+ }
75
+ throw lastError ?? new Error(`Failed to download model from ${url}`);
76
+ }
77
+ async function downloadFile(url, destPath, onProgress) {
78
+ return new Promise((resolve, reject) => {
79
+ const mod = url.startsWith("https") ? https : http;
80
+ const request = (targetUrl, redirectCount = 0) => {
81
+ if (redirectCount > 5) return reject(new Error("Too many redirects"));
82
+ mod.get(targetUrl, (res) => {
83
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
84
+ return request(res.headers.location, redirectCount + 1);
85
+ }
86
+ if (res.statusCode !== 200) {
87
+ return reject(new Error(`HTTP ${res.statusCode} downloading ${targetUrl}`));
88
+ }
89
+ const total = parseInt(res.headers["content-length"] ?? "0", 10);
90
+ let downloaded = 0;
91
+ const fileStream = fs.createWriteStream(destPath);
92
+ res.on("data", (chunk) => {
93
+ downloaded += chunk.length;
94
+ onProgress?.(downloaded, total);
95
+ });
96
+ res.pipe(fileStream);
97
+ fileStream.on("finish", () => resolve(downloaded));
98
+ fileStream.on("error", reject);
99
+ res.on("error", reject);
100
+ }).on("error", reject);
101
+ };
102
+ request(url);
103
+ });
104
+ }
105
+ async function computeSha256(filePath) {
106
+ return new Promise((resolve, reject) => {
107
+ const hash = createHash("sha256");
108
+ const stream = fs.createReadStream(filePath);
109
+ stream.on("data", (chunk) => hash.update(chunk));
110
+ stream.on("end", () => resolve(hash.digest("hex")));
111
+ stream.on("error", reject);
112
+ });
113
+ }
114
+
115
+ // src/python/python-env-manager.ts
116
+ import { execFile, spawn } from "child_process";
117
+ import { promisify } from "util";
118
+ import * as fs2 from "fs";
119
+ import * as path2 from "path";
120
+ var execFileAsync = promisify(execFile);
121
+ var PythonEnvManager = class {
122
+ constructor(dataDir) {
123
+ this.dataDir = dataDir;
124
+ this.venvPath = path2.join(dataDir, ".venv");
125
+ }
126
+ venvPath;
127
+ cachedProbe = null;
128
+ async probe() {
129
+ if (this.cachedProbe) return this.cachedProbe;
130
+ for (const cmd of ["python3", "python"]) {
131
+ try {
132
+ const { stdout } = await execFileAsync(cmd, ["--version"]);
133
+ const version = stdout.trim().replace("Python ", "");
134
+ const major = parseInt(version.split(".")[0] ?? "0", 10);
135
+ const minor = parseInt(version.split(".")[1] ?? "0", 10);
136
+ if (major < 3 || major === 3 && minor < 10) continue;
137
+ const { stdout: pathOut } = await execFileAsync(cmd, ["-c", "import sys; print(sys.executable)"]);
138
+ this.cachedProbe = {
139
+ available: true,
140
+ version,
141
+ path: pathOut.trim()
142
+ };
143
+ return this.cachedProbe;
144
+ } catch {
145
+ continue;
146
+ }
147
+ }
148
+ this.cachedProbe = { available: false };
149
+ return this.cachedProbe;
150
+ }
151
+ async ensure(options) {
152
+ const probe = await this.probe();
153
+ if (!probe.available || !probe.path) {
154
+ throw new Error("Python 3.10+ is required but not found on this system");
155
+ }
156
+ if (!fs2.existsSync(path2.join(this.venvPath, "bin", "python"))) {
157
+ await execFileAsync(probe.path, ["-m", "venv", this.venvPath]);
158
+ }
159
+ const venvPython = path2.join(this.venvPath, "bin", "python");
160
+ if (options.packages.length > 0) {
161
+ await execFileAsync(venvPython, ["-m", "pip", "install", "-q", ...options.packages]);
162
+ }
163
+ return {
164
+ pythonPath: venvPython,
165
+ venvPath: this.venvPath,
166
+ spawn: (script, args) => {
167
+ return spawn(venvPython, [script, ...args]);
168
+ }
169
+ };
170
+ }
171
+ spawn(script, args) {
172
+ const venvPython = path2.join(this.venvPath, "bin", "python");
173
+ if (!fs2.existsSync(venvPython)) {
174
+ throw new Error("Python venv not initialized. Call ensure() first.");
175
+ }
176
+ return spawn(venvPython, [script, ...args]);
177
+ }
178
+ };
179
+
180
+ // src/addon/addon-loader.ts
181
+ import * as fs3 from "fs";
182
+ import * as path3 from "path";
183
+ var AddonLoader = class {
184
+ addons = /* @__PURE__ */ new Map();
185
+ /** Load all addons from an npm package */
186
+ async loadPackage(packageName) {
187
+ const pkgJsonPath = __require.resolve(`${packageName}/package.json`);
188
+ const pkgJson = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
189
+ const manifest = pkgJson["camstack"];
190
+ if (!manifest?.addons?.length) {
191
+ throw new Error(`Package ${packageName} has no camstack.addons manifest`);
192
+ }
193
+ for (const declaration of manifest.addons) {
194
+ const entryPath = __require.resolve(
195
+ `${packageName}/${declaration.entry.replace("./dist/", "").replace(/\.js$/, "")}`
196
+ );
197
+ const mod = await import(entryPath);
198
+ const AddonClass = mod["default"] ?? mod[Object.keys(mod)[0]];
199
+ if (!AddonClass) {
200
+ throw new Error(`Addon ${declaration.id} from ${packageName} has no default export`);
201
+ }
202
+ this.addons.set(declaration.id, {
203
+ declaration,
204
+ packageName,
205
+ addonClass: AddonClass
206
+ });
207
+ }
208
+ }
209
+ /** Load addon from a direct path (for development/testing) */
210
+ async loadFromPath(addonId, modulePath, packageName) {
211
+ const mod = await import(modulePath);
212
+ const AddonClass = mod["default"] ?? mod[Object.keys(mod)[0]];
213
+ if (!AddonClass) {
214
+ throw new Error(`Module ${modulePath} has no default export`);
215
+ }
216
+ this.addons.set(addonId, {
217
+ declaration: { id: addonId, entry: modulePath, slot: "detector" },
218
+ packageName,
219
+ addonClass: AddonClass
220
+ });
221
+ }
222
+ /** Get a registered addon by ID */
223
+ getAddon(addonId) {
224
+ return this.addons.get(addonId);
225
+ }
226
+ /** List all registered addons */
227
+ listAddons() {
228
+ return [...this.addons.values()];
229
+ }
230
+ /** Check if an addon is registered */
231
+ hasAddon(addonId) {
232
+ return this.addons.has(addonId);
233
+ }
234
+ /** Create a new instance of an addon (not yet initialized) */
235
+ createInstance(addonId) {
236
+ const registered = this.addons.get(addonId);
237
+ if (!registered) {
238
+ throw new Error(`Addon "${addonId}" is not registered`);
239
+ }
240
+ return new registered.addonClass();
241
+ }
242
+ /** Load all installed addon packages from the addon directory */
243
+ async loadAllInstalled(installer) {
244
+ const installed = installer.listInstalled();
245
+ for (const pkg of installed) {
246
+ try {
247
+ await this.loadPackageFromPath(pkg.name, pkg.path);
248
+ } catch (error) {
249
+ const message = error instanceof Error ? error.message : String(error);
250
+ console.warn(`Failed to load addon package ${pkg.name}: ${message}`);
251
+ }
252
+ }
253
+ }
254
+ /** Load addon package from a specific filesystem path */
255
+ async loadPackageFromPath(packageName, packagePath) {
256
+ const pkgJsonPath = path3.join(packagePath, "package.json");
257
+ const pkgJson = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
258
+ const manifest = pkgJson["camstack"];
259
+ if (!manifest?.addons?.length) {
260
+ throw new Error(`Package ${packageName} at ${packagePath} has no camstack.addons manifest`);
261
+ }
262
+ for (const declaration of manifest.addons) {
263
+ const entryPath = path3.join(packagePath, declaration.entry);
264
+ const mod = await import(entryPath);
265
+ const AddonClass = mod["default"] ?? mod[Object.keys(mod)[0]];
266
+ if (!AddonClass) {
267
+ throw new Error(`Addon ${declaration.id} from ${packageName} has no default export`);
268
+ }
269
+ this.addons.set(declaration.id, {
270
+ declaration,
271
+ packageName,
272
+ addonClass: AddonClass
273
+ });
274
+ }
275
+ }
276
+ };
277
+
278
+ // src/addon/addon-engine-manager.ts
279
+ import { createHash as createHash2 } from "crypto";
280
+ var AddonEngineManager = class {
281
+ constructor(loader, baseContext) {
282
+ this.loader = loader;
283
+ this.baseContext = baseContext;
284
+ }
285
+ engines = /* @__PURE__ */ new Map();
286
+ /**
287
+ * Get or create an addon engine for the given effective config.
288
+ * Cameras with the same addonId + effective config share the same engine.
289
+ */
290
+ async getOrCreateEngine(addonId, globalConfig, cameraOverride) {
291
+ const effectiveConfig = { ...globalConfig, ...cameraOverride };
292
+ const configKey = `${addonId}:${this.hashConfig(effectiveConfig)}`;
293
+ const existing = this.engines.get(configKey);
294
+ if (existing) return existing;
295
+ const addon = this.loader.createInstance(addonId);
296
+ await addon.initialize({ ...this.baseContext, addonConfig: effectiveConfig });
297
+ this.engines.set(configKey, addon);
298
+ return addon;
299
+ }
300
+ /** Get all active engines */
301
+ getActiveEngines() {
302
+ return new Map(this.engines);
303
+ }
304
+ /** Shutdown a specific engine by its config key */
305
+ async shutdownEngine(configKey) {
306
+ const engine = this.engines.get(configKey);
307
+ if (engine) {
308
+ await engine.shutdown();
309
+ this.engines.delete(configKey);
310
+ }
311
+ }
312
+ /** Shutdown all engines */
313
+ async shutdownAll() {
314
+ for (const [, engine] of this.engines) {
315
+ await engine.shutdown();
316
+ }
317
+ this.engines.clear();
318
+ }
319
+ /** Compute a deterministic config key (visible for tests) */
320
+ computeConfigKey(addonId, effectiveConfig) {
321
+ return `${addonId}:${this.hashConfig(effectiveConfig)}`;
322
+ }
323
+ hashConfig(config) {
324
+ const sorted = JSON.stringify(config, Object.keys(config).sort());
325
+ return createHash2("md5").update(sorted).digest("hex").slice(0, 12);
326
+ }
327
+ };
328
+
329
+ // src/addon/addon-installer.ts
330
+ import { execFile as execFile2 } from "child_process";
331
+ import { promisify as promisify2 } from "util";
332
+ import * as fs4 from "fs";
333
+ import * as path4 from "path";
334
+ var execFileAsync2 = promisify2(execFile2);
335
+ var AddonInstaller = class {
336
+ constructor(config) {
337
+ this.config = config;
338
+ }
339
+ /** Initialize addon directory — create if not exists, install builtins if needed */
340
+ async initialize() {
341
+ await this.ensureAddonDirectory();
342
+ await this.installBuiltins();
343
+ }
344
+ /** Ensure addon directory exists with a package.json */
345
+ async ensureAddonDirectory() {
346
+ const { addonsDir } = this.config;
347
+ fs4.mkdirSync(addonsDir, { recursive: true });
348
+ const pkgJsonPath = path4.join(addonsDir, "package.json");
349
+ if (!fs4.existsSync(pkgJsonPath)) {
350
+ const pkgJson = {
351
+ name: "camstack-addons",
352
+ version: "1.0.0",
353
+ private: true,
354
+ description: "CamStack addon packages \u2014 managed automatically",
355
+ dependencies: {}
356
+ };
357
+ fs4.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
358
+ }
359
+ }
360
+ /** Install builtin packages if not already present */
361
+ async installBuiltins() {
362
+ const missing = this.config.builtinPackages.filter((pkg) => !this.isInstalled(pkg));
363
+ if (missing.length === 0) return;
364
+ await this.installPackages(missing);
365
+ }
366
+ /** Check if a package is installed */
367
+ isInstalled(packageName) {
368
+ const pkgPath = path4.join(this.config.addonsDir, "node_modules", packageName, "package.json");
369
+ return fs4.existsSync(pkgPath);
370
+ }
371
+ /** Get installed package info */
372
+ getInstalledPackage(packageName) {
373
+ const pkgPath = path4.join(this.config.addonsDir, "node_modules", packageName, "package.json");
374
+ if (!fs4.existsSync(pkgPath)) return null;
375
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
376
+ return {
377
+ name: pkg.name,
378
+ version: pkg.version,
379
+ path: path4.dirname(pkgPath)
380
+ };
381
+ }
382
+ /** List all installed addon packages (those with camstack.addons in package.json) */
383
+ listInstalled() {
384
+ const nodeModulesDir = path4.join(this.config.addonsDir, "node_modules");
385
+ if (!fs4.existsSync(nodeModulesDir)) return [];
386
+ const packages = [];
387
+ const scopeDirs = fs4.readdirSync(nodeModulesDir).filter((d) => d.startsWith("@"));
388
+ for (const scope of scopeDirs) {
389
+ const scopePath = path4.join(nodeModulesDir, scope);
390
+ if (!fs4.statSync(scopePath).isDirectory()) continue;
391
+ for (const pkg of fs4.readdirSync(scopePath)) {
392
+ const pkgJsonPath = path4.join(scopePath, pkg, "package.json");
393
+ if (!fs4.existsSync(pkgJsonPath)) continue;
394
+ const pkgJson = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
395
+ if (pkgJson.camstack?.addons) {
396
+ packages.push({ name: pkgJson.name, version: pkgJson.version, path: path4.join(scopePath, pkg) });
397
+ }
398
+ }
399
+ }
400
+ for (const dir of fs4.readdirSync(nodeModulesDir)) {
401
+ if (dir.startsWith("@") || dir.startsWith(".")) continue;
402
+ const pkgJsonPath = path4.join(nodeModulesDir, dir, "package.json");
403
+ if (!fs4.existsSync(pkgJsonPath)) continue;
404
+ const pkgJson = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
405
+ if (pkgJson.camstack?.addons) {
406
+ packages.push({ name: pkgJson.name, version: pkgJson.version, path: path4.join(nodeModulesDir, dir) });
407
+ }
408
+ }
409
+ return packages;
410
+ }
411
+ /** Install one or more packages from npm */
412
+ async installPackages(packages) {
413
+ if (packages.length === 0) return;
414
+ const args = ["install", "--save", ...packages];
415
+ if (this.config.registry) {
416
+ args.push("--registry", this.config.registry);
417
+ }
418
+ await execFileAsync2("npm", args, {
419
+ cwd: this.config.addonsDir,
420
+ timeout: 12e4
421
+ // 2 min timeout
422
+ });
423
+ const pkgJsonPath = path4.join(this.config.addonsDir, "package.json");
424
+ const pkgJson = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
425
+ fs4.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
426
+ }
427
+ /** Uninstall a package */
428
+ async uninstallPackage(packageName) {
429
+ if (this.config.builtinPackages.includes(packageName)) {
430
+ throw new Error(`Cannot uninstall builtin package: ${packageName}`);
431
+ }
432
+ await execFileAsync2("npm", ["uninstall", packageName], {
433
+ cwd: this.config.addonsDir,
434
+ timeout: 6e4
435
+ });
436
+ }
437
+ /** Update a package to latest version */
438
+ async updatePackage(packageName) {
439
+ await execFileAsync2("npm", ["update", packageName], {
440
+ cwd: this.config.addonsDir,
441
+ timeout: 12e4
442
+ });
443
+ }
444
+ /** Update all packages */
445
+ async updateAll() {
446
+ await execFileAsync2("npm", ["update"], {
447
+ cwd: this.config.addonsDir,
448
+ timeout: 3e5
449
+ });
450
+ }
451
+ /** Get the node_modules path for require/import resolution */
452
+ getNodeModulesPath() {
453
+ return path4.join(this.config.addonsDir, "node_modules");
454
+ }
455
+ };
456
+ var BUILTIN_PACKAGES = [
457
+ "@camstack/vision",
458
+ "@camstack/pipeline-analysis",
459
+ "@camstack/benchmark"
460
+ ];
461
+
462
+ // src/pipeline/pipeline-validator.ts
463
+ var PipelineValidator = class {
464
+ constructor(loader) {
465
+ this.loader = loader;
466
+ }
467
+ validate(config) {
468
+ const errors = [];
469
+ const warnings = [];
470
+ for (const node of config.video) {
471
+ this.validateNode(node, errors, warnings, []);
472
+ }
473
+ if (config.audio) {
474
+ this.validateNode(config.audio, errors, warnings, []);
475
+ }
476
+ return {
477
+ valid: errors.length === 0,
478
+ errors,
479
+ warnings
480
+ };
481
+ }
482
+ validateNode(node, errors, warnings, ancestors) {
483
+ if (!this.loader.hasAddon(node.addon)) {
484
+ errors.push({
485
+ step: node.step,
486
+ addon: node.addon,
487
+ message: `Addon "${node.addon}" is not registered`,
488
+ severity: "error"
489
+ });
490
+ return;
491
+ }
492
+ const registered = this.loader.getAddon(node.addon);
493
+ if (registered) {
494
+ const manifest = registered.declaration;
495
+ void manifest;
496
+ }
497
+ if (node.children) {
498
+ for (const child of node.children) {
499
+ this.validateNode(child, errors, warnings, [...ancestors, node]);
500
+ }
501
+ }
502
+ }
503
+ };
504
+
505
+ // src/pipeline/pipeline-runner.ts
506
+ import { randomUUID } from "crypto";
507
+ var PipelineRunner = class {
508
+ constructor(engineManager, addonConfigs) {
509
+ this.engineManager = engineManager;
510
+ this.addonConfigs = addonConfigs;
511
+ }
512
+ async run(frame, config) {
513
+ const startTime = performance.now();
514
+ const results = [];
515
+ const timings = {};
516
+ for (const rootNode of config.video) {
517
+ await this.executeNode(rootNode, frame, null, results, timings);
518
+ }
519
+ if (config.audio) {
520
+ await this.executeNode(config.audio, frame, null, results, timings);
521
+ }
522
+ return {
523
+ results,
524
+ totalMs: performance.now() - startTime,
525
+ timings,
526
+ frameTimestamp: frame.timestamp
527
+ };
528
+ }
529
+ async executeNode(node, frame, parentDetection, results, timings) {
530
+ const resultId = randomUUID();
531
+ const stepStart = performance.now();
532
+ try {
533
+ const globalConfig = this.addonConfigs.get(node.addon) ?? {};
534
+ const engine = await this.engineManager.getOrCreateEngine(
535
+ node.addon,
536
+ globalConfig,
537
+ node.configOverride
538
+ );
539
+ let output;
540
+ if (parentDetection) {
541
+ const cropInput = {
542
+ frame,
543
+ roi: parentDetection.det.bbox,
544
+ parentDetection: parentDetection.det
545
+ };
546
+ if ("crop" in engine && typeof engine["crop"] === "function") {
547
+ output = await engine.crop(cropInput);
548
+ } else if ("classify" in engine && typeof engine["classify"] === "function") {
549
+ output = await engine.classify(cropInput);
550
+ } else if ("detect" in engine && typeof engine["detect"] === "function") {
551
+ output = await engine.detect(frame);
552
+ } else {
553
+ throw new Error(`Addon "${node.addon}" has no detect/crop/classify method`);
554
+ }
555
+ } else {
556
+ if ("detect" in engine && typeof engine["detect"] === "function") {
557
+ output = await engine.detect(frame);
558
+ } else if ("classify" in engine && typeof engine["classify"] === "function") {
559
+ const rootCropInput = {
560
+ frame,
561
+ roi: { x: 0, y: 0, w: frame.width, h: frame.height },
562
+ parentDetection: { class: "", originalClass: "", score: 1, bbox: { x: 0, y: 0, w: frame.width, h: frame.height } }
563
+ };
564
+ output = await engine.classify(rootCropInput);
565
+ } else {
566
+ throw new Error(`Addon "${node.addon}" has no detect/classify method`);
567
+ }
568
+ }
569
+ const stepMs = performance.now() - stepStart;
570
+ const slot = "detections" in output ? "detector" : "crops" in output ? "cropper" : "classifier";
571
+ const stepResult = {
572
+ addon: node.addon,
573
+ slot,
574
+ output,
575
+ parentResultId: parentDetection?.resultId,
576
+ resultId,
577
+ inferenceMs: output.inferenceMs,
578
+ preprocessMs: 0,
579
+ totalMs: stepMs
580
+ };
581
+ results.push(stepResult);
582
+ timings[node.step] = stepMs;
583
+ if (node.children?.length) {
584
+ const detections = "detections" in output ? output.detections : "crops" in output ? output.crops : [];
585
+ await this.executeChildren(node.children, frame, detections, resultId, results, timings);
586
+ }
587
+ } catch (error) {
588
+ const stepMs = performance.now() - stepStart;
589
+ const message = error instanceof Error ? error.message : String(error);
590
+ results.push({
591
+ addon: node.addon,
592
+ slot: "detector",
593
+ output: { detections: [], inferenceMs: 0, modelId: "" },
594
+ parentResultId: parentDetection?.resultId,
595
+ resultId,
596
+ inferenceMs: 0,
597
+ preprocessMs: 0,
598
+ totalMs: stepMs,
599
+ error: {
600
+ code: "ADDON_ERROR",
601
+ message,
602
+ childrenSkipped: true
603
+ }
604
+ });
605
+ }
606
+ }
607
+ async executeChildren(children, frame, parentDetections, parentResultId, results, timings) {
608
+ const promises = [];
609
+ for (const detection of parentDetections) {
610
+ for (const child of children) {
611
+ promises.push(
612
+ this.executeNode(child, frame, { det: detection, resultId: parentResultId }, results, timings)
613
+ );
614
+ }
615
+ }
616
+ await Promise.allSettled(promises);
617
+ }
618
+ };
619
+ export {
620
+ AddonEngineManager,
621
+ AddonInstaller,
622
+ AddonLoader,
623
+ BUILTIN_PACKAGES,
624
+ EventBus,
625
+ PipelineRunner,
626
+ PipelineValidator,
627
+ PythonEnvManager,
628
+ downloadModel
629
+ };
630
+ //# sourceMappingURL=index.mjs.map