@camstack/addon-pipeline 1.0.0 → 1.0.2

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.
@@ -1,9 +1,9 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { A as detectionPipelineCapability, E as createEvent, M as evaluateZoneRules, N as hfModelUrl, P as hydrateSchema, U as pipelineExecutorCapability, V as parseJsonUnknown, i as BaseAddon, j as errMsg, o as COCO_80_LABELS, p as EventCategory, q as sleep, r as AUDIO_MACRO_LABELS, s as COCO_TO_MACRO, t as APPLE_SA_TO_MACRO, v as YAMNET_TO_MACRO } from "../dist-C5jnNl0n.mjs";
2
+ import { A as detectionPipelineCapability, E as createEvent, M as evaluateZoneRules, N as hfModelUrl, P as hydrateSchema, U as pipelineExecutorCapability, V as parseJsonUnknown, i as BaseAddon, j as errMsg, o as COCO_80_LABELS, p as EventCategory, q as sleep, r as AUDIO_MACRO_LABELS, s as COCO_TO_MACRO, t as APPLE_SA_TO_MACRO, v as YAMNET_TO_MACRO } from "../dist-XRXnZrVC.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
5
5
  import * as os from "node:os";
6
- import { deleteModelFromDisk, ensureModel, isModelDownloaded } from "@camstack/core";
6
+ import { deleteModelFromDisk, ensureModel, isModelDownloaded } from "@camstack/system";
7
7
  import { spawn } from "node:child_process";
8
8
  import sharp from "sharp";
9
9
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
@@ -1955,215 +1955,6 @@ function getDefaultModelForFormat(stepId, format) {
1955
1955
  })[0].id;
1956
1956
  }
1957
1957
  //#endregion
