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