@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
|
@@ -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-
|
|
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/
|
|
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
|
-
/**
|
|
1980
|
+
/** Detection always uses the Python pool. */
|
|
2191
1981
|
get usesPythonPool() {
|
|
2192
|
-
return
|
|
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
|
-
|
|
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)
|
|
2207
|
-
|
|
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)
|
|
2220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2237
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
4125
|
-
backend: "
|
|
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: "
|
|
3958
|
+
runtime: "python",
|
|
4212
3959
|
backend: "coreml",
|
|
4213
3960
|
format: "coreml"
|
|
4214
3961
|
} : {
|
|
4215
|
-
runtime: "
|
|
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:
|
|
5394
|
-
backend,
|
|
5395
|
-
format: backendToFormat(
|
|
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
|
|
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: "
|
|
5567
|
+
runtime: "python",
|
|
5812
5568
|
backend: "coreml",
|
|
5813
5569
|
format: "coreml"
|
|
5814
5570
|
} : {
|
|
5815
|
-
runtime: "
|
|
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
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
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 =
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
*
|