1958
- //#region src/detection-pipeline/engine/node-engine-manager.ts
1959
- var NodeEngineManager = class {
1960
- backend;
1961
- resolveModelPath;
1962
- createEngine;
1963
- engines = /* @__PURE__ */ new Map();
1964
- log;
1965
- constructor(backend, resolveModelPath, createEngine, logger) {
1966
- this.backend = backend;
1967
- this.resolveModelPath = resolveModelPath;
1968
- this.createEngine = createEngine;
1969
- this.log = logger;
1970
- }
1971
- /**
1972
- * Apply a new pipeline configuration.
1973
- * Loads/unloads engines for steps that changed.
1974
- */
1975
- async applyConfig(newSteps) {
1976
- const enabledSteps = flattenEnabledVideoSteps(newSteps);
1977
- const desiredMap = /* @__PURE__ */ new Map();
1978
- for (const step of enabledSteps) desiredMap.set(step.addonId, step);
1979
- for (const [stepId, loaded] of this.engines) if (!desiredMap.has(stepId)) {
1980
- this.log.info("Unloading ONNX engine for step", { meta: { step: stepId } });
1981
- await loaded.engine.dispose();
1982
- this.engines.delete(stepId);
1983
- }
1984
- for (const [stepId, step] of desiredMap) {
1985
- const existing = this.engines.get(stepId);
1986
- if (existing && existing.modelId === step.modelId) continue;
1987
- if (existing) {
1988
- this.log.info("Replacing ONNX engine for step", { meta: {
1989
- step: stepId,
1990
- fromModelId: existing.modelId,
1991
- toModelId: step.modelId
1992
- } });
1993
- await existing.engine.dispose();
1994
- } else this.log.info("Loading ONNX engine for step", { meta: {
1995
- step: stepId,
1996
- modelId: step.modelId
1997
- } });
1998
- const modelEntry = getStepDefinition(stepId).models.find((m) => m.id === step.modelId);
1999
- if (!modelEntry) throw new Error(`Model "${step.modelId}" not found in step "${stepId}" catalog`);
2000
- const modelPath = this.resolveModelPath(stepId, step.modelId);
2001
- const meta = {
2002
- inputSize: modelEntry.inputSize,
2003
- inputNormalization: modelEntry.inputNormalization ?? "zero-one",
2004
- inputLayout: modelEntry.inputLayout ?? "nchw",
2005
- preprocessMode: modelEntry.preprocessMode ?? "letterbox"
2006
- };
2007
- const engine = await this.createEngine(modelPath, this.backend, meta, this.log.child(stepId));
2008
- this.engines.set(stepId, {
2009
- stepId,
2010
- modelId: step.modelId,
2011
- engine
2012
- });
2013
- }
2014
- }
2015
- /**
2016
- * Get an IInferenceEngine for a step.
2017
- * @throws if the step is not loaded.
2018
- */
2019
- getEngine(stepId) {
2020
- const loaded = this.engines.get(stepId);
2021
- if (!loaded) throw new Error(`ONNX engine for step "${stepId}" is not loaded`);
2022
- return loaded.engine;
2023
- }
2024
- isLoaded(stepId) {
2025
- return this.engines.has(stepId);
2026
- }
2027
- isLoadedWithModel(stepId, modelId) {
2028
- const loaded = this.engines.get(stepId);
2029
- return loaded !== void 0 && loaded.modelId === modelId;
2030
- }
2031
- async loadAdditional(steps) {
2032
- const enabledSteps = flattenEnabledVideoSteps(steps);
2033
- for (const step of enabledSteps) {
2034
- const existing = this.engines.get(step.addonId);
2035
- if (existing && existing.modelId === step.modelId) continue;
2036
- if (existing) {
2037
- this.log.info("Replacing ONNX engine for step", { meta: {
2038
- step: step.addonId,
2039
- fromModelId: existing.modelId,
2040
- toModelId: step.modelId
2041
- } });
2042
- await existing.engine.dispose();
2043
- } else this.log.info("Loading additional ONNX engine for step", { meta: {
2044
- step: step.addonId,
2045
- modelId: step.modelId
2046
- } });
2047
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
2048
- if (!modelEntry) throw new Error(`Model "${step.modelId}" not found in step "${step.addonId}" catalog`);
2049
- const modelPath = this.resolveModelPath(step.addonId, step.modelId);
2050
- const meta = {
2051
- inputSize: modelEntry.inputSize,
2052
- inputNormalization: modelEntry.inputNormalization ?? "zero-one",
2053
- inputLayout: modelEntry.inputLayout ?? "nchw",
2054
- preprocessMode: modelEntry.preprocessMode ?? "letterbox"
2055
- };
2056
- const engine = await this.createEngine(modelPath, this.backend, meta, this.log.child(step.addonId));
2057
- this.engines.set(step.addonId, {
2058
- stepId: step.addonId,
2059
- modelId: step.modelId,
2060
- engine
2061
- });
2062
- }
2063
- }
2064
- listLoaded() {
2065
- return [...this.engines.values()].map((l) => ({
2066
- stepId: l.stepId,
2067
- modelId: l.modelId
2068
- }));
2069
- }
2070
- async disposeAll() {
2071
- for (const [, loaded] of this.engines) await loaded.engine.dispose();
2072
- this.engines.clear();
2073
- }
2074
- };
2075
- //#endregion
2076
- //#region src/detection-pipeline/engine/model-shape-validator.ts
2077
- /**
2078
- * Runtime input-shape validator for ONNX models.
2079
- *
2080
- * Reads the actual graph input dimensions from a locally-downloaded
2081
- * ONNX model and compares them against the catalog declaration. Used
2082
- * by the engine factory at load time so a converter regression or a
2083
- * stale catalog entry surfaces as a one-line warning rather than
2084
- * silent incorrect inference (preprocess uses one size, the model
2085
- * expects another → garbage detections).
2086
- *
2087
- * Only ONNX is validated here. CoreML and OpenVINO converters in our
2088
- * pipeline derive shape from the same source ONNX, so a passing ONNX
2089
- * check covers all three formats. If they ever diverge, extend with a
2090
- * format-specific reader (Manifest.json for .mlpackage, root <input>
2091
- * tag for OpenVINO XML).
2092
- */
2093
- /**
2094
- * Probe an ONNX file for its first input's shape. Returns null if the
2095
- * file is missing or unparseable — callers should treat null as
2096
- * "validation skipped" (not a mismatch).
2097
- *
2098
- * Implementation: uses `onnxruntime-node` to create a session and read
2099
- * `inputMetadata`. The session is released immediately after probing.
2100
- */
2101
- async function readOnnxInputShape(onnxPath) {
2102
- if (!fs.existsSync(onnxPath)) return null;
2103
- try {
2104
- const session = await (await import("onnxruntime-node")).InferenceSession.create(onnxPath);
2105
- const firstInputName = session.inputNames[0];
2106
- if (!firstInputName) return null;
2107
- const numeric = (session.inputMetadata?.[firstInputName]?.dimensions ?? []).map((d) => typeof d === "number" ? d : null);
2108
- let height = null;
2109
- let width = null;
2110
- if (numeric.length === 4) {
2111
- const candidates = numeric.map((v, i) => ({
2112
- v,
2113
- i
2114
- })).filter((e) => typeof e.v === "number" && e.v > 4).toSorted((a, b) => b.v - a.v);
2115
- if (candidates.length >= 2) {
2116
- const sorted = candidates.slice(0, 2).toSorted((a, b) => a.i - b.i);
2117
- height = sorted[0]?.v ?? null;
2118
- width = sorted[1]?.v ?? null;
2119
- }
2120
- }
2121
- if (typeof session.release === "function") await session.release();
2122
- return {
2123
- height,
2124
- width
2125
- };
2126
- } catch {
2127
- return null;
2128
- }
2129
- }
2130
- /**
2131
- * Check the catalog `expected` size against the actual ONNX input shape.
2132
- * Returns a ShapeMismatch when the actual shape is known and disagrees;
2133
- * returns null when the file is missing, unparseable, or matches.
2134
- */
2135
- async function validateOnnxInputShape(opts) {
2136
- const actual = await readOnnxInputShape(opts.modelPath);
2137
- if (!actual) return null;
2138
- const matchesH = actual.height === null || actual.height === opts.expected.height;
2139
- const matchesW = actual.width === null || actual.width === opts.expected.width;
2140
- if (matchesH && matchesW) return null;
2141
- return {
2142
- modelId: opts.modelId,
2143
- modelPath: opts.modelPath,
2144
- expected: opts.expected,
2145
- actual
2146
- };
2147
- }
2148
- /**
2149
- * Convenience wrapper: validate, log on mismatch via the supplied
2150
- * logger. Returns true when validation passed (or was skipped) so
2151
- * callers can keep loading; mismatches do not throw — they warn.
2152
- */
2153
- async function checkAndLogModelShape(opts, logger) {
2154
- const mismatch = await validateOnnxInputShape(opts);
2155
- if (!mismatch) return true;
2156
- logger.warn("Model input shape mismatch — catalog declaration disagrees with model file", { meta: {
2157
- modelId: mismatch.modelId,
2158
- expectedWidth: mismatch.expected.width,
2159
- expectedHeight: mismatch.expected.height,
2160
- actualWidth: mismatch.actual.width,
2161
- actualHeight: mismatch.actual.height,
2162
- modelPath: mismatch.modelPath
2163
- } });
2164
- return false;
2165
- }
2166
- //#endregion
2167
1958
  //#region src/detection-pipeline/engine/engine-factory.ts
