@camstack/addon-pipeline 1.2.294 → 1.2.295

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.
Files changed (39) hide show
  1. package/THIRD_PARTY_MODELS.md +233 -0
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/{default-detection-model-0dPKRKUD.mjs → default-detection-model-CAUBVbgK.mjs} +107 -54
  5. package/dist/{default-detection-model-D24AJOTn.js → default-detection-model-wNEISVA9.js} +107 -54
  6. package/dist/detection-pipeline/index.js +284 -457
  7. package/dist/detection-pipeline/index.mjs +284 -457
  8. package/dist/{dist-CJR259Xf.js → dist-BsVcf5wO.js} +1310 -832
  9. package/dist/{dist-RXbmRAwP.mjs → dist-CuxSNLKW.mjs} +1305 -833
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-atmRSHPk.mjs → node-Cqb1QiXk.mjs} +1 -1
  13. package/dist/{node-DWg9zbY1.js → node-UFk6I2f6.js} +1 -1
  14. package/dist/pipeline-runner/index.js +189 -70
  15. package/dist/pipeline-runner/index.mjs +189 -70
  16. package/dist/{process-memory-BJUXvTjd.js → process-memory-CJV29sPf.js} +1 -1
  17. package/dist/{process-memory-BgFOHFnx.mjs → process-memory-D0Nvs9rr.mjs} +1 -1
  18. package/dist/recorder/index.js +12 -2
  19. package/dist/recorder/index.mjs +12 -2
  20. package/dist/{segment-demux-js-C_fPJub3.js → segment-demux-js-DGwmf5hu.js} +1 -1
  21. package/dist/{segment-demux-js-G7wFpHzn.mjs → segment-demux-js-DIDWw1iE.mjs} +1 -1
  22. package/dist/stream-broker/_stub.js +2 -2
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6IyM-BIn.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B7nBFqva.mjs} +2 -2
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCIyBvRa.mjs +26 -0
  25. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DoyA71_q.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DeD_UFdb.mjs} +1 -1
  26. package/dist/stream-broker/demux-worker-child.js +1 -1
  27. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  28. package/dist/stream-broker/{hostInit-BBYHWS3M.mjs → hostInit-BKlb3qac.mjs} +2 -2
  29. package/dist/stream-broker/index.js +3 -3
  30. package/dist/stream-broker/index.mjs +3 -3
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/package.json +3 -2
  33. package/python/postprocessors/__init__.py +2 -0
  34. package/python/postprocessors/ssd.py +73 -17
  35. package/python/postprocessors/test_ssd.py +205 -0
  36. package/python/postprocessors/test_yunet.py +292 -0
  37. package/python/postprocessors/testdata/ssdlite_mobiledet_outputs.json +1 -0
  38. package/python/postprocessors/yunet.py +275 -0
  39. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-6IHzlLJ_.mjs +0 -26
@@ -1,9 +1,9 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $ as defaultDeviceFor$1, $t as errMsg, A as PoolMemoryWatchdog, En as array, Et as overlayClusterStepSettings, Fn as string, Gt as runtimeDevices$1, In as union, Ln as EventCategory, Mn as object, Mt as pipelineExecutorCapability, Ot as pickClusterStepModels, Ut as resolvePoolMemoryPolicy, V as YAMNET_TO_MACRO, Xt as supportedRuntimes$1, _t as loadContributionCapability, bn as nodePin, ct as evaluateZoneRules, f as DEFAULT_CLUSTER_STEP_MODELS, fn as createEvent, gn as hydrateSchema, ht as inferModelProvider, kt as pickClusterStepSettings, on as BaseAddon, ot as enumerateInferenceDevices, p as DEFAULT_CLUSTER_STEP_SETTINGS, rt as detectionPipelineCapability, t as APPLE_SA_TO_MACRO, v as DEVICE_BACKEND_TO_FORMAT, wn as sleep, xn as parseJsonUnknown, zt as resolveClusterStepModelId } from "../dist-RXbmRAwP.mjs";
3
- import { s as readProcessCost } from "../node-atmRSHPk.mjs";
4
- import { a as localFrameRegistry, c as ALL_STEPS, d as getStepDefinition, f as resolveModelForFormat, l as getDefaultModelForFormat, m as landmarkPrecisionVerdict, n as resolveDefaultDetectionModel, r as macroGateVerdict, s as ALL_PIPELINE_STEPS, u as getStep } from "../default-detection-model-0dPKRKUD.mjs";
2
+ import { At as pickClusterStepSettings, Bt as resolveClusterStepModelId, Dn as array, Dt as overlayClusterStepSettings, H as YAMNET_TO_MACRO, In as string, Kt as runtimeDevices$1, Ln as union, Nn as object, Nt as pipelineExecutorCapability, Rn as EventCategory, Sn as parseJsonUnknown, Tn as sleep, Wt as resolvePoolMemoryPolicy, Zt as supportedRuntimes$1, _n as hydrateSchema, en as errMsg, et as defaultDeviceFor$1, gt as inferModelProvider, it as detectionPipelineCapability, j as PoolMemoryWatchdog, kt as pickClusterStepModels, lt as evaluateZoneRules, m as DEFAULT_CLUSTER_STEP_SETTINGS, p as DEFAULT_CLUSTER_STEP_MODELS, pn as createEvent, sn as BaseAddon, st as enumerateInferenceDevices, t as APPLE_SA_TO_MACRO, vt as loadContributionCapability, xn as nodePin, y as DEVICE_BACKEND_TO_FORMAT } from "../dist-CuxSNLKW.mjs";
3
+ import { s as readProcessCost } from "../node-Cqb1QiXk.mjs";
4
+ import { a as localFrameRegistry, c as ALL_STEPS, d as getStepDefinition, f as resolveModelForFormat, l as getDefaultModelForFormat, m as landmarkPrecisionVerdict, n as resolveDefaultDetectionModel, r as macroGateVerdict, s as ALL_PIPELINE_STEPS, u as getStep } from "../default-detection-model-CAUBVbgK.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
6
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-BgFOHFnx.mjs";
6
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-D0Nvs9rr.mjs";
7
7
  import * as os from "node:os";
8
8
  import * as fs from "node:fs";
9
9
  import * as path$1 from "node:path";
