@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.
- package/dist/audio-analyzer/index.js +146 -56
- package/dist/audio-analyzer/index.mjs +145 -54
- package/dist/audio-codec-nodeav/index.js +1 -1
- package/dist/audio-codec-nodeav/index.mjs +1 -1
- package/dist/decoder-nodeav/index.js +2 -2
- package/dist/decoder-nodeav/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +127 -330
- package/dist/detection-pipeline/index.mjs +120 -324
- package/dist/{dist-7ewQjTle.js → dist-C1goFC50.js} +4 -4
- package/dist/{dist-C5jnNl0n.mjs → dist-XRXnZrVC.mjs} +4 -4
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +1 -1
- package/dist/pipeline-runner/index.mjs +1 -1
- package/dist/recorder/index.js +3 -3
- package/dist/recorder/index.mjs +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bak8zYXf.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-qX99--rF.mjs} +3 -3
- package/dist/stream-broker/{hostInit-CYCw2DW3.mjs → hostInit-Bx41KdYV.mjs} +3 -3
- package/dist/stream-broker/index.js +5 -5
- package/dist/stream-broker/index.mjs +4 -4
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +4 -9
- package/python/requirements-audio.txt +5 -0
- package/python/yamnet_audio.py +113 -0
- package/dist/constants-B_b0a-6h.mjs +0 -3119
- package/dist/constants-D65v6yp6.js +0 -5963
|
@@ -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-
|
|
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
|
|
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
|
-
/**
|
|
1988
|
+
/** Detection always uses the Python pool. */
|
|
2199
1989
|
get usesPythonPool() {
|
|
2200
|
-
return
|
|
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
|
-
|
|
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)
|
|
2215
|
-
|
|
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)
|
|
2228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2245
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
4133
|
-
backend: "
|
|
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: "
|
|
3966
|
+
runtime: "python",
|
|
4220
3967
|
backend: "coreml",
|
|
4221
3968
|
format: "coreml"
|
|
4222
3969
|
} : {
|
|
4223
|
-
runtime: "
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
5402
|
-
backend,
|
|
5403
|
-
format: backendToFormat(
|
|
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
|
|
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,
|
|
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: "
|
|
5575
|
+
runtime: "python",
|
|
5820
5576
|
backend: "coreml",
|
|
5821
5577
|
format: "coreml"
|
|
5822
5578
|
} : {
|
|
5823
|
-
runtime: "
|
|
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
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
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 =
|
|
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;
|