2168
1959
  var BACKEND_TO_POOL_RUNTIME = {
2169
1960
  coreml: "coreml",
@@ -2180,32 +1971,29 @@ var RUNTIME_TO_FORMAT = {
2180
1971
  var EngineFactory = class {
2181
1972
  pool = null;
2182
1973
  poolManager = null;
2183
- nodeManager = null;
2184
1974
  log;
2185
1975
  opts;
2186
1976
  constructor(opts) {
2187
1977
  this.opts = opts;
2188
1978
  this.log = opts.logger;
2189
1979
  }
2190
- /** Whether this factory uses a Python pool (vs Node.js ONNX). */
1980
+ /** Detection always uses the Python pool. */
2191
1981
  get usesPythonPool() {
2192
- return this.opts.engine.runtime === "python";
1982
+ return true;
2193
1983
  }
2194
1984
  /**
2195
1985
  * Initialize the engine layer and apply initial pipeline config.
2196
1986
  */
2197
1987
  async initialize(steps) {
2198
- if (this.usesPythonPool) await this.initPythonPool(steps);
2199
- else await this.initNodeEngines(steps);
1988
+ await this.initPythonPool(steps);
2200
1989
  }
2201
1990
  /**
2202
1991
  * Apply a new pipeline config (hot swap).
2203
1992
  * Only loads/unloads models that changed.
2204
1993
  */
2205
1994
  async applyConfig(steps) {
2206
- if (this.poolManager) await this.poolManager.applyConfig(steps);
2207
- else if (this.nodeManager) await this.nodeManager.applyConfig(steps);
2208
- else throw new Error("EngineFactory not initialized");
1995
+ if (!this.poolManager) throw new Error("EngineFactory not initialized");
1996
+ await this.poolManager.applyConfig(steps);
2209
1997
  }
2210
1998
  /**
2211
1999
  * Get an IInferenceEngine for a pipeline step. Without `modelId`,
@@ -2216,49 +2004,35 @@ var EngineFactory = class {
2216
2004
  * @throws if step not loaded with the requested (or active) model.
2217
2005
  */
2218
2006
  getEngine(stepId, modelId) {
2219
- if (this.poolManager) return this.poolManager.getHandle(stepId, modelId);
2220
- if (this.nodeManager) return this.nodeManager.getEngine(stepId);
2221
- throw new Error("EngineFactory not initialized");
2007
+ if (!this.poolManager) throw new Error("EngineFactory not initialized");
2008
+ return this.poolManager.getHandle(stepId, modelId);
2222
2009
  }
2223
2010
  /** Check if a step is loaded (any variant). */
2224
2011
  isLoaded(stepId) {
2225
- if (this.poolManager) return this.poolManager.isLoaded(stepId);
2226
- if (this.nodeManager) return this.nodeManager.isLoaded(stepId);
2227
- return false;
2012
+ return this.poolManager?.isLoaded(stepId) ?? false;
2228
2013
  }
2229
2014
  /** Check if a specific (stepId, modelId) variant is resident. */
2230
2015
  isLoadedWithModel(stepId, modelId) {
2231
- if (this.poolManager) return this.poolManager.isLoadedWithModel(stepId, modelId);
2232
- if (this.nodeManager) return this.nodeManager.isLoadedWithModel(stepId, modelId);
2233
- return false;
2016
+ return this.poolManager?.isLoadedWithModel(stepId, modelId) ?? false;
2234
2017
  }
2235
2018
  /**
2236
- * List every loaded (stepId, modelId) variant — the pool path
2237
- * surfaces each warm slot independently (including bench overrides);
2238
- * the Node path surfaces just the active model per step.
2019
+ * List every loaded (stepId, modelId) variant — the pool surfaces each
2020
+ * warm slot independently (including bench overrides).
2239
2021
  */
2240
2022
  listLoaded() {
2241
- if (this.poolManager) return this.poolManager.getLoadedSteps().map((l) => ({
2023
+ if (!this.poolManager) return [];
2024
+ return this.poolManager.getLoadedSteps().map((l) => ({
2242
2025
  stepId: l.stepId,
2243
2026
  modelId: l.modelId,
2244
2027
  active: l.active
2245
2028
  }));
2246
- if (this.nodeManager) return this.nodeManager.listLoaded().map((l) => ({
2247
- ...l,
2248
- active: true
2249
- }));
2250
- return [];
2251
2029
  }
2252
2030
  /** Native pid of the underlying Python pool, if any. */
2253
2031
  getPoolPid() {
2254
2032
  return this.pool?.getPid() ?? null;
2255
2033
  }
2256
2034
  /**
2257
- * Whether this factory exposes the batched fast path
2258
- * (`batchInferRaw`). Only the Python pool path supports it today —
2259
- * Node.js ONNX engines run one inference per call with their own
2260
- * thread pool inside InferenceSession, so batching at this layer
2261
- * would just queue serially.
2035
+ * Whether this factory exposes the batched fast path (`batchInferRaw`).
2262
2036
  */
2263
2037
  supportsBatch() {
2264
2038
  return this.poolManager !== null;
@@ -2272,7 +2046,7 @@ var EngineFactory = class {
2272
2046
  * in one IPC round-trip, amortising the per-call envelope overhead.
2273
2047
  */
2274
2048
  async batchInferRaw(stepId, items, modelId) {
2275
- if (!this.poolManager) throw new Error("EngineFactory.batchInferRaw: pool path not available (Node ONNX factory)");
2049
+ if (!this.poolManager) throw new Error("EngineFactory.batchInferRaw: pool not initialized");
2276
2050
  const idx = this.poolManager.getPoolIndex(stepId, modelId);
2277
2051
  if (idx === null) throw new Error(`EngineFactory.batchInferRaw: step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded`);
2278
2052
  return this.poolManager.getPool().inferBatch(idx, items);
@@ -2294,7 +2068,6 @@ var EngineFactory = class {
2294
2068
  /** Load additional models without unloading existing ones (for benchmark/test). */
2295
2069
  async loadAdditional(steps) {
2296
2070
  if (this.poolManager) await this.poolManager.loadAdditional(steps);
2297
- else if (this.nodeManager) await this.nodeManager.loadAdditional(steps);
2298
2071
  }
2299
2072
  /** Shut down all engines and the pool process. */
2300
2073
  async dispose() {
@@ -2303,14 +2076,10 @@ var EngineFactory = class {
2303
2076
  this.pool = null;
2304
2077
  this.poolManager = null;
2305
2078
  }
2306
- if (this.nodeManager) {
2307
- await this.nodeManager.disposeAll();
2308
- this.nodeManager = null;
2309
- }
2310
2079
  }
2311
2080
  async initPythonPool(steps) {
2312
2081
  const pythonPath = this.opts.pythonPath;
2313
- if (!pythonPath) throw new Error("EngineFactory: pythonPath is required for runtime=\"python\" — the addon must call ctx.deps.ensurePython() and pass the result. The embedded portable Python download likely failed; check the addon logs for the download error.");
2082
+ if (!pythonPath) throw new Error("EngineFactory: pythonPath is required — the addon must call ctx.deps.ensurePython() and pass the result. The embedded portable Python download likely failed; check the addon logs for the download error.");
2314
2083
  const poolRuntime = BACKEND_TO_POOL_RUNTIME[this.opts.engine.backend];
2315
2084
  if (!poolRuntime) throw new Error(`No pool runtime mapping for backend "${this.opts.engine.backend}"`);
2316
2085
  const concurrency = this.opts.concurrency ?? {
@@ -2361,16 +2130,6 @@ var EngineFactory = class {
2361
2130
  const filename = urlParts[urlParts.length - 1] ?? `${modelId}.${format}`;
2362
2131
  const modelPath = `${this.opts.modelsDir}/${filename}`;
2363
2132
  const inputSize = Math.max(modelEntry.inputSize.width, modelEntry.inputSize.height);
2364
- const onnxEntry = modelEntry.formats.onnx;
2365
- if (onnxEntry) {
2366
- const onnxUrlParts = onnxEntry.url.split("/");
2367
- const onnxFilename = onnxUrlParts[onnxUrlParts.length - 1] ?? `${modelId}.onnx`;
2368
- checkAndLogModelShape({
2369
- modelId,
2370
- modelPath: `${this.opts.modelsDir}/${onnxFilename}`,
2371
- expected: modelEntry.inputSize
2372
- }, this.log);
2373
- }
2374
2133
  let labels = def.labels;
2375
2134
  if (!labels && modelEntry.extraFiles) {
2376
2135
  const labelsFile = modelEntry.extraFiles.find((f) => f.filename.endsWith("-labels.json"));
@@ -2397,20 +2156,6 @@ var EngineFactory = class {
2397
2156
  device: this.opts.engine.device ?? (poolRuntime === "coreml" ? "all" : void 0)
2398
2157
  };
2399
2158
  }
2400
- async initNodeEngines(steps) {
2401
- if (!this.opts.createNodeEngine) throw new Error("createNodeEngine function required for nodejs+onnx backend");
2402
- this.nodeManager = new NodeEngineManager(this.opts.engine.backend, (stepId, modelId) => this.resolveOnnxModelPath(stepId, modelId), this.opts.createNodeEngine, this.log.child("onnx-mgr"));
2403
- await this.nodeManager.applyConfig(steps);
2404
- }
2405
- resolveOnnxModelPath(stepId, modelId) {
2406
- const modelEntry = getStepDefinition(stepId).models.find((m) => m.id === modelId);
2407
- if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog`);
2408
- const onnxFormat = modelEntry.formats["onnx"];
2409
- if (!onnxFormat) throw new Error(`Model "${modelId}" has no ONNX format`);
2410
- const urlParts = onnxFormat.url.split("/");
2411
- const filename = urlParts[urlParts.length - 1] ?? `${modelId}.onnx`;
2412
- return `${this.opts.modelsDir}/${filename}`;
2413
- }
2414
2159
  };
2415
2160
  //#endregion
2416
2161
  //#region src/detection-pipeline/postprocess/dispatch.ts
@@ -4120,10 +3865,23 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4120
3865
  device: "cuda"
4121
3866
  };
4122
3867
  } catch {}
3868
+ try {
3869
+ const fsmod = __require("node:fs");
3870
+ const isIntel = __require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
3871
+ const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
3872
+ const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
3873
+ if (isIntel && (hasIgpu || hasNpu)) return {
3874
+ runtime: "python",
3875
+ backend: "openvino",
3876
+ format: "openvino",
3877
+ device: "auto"
3878
+ };
3879
+ } catch {}
4123
3880
  return {
4124
- runtime: "node",
4125
- backend: "cpu",
4126
- format: "onnx"
3881
+ runtime: "python",
3882
+ backend: "onnx",
3883
+ format: "onnx",
3884
+ device: "cpu"
4127
3885
  };
4128
3886
  }
4129
3887
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
@@ -4144,18 +3902,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4144
3902
  return this.getAvailableEnginesWithDevices().map((e) => e.engine);
4145
3903
  }
4146
3904
  getAvailableEnginesWithDevices() {
4147
- const engines = [{
4148
- engine: {
4149
- runtime: "node",
4150
- backend: "cpu",
4151
- format: "onnx"
4152
- },
4153
- devices: [{
4154
- id: "cpu",
4155
- label: "CPU"
4156
- }],
4157
- defaultDevice: "cpu"
4158
- }];
3905
+ const engines = [];
4159
3906
  if (process.platform === "darwin") engines.push({
4160
3907
  engine: {
4161
3908
  runtime: "python",
@@ -4208,11 +3955,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4208
3955
  if (audioDef.models.some((m) => m.formats[format])) {
4209
3956
  const modelId = getDefaultModelForFormat("audio-classifier", format);
4210
3957
  const audioEngine = modelId === "apple-soundanalysis" ? {
4211
- runtime: "node",
3958
+ runtime: "python",
4212
3959
  backend: "coreml",
4213
3960
  format: "coreml"
4214
3961
  } : {
4215
- runtime: "node",
3962
+ runtime: "python",
4216
3963
  backend: "cpu",
4217
3964
  format: "onnx"
4218
3965
  };
@@ -5381,18 +5128,19 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5381
5128
  if (typeof runtime === "string" && typeof backend === "string" && runtime && backend) {
5382
5129
  const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5383
5130
  const detected = DetectionPipelineProvider.detectBestEngine();
5384
- if (runtime === "python" && !DetectionPipelineProvider.isPythonBackendAvailable(backend)) {
5131
+ if (runtime === "python" && !DetectionPipelineProvider.isPythonBackendAvailable(backend, this.executorOptions.pythonPath ?? "")) {
5385
5132
  this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5386
5133
  stored: `${runtime}/${backend}`,
5387
5134
  fallback: `${detected.runtime}/${detected.backend}`
5388
5135
  } });
5389
5136
  return detected;
5390
5137
  }
5138
+ const migratedBackend = runtime === "node" && backend === "cpu" ? "onnx" : backend;
5391
5139
  const device = storedDevice || detected.device;
5392
5140
  return {
5393
- runtime: runtime === "node" ? "node" : "python",
5394
- backend,
5395
- format: backendToFormat(backend),
5141
+ runtime: "python",
5142
+ backend: migratedBackend,
5143
+ format: backendToFormat(migratedBackend),
5396
5144
  ...device ? { device } : {}
5397
5145
  };
5398
5146
  }
@@ -5401,13 +5149,21 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5401
5149
  }
5402
5150
  /**
5403
5151
  * Synchronous availability probe for a Python inference backend. Runs a
5404
- * short `python3 -c "import <mod>"` with a 3s timeout. Used at
5152
+ * short `<python> -c "import <mod>"` with a 3s timeout. Used at
5405
5153
  * `loadEngine` time to reject a persisted backend choice the Python
5406
5154
  * interpreter on this host can't actually import — without this the
5407
5155
  * detection-pipeline child process exits with code 1 the moment
5408
5156
  * `inference_pool.py` hits its `from openvino.runtime import Core`.
5157
+ *
5158
+ * MUST probe the EMBEDDED portable interpreter (`pythonPath`, resolved via
5159
+ * `ctx.deps.ensurePython()`) — that's where `installPythonRequirements`
5160
+ * puts the backend packages and what `SharedInferencePool` actually spawns.
5161
+ * Probing the system `python3` checks the wrong site-packages (and on a
5162
+ * slim container there is no system python3 at all → every backend would
5163
+ * be reported unavailable, silently disabling ML). Falls back to `python3`
5164
+ * only when no embedded interpreter is resolved.
5409
5165
  */
5410
- static isPythonBackendAvailable(backend) {
5166
+ static isPythonBackendAvailable(backend, pythonPath) {
5411
5167
  const mod = {
5412
5168
  coreml: "coremltools",
5413
5169
  openvino: "openvino.runtime",
@@ -5417,7 +5173,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5417
5173
  if (!mod) return false;
5418
5174
  try {
5419
5175
  const { execFileSync } = __require("node:child_process");
5420
- execFileSync("python3", ["-c", `import ${mod}`], {
5176
+ execFileSync(pythonPath || "python3", ["-c", `import ${mod}`], {
5421
5177
  timeout: 3e3,
5422
5178
  stdio: "pipe"
5423
5179
  });
@@ -5808,11 +5564,11 @@ function buildDefaultStepTree(format) {
5808
5564
  makeStep("instance-segmentation", [], { enabled: false })
5809
5565
  ].filter((s) => s !== null));
5810
5566
  const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
5811
- runtime: "node",
5567
+ runtime: "python",
5812
5568
  backend: "coreml",
5813
5569
  format: "coreml"
5814
5570
  } : {
5815
- runtime: "node",
5571
+ runtime: "python",
5816
5572
  backend: "cpu",
5817
5573
  format: "onnx"
5818
5574
  };
@@ -5949,33 +5705,46 @@ function resolveAddonPythonDir() {
5949
5705
  for (const c of candidates) if (fs.existsSync(path$1.join(c, "inference_pool.py"))) return c;
5950
5706
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
5951
5707
  }
5708
+ /**
5709
+ * Returns true when proactive OpenVINO installation is warranted.
5710
+ *
5711
+ * Gate: Linux host + Intel iGPU or Intel NPU detected.
5712
+ *
5713
+ * Intentionally addon-local — addons are self-contained and cannot import
5714
+ * `@camstack/system` internals (see architecture invariant: no cross-addon
5715
+ * imports). This mirrors the logic in `resolveRuntimePackages` from that
5716
+ * package without importing it.
5717
+ *
5718
+ * darwin is never true: coremltools handles Apple Silicon + Intel Mac.
5719
+ * linux-amd (or any non-Intel linux GPU) is never true: the openvino
5720
+ * package has no AMD backend and would fail at import time.
5721
+ *
5722
+ * @internal exported only for unit tests in the same package
5723
+ */
5724
+ function shouldInstallOpenvino(hardware) {
5725
+ if (hardware.platform !== "linux") return false;
5726
+ const hasIntelGpu = hardware.gpu?.type === "intel";
5727
+ const hasIntelNpu = hardware.npu?.type === "intel-npu";
5728
+ return hasIntelGpu || hasIntelNpu;
5729
+ }
5952
5730
  var RUNTIMES = [{
5953
5731
  value: "python",
5954
5732
  label: "Python (CoreML / OpenVINO / ONNX Runtime)"
5955
- }, {
5956
- value: "node",
5957
- label: "Node.js (onnxruntime-node)"
5958
5733
  }];
5959
- var BACKENDS_BY_RUNTIME = {
5960
- python: [
5961
- {
5962
- value: "coreml",
5963
- label: "CoreML"
5964
- },
5965
- {
5966
- value: "openvino",
5967
- label: "OpenVINO"
5968
- },
5969
- {
5970
- value: "onnx",
5971
- label: "ONNX Runtime"
5972
- }
5973
- ],
5974
- node: [{
5975
- value: "cpu",
5976
- label: "CPU (onnxruntime-node)"
5977
- }]
5978
- };
5734
+ var BACKENDS_BY_RUNTIME = { python: [
5735
+ {
5736
+ value: "coreml",
5737
+ label: "CoreML"
5738
+ },
5739
+ {
5740
+ value: "openvino",
5741
+ label: "OpenVINO"
5742
+ },
5743
+ {
5744
+ value: "onnx",
5745
+ label: "ONNX Runtime"
5746
+ }
5747
+ ] };
5979
5748
  var DEVICES_BY_BACKEND = {
5980
5749
  coreml: [
5981
5750
  {
@@ -6327,7 +6096,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6327
6096
  ...stored,
6328
6097
  ...overlay
6329
6098
  } : stored;
6330
- const runtime = merged.engineRuntime === "node" ? "node" : "python";
6099
+ const runtime = "python";
6331
6100
  const availableBackends = await this.resolveAvailableBackends(ctx, runtime);
6332
6101
  const runtimeBackends = availableBackends.length > 0 ? BACKENDS_BY_RUNTIME[runtime].filter((b) => availableBackends.includes(b.value)) : BACKENDS_BY_RUNTIME[runtime];
6333
6102
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
@@ -6447,6 +6216,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6447
6216
  const pythonPath = await this.ctx.deps.ensurePython();
6448
6217
  if (pythonPath) this.pythonPath = pythonPath;
6449
6218
  else this.ctx.logger.warn("Embedded Python unavailable — runtime=\"python\" pipelines will fail until the download succeeds.");
6219
+ await this.proactivelyInstallOpenvino();
6450
6220
  const effectiveTuning = this.resolveBackendTuning();
6451
6221
  this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
6452
6222
  concurrency: effectiveTuning.concurrency,
@@ -6512,6 +6282,32 @@ var DetectionPipelineAddon = class extends BaseAddon {
6512
6282
  getModelCatalog() {
6513
6283
  return ALL_STEPS.flatMap((s) => [...s.models]);
6514
6284
  }
6285
+ /**
6286
+ * Proactively install the OpenVINO Python package when Intel hardware
6287
+ * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6288
+ * provider is constructed, so the module is available when the platform
6289
+ * probe runs its next `import openvino.runtime` check.
6290
+ *
6291
+ * Failure is non-fatal: a warning is logged and the addon continues with
6292
+ * the onnx-cpu baseline. The hardware query itself is also best-effort —
6293
+ * if the platform-probe cap isn't wired yet the error is caught here.
6294
+ */
6295
+ async proactivelyInstallOpenvino() {
6296
+ if (!this.pythonAddonDir || !this.pythonPath) return;
6297
+ try {
6298
+ const hardware = await this.ctx.api.platformProbe.getHardware.query();
6299
+ if (!shouldInstallOpenvino(hardware)) return;
6300
+ this.ctx.logger.info("Intel hardware detected — proactively installing OpenVINO Python package", { meta: {
6301
+ gpu: hardware.gpu?.type ?? null,
6302
+ npu: hardware.npu?.type ?? null
6303
+ } });
6304
+ const requirementsFile = path$1.join(this.pythonAddonDir, "requirements-openvino.txt");
6305
+ await this.ctx.deps.installPythonRequirements(requirementsFile);
6306
+ this.ctx.logger.info("Proactive OpenVINO install complete");
6307
+ } catch (err) {
6308
+ this.ctx.logger.warn("Proactive OpenVINO install failed — falling back to onnx-cpu baseline", { meta: { error: err instanceof Error ? err.message : String(err) } });
6309
+ }
6310
+ }
6515
6311
  async onShutdown() {
6516
6312
  if (this.engineMetricsTimer) {
6517
6313
  clearInterval(this.engineMetricsTimer);
@@ -6600,4 +6396,4 @@ var DetectionPipelineAddon = class extends BaseAddon {
6600
6396
  }
6601
6397
  };
6602
6398
  //#endregion
6603
- export { ALL_STEPS, DetectionPipelineProvider, backendToFormat, DetectionPipelineAddon as default, getDefaultModelForFormat, getStepDefinition };
6399
+ export { ALL_STEPS, DetectionPipelineProvider, backendToFormat, DetectionPipelineAddon as default, getDefaultModelForFormat, getStepDefinition, shouldInstallOpenvino };
@@ -13314,7 +13314,7 @@ method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchem
13314
13314
  * rebuilds without manual reload.
13315
13315
  *
13316
13316
  * The hub-local builtin `addon-pages-aggregator` (see
13317
- * `@camstack/core/builtins/addon-pages-aggregator`) registers the
13317
+ * `@camstack/system/builtins/addon-pages-aggregator`) registers the
13318
13318
  * provider. Splitting the public aggregator from the raw collection
13319
13319
  * keeps both ends in codegen — there's no hand-written
13320
13320
  * `addon-pages.router.ts` wrapper anymore.
@@ -13503,7 +13503,7 @@ var addonWidgetsSourceCapability = {
13503
13503
  * manual reload — same scheme used by `addon-pages`.
13504
13504
  *
13505
13505
  * The hub-local builtin `addon-widgets-aggregator` (see
13506
- * `@camstack/core/builtins/addon-widgets-aggregator`) registers the
13506
+ * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
13507
13507
  * provider. Splitting the public aggregator from the raw collection
13508
13508
  * keeps both ends in codegen — there's no hand-written wrapper.
13509
13509
  */
@@ -16569,7 +16569,7 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
16569
16569
  * clients — they reverse-connect to the hub. Exposing their interfaces
16570
16570
  * via the same surface would leak internal topology with no upside.
16571
16571
  *
16572
- * Implementation in `@camstack/core/builtins/local-network/`.
16572
+ * Implementation in `@camstack/system/builtins/local-network/`.
16573
16573
  */
16574
16574
  /** Coarse classification derived from the interface name + IP range. */
16575
16575
  var InterfaceKindEnum = _enum([
@@ -17116,7 +17116,7 @@ method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), met
17116
17116
  * jitter, and observed/peak bandwidth per device + per client.
17117
17117
  *
17118
17118
  * Implementation lives in the server's `NetworkQualityService` (thin
17119
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/core`).
17119
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
17120
17120
  * The provider is registered from `trpc.router.ts` against the existing
17121
17121
  * service instance — no addon owns this state.
17122
17122
  *