@@ -1696,12 +1696,13 @@ function resolveEffectivePostprocessorForEntry(definition, entry) {
1696
1696
  //#region src/detection-pipeline/registry/model-knob-applicability.ts
1697
1697
  /** Decodes that run NMS in the pipeline (and hence consult `nmsIouThreshold`).
1698
1698
  * Mirrors the Python postprocessors that read the knob — yolo.py,
1699
- * yolo_seg.py, rfdetr.py, scrfd.py. */
1699
+ * yolo_seg.py, rfdetr.py, scrfd.py, yunet.py. */
1700
1700
  var NMS_CONSUMING_POSTPROCESSORS = new Set([
1701
1701
  "yolo",
1702
1702
  "yolo-seg",
1703
1703
  "rfdetr",
1704
- "scrfd"
1704
+ "scrfd",
1705
+ "yunet"
1705
1706
  ]);
1706
1707
  /**
1707
1708
  * Does this decode consult `nmsIouThreshold`?
@@ -3158,6 +3159,52 @@ function createInferenceTimeoutGuard(deps) {
3158
3159
  };
3159
3160
  }
3160
3161
  //#endregion
3162
+ //#region src/detection-pipeline/model-pin.ts
3163
+ function requestedModels(steps) {
3164
+ const out = /* @__PURE__ */ new Map();
3165
+ const walk = (nodes) => {
3166
+ for (const step of nodes) {
3167
+ if (!step.enabled) continue;
3168
+ out.set(step.addonId, step.modelId);
3169
+ if (step.children !== void 0) walk(step.children);
3170
+ }
3171
+ };
3172
+ walk(steps);
3173
+ return out;
3174
+ }
3175
+ function checkReplayPin(input) {
3176
+ if (input.requestedDeviceKey !== void 0 && input.dispatchDeviceKey !== input.requestedDeviceKey) return {
3177
+ kind: "refused",
3178
+ reason: "device-not-servable",
3179
+ detail: `device pool ${input.requestedDeviceKey} cannot run this tree (the capability gate would fall back to ${input.dispatchDeviceKey ?? "the node default"})`
3180
+ };
3181
+ const asked = requestedModels(input.requested);
3182
+ for (const step of input.executing) {
3183
+ const want = asked.get(step.addonId);
3184
+ if (want === void 0 || want.length === 0) return {
3185
+ kind: "refused",
3186
+ reason: "model-not-servable",
3187
+ detail: `${step.addonId}: no model pinned by the caller (would run ${step.modelId}, ${input.format})`
3188
+ };
3189
+ if (want !== step.modelId) return {
3190
+ kind: "refused",
3191
+ reason: "model-not-servable",
3192
+ detail: `${step.addonId}: asked for ${want}, this pool would run ${step.modelId} (${input.format})`
3193
+ };
3194
+ }
3195
+ return { kind: "pinned" };
3196
+ }
3197
+ var PRIMARY_ROOT_STEP = "object-detection";
3198
+ /**
3199
+ * The root model a frame result reports: `object-detection` when it runs (it
3200
+ * is the detector the tracks come from), else the first enabled video root.
3201
+ * `undefined` when no root runs — never a fabricated id.
3202
+ */
3203
+ function resolveRootModelId(roots) {
3204
+ const enabled = roots.filter((s) => s.enabled && s.slot !== "audio-classifier");
3205
+ return (enabled.find((s) => s.addonId === PRIMARY_ROOT_STEP) ?? enabled[0])?.modelId;
3206
+ }
3207
+ //#endregion
3161
3208
  //#region src/detection-pipeline/engine-provisioner.ts
3162
3209
  /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3163
