@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.
@@ -3,14 +3,14 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-7ewQjTle.js");
6
+ const require_dist = require("../dist-C1goFC50.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
10
10
  node_path = require_chunk.__toESM(node_path);
11
11
  let node_os = require("node:os");
12
12
  node_os = require_chunk.__toESM(node_os);
13
- let _camstack_core = require("@camstack/core");
13
+ let _camstack_system = require("@camstack/system");
14
14
  let node_child_process = require("node:child_process");
15
15
  let sharp = require("sharp");
16
16
  sharp = require_chunk.__toESM(sharp);
@@ -1963,215 +1963,6 @@ function getDefaultModelForFormat(stepId, format) {
1963
1963
  })[0].id;
1964
1964
  }
1965
1965
  //#endregion
1966
- //#region src/detection-pipeline/engine/node-engine-manager.ts
1967
- var NodeEngineManager = class {
1968
- backend;
1969
- resolveModelPath;
1970
- createEngine;
1971
- engines = /* @__PURE__ */ new Map();
1972
- log;
1973
- constructor(backend, resolveModelPath, createEngine, logger) {
1974
- this.backend = backend;
1975
- this.resolveModelPath = resolveModelPath;
1976
- this.createEngine = createEngine;
1977
- this.log = logger;
1978
- }
1979
- /**
1980
- * Apply a new pipeline configuration.
1981
- * Loads/unloads engines for steps that changed.
1982
- */
1983
- async applyConfig(newSteps) {
1984
- const enabledSteps = flattenEnabledVideoSteps(newSteps);
1985
- const desiredMap = /* @__PURE__ */ new Map();
1986
- for (const step of enabledSteps) desiredMap.set(step.addonId, step);
1987
- for (const [stepId, loaded] of this.engines) if (!desiredMap.has(stepId)) {
1988
- this.log.info("Unloading ONNX engine for step", { meta: { step: stepId } });
1989
- await loaded.engine.dispose();
1990
- this.engines.delete(stepId);
1991
- }
1992
- for (const [stepId, step] of desiredMap) {
1993
- const existing = this.engines.get(stepId);
1994
- if (existing && existing.modelId === step.modelId) continue;
1995
- if (existing) {
1996
- this.log.info("Replacing ONNX engine for step", { meta: {
1997
- step: stepId,
1998
- fromModelId: existing.modelId,
1999
- toModelId: step.modelId
2000
- } });
2001
- await existing.engine.dispose();
2002
- } else this.log.info("Loading ONNX engine for step", { meta: {
2003
- step: stepId,
2004
- modelId: step.modelId
2005
- } });
2006
- const modelEntry = getStepDefinition(stepId).models.find((m) => m.id === step.modelId);
2007
- if (!modelEntry) throw new Error(`Model "${step.modelId}" not found in step "${stepId}" catalog`);
2008
- const modelPath = this.resolveModelPath(stepId, step.modelId);
2009
- const meta = {
2010
- inputSize: modelEntry.inputSize,
2011
- inputNormalization: modelEntry.inputNormalization ?? "zero-one",
2012
- inputLayout: modelEntry.inputLayout ?? "nchw",
2013
- preprocessMode: modelEntry.preprocessMode ?? "letterbox"
2014
- };
2015
- const engine = await this.createEngine(modelPath, this.backend, meta, this.log.child(stepId));
2016
- this.engines.set(stepId, {
2017
- stepId,
2018
- modelId: step.modelId,
2019
- engine
2020
- });
2021
- }
2022
- }
2023
- /**
2024
- * Get an IInferenceEngine for a step.
2025
- * @throws if the step is not loaded.
2026
- */
2027
- getEngine(stepId) {
2028
- const loaded = this.engines.get(stepId);
2029
- if (!loaded) throw new Error(`ONNX engine for step "${stepId}" is not loaded`);
2030
- return loaded.engine;
2031
- }
2032
- isLoaded(stepId) {
2033
- return this.engines.has(stepId);
2034
- }
2035
- isLoadedWithModel(stepId, modelId) {
2036
- const loaded = this.engines.get(stepId);
2037
- return loaded !== void 0 && loaded.modelId === modelId;
2038
- }
2039
- async loadAdditional(steps) {
2040
- const enabledSteps = flattenEnabledVideoSteps(steps);
2041
- for (const step of enabledSteps) {
2042
- const existing = this.engines.get(step.addonId);
2043
- if (existing && existing.modelId === step.modelId) continue;
2044
- if (existing) {
2045
- this.log.info("Replacing ONNX engine for step", { meta: {
2046
- step: step.addonId,
2047
- fromModelId: existing.modelId,
2048
- toModelId: step.modelId
2049
- } });
2050
- await existing.engine.dispose();
2051
- } else this.log.info("Loading additional ONNX engine for step", { meta: {
2052
- step: step.addonId,
2053
- modelId: step.modelId
2054
- } });
2055
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
2056
- if (!modelEntry) throw new Error(`Model "${step.modelId}" not found in step "${step.addonId}" catalog`);
2057
- const modelPath = this.resolveModelPath(step.addonId, step.modelId);
2058
- const meta = {
2059
- inputSize: modelEntry.inputSize,
2060
- inputNormalization: modelEntry.inputNormalization ?? "zero-one",
2061
- inputLayout: modelEntry.inputLayout ?? "nchw",
2062
- preprocessMode: modelEntry.preprocessMode ?? "letterbox"
2063
- };
2064
- const engine = await this.createEngine(modelPath, this.backend, meta, this.log.child(step.addonId));
2065
- this.engines.set(step.addonId, {
2066
- stepId: step.addonId,
2067
- modelId: step.modelId,
2068
- engine
2069
- });
2070
- }
2071
- }
2072
- listLoaded() {
2073
- return [...this.engines.values()].map((l) => ({
2074
- stepId: l.stepId,
2075
- modelId: l.modelId
2076
- }));
2077
- }
2078
- async disposeAll() {
2079
- for (const [, loaded] of this.engines) await loaded.engine.dispose();
2080
- this.engines.clear();
2081
- }
2082
- };
2083
- //#endregion
2084
- //#region src/detection-pipeline/engine/model-shape-validator.ts
2085
- /**
2086
- * Runtime input-shape validator for ONNX models.
2087
- *
2088
- * Reads the actual graph input dimensions from a locally-downloaded
2089
- * ONNX model and compares them against the catalog declaration. Used
2090
- * by the engine factory at load time so a converter regression or a
2091
- * stale catalog entry surfaces as a one-line warning rather than
2092
- * silent incorrect inference (preprocess uses one size, the model
2093
- * expects another → garbage detections).
2094
- *
2095
- * Only ONNX is validated here. CoreML and OpenVINO converters in our
2096
- * pipeline derive shape from the same source ONNX, so a passing ONNX
2097
- * check covers all three formats. If they ever diverge, extend with a
2098
- * format-specific reader (Manifest.json for .mlpackage, root <input>
2099
- * tag for OpenVINO XML).
2100
- */
2101
- /**
2102
- * Probe an ONNX file for its first input's shape. Returns null if the
2103
- * file is missing or unparseable — callers should treat null as
2104
- * "validation skipped" (not a mismatch).
2105
- *
2106
- * Implementation: uses `onnxruntime-node` to create a session and read
2107
- * `inputMetadata`. The session is released immediately after probing.
2108
- */
2109
- async function readOnnxInputShape(onnxPath) {
2110
- if (!node_fs.existsSync(onnxPath)) return null;
2111
- try {
2112
- const session = await (await import("onnxruntime-node")).InferenceSession.create(onnxPath);
2113
- const firstInputName = session.inputNames[0];
2114
- if (!firstInputName) return null;
2115
- const numeric = (session.inputMetadata?.[firstInputName]?.dimensions ?? []).map((d) => typeof d === "number" ? d : null);
2116
- let height = null;
2117
- let width = null;
2118
- if (numeric.length === 4) {
2119
- const candidates = numeric.map((v, i) => ({
2120
- v,
2121
- i
2122
- })).filter((e) => typeof e.v === "number" && e.v > 4).toSorted((a, b) => b.v - a.v);
2123
- if (candidates.length >= 2) {
2124
- const sorted = candidates.slice(0, 2).toSorted((a, b) => a.i - b.i);
2125
- height = sorted[0]?.v ?? null;
2126
- width = sorted[1]?.v ?? null;
2127
- }
2128
- }
2129
- if (typeof session.release === "function") await session.release();
2130
- return {
2131
- height,
2132
- width
2133
- };
2134
- } catch {
2135
- return null;
2136
- }
2137
- }
2138
- /**
2139
- * Check the catalog `expected` size against the actual ONNX input shape.
2140
- * Returns a ShapeMismatch when the actual shape is known and disagrees;
2141
- * returns null when the file is missing, unparseable, or matches.
2142
- */
2143
- async function validateOnnxInputShape(opts) {
2144
- const actual = await readOnnxInputShape(opts.modelPath);
2145
- if (!actual) return null;
2146
- const matchesH = actual.height === null || actual.height === opts.expected.height;
2147
- const matchesW = actual.width === null || actual.width === opts.expected.width;
2148
- if (matchesH && matchesW) return null;
2149
- return {
2150
- modelId: opts.modelId,
2151
- modelPath: opts.modelPath,
2152
- expected: opts.expected,
2153
- actual
2154
- };
2155
- }
2156
- /**
2157
- * Convenience wrapper: validate, log on mismatch via the supplied
2158
- * logger. Returns true when validation passed (or was skipped) so
2159
- * callers can keep loading; mismatches do not throw — they warn.
2160
- */
2161
- async function checkAndLogModelShape(opts, logger) {
2162
- const mismatch = await validateOnnxInputShape(opts);
2163
- if (!mismatch) return true;
2164
- logger.warn("Model input shape mismatch — catalog declaration disagrees with model file", { meta: {
2165
- modelId: mismatch.modelId,
2166
- expectedWidth: mismatch.expected.width,
2167
- expectedHeight: mismatch.expected.height,
2168
- actualWidth: mismatch.actual.width,
2169
- actualHeight: mismatch.actual.height,
2170
- modelPath: mismatch.modelPath
2171
- } });
2172
- return false;
2173
- }
2174
- //#endregion
2175
1966
  //#region src/detection-pipeline/engine/engine-factory.ts