3210
  var BACKOFF_SCHEDULE_MS = [
@@ -3428,6 +3475,7 @@ var KNOWN_POSTPROCESSORS = new Set([
3428
3475
  "rfdetr",
3429
3476
  "yolo-seg",
3430
3477
  "scrfd",
3478
+ "yunet",
3431
3479
  "arcface",
3432
3480
  "clip",
3433
3481
  "softmax",
@@ -3529,9 +3577,12 @@ var DropSampler = class {
3529
3577
  * accumulates and a summary is released once per interval — carrying the
3530
3578
  * number suppressed, so nothing about the RATE is lost even though the lines
3531
3579
  * are.
3580
+ *
3581
+ * `channel` separates occasions that share a device — a replay's drops are
3582
+ * sampled apart from the live ones.
3532
3583
  */
3533
- record(deviceId, macroClass) {
3534
- const key = `${deviceId}:${macroClass}`;
3584
+ record(deviceId, macroClass, channel = "live") {
3585
+ const key = `${channel}:${deviceId}:${macroClass}`;
3535
3586
  const at = this.now();
3536
3587
  const prev = this.seen.get(key);
3537
3588
  if (prev === void 0) {
@@ -5317,6 +5368,11 @@ var PipelineExecutor = class {
5317
5368
  async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeCropProvider, cropZoneBbox, rootInputViewProvider) {
5318
5369
  const startMs = Date.now();
5319
5370
  const verbosity = runOpts?.traceVerbosity ?? "off";
5371
+ const occasion = runOpts?.occasion ?? "live";
5372
+ const logTags = occasion === "replay" ? {
5373
+ deviceId,
5374
+ occasion
5375
+ } : { deviceId };
5320
5376
  const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
5321
5377
  const firstLevel = [];
5322
5378
  const details = [];
@@ -5332,9 +5388,9 @@ var PipelineExecutor = class {
5332
5388
  idGen,
5333
5389
  details,
5334
5390
  onClassifierClassDropped: (drop) => {
5335
- const sample = this.classFilterDropSampler.record(deviceId, `${drop.stepId}:${drop.droppedClass}`);
5391
+ const sample = this.classFilterDropSampler.record(deviceId, `${drop.stepId}:${drop.droppedClass}`, occasion);
5336
5392
  if (sample.logExample) this.opts.logger?.info("classifier winner excluded by class filter — label dropped", {
5337
- tags: { deviceId },
5393
+ tags: logTags,
5338
5394
  meta: {
5339
5395
  step: drop.stepId,
5340
5396
  model: drop.modelId,
@@ -5345,7 +5401,7 @@ var PipelineExecutor = class {
5345
5401
  }
5346
5402
  });
5347
5403
  if (sample.summaryCount !== null) this.opts.logger?.info("classifier class filter: still dropping", {
5348
- tags: { deviceId },
5404
+ tags: logTags,
5349
5405
  meta: {
5350
5406
  step: drop.stepId,
5351
5407
  droppedClass: drop.droppedClass,
@@ -5378,7 +5434,7 @@ var PipelineExecutor = class {
5378
5434
  await this.executeChildren(rootStep.children, mutable, rootView?.jpegProvider ?? fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
5379
5435
  } catch (err) {
5380
5436
  this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
5381
- tags: { deviceId },
5437
+ tags: logTags,
5382
5438
  meta: {
5383
5439
  rootStepId: rootStep.stepId,
5384
5440
  parentClass: mutable.macroClass,
@@ -5432,9 +5488,9 @@ var PipelineExecutor = class {
5432
5488
  const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
5433
5489
  const fullFrameHardArea = rootStep.settings?.["fullFrameGuardHardAreaRatio"];
5434
5490
  if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0, typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0)) {
5435
- const sample = this.dropSampler.record(deviceId, String(mutable.macroClass));
5491
+ const sample = this.dropSampler.record(deviceId, String(mutable.macroClass), occasion);
5436
5492
  if (sample.summaryCount !== null) this.opts.logger?.info("full-frame phantom guard: still dropping", {
5437
- tags: { deviceId },
5493
+ tags: logTags,
5438
5494
  meta: {
5439
5495
  macroClass: mutable.macroClass,
5440
5496
  suppressed: sample.summaryCount,
@@ -5443,7 +5499,7 @@ var PipelineExecutor = class {
5443
5499
  }
5444
5500
  });
5445
5501
  if (sample.logExample) this.opts.logger?.info("full-frame phantom guard: detection dropped", {
5446
- tags: { deviceId },
5502
+ tags: logTags,
5447
5503
  meta: {
5448
5504
  macroClass: mutable.macroClass,
5449
5505
  score: mutable.score,
@@ -5469,7 +5525,7 @@ var PipelineExecutor = class {
5469
5525
  continue;
5470
5526
  }
5471
5527
  if (isFullFrameSurvivor(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0)) this.opts.logger?.info("full-frame box SURVIVED the guard on score", {
5472
- tags: { deviceId },
5528
+ tags: logTags,
5473
5529
  meta: {
5474
5530
  macroClass: mutable.macroClass,
5475
5531
  score: mutable.score,
@@ -5483,7 +5539,7 @@ var PipelineExecutor = class {
5483
5539
  await this.executeChildren(rootStep.children, mutable, rootView?.jpegProvider ?? fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
5484
5540
  } catch (err) {
5485
5541
  this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
5486
- tags: { deviceId },
5542
+ tags: logTags,
5487
5543
  meta: {
5488
5544
  rootStepId: rootStep.stepId,
5489
5545
  parentClass: mutable.macroClass,
@@ -6541,25 +6597,6 @@ var PROBE_READY_TIMEOUT_MS = 12e4;
6541
6597
  * race bound keeps a wedged transport from wedging `setApi`.
6542
6598
  */
6543
6599
  var PROBE_DIRECT_RPC_TIMEOUT_MS = 6e4;
6544
- /**
6545
- * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
6546
- * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
6547
- * result is identical (onnx / cpu) but is now derived through the shared rules
6548
- * instead of duplicated inline.
6549
- */
6550
- /**
6551
- * Is `backend` even POSSIBLE on this node's OS/arch (ignoring gpu detail)?
6552
- * coreml ⇒ darwin only; openvino ⇒ x64 non-darwin only; onnx ⇒ anywhere. Used to
6553
- * reject a persisted/global engine choice that the node's PLATFORM fundamentally
6554
- * cannot run (e.g. the cluster's OpenVINO default landing on a Mac) — distinct
6555
- * from the gpu-dependent support (linux without a probed Intel iGPU still keeps
6556
- * openvino as a valid platform choice; the device falls back to cpu).
6557
- */
6558
- function backendPossibleOnPlatform(backend) {
6559
- if (backend === "coreml") return process.platform === "darwin";
6560
- if (backend === "openvino") return process.arch === "x64" && process.platform !== "darwin";
6561
- return true;
6562
- }
6563
6600
  var DetectionPipelineProvider = class DetectionPipelineProvider {
6564
6601
  modelsDir;
6565
6602
  eventBus;
@@ -6699,22 +6736,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6699
6736
  */
6700
6737
  needsAutoPick = false;
6701
6738
  /**
6702
- * Warm cache for benchmark engine-override runs.
6703
- *
6704
- * Each override rebuild costs a full Python pool spin-up (~300-500ms)
6705
- * plus the per-model load. Benchmark tabs iterate up to 800× against
6706
- * the same override (e.g. yolov9s / coreml / all) — paying that spin-up
6707
- * each iteration dwarfs actual inference time.
6708
- *
6709
- * When the same override engine is requested within `OVERRIDE_CACHE_TTL_MS`,
6710
- * we reuse the prior transient factory. Hitting a different override
6711
- * or exceeding the TTL disposes the cache and spins a new factory.
6712
- * The prior "main" factory (`priorFactory`) is still restored so
6713
- * runtime dispatch (camera frames) keeps its own resident engine.
6714
- */
6715
- overrideCache = null;
6716
- overrideCacheTimer = null;
6717
- /**
6718
6739
  * Idle-TTL for a per-device inference pool (multi-device C3). A device pool
6719
6740
  * with no dispatch for this long is disposed and recreated on next use — the
6720
6741
  * memory guardrail that keeps `factoriesByDevice` from accumulating warm pools
@@ -6747,7 +6768,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6747
6768
  error: err instanceof Error ? err.message : String(err)
6748
6769
  } })
6749
6770
  });
6750
- static OVERRIDE_CACHE_TTL_MS = 6e4;
6751
6771
  /**
6752
6772
  * RSS bound + periodic memory telemetry for the Python inference pools
6753
6773
  * (2026-08-18 OOM audit: the pools, not the recorder, were the host's OOM
@@ -6884,15 +6904,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6884
6904
  /**
6885
6905
  * Lazy-install the pip requirements file matching `engine.backend` into
6886
6906
  * the embedded Python before the inference pool spawns. No-op for
6887
- * `runtime: 'node'`, for backends with no requirements file on disk,
6888
- * or before the addon context has been wired (warm-pool race window).
6907
+ * backends with no requirements file on disk, or before the addon
6908
+ * context has been wired (warm-pool race window).
6889
6909
  *
6890
6910
  * Idempotent — `installPythonRequirements` short-circuits on a hash
6891
6911
  * marker, so back-to-back EngineFactory rebuilds (benchmark overrides,
6892
6912
  * tuning respawns) skip the pip subprocess after the first install.
6893
6913
  */
6894
6914
  async ensureBackendDeps(engine) {
6895
- if (engine.runtime !== "python") return;
6896
6915
  if (!this.addonCtx) return;
6897
6916
  const pythonAddonDir = this.executorOptions.pythonAddonDir;
6898
6917
  if (!pythonAddonDir) return;
@@ -7457,8 +7476,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7457
7476
  * result with a warning logged, same posture as `recomputeConfigIssues`.
7458
7477
  */
7459
7478
  async validatePipeline(input) {
7460
- const format = this.currentEngine.format;
7479
+ let format = this.currentEngine.format;
7461
7480
  try {
7481
+ if (input.deviceKey !== void 0) try {
7482
+ format = resolveDeviceEngine(input.deviceKey).format;
7483
+ } catch (err) {
7484
+ return {
7485
+ ok: false,
7486
+ issues: [{
7487
+ addonId: "*",
7488
+ kind: "unknown-device",
7489
+ detail: errMsg(err)
7490
+ }],
7491
+ substitutions: [],
7492
+ format
7493
+ };
7494
+ }
7462
7495
  const steps = input.steps;
7463
7496
  const enabledSteps = this.pruneToEnabledSteps(steps);
7464
7497
  const substitutions = collectSubstitutions(enabledSteps, format);
@@ -7783,7 +7816,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7783
7816
  const nodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
7784
7817
  const sessionId = input.sessionId ?? `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
7785
7818
  const isRuntime = Boolean(input.frame || input.frameRef);
7786
- const progressToBus = !isRuntime || input.sessionId !== void 0;
7819
+ const progressToBus = !isRuntime && input.replay !== true || input.sessionId !== void 0;
7787
7820
  const emit = (message, extra) => {
7788
7821
  onProgress?.(message);
7789
7822
  if (!progressToBus) return;
@@ -7925,7 +7958,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7925
7958
  const dispatchResolution = this.resolveStepsForDispatch({
7926
7959
  steps: input.steps,
7927
7960
  deviceKey: input.deviceKey,
7928
- engineOverride: input.engine,
7929
7961
  deviceId: input.deviceId,
7930
7962
  plane: input.plane
7931
7963
  });
@@ -7945,322 +7977,150 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7945
7977
  }
7946
7978
  const enabledSteps = flattenSteps(benchmarkSteps);
7947
7979
  emit(`Pipeline: ${enabledSteps.length} step(s) — ${enabledSteps.map((s) => s.addonId).join(" → ")}`);
7948
- await this.ensureEngineFactory();
7949
- const priorEngine = this.currentEngine;
7950
- const priorFactory = this.engineFactory;
7951
- const priorExecutor = this.executor;
7952
- const runEngine = input.engine ? {
7953
- runtime: input.engine.runtime,
7954
- backend: input.engine.backend,
7955
- format: input.engine.format,
7956
- ...input.engine.device || priorEngine.device ? { device: input.engine.device ?? priorEngine.device } : {}
7957
- } : priorEngine;
7958
- const differs = input.engine && (priorEngine.runtime !== runEngine.runtime || priorEngine.backend !== runEngine.backend || priorEngine.format !== runEngine.format || (priorEngine.device ?? "") !== (runEngine.device ?? ""));
7959
- if (differs) if (this.overrideCache && enginesEqual(this.overrideCache.engine, runEngine)) {
7960
- const cached = this.overrideCache;
7961
- if (cached.initPromise) await cached.initPromise;
7962
- this.log.debug("Benchmark: reusing warm override factory", { meta: { engine: `${runEngine.runtime}/${runEngine.backend}/${runEngine.device ?? "default"}` } });
7963
- this.currentEngine = runEngine;
7964
- this.engineFactory = cached.factory;
7965
- this.executor = cached.executor;
7966
- } else {
7967
- if (this.overrideCache) {
7968
- try {
7969
- await this.overrideCache.factory.dispose();
7970
- } catch {}
7971
- this.overrideCache = null;
7972
- }
7973
- this.log.info("Benchmark: engine override", { meta: {
7974
- from: `${priorEngine.runtime}/${priorEngine.backend}`,
7975
- to: `${runEngine.runtime}/${runEngine.backend}`
7976
- } });
7977
- await this.ensureBackendDeps(runEngine);
7978
- const newFactory = new EngineFactory({
7979
- engine: runEngine,
7980
- modelsDir: this.modelsDir,
7981
- logger: this.log.child("engine-override"),
7982
- pythonPath: this.executorOptions.pythonPath ?? "",
7983
- provisioning: this.executorOptions.provisioning,
7984
- resolveCustomModel: this.customModelResolver
7985
- });
7986
- const initPromise = newFactory.initialize([]);
7987
- this.overrideCache = {
7988
- engine: runEngine,
7989
- factory: newFactory,
7990
- executor: null,
7991
- lastUsedMs: Date.now(),
7992
- initPromise
7993
- };
7994
- this.scheduleOverrideCacheEviction();
7995
- try {
7996
- await initPromise;
7997
- this.overrideCache.initPromise = null;
7998
- } catch (err) {
7999
- this.overrideCache = null;
8000
- if (this.overrideCacheTimer) {
8001
- clearTimeout(this.overrideCacheTimer);
8002
- this.overrideCacheTimer = null;
8003
- }
8004
- throw err;
8005
- }
8006
- this.currentEngine = runEngine;
8007
- this.engineFactory = newFactory;
8008
- this.executor = null;
8009
- }
8010
- try {
8011
- const deviceLabel = runEngine.device ?? "default";
8012
- emit(`Engine: ${runEngine.runtime}/${runEngine.backend}/${deviceLabel}${input.engine ? " (override)" : ""}`, {
8013
- step: "engine",
8014
- engine: {
8015
- runtime: runEngine.runtime,
8016
- backend: runEngine.backend,
8017
- device: deviceLabel
8018
- }
8019
- });
8020
- const runtimeStr = `${runEngine.runtime}+${runEngine.backend}`;
8021
- const executor = this.executor ?? new PipelineExecutor({
8022
- engineRuntime: runtimeStr,
8023
- logger: this.log
7980
+ const rootModelId = resolveRootModelId(benchmarkSteps);
7981
+ const stampRoot = (r) => rootModelId === void 0 ? r : {
7982
+ ...r,
7983
+ rootModelId
7984
+ };
7985
+ if (input.replay === true) {
7986
+ const verdict = checkReplayPin({
7987
+ requested: input.steps,
7988
+ executing: input.plane === "frame" ? collectFramePlaneSteps(benchmarkSteps, isDetailPlaneStep) : enabledSteps,
7989
+ requestedDeviceKey: input.deviceKey,
7990
+ dispatchDeviceKey,
7991
+ format: dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey).format : this.currentEngine.format
8024
7992
  });
8025
- this.executor = executor;
8026
- const dispatchEngine = dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey) : runEngine;
8027
- const dispatchFactory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
8028
- const needed = enabledSteps.filter((s) => dispatchFactory.needsPoolUpdate(s));
8029
- if (needed.length > 0) {
8030
- this.log.info("Benchmark: models to load", { meta: {
8031
- count: needed.length,
8032
- models: needed.map((s) => `${s.addonId}/${s.modelId}`)
8033
- } });
8034
- for (const s of needed) emit(`Loading model: ${s.addonName} (${s.modelId})...`, {
8035
- step: s.addonId,
8036
- addonId: s.addonId,
8037
- modelId: s.modelId
7993
+ if (verdict.kind === "refused") {
7994
+ this.log.warn("replay refused — the node cannot run exactly what was pinned", {
7995
+ ...input.deviceId !== void 0 ? { tags: { deviceId: input.deviceId } } : {},
7996
+ meta: {
7997
+ reason: verdict.reason,
7998
+ detail: verdict.detail,
7999
+ deviceKey: input.deviceKey
8000
+ }
8038
8001
  });
8039
- await this.ensureModelsForSteps(benchmarkSteps, dispatchFactory, dispatchEngine.format);
8040
- emit(`All models loaded`);
8041
- }
8042
- emit("Running inference...");
8043
- const tree = buildExecutableTree(benchmarkSteps, (stepId) => dispatchFactory.getEngine(stepId), this.customModelResolver);
8044
- setupMs = performance.now() - wallT0 - decodeMs;
8045
- const effectiveDeviceId = input.deviceId ?? 0;
8046
- const deviceOverrides = effectiveDeviceId > 0 ? await this.deviceOverrides.resolve(effectiveDeviceId) : {};
8047
- const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
8048
- const nativeCropProvider = this.buildNativeCropProviderFromRef(input.nativeCropRef) ?? this.buildNativeFaceCropProvider(input.frameHandle);
8049
- let cropZoneBbox;
8050
- if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
8051
- await this.ensureDeviceProxy(effectiveDeviceId);
8052
- const proxy = this.deviceProxies.get(effectiveDeviceId);
8053
- if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
8054
- }
8055
- let frameViewResolver;
8056
- let rootInputViewProvider;
8057
- if (runtimeFrameRef) {
8058
- frameViewResolver = createRootFrameViewResolver(localFrameRegistry, runtimeFrameRef);
8059
- rootInputViewProvider = async (root, crop) => {
8060
- const view = await frameViewResolver(root, crop ? {
8061
- left: crop[0],
8062
- top: crop[1],
8063
- width: crop[2] - crop[0],
8064
- height: crop[3] - crop[1]
8065
- } : void 0);
8066
- const data = Buffer.from(view.data.buffer, view.data.byteOffset, view.data.byteLength);
8067
- return {
8068
- input: {
8069
- kind: "jpeg",
8070
- data
8071
- },
8072
- jpegProvider: async () => data,
8073
- width: view.width,
8074
- height: view.height,
8075
- geometry: view.geometry
8076
- };
8077
- };
8078
- }
8079
- let executionSucceeded = false;
8080
- let execution;
8081
- try {
8082
- execution = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
8083
- traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
8084
- plane: input.plane
8085
- }, nativeCropProvider, cropZoneBbox, rootInputViewProvider);
8086
- executionSucceeded = true;
8087
- } finally {
8088
- frameViewResolver?.release(executionSucceeded ? "success" : "error");
8089
- }
8090
- const { result, trace } = execution;
8091
- if (isRuntime) {
8092
- if (trace && this.eventBus) this.eventBus.emit(createEvent(EventCategory.PipelineTrace, {
8093
- type: "device",
8094
- id: trace.deviceId,
8095
- nodeId: "hub"
8096
- }, trace));
8097
- if (effectiveDeviceId > 0) {
8098
- await this.ensureDeviceProxy(effectiveDeviceId);
8099
- return this.gateDetectionsByZoneRules(effectiveDeviceId, result);
8100
- }
8101
- return result;
8102
- }
8103
- for (const t of result.debug?.stepTimings ?? []) emit(`${t.source}${t.modelId ? ` (${t.modelId})` : ""} → ${t.ms}ms`, {
8104
- step: t.source,
8105
- addonId: t.source,
8106
- modelId: t.modelId ?? void 0,
8107
- ms: t.ms
8108
- });
8109
- emit(`Done — ${result.detections.length} detection(s) in ${result.debug?.totalInferenceMs ?? 0}ms`, { ms: result.debug?.totalInferenceMs });
8110
- const wallMs = performance.now() - wallT0;
8111
- const inferMs = result.debug?.totalInferenceMs ?? 0;
8112
- const overheadMs = Math.max(0, wallMs - decodeMs - setupMs - inferMs);
8113
- return {
8114
- ...result,
8115
- debug: {
8116
- ...result.debug,
8117
- decodeMs: Math.round(decodeMs * 100) / 100,
8118
- setupMs: Math.round(setupMs * 100) / 100,
8119
- wallMs: Math.round(wallMs * 100) / 100,
8120
- overheadMs: Math.round(overheadMs * 100) / 100
8121
- }
8122
- };
8123
- } finally {
8124
- if (differs && this.overrideCache) {
8125
- this.overrideCache.executor = this.executor;
8126
- this.overrideCache.lastUsedMs = Date.now();
8127
- this.scheduleOverrideCacheEviction();
8128
- }
8129
- if (differs) {
8130
- this.currentEngine = priorEngine;
8131
- this.engineFactory = priorFactory;
8132
- this.executor = priorExecutor;
8002
+ throw new Error(`${verdict.reason}: ${verdict.detail}`);
8133
8003
  }
8134
8004
  }
8135
- }
8136
- /**
8137
- * Apply a benchmark engine override (mirroring the body of `runPipeline`
8138
- * lines 863-953). Swaps `currentEngine` / `engineFactory` / `executor`
8139
- * to the override's warm factory and returns a restore function the
8140
- * caller MUST invoke in `finally`. When `override` is undefined or
8141
- * matches the current engine, this is a no-op and the restore function
8142
- * does nothing — same shape so callers don't need a conditional path.
8143
- *
8144
- * Used by both `runPipeline` (single-frame benchmark) and
8145
- * `runPipelineBatch` (batched fast path). Without this, the batch
8146
- * path silently ignored `input.engine` and ran the user's override
8147
- * against the addon's saved engine — making the override matrix in
8148
- * the bench UI a no-op for batchSize > 1.
8149
- */
8150
- async applyEngineOverride(override) {
8151
- const priorEngine = this.currentEngine;
8152
- const priorFactory = this.engineFactory;
8153
- const priorExecutor = this.executor;
8154
- if (!override) return () => {};
8155
- if (!backendPossibleOnPlatform(override.backend)) {
8156
- this.log.warn("Engine override impossible on this platform — running on own engine", { meta: {
8157
- override: `${override.runtime}/${override.backend}/${override.device ?? "default"}`,
8158
- platform: process.platform,
8159
- arch: process.arch,
8160
- engine: `${priorEngine.runtime}/${priorEngine.backend}`
8161
- } });
8162
- return () => {};
8163
- }
8164
- const runEngine = {
8165
- runtime: override.runtime,
8166
- backend: override.backend,
8167
- format: override.format,
8168
- ...override.device || priorEngine.device ? { device: override.device ?? priorEngine.device } : {}
8169
- };
8170
- if (!(priorEngine.runtime !== runEngine.runtime || priorEngine.backend !== runEngine.backend || priorEngine.format !== runEngine.format || (priorEngine.device ?? "") !== (runEngine.device ?? ""))) return () => {};
8171
- if (this.overrideCache && enginesEqual(this.overrideCache.engine, runEngine)) {
8172
- const cached = this.overrideCache;
8173
- if (cached.initPromise) await cached.initPromise;
8174
- this.log.debug("Benchmark: reusing warm override factory", { meta: { engine: `${runEngine.runtime}/${runEngine.backend}/${runEngine.device ?? "default"}` } });
8175
- this.currentEngine = runEngine;
8176
- this.engineFactory = cached.factory;
8177
- this.executor = cached.executor;
8178
- } else {
8179
- if (this.overrideCache) {
8180
- try {
8181
- await this.overrideCache.factory.dispose();
8182
- } catch {}
8183
- this.overrideCache = null;
8005
+ const appliesCameraGates = isRuntime || input.replay === true;
8006
+ await this.ensureEngineFactory();
8007
+ const runEngine = this.currentEngine;
8008
+ const deviceLabel = runEngine.device ?? "default";
8009
+ emit(`Engine: ${runEngine.runtime}/${runEngine.backend}/${deviceLabel}`, {
8010
+ step: "engine",
8011
+ engine: {
8012
+ runtime: runEngine.runtime,
8013
+ backend: runEngine.backend,
8014
+ device: deviceLabel
8184
8015
  }
8185
- this.log.info("Benchmark: engine override", { meta: {
8186
- from: `${priorEngine.runtime}/${priorEngine.backend}`,
8187
- to: `${runEngine.runtime}/${runEngine.backend}`
8016
+ });
8017
+ const runtimeStr = `${runEngine.runtime}+${runEngine.backend}`;
8018
+ const executor = this.executor ?? new PipelineExecutor({
8019
+ engineRuntime: runtimeStr,
8020
+ logger: this.log
8021
+ });
8022
+ this.executor = executor;
8023
+ const dispatchEngine = dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey) : runEngine;
8024
+ const dispatchFactory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
8025
+ const needed = enabledSteps.filter((s) => dispatchFactory.needsPoolUpdate(s));
8026
+ if (needed.length > 0) {
8027
+ this.log.info("Benchmark: models to load", { meta: {
8028
+ count: needed.length,
8029
+ models: needed.map((s) => `${s.addonId}/${s.modelId}`)
8188
8030
  } });
8189
- await this.ensureBackendDeps(runEngine);
8190
- const newFactory = new EngineFactory({
8191
- engine: runEngine,
8192
- modelsDir: this.modelsDir,
8193
- logger: this.log.child("engine-override"),
8194
- pythonPath: this.executorOptions.pythonPath ?? "",
8195
- provisioning: this.executorOptions.provisioning,
8196
- resolveCustomModel: this.customModelResolver
8031
+ for (const s of needed) emit(`Loading model: ${s.addonName} (${s.modelId})...`, {
8032
+ step: s.addonId,
8033
+ addonId: s.addonId,
8034
+ modelId: s.modelId
8197
8035
  });
8198
- const initPromise = newFactory.initialize([]);
8199
- this.overrideCache = {
8200
- engine: runEngine,
8201
- factory: newFactory,
8202
- executor: null,
8203
- lastUsedMs: Date.now(),
8204
- initPromise
8036
+ await this.ensureModelsForSteps(benchmarkSteps, dispatchFactory, dispatchEngine.format);
8037
+ emit(`All models loaded`);
8038
+ }
8039
+ emit("Running inference...");
8040
+ const tree = buildExecutableTree(benchmarkSteps, (stepId) => dispatchFactory.getEngine(stepId), this.customModelResolver);
8041
+ setupMs = performance.now() - wallT0 - decodeMs;
8042
+ const effectiveDeviceId = input.deviceId ?? 0;
8043
+ const deviceOverrides = effectiveDeviceId > 0 ? await this.deviceOverrides.resolve(effectiveDeviceId) : {};
8044
+ const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
8045
+ const nativeCropProvider = this.buildNativeCropProviderFromRef(input.nativeCropRef) ?? this.buildNativeFaceCropProvider(input.frameHandle);
8046
+ let cropZoneBbox;
8047
+ if (appliesCameraGates && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
8048
+ await this.ensureDeviceProxy(effectiveDeviceId);
8049
+ const proxy = this.deviceProxies.get(effectiveDeviceId);
8050
+ if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
8051
+ }
8052
+ let frameViewResolver;
8053
+ let rootInputViewProvider;
8054
+ if (runtimeFrameRef) {
8055
+ frameViewResolver = createRootFrameViewResolver(localFrameRegistry, runtimeFrameRef);
8056
+ rootInputViewProvider = async (root, crop) => {
8057
+ const view = await frameViewResolver(root, crop ? {
8058
+ left: crop[0],
8059
+ top: crop[1],
8060
+ width: crop[2] - crop[0],
8061
+ height: crop[3] - crop[1]
8062
+ } : void 0);
8063
+ const data = Buffer.from(view.data.buffer, view.data.byteOffset, view.data.byteLength);
8064
+ return {
8065
+ input: {
8066
+ kind: "jpeg",
8067
+ data
8068
+ },
8069
+ jpegProvider: async () => data,
8070
+ width: view.width,
8071
+ height: view.height,
8072
+ geometry: view.geometry
8073
+ };
8205
8074
  };
8206
- this.scheduleOverrideCacheEviction();
8207
- try {
8208
- await initPromise;
8209
- this.overrideCache.initPromise = null;
8210
- } catch (err) {
8211
- this.overrideCache = null;
8212
- if (this.overrideCacheTimer) {
8213
- clearTimeout(this.overrideCacheTimer);
8214
- this.overrideCacheTimer = null;
8215
- }
8216
- throw err;
8075
+ }
8076
+ let executionSucceeded = false;
8077
+ let execution;
8078
+ try {
8079
+ execution = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
8080
+ traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
8081
+ plane: input.plane,
8082
+ occasion: input.replay === true ? "replay" : "live"
8083
+ }, nativeCropProvider, cropZoneBbox, rootInputViewProvider);
8084
+ executionSucceeded = true;
8085
+ } finally {
8086
+ frameViewResolver?.release(executionSucceeded ? "success" : "error");
8087
+ }
8088
+ const { result, trace } = execution;
8089
+ if (isRuntime) {
8090
+ if (trace && this.eventBus) this.eventBus.emit(createEvent(EventCategory.PipelineTrace, {
8091
+ type: "device",
8092
+ id: trace.deviceId,
8093
+ nodeId: "hub"
8094
+ }, trace));
8095
+ if (effectiveDeviceId > 0) {
8096
+ await this.ensureDeviceProxy(effectiveDeviceId);
8097
+ return stampRoot(this.gateDetectionsByZoneRules(effectiveDeviceId, result));
8217
8098
  }
8218
- this.currentEngine = runEngine;
8219
- this.engineFactory = newFactory;
8220
- this.executor = null;
8099
+ return stampRoot(result);
8221
8100
  }
8222
- return () => {
8223
- if (this.overrideCache) {
8224
- this.overrideCache.executor = this.executor;
8225
- this.overrideCache.lastUsedMs = Date.now();
8226
- this.scheduleOverrideCacheEviction();
8101
+ for (const t of result.debug?.stepTimings ?? []) emit(`${t.source}${t.modelId ? ` (${t.modelId})` : ""} → ${t.ms}ms`, {
8102
+ step: t.source,
8103
+ addonId: t.source,
8104
+ modelId: t.modelId ?? void 0,
8105
+ ms: t.ms
8106
+ });
8107
+ emit(`Done — ${result.detections.length} detection(s) in ${result.debug?.totalInferenceMs ?? 0}ms`, { ms: result.debug?.totalInferenceMs });
8108
+ const wallMs = performance.now() - wallT0;
8109
+ const inferMs = result.debug?.totalInferenceMs ?? 0;
8110
+ const overheadMs = Math.max(0, wallMs - decodeMs - setupMs - inferMs);
8111
+ const gated = input.replay === true && effectiveDeviceId > 0 ? await this.ensureDeviceProxy(effectiveDeviceId).then(() => this.gateDetectionsByZoneRules(effectiveDeviceId, result)) : result;
8112
+ return {
8113
+ ...stampRoot(gated),
8114
+ debug: {
8115
+ ...gated.debug,
8116
+ decodeMs: Math.round(decodeMs * 100) / 100,
8117
+ setupMs: Math.round(setupMs * 100) / 100,
8118
+ wallMs: Math.round(wallMs * 100) / 100,
8119
+ overheadMs: Math.round(overheadMs * 100) / 100
8227
8120
  }
8228
- this.currentEngine = priorEngine;
8229
- this.engineFactory = priorFactory;
8230
- this.executor = priorExecutor;
8231
8121
  };
8232
8122
  }
8233
8123
  /**
8234
- * Schedule eviction of the override cache after `OVERRIDE_CACHE_TTL_MS`
8235
- * of idleness. Rescheduling resets the timer so an active benchmark
8236
- * keeps its warm factory alive until iterations stop arriving.
8237
- */
8238
- scheduleOverrideCacheEviction() {
8239
- if (this.overrideCacheTimer) clearTimeout(this.overrideCacheTimer);
8240
- this.overrideCacheTimer = setTimeout(() => {
8241
- this.evictOverrideCache("idle TTL");
8242
- }, DetectionPipelineProvider.OVERRIDE_CACHE_TTL_MS);
8243
- }
8244
- async evictOverrideCache(reason) {
8245
- if (!this.overrideCache) return;
8246
- const entry = this.overrideCache;
8247
- this.overrideCache = null;
8248
- if (this.overrideCacheTimer) {
8249
- clearTimeout(this.overrideCacheTimer);
8250
- this.overrideCacheTimer = null;
8251
- }
8252
- try {
8253
- await entry.factory.dispose();
8254
- this.log.info("Override cache evicted", { meta: {
8255
- reason,
8256
- engine: `${entry.engine.runtime}/${entry.engine.backend}/${entry.engine.device ?? "default"}`,
8257
- ageMs: Date.now() - entry.lastUsedMs
8258
- } });
8259
- } catch (err) {
8260
- this.log.warn("Override cache dispose failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
8261
- }
8262
- }
8263
- /**
8264
8124
  * Convert PipelineStepInput[] → PipelineDefaultStep[] by enriching from
8265
8125
  * StepDefinitions. This is the LIVE per-camera dispatch path — it runs
8266
8126
  * once per decoded frame (up to detectionFps × N cameras/sec) — so it
@@ -8276,13 +8136,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8276
8136
  * `resolveStepModels` (see its doc comment); a second writer here would
8277
8137
  * corrupt it.
8278
8138
  *
8279
- * `format` defaults to `this.currentEngine.format` (the LIVE per-camera
8280
- * dispatch path never passes it explicitly). Benchmark call sites with a
8281
- * per-run engine override (`input.engine`) MUST pass the override's format
8282
- * explicitly — `this.currentEngine` only gets swapped to the override
8283
- * LATER in `runPipeline`/`runPipelineBatch`, so relying on the default here
8284
- * would resolve models against the node's persisted format instead of the
8285
- * format actually being benchmarked.
8139
+ * `format` defaults to `this.currentEngine.format` (the node-default
8140
+ * pool); a `deviceKey` dispatch passes that device's format explicitly.
8286
8141
  */
8287
8142
  inputStepsToPipelineSteps(steps, format = this.currentEngine.format, engine = {
8288
8143
  backend: this.currentEngine.backend,
@@ -8364,8 +8219,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8364
8219
  * devices — so the whole-tree gate stays.
8365
8220
  *
8366
8221
  * Fallback is strictly NODE-LOCAL and ordered: the node's own selected
8367
- * engine (or the benchmark's explicit `engineOverride`) is the one and only
8368
- * fallback — frames never migrate to another node from here (that is the
8222
+ * engine is the one and only fallback — frames never migrate to another node from here (that is the
8369
8223
  * orchestrator's tier). The fallback is logged ONCE per distinct
8370
8224
  * (deviceKey, steps, format) signature, with `tags: { deviceId }` when the
8371
8225
  * dispatch is camera-scoped.
@@ -8376,12 +8230,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8376
8230
  * configuration must stay loud, not vanish into a silent fallback chain.
8377
8231
  */
8378
8232
  resolveStepsForDispatch(args) {
8379
- const { deviceKey, engineOverride, deviceId } = args;
8380
- const nodeFormat = engineOverride?.format ?? this.currentEngine.format;
8381
- const nodeEngine = engineOverride ? {
8382
- backend: engineOverride.backend,
8383
- device: engineOverride.device ?? null
8384
- } : {
8233
+ const { deviceKey, deviceId } = args;
8234
+ const nodeFormat = this.currentEngine.format;
8235
+ const nodeEngine = {
8385
8236
  backend: this.currentEngine.backend,
8386
8237
  device: this.currentEngine.device ?? null
8387
8238
  };
@@ -8626,12 +8477,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8626
8477
  const dispatchResolution = this.resolveStepsForDispatch({
8627
8478
  steps: input.steps,
8628
8479
  deviceKey: input.deviceKey,
8629
- engineOverride: input.engine,
8630
8480
  deviceId: input.deviceId,
8631
8481
  plane: void 0
8632
8482
  });
8633
8483
  const dispatchDeviceKey = dispatchResolution.deviceKey;
8634
- const resolveFormat = dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey).format : input.engine?.format ?? this.currentEngine.format;
8484
+ const resolveFormat = dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey).format : this.currentEngine.format;
8635
8485
  const benchmarkSteps = dispatchResolution.steps;
8636
8486
  const enabledSteps = flattenEnabledVideoSteps(benchmarkSteps);
8637
8487
  if (enabledSteps.length === 0) throw new Error("runPipelineBatch: no enabled steps");
@@ -8652,53 +8502,48 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8652
8502
  const uniformDims = input.frames.every((f) => f.width === firstFrame.width && f.height === firstFrame.height);
8653
8503
  const singleRoot = enabledSteps.length === 1;
8654
8504
  await this.ensureEngineFactory();
8655
- const restoreEngine = await this.applyEngineOverride(input.engine);
8656
- try {
8657
- const factory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
8658
- if (!factory) throw new Error("runPipelineBatch: factory not initialised");
8659
- if (enabledSteps.filter((s) => factory.needsPoolUpdate(s)).length > 0) await this.ensureModelsForSteps(benchmarkSteps, factory, resolveFormat);
8660
- const canFastPath = singleRoot && allRaw && uniformDims && factory.supportsBatch();
8661
- this.log.info("runPipelineBatch path decision", { meta: {
8662
- phase: "batch",
8663
- canFastPath,
8664
- singleRoot,
8665
- allRaw,
8666
- uniformDims,
8667
- supportsBatch: factory.supportsBatch()
8668
- } });
8669
- if (!canFastPath) return { results: await Promise.all(input.frames.map((frame) => this.runPipeline({
8670
- steps: input.steps,
8671
- frame,
8672
- deviceId: input.deviceId,
8673
- sessionId: input.sessionId,
8674
- deviceKey: dispatchDeviceKey
8675
- }))) };
8676
- const items = input.frames.map((frame) => ({
8677
- raw: Buffer.from(frame.data),
8678
- width: frame.width,
8679
- height: frame.height,
8680
- format: frame.format
8681
- }));
8682
- this.log.info("runPipelineBatch fast path dispatch", { meta: {
8683
- phase: "batch",
8684
- stepId: rootStep.addonId,
8685
- modelId: rootStep.modelId,
8686
- items: items.length,
8687
- totalRawBytes: items.reduce((s, it) => s + it.raw.length, 0)
8688
- } });
8689
- const start = performance.now();
8690
- const rawResults = await factory.batchInferRaw(rootStep.addonId, items, rootStep.modelId, input.frameId ?? 0);
8691
- const totalMs = performance.now() - start;
8692
- this.log.info("runPipelineBatch fast path returned", { meta: {
8693
- phase: "batch",
8694
- resultCount: rawResults.length,
8695
- ms: Math.round(totalMs)
8696
- } });
8697
- const perFrameMs = totalMs / rawResults.length;
8698
- return { results: rawResults.map((raw, i) => assembleBatchedFrameResult(raw, input.frames[i], rootStep.addonId, input.deviceId ?? 0, perFrameMs, perFrameMs)) };
8699
- } finally {
8700
- restoreEngine();
8701
- }
8505
+ const factory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
8506
+ if (!factory) throw new Error("runPipelineBatch: factory not initialised");
8507
+ if (enabledSteps.filter((s) => factory.needsPoolUpdate(s)).length > 0) await this.ensureModelsForSteps(benchmarkSteps, factory, resolveFormat);
8508
+ const canFastPath = singleRoot && allRaw && uniformDims && factory.supportsBatch();
8509
+ this.log.info("runPipelineBatch path decision", { meta: {
8510
+ phase: "batch",
8511
+ canFastPath,
8512
+ singleRoot,
8513
+ allRaw,
8514
+ uniformDims,
8515
+ supportsBatch: factory.supportsBatch()
8516
+ } });
8517
+ if (!canFastPath) return { results: await Promise.all(input.frames.map((frame) => this.runPipeline({
8518
+ steps: input.steps,
8519
+ frame,
8520
+ deviceId: input.deviceId,
8521
+ sessionId: input.sessionId,
8522
+ deviceKey: dispatchDeviceKey
8523
+ }))) };
8524
+ const items = input.frames.map((frame) => ({
8525
+ raw: Buffer.from(frame.data),
8526
+ width: frame.width,
8527
+ height: frame.height,
8528
+ format: frame.format
8529
+ }));
8530
+ this.log.info("runPipelineBatch fast path dispatch", { meta: {
8531
+ phase: "batch",
8532
+ stepId: rootStep.addonId,
8533
+ modelId: rootStep.modelId,
8534
+ items: items.length,
8535
+ totalRawBytes: items.reduce((s, it) => s + it.raw.length, 0)
8536
+ } });
8537
+ const start = performance.now();
8538
+ const rawResults = await factory.batchInferRaw(rootStep.addonId, items, rootStep.modelId, input.frameId ?? 0);
8539
+ const totalMs = performance.now() - start;
8540
+ this.log.info("runPipelineBatch fast path returned", { meta: {
8541
+ phase: "batch",
8542
+ resultCount: rawResults.length,
8543
+ ms: Math.round(totalMs)
8544
+ } });
8545
+ const perFrameMs = totalMs / rawResults.length;
8546
+ return { results: rawResults.map((raw, i) => assembleBatchedFrameResult(raw, input.frames[i], rootStep.addonId, input.deviceId ?? 0, perFrameMs, perFrameMs)) };
8702
8547
  }
8703
8548
  async listReferenceImages() {
8704
8549
  const dir = this.resolveRefImagesDir();
@@ -8803,7 +8648,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8803
8648
  this.poolMemoryGuard = null;
8804
8649
  this.inferenceTimeoutGuard?.stop();
8805
8650
  this.inferenceTimeoutGuard = null;
8806
- await this.evictOverrideCache("shutdown");
8807
8651
  if (this.engineFactory) {
8808
8652
  await this.engineFactory.dispose();
8809
8653
  this.engineFactory = null;
@@ -8885,13 +8729,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8885
8729
  * own runtime's row, so this is not "the node's tuning" — the per-pool truth
8886
8730
  * is on each pool's `Python inference pool` log line and in `listLoadedEngines`.
8887
8731
  */
8888
- async getEffectiveTuning() {
8889
- const p = resolvePoolProvisioning(this.currentEngine, this.executorOptions.provisioning);
8732
+ /**
8733
+ * One pool's provisioning — the device pool named by `deviceKey`, else the
8734
+ * node default. Resolved with the SAME override `resolveDeviceFactory` and
8735
+ * `ensureEngineFactory` build the pool with, so the answer describes the
8736
+ * pool that ran the work (D646).
8737
+ */
8738
+ async getEffectiveTuning(input = {}) {
8739
+ const p = resolvePoolProvisioning(input.deviceKey !== void 0 ? resolveDeviceEngine(input.deviceKey) : this.currentEngine, this.executorOptions.provisioning);
8890
8740
  return {
8741
+ deviceKey: input.deviceKey ?? null,
8742
+ runtime: p.runtime,
8891
8743
  batchMode: p.batchMode,
8892
8744
  windowMs: p.windowMs,
8893
8745
  maxBatchSize: p.maxBatchSize,
8894
- concurrency: p.concurrency
8746
+ concurrency: p.concurrency,
8747
+ numWorkers: p.numWorkers
8895
8748
  };
8896
8749
  }
8897
8750
  /**
@@ -9279,9 +9132,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9279
9132
  }
9280
9133
  async listLoadedEngines() {
9281
9134
  const out = [];
9282
- const overrideFactory = this.overrideCache?.factory ?? null;
9283
- const runtimeFactoryIsOverride = this.engineFactory !== null && this.engineFactory === overrideFactory;
9284
- if (this.engineFactory && !runtimeFactoryIsOverride) {
9135
+ if (this.engineFactory) {
9285
9136
  const eng = this.currentEngine;
9286
9137
  const engineKey = `${eng.runtime}/${eng.backend}/${eng.device ?? "default"}`;
9287
9138
  const loaded = this.engineFactory.listLoaded();
@@ -9296,22 +9147,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9296
9147
  idleTtlMs: null
9297
9148
  });
9298
9149
  }
9299
- if (this.overrideCache) {
9300
- const eng = this.overrideCache.engine;
9301
- const engineKey = `${eng.runtime}/${eng.backend}/${eng.device ?? "default"} (warm)`;
9302
- const loaded = this.overrideCache.factory.listLoaded();
9303
- const idleMs = Math.max(0, Date.now() - this.overrideCache.lastUsedMs);
9304
- if (idleMs <= DetectionPipelineProvider.OVERRIDE_CACHE_TTL_MS) out.push({
9305
- engineKey,
9306
- engine: eng,
9307
- modelsLoaded: loaded.map((l) => `${l.stepId}/${l.modelId}`),
9308
- inUseByCameras: [],
9309
- kind: "warm-override",
9310
- poolPid: this.overrideCache.factory.getPoolPid(),
9311
- idleMs,
9312
- idleTtlMs: DetectionPipelineProvider.OVERRIDE_CACHE_TTL_MS
9313
- });
9314
- }
9315
9150
  for (const [deviceKey, factory] of this.factoriesByDevice) {
9316
9151
  const eng = resolveDeviceEngine(deviceKey);
9317
9152
  const loaded = factory.listLoaded();
@@ -9340,14 +9175,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9340
9175
  return { success: true };
9341
9176
  }
9342
9177
  async killEngine(input) {
9343
- if (this.overrideCache && enginesEqual(this.overrideCache.engine, input.engine)) {
9344
- await this.evictOverrideCache("killEngine");
9345
- this.log.info("Override engine killed", { meta: {
9346
- ...input.engine,
9347
- force: input.force ?? false
9348
- } });
9349
- return { success: true };
9350
- }
9351
9178
  if (!this.engineFactory) return {
9352
9179
  success: false,
9353
9180
  reason: "not loaded"
@@ -9865,7 +9692,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
9865
9692
  /**
9866
9693
  * Embedded Python path resolved once at boot via
9867
9694
  * `ctx.deps.ensurePython()`. Empty string means the download failed
9868
- * or `runtime: 'node'` was selected — the provider's
9695
+ * — the provider's
9869
9696
  * `ensureBackendDeps` and EngineFactory's `initPythonPool` raise a
9870
9697
  * clear error in that case.
9871
9698
  */