2176
1967
  var BACKEND_TO_POOL_RUNTIME = {
2177
1968
  coreml: "coreml",
@@ -2188,32 +1979,29 @@ var RUNTIME_TO_FORMAT = {
2188
1979
  var EngineFactory = class {
2189
1980
  pool = null;
2190
1981
  poolManager = null;
2191
- nodeManager = null;
2192
1982
  log;
2193
1983
  opts;
2194
1984
  constructor(opts) {
2195
1985
  this.opts = opts;
2196
1986
  this.log = opts.logger;
2197
1987
  }
2198
- /** Whether this factory uses a Python pool (vs Node.js ONNX). */
1988
+ /** Detection always uses the Python pool. */
2199
1989
  get usesPythonPool() {
2200
- return this.opts.engine.runtime === "python";
1990
+ return true;
2201
1991
  }
2202
1992
  /**
2203
1993
  * Initialize the engine layer and apply initial pipeline config.
2204
1994
  */
2205
1995
  async initialize(steps) {
2206
- if (this.usesPythonPool) await this.initPythonPool(steps);
2207
- else await this.initNodeEngines(steps);
1996
+ await this.initPythonPool(steps);
2208
1997
  }
2209
1998
  /**
2210
1999
  * Apply a new pipeline config (hot swap).
2211
2000
  * Only loads/unloads models that changed.
2212
2001
  */
2213
2002
  async applyConfig(steps) {
2214
- if (this.poolManager) await this.poolManager.applyConfig(steps);
2215
- else if (this.nodeManager) await this.nodeManager.applyConfig(steps);
2216
- else throw new Error("EngineFactory not initialized");
2003
+ if (!this.poolManager) throw new Error("EngineFactory not initialized");
2004
+ await this.poolManager.applyConfig(steps);
2217
2005
  }
2218
2006
  /**
2219
2007
  * Get an IInferenceEngine for a pipeline step. Without `modelId`,
@@ -2224,49 +2012,35 @@ var EngineFactory = class {
2224
2012
  * @throws if step not loaded with the requested (or active) model.
2225
2013
  */
2226
2014
  getEngine(stepId, modelId) {
2227
- if (this.poolManager) return this.poolManager.getHandle(stepId, modelId);
2228
- if (this.nodeManager) return this.nodeManager.getEngine(stepId);
2229
- throw new Error("EngineFactory not initialized");
2015
+ if (!this.poolManager) throw new Error("EngineFactory not initialized");
2016
+ return this.poolManager.getHandle(stepId, modelId);
2230
2017
  }
2231
2018
  /** Check if a step is loaded (any variant). */
2232
2019
  isLoaded(stepId) {
2233
- if (this.poolManager) return this.poolManager.isLoaded(stepId);
2234
- if (this.nodeManager) return this.nodeManager.isLoaded(stepId);
2235
- return false;
2020
+ return this.poolManager?.isLoaded(stepId) ?? false;
2236
2021
  }
2237
2022
  /** Check if a specific (stepId, modelId) variant is resident. */
2238
2023
  isLoadedWithModel(stepId, modelId) {
2239
- if (this.poolManager) return this.poolManager.isLoadedWithModel(stepId, modelId);
2240
- if (this.nodeManager) return this.nodeManager.isLoadedWithModel(stepId, modelId);
2241
- return false;
2024
+ return this.poolManager?.isLoadedWithModel(stepId, modelId) ?? false;
2242
2025
  }
2243
2026
  /**
2244
- * List every loaded (stepId, modelId) variant — the pool path
2245
- * surfaces each warm slot independently (including bench overrides);
2246
- * the Node path surfaces just the active model per step.
2027
+ * List every loaded (stepId, modelId) variant — the pool surfaces each
2028
+ * warm slot independently (including bench overrides).
2247
2029
  */
2248
2030
  listLoaded() {
2249
- if (this.poolManager) return this.poolManager.getLoadedSteps().map((l) => ({
2031
+ if (!this.poolManager) return [];
2032
+ return this.poolManager.getLoadedSteps().map((l) => ({
2250
2033
  stepId: l.stepId,
2251
2034
  modelId: l.modelId,
2252
2035
  active: l.active
2253
2036
  }));
2254
- if (this.nodeManager) return this.nodeManager.listLoaded().map((l) => ({
2255
- ...l,
2256
- active: true
2257
- }));
2258
- return [];
2259
2037
  }
2260
2038
  /** Native pid of the underlying Python pool, if any. */
2261
2039
  getPoolPid() {
2262
2040
  return this.pool?.getPid() ?? null;
2263
2041
  }
2264
2042
  /**
2265
- * Whether this factory exposes the batched fast path
2266
- * (`batchInferRaw`). Only the Python pool path supports it today —
2267
- * Node.js ONNX engines run one inference per call with their own
2268
- * thread pool inside InferenceSession, so batching at this layer
2269
- * would just queue serially.
2043
+ * Whether this factory exposes the batched fast path (`batchInferRaw`).
2270
2044
  */
2271
2045
  supportsBatch() {
2272
2046
  return this.poolManager !== null;
@@ -2280,7 +2054,7 @@ var EngineFactory = class {
2280
2054
  * in one IPC round-trip, amortising the per-call envelope overhead.
2281
2055
  */
2282
2056
  async batchInferRaw(stepId, items, modelId) {
2283
- if (!this.poolManager) throw new Error("EngineFactory.batchInferRaw: pool path not available (Node ONNX factory)");
2057
+ if (!this.poolManager) throw new Error("EngineFactory.batchInferRaw: pool not initialized");
2284
2058
  const idx = this.poolManager.getPoolIndex(stepId, modelId);
2285
2059
  if (idx === null) throw new Error(`EngineFactory.batchInferRaw: step "${stepId}"${modelId ? ` (model "${modelId}")` : ""} is not loaded`);
2286
2060
  return this.poolManager.getPool().inferBatch(idx, items);
@@ -2302,7 +2076,6 @@ var EngineFactory = class {
2302
2076
  /** Load additional models without unloading existing ones (for benchmark/test). */
2303
2077
  async loadAdditional(steps) {
2304
2078
  if (this.poolManager) await this.poolManager.loadAdditional(steps);
2305
- else if (this.nodeManager) await this.nodeManager.loadAdditional(steps);
2306
2079
  }
2307
2080
  /** Shut down all engines and the pool process. */
2308
2081
  async dispose() {
@@ -2311,14 +2084,10 @@ var EngineFactory = class {
2311
2084
  this.pool = null;
2312
2085
  this.poolManager = null;
2313
2086
  }
2314
- if (this.nodeManager) {
2315
- await this.nodeManager.disposeAll();
2316
- this.nodeManager = null;
2317
- }
2318
2087
  }
2319
2088
  async initPythonPool(steps) {
2320
2089
  const pythonPath = this.opts.pythonPath;
2321
- 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.");
2090
+ 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.");
2322
2091
  const poolRuntime = BACKEND_TO_POOL_RUNTIME[this.opts.engine.backend];
2323
2092
  if (!poolRuntime) throw new Error(`No pool runtime mapping for backend "${this.opts.engine.backend}"`);
2324
2093
  const concurrency = this.opts.concurrency ?? {
@@ -2369,16 +2138,6 @@ var EngineFactory = class {
2369
2138
  const filename = urlParts[urlParts.length - 1] ?? `${modelId}.${format}`;
2370
2139
  const modelPath = `${this.opts.modelsDir}/${filename}`;
2371
2140
  const inputSize = Math.max(modelEntry.inputSize.width, modelEntry.inputSize.height);
2372
- const onnxEntry = modelEntry.formats.onnx;
2373
- if (onnxEntry) {
2374
- const onnxUrlParts = onnxEntry.url.split("/");
2375
- const onnxFilename = onnxUrlParts[onnxUrlParts.length - 1] ?? `${modelId}.onnx`;
2376
- checkAndLogModelShape({
2377
- modelId,
2378
- modelPath: `${this.opts.modelsDir}/${onnxFilename}`,
2379
- expected: modelEntry.inputSize
2380
- }, this.log);
2381
- }
2382
2141
  let labels = def.labels;
2383
2142
  if (!labels && modelEntry.extraFiles) {
2384
2143
  const labelsFile = modelEntry.extraFiles.find((f) => f.filename.endsWith("-labels.json"));
@@ -2405,20 +2164,6 @@ var EngineFactory = class {
2405
2164
  device: this.opts.engine.device ?? (poolRuntime === "coreml" ? "all" : void 0)
2406
2165
  };
2407
2166
  }
2408
- async initNodeEngines(steps) {
2409
- if (!this.opts.createNodeEngine) throw new Error("createNodeEngine function required for nodejs+onnx backend");
2410
- this.nodeManager = new NodeEngineManager(this.opts.engine.backend, (stepId, modelId) => this.resolveOnnxModelPath(stepId, modelId), this.opts.createNodeEngine, this.log.child("onnx-mgr"));
2411
- await this.nodeManager.applyConfig(steps);
2412
- }
2413
- resolveOnnxModelPath(stepId, modelId) {
2414
- const modelEntry = getStepDefinition(stepId).models.find((m) => m.id === modelId);
2415
- if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog`);
2416
- const onnxFormat = modelEntry.formats["onnx"];
2417
- if (!onnxFormat) throw new Error(`Model "${modelId}" has no ONNX format`);
2418
- const urlParts = onnxFormat.url.split("/");
2419
- const filename = urlParts[urlParts.length - 1] ?? `${modelId}.onnx`;
2420
- return `${this.opts.modelsDir}/${filename}`;
2421
- }
2422
2167
  };
2423
2168
  //#endregion
2424
2169
  //#region src/detection-pipeline/postprocess/dispatch.ts
@@ -4128,10 +3873,23 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4128
3873
  device: "cuda"
4129
3874
  };
4130
3875
  } catch {}
3876
+ try {
3877
+ const fsmod = require("node:fs");
3878
+ const isIntel = require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
3879
+ const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
3880
+ const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
3881
+ if (isIntel && (hasIgpu || hasNpu)) return {
3882
+ runtime: "python",
3883
+ backend: "openvino",
3884
+ format: "openvino",
3885
+ device: "auto"
3886
+ };
3887
+ } catch {}
4131
3888
  return {
4132
- runtime: "node",
4133
- backend: "cpu",
4134
- format: "onnx"
3889
+ runtime: "python",
3890
+ backend: "onnx",
3891
+ format: "onnx",
3892
+ device: "cpu"
4135
3893
  };
4136
3894
  }
4137
3895
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
@@ -4152,18 +3910,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4152
3910
  return this.getAvailableEnginesWithDevices().map((e) => e.engine);
4153
3911
  }
4154
3912
  getAvailableEnginesWithDevices() {
4155
- const engines = [{
4156
- engine: {
4157
- runtime: "node",
4158
- backend: "cpu",
4159
- format: "onnx"
4160
- },
4161
- devices: [{
4162
- id: "cpu",
4163
- label: "CPU"
4164
- }],
4165
- defaultDevice: "cpu"
4166
- }];
3913
+ const engines = [];
4167
3914
  if (process.platform === "darwin") engines.push({
4168
3915
  engine: {
4169
3916
  runtime: "python",
@@ -4216,11 +3963,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4216
3963
  if (audioDef.models.some((m) => m.formats[format])) {
4217
3964
  const modelId = getDefaultModelForFormat("audio-classifier", format);
4218
3965
  const audioEngine = modelId === "apple-soundanalysis" ? {
4219
- runtime: "node",
3966
+ runtime: "python",
4220
3967
  backend: "coreml",
4221
3968
  format: "coreml"
4222
3969
  } : {
4223
- runtime: "node",
3970
+ runtime: "python",
4224
3971
  backend: "cpu",
4225
3972
  format: "onnx"
4226
3973
  };
@@ -4266,7 +4013,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4266
4013
  const formats = {};
4267
4014
  for (const [formatKey, entry] of Object.entries(m.formats)) {
4268
4015
  if (!entry) continue;
4269
- const downloaded = (0, _camstack_core.isModelDownloaded)(this.modelsDir, m, formatKey);
4016
+ const downloaded = (0, _camstack_system.isModelDownloaded)(this.modelsDir, m, formatKey);
4270
4017
  formats[formatKey] = {
4271
4018
  url: entry.url,
4272
4019
  sizeMB: entry.sizeMB,
@@ -4393,7 +4140,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4393
4140
  const { modelId, format, addonId } = input;
4394
4141
  const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4395
4142
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4396
- if (!(0, _camstack_core.deleteModelFromDisk)(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4143
+ if (!(0, _camstack_system.deleteModelFromDisk)(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4397
4144
  this.log.info("Model deleted from disk", { meta: {
4398
4145
  modelId,
4399
4146
  format
@@ -4848,7 +4595,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4848
4595
  const format = this.currentEngine?.format ?? "onnx";
4849
4596
  for (const step of needed) {
4850
4597
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4851
- if (modelEntry && !(0, _camstack_core.isModelDownloaded)(this.modelsDir, modelEntry, format)) {
4598
+ if (modelEntry && !(0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) {
4852
4599
  this.log.info("Downloading model for step", { meta: {
4853
4600
  modelId: step.modelId,
4854
4601
  format,
@@ -4873,7 +4620,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4873
4620
  /** Download a model with retry + exponential backoff */
4874
4621
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
4875
4622
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
4876
- await (0, _camstack_core.ensureModel)(this.modelsDir, entry, format, onProgress);
4623
+ await (0, _camstack_system.ensureModel)(this.modelsDir, entry, format, onProgress);
4877
4624
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
4878
4625
  return;
4879
4626
  } catch (err) {
@@ -5348,11 +5095,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5348
5095
  } });
5349
5096
  continue;
5350
5097
  }
5351
- if (!(0, _camstack_core.isModelDownloaded)(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5098
+ if (!(0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5352
5099
  modelId: step.modelId,
5353
5100
  format
5354
5101
  } });
5355
- downloads.push((0, _camstack_core.ensureModel)(this.modelsDir, modelEntry, format).then(() => {}));
5102
+ downloads.push((0, _camstack_system.ensureModel)(this.modelsDir, modelEntry, format).then(() => {}));
5356
5103
  }
5357
5104
  await Promise.all(downloads);
5358
5105
  await this.ensureBackendDeps(this.currentEngine);
@@ -5389,18 +5136,19 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5389
5136
  if (typeof runtime === "string" && typeof backend === "string" && runtime && backend) {
5390
5137
  const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5391
5138
  const detected = DetectionPipelineProvider.detectBestEngine();
5392
- if (runtime === "python" && !DetectionPipelineProvider.isPythonBackendAvailable(backend)) {
5139
+ if (runtime === "python" && !DetectionPipelineProvider.isPythonBackendAvailable(backend, this.executorOptions.pythonPath ?? "")) {
5393
5140
  this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5394
5141
  stored: `${runtime}/${backend}`,
5395
5142
  fallback: `${detected.runtime}/${detected.backend}`
5396
5143
  } });
5397
5144
  return detected;
5398
5145
  }
5146
+ const migratedBackend = runtime === "node" && backend === "cpu" ? "onnx" : backend;
5399
5147
  const device = storedDevice || detected.device;
5400
5148
  return {
5401
- runtime: runtime === "node" ? "node" : "python",
5402
- backend,
5403
- format: backendToFormat(backend),
5149
+ runtime: "python",
5150
+ backend: migratedBackend,
5151
+ format: backendToFormat(migratedBackend),
5404
5152
  ...device ? { device } : {}
5405
5153
  };
5406
5154
  }
@@ -5409,13 +5157,21 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5409
5157
  }
5410
5158
  /**
5411
5159
  * Synchronous availability probe for a Python inference backend. Runs a
5412
- * short `python3 -c "import <mod>"` with a 3s timeout. Used at
5160
+ * short `<python> -c "import <mod>"` with a 3s timeout. Used at
5413
5161
  * `loadEngine` time to reject a persisted backend choice the Python
5414
5162
  * interpreter on this host can't actually import — without this the
5415
5163
  * detection-pipeline child process exits with code 1 the moment
5416
5164
  * `inference_pool.py` hits its `from openvino.runtime import Core`.
5165
+ *
5166
+ * MUST probe the EMBEDDED portable interpreter (`pythonPath`, resolved via
5167
+ * `ctx.deps.ensurePython()`) — that's where `installPythonRequirements`
5168
+ * puts the backend packages and what `SharedInferencePool` actually spawns.
5169
+ * Probing the system `python3` checks the wrong site-packages (and on a
5170
+ * slim container there is no system python3 at all → every backend would
5171
+ * be reported unavailable, silently disabling ML). Falls back to `python3`
5172
+ * only when no embedded interpreter is resolved.
5417
5173
  */
5418
- static isPythonBackendAvailable(backend) {
5174
+ static isPythonBackendAvailable(backend, pythonPath) {
5419
5175
  const mod = {
5420
5176
  coreml: "coremltools",
5421
5177
  openvino: "openvino.runtime",
@@ -5425,7 +5181,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5425
5181
  if (!mod) return false;
5426
5182
  try {
5427
5183
  const { execFileSync } = require("node:child_process");
5428
- execFileSync("python3", ["-c", `import ${mod}`], {
5184
+ execFileSync(pythonPath || "python3", ["-c", `import ${mod}`], {
5429
5185
  timeout: 3e3,
5430
5186
  stdio: "pipe"
5431
5187
  });
@@ -5723,7 +5479,7 @@ function buildSchemaSlots(format, modelsDir) {
5723
5479
  id: m.id,
5724
5480
  name: m.name,
5725
5481
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
5726
- downloaded: (0, _camstack_core.isModelDownloaded)(modelsDir, m, f),
5482
+ downloaded: (0, _camstack_system.isModelDownloaded)(modelsDir, m, f),
5727
5483
  sizeMB: entry.sizeMB
5728
5484
  }]))
5729
5485
  })),
@@ -5816,11 +5572,11 @@ function buildDefaultStepTree(format) {
5816
5572
  makeStep("instance-segmentation", [], { enabled: false })
5817
5573
  ].filter((s) => s !== null));
5818
5574
  const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
5819
- runtime: "node",
5575
+ runtime: "python",
5820
5576
  backend: "coreml",
5821
5577
  format: "coreml"
5822
5578
  } : {
5823
- runtime: "node",
5579
+ runtime: "python",
5824
5580
  backend: "cpu",
5825
5581
  format: "onnx"
5826
5582
  };
@@ -5957,33 +5713,46 @@ function resolveAddonPythonDir() {
5957
5713
  for (const c of candidates) if (node_fs.existsSync(node_path.join(c, "inference_pool.py"))) return c;
5958
5714
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
5959
5715
  }
5716
+ /**
5717
+ * Returns true when proactive OpenVINO installation is warranted.
5718
+ *
5719
+ * Gate: Linux host + Intel iGPU or Intel NPU detected.
5720
+ *
5721
+ * Intentionally addon-local — addons are self-contained and cannot import
5722
+ * `@camstack/system` internals (see architecture invariant: no cross-addon
5723
+ * imports). This mirrors the logic in `resolveRuntimePackages` from that
5724
+ * package without importing it.
5725
+ *
5726
+ * darwin is never true: coremltools handles Apple Silicon + Intel Mac.
5727
+ * linux-amd (or any non-Intel linux GPU) is never true: the openvino
5728
+ * package has no AMD backend and would fail at import time.
5729
+ *
5730
+ * @internal exported only for unit tests in the same package
5731
+ */
5732
+ function shouldInstallOpenvino(hardware) {
5733
+ if (hardware.platform !== "linux") return false;
5734
+ const hasIntelGpu = hardware.gpu?.type === "intel";
5735
+ const hasIntelNpu = hardware.npu?.type === "intel-npu";
5736
+ return hasIntelGpu || hasIntelNpu;
5737
+ }
5960
5738
  var RUNTIMES = [{
5961
5739
  value: "python",
5962
5740
  label: "Python (CoreML / OpenVINO / ONNX Runtime)"
5963
- }, {
5964
- value: "node",
5965
- label: "Node.js (onnxruntime-node)"
5966
5741
  }];
5967
- var BACKENDS_BY_RUNTIME = {
5968
- python: [
5969
- {
5970
- value: "coreml",
5971
- label: "CoreML"
5972
- },
5973
- {
5974
- value: "openvino",
5975
- label: "OpenVINO"
5976
- },
5977
- {
5978
- value: "onnx",
5979
- label: "ONNX Runtime"
5980
- }
5981
- ],
5982
- node: [{
5983
- value: "cpu",
5984
- label: "CPU (onnxruntime-node)"
5985
- }]
5986
- };
5742
+ var BACKENDS_BY_RUNTIME = { python: [
5743
+ {
5744
+ value: "coreml",
5745
+ label: "CoreML"
5746
+ },
5747
+ {
5748
+ value: "openvino",
5749
+ label: "OpenVINO"
5750
+ },
5751
+ {
5752
+ value: "onnx",
5753
+ label: "ONNX Runtime"
5754
+ }
5755
+ ] };
5987
5756
  var DEVICES_BY_BACKEND = {
5988
5757
  coreml: [
5989
5758
  {
@@ -6335,7 +6104,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6335
6104
  ...stored,
6336
6105
  ...overlay
6337
6106
  } : stored;
6338
- const runtime = merged.engineRuntime === "node" ? "node" : "python";
6107
+ const runtime = "python";
6339
6108
  const availableBackends = await this.resolveAvailableBackends(ctx, runtime);
6340
6109
  const runtimeBackends = availableBackends.length > 0 ? BACKENDS_BY_RUNTIME[runtime].filter((b) => availableBackends.includes(b.value)) : BACKENDS_BY_RUNTIME[runtime];
6341
6110
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
@@ -6455,6 +6224,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6455
6224
  const pythonPath = await this.ctx.deps.ensurePython();
6456
6225
  if (pythonPath) this.pythonPath = pythonPath;
6457
6226
  else this.ctx.logger.warn("Embedded Python unavailable — runtime=\"python\" pipelines will fail until the download succeeds.");
6227
+ await this.proactivelyInstallOpenvino();
6458
6228
  const effectiveTuning = this.resolveBackendTuning();
6459
6229
  this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
6460
6230
  concurrency: effectiveTuning.concurrency,
@@ -6520,6 +6290,32 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6520
6290
  getModelCatalog() {
6521
6291
  return ALL_STEPS.flatMap((s) => [...s.models]);
6522
6292
  }
6293
+ /**
6294
+ * Proactively install the OpenVINO Python package when Intel hardware
6295
+ * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6296
+ * provider is constructed, so the module is available when the platform
6297
+ * probe runs its next `import openvino.runtime` check.
6298
+ *
6299
+ * Failure is non-fatal: a warning is logged and the addon continues with
6300
+ * the onnx-cpu baseline. The hardware query itself is also best-effort —
6301
+ * if the platform-probe cap isn't wired yet the error is caught here.
6302
+ */
6303
+ async proactivelyInstallOpenvino() {
6304
+ if (!this.pythonAddonDir || !this.pythonPath) return;
6305
+ try {
6306
+ const hardware = await this.ctx.api.platformProbe.getHardware.query();
6307
+ if (!shouldInstallOpenvino(hardware)) return;
6308
+ this.ctx.logger.info("Intel hardware detected — proactively installing OpenVINO Python package", { meta: {
6309
+ gpu: hardware.gpu?.type ?? null,
6310
+ npu: hardware.npu?.type ?? null
6311
+ } });
6312
+ const requirementsFile = node_path.join(this.pythonAddonDir, "requirements-openvino.txt");
6313
+ await this.ctx.deps.installPythonRequirements(requirementsFile);
6314
+ this.ctx.logger.info("Proactive OpenVINO install complete");
6315
+ } catch (err) {
6316
+ this.ctx.logger.warn("Proactive OpenVINO install failed — falling back to onnx-cpu baseline", { meta: { error: err instanceof Error ? err.message : String(err) } });
6317
+ }
6318
+ }
6523
6319
  async onShutdown() {
6524
6320
  if (this.engineMetricsTimer) {
6525
6321
  clearInterval(this.engineMetricsTimer);
@@ -6614,3 +6410,4 @@ exports.backendToFormat = backendToFormat;
6614
6410
  exports.default = DetectionPipelineAddon;
6615
6411
  exports.getDefaultModelForFormat = getDefaultModelForFormat;
6616
6412
  exports.getStepDefinition = getStepDefinition;
6413
+ exports.shouldInstallOpenvino = shouldInstallOpenvino;