@camstack/addon-pipeline 1.2.128 → 1.2.130

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 (123) hide show
  1. package/dist/{addon-utils-BnprivSs.js → addon-utils-DH78p79i.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +241 -95
  5. package/dist/detection-pipeline/index.mjs +239 -93
  6. package/dist/{dist-BZUXb5S5.js → dist-DOw-izoI.js} +243 -75
  7. package/dist/{dist-HLAOw6Pe.mjs → dist-Wt81pN5Y.mjs} +243 -75
  8. package/dist/{event-loop-stall-monitor-BdArVUlx.js → event-loop-stall-monitor-Bld7jNCb.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-BH-cL_hg.mjs → event-loop-stall-monitor-CtQCOZqm.mjs} +1 -1
  10. package/dist/{lazy-sharp-DTDXrhMG.js → lazy-sharp-BbNI2ZD-.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-CDPRLalN.mjs → process-memory-1_D3EZT9.mjs} +1 -1
  16. package/dist/{process-memory-Dh1SyzdJ.js → process-memory-D9H5qjDl.js} +1 -1
  17. package/dist/recorder/index.js +467 -29
  18. package/dist/recorder/index.mjs +465 -28
  19. package/dist/{segment-demux-js-D-TLcHW5.mjs → segment-demux-js-BW-Q-j2A.mjs} +1 -1
  20. package/dist/{segment-demux-js-DRutggd6.js → segment-demux-js-C2jBBqGW.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +2 -2
  22. package/dist/session-decode/decode-worker-child.mjs +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BDR5z0Z9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dw-iwP4y.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs +26 -0
  26. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Do2DEtRt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs} +1 -1
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-Dmw5LWmP.mjs → hostInit-DEVCEYgu.mjs} +3 -3
  30. package/dist/stream-broker/index.js +906 -423
  31. package/dist/stream-broker/index.mjs +906 -423
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-BxkCoVjw.js → worker-protocol-nAZl_Rsv.js} +1 -1
  34. package/dist/{worker-protocol-h7Smb_0i.mjs → worker-protocol-rpnr7Ldd.mjs} +1 -1
  35. package/package.json +1 -1
  36. package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
  37. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  38. package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
  39. package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
  43. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
  44. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  45. package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
  46. package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
  47. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  48. package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
  49. package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
  50. package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
  51. package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
  52. package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
  53. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
  54. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
  55. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
  56. package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
  57. package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
  58. package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
  59. package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
  60. package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
  61. package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
  62. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  63. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  64. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  65. package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  67. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
  70. package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
  71. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  72. package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
  73. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  75. package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
  76. package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
  77. package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
  78. package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
  79. package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
  80. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  81. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  82. package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
  83. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  84. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  85. package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
  86. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  87. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  88. package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
  89. package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
  90. package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
  91. package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
  92. package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
  93. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  94. package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
  95. package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
  96. package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
  97. package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
  98. package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
  99. package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
  100. package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
  101. package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
  102. package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
  103. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  104. package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
  105. package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
  106. package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
  107. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
  108. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
  109. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
  110. package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
  111. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  112. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  113. package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
  114. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  115. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  116. package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
  117. package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
  118. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  119. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  120. package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
  121. package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
  122. package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
  123. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cs8bmrWP.mjs +0 -26
@@ -1,8 +1,8 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $ as pickClusterStepModels, D as YAMNET_TO_MACRO, Dt as hydrateSchema, Et as createEvent, Ft as sleep, G as evaluateZoneRules, Gt as string, I as defaultDeviceFor$1, Kt as union, Lt as array, Mt as parseJsonUnknown, St as BaseAddon, Ut as object, V as detectionPipelineCapability, W as enumerateInferenceDevices, Z as overlayClusterStepSettings, c as DEFAULT_CLUSTER_STEP_MODELS, dt as runtimeDevices$1, et as pickClusterStepSettings, gt as errMsg, jt as nodePin, l as DEFAULT_CLUSTER_STEP_SETTINGS, lt as resolvePoolMemoryPolicy, mt as supportedRuntimes$1, p as DEVICE_BACKEND_TO_FORMAT, q as inferModelProvider, qt as EventCategory, rt as pipelineExecutorCapability, st as resolveClusterStepModelId, t as APPLE_SA_TO_MACRO, x as PoolMemoryWatchdog } from "../dist-HLAOw6Pe.mjs";
3
- import { a as ALL_PIPELINE_STEPS, c as getDefaultModelForFormatFromDef, d as resolveModelForFormat, f as landmarkPrecisionVerdict, l as getStep, n as startEventLoopStallMonitor, o as ALL_STEPS, r as localFrameRegistry, s as getDefaultModelForFormat, u as getStepDefinition } from "../event-loop-stall-monitor-BH-cL_hg.mjs";
2
+ import { $ as pickClusterStepModels, D as YAMNET_TO_MACRO, Dt as hydrateSchema, Et as createEvent, Ft as sleep, G as evaluateZoneRules, Gt as string, I as defaultDeviceFor$1, Kt as union, Lt as array, Mt as parseJsonUnknown, St as BaseAddon, Ut as object, V as detectionPipelineCapability, W as enumerateInferenceDevices, Z as overlayClusterStepSettings, c as DEFAULT_CLUSTER_STEP_MODELS, dt as runtimeDevices$1, et as pickClusterStepSettings, gt as errMsg, jt as nodePin, l as DEFAULT_CLUSTER_STEP_SETTINGS, lt as resolvePoolMemoryPolicy, mt as supportedRuntimes$1, p as DEVICE_BACKEND_TO_FORMAT, q as inferModelProvider, qt as EventCategory, rt as pipelineExecutorCapability, st as resolveClusterStepModelId, t as APPLE_SA_TO_MACRO, x as PoolMemoryWatchdog } from "../dist-Wt81pN5Y.mjs";
3
+ import { a as ALL_PIPELINE_STEPS, c as getDefaultModelForFormatFromDef, d as resolveModelForFormat, f as landmarkPrecisionVerdict, l as getStep, n as startEventLoopStallMonitor, o as ALL_STEPS, r as localFrameRegistry, s as getDefaultModelForFormat, u as getStepDefinition } from "../event-loop-stall-monitor-CtQCOZqm.mjs";
4
4
  import { t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
5
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-CDPRLalN.mjs";
5
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-1_D3EZT9.mjs";
6
6
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-CgBCF-mE.mjs";
7
7
  import sharp from "sharp";
8
8
  import { spawn } from "node:child_process";
@@ -228,6 +228,94 @@ function pickBestRuntime(env, bestBackendHint) {
228
228
  };
229
229
  }
230
230
  //#endregion
231
+ //#region src/detection-pipeline/engine/pool-provisioning.ts
232
+ /** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
233
+ var OVERRIDABLE_KEYS = [
234
+ "concurrency",
235
+ "numWorkers",
236
+ "batchMode",
237
+ "windowMs",
238
+ "maxBatchSize",
239
+ "numStreams",
240
+ "intraOpThreads"
241
+ ];
242
+ /** Backends that are not a runtime of their own and run on onnxruntime. */
243
+ var BACKEND_TO_RUNTIME = {
244
+ onnx: "onnx",
245
+ cpu: "onnx",
246
+ cuda: "onnx",
247
+ openvino: "openvino",
248
+ coreml: "coreml",
249
+ edgetpu: "edgetpu"
250
+ };
251
+ /**
252
+ * The registry runtime a device-pool backend provisions from. Unknown backends
253
+ * floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
254
+ * format, so a pool never comes up unprovisioned.
255
+ */
256
+ function runtimeIdForBackend(backend) {
257
+ return BACKEND_TO_RUNTIME[backend] ?? "onnx";
258
+ }
259
+ /**
260
+ * Resolve the provisioning for a pool running `engine`, applying the precedence
261
+ * documented at the top of this module. `engine.device` is not consulted today
262
+ * (see the module doc) but is part of the signature so a bench-backed
263
+ * per-device row lands here and nowhere else.
264
+ */
265
+ function resolvePoolProvisioning(engine, override = {}) {
266
+ const runtime = runtimeIdForBackend(engine.backend);
267
+ const profile = tuningFor(runtime);
268
+ return {
269
+ runtime,
270
+ overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
271
+ concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
272
+ numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
273
+ batchMode: override.batchMode ?? profile.batchMode,
274
+ windowMs: override.windowMs ?? profile.windowMs,
275
+ maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
276
+ numStreams: override.numStreams ?? profile.numStreams,
277
+ intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
278
+ };
279
+ }
280
+ function positiveNumber(value) {
281
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
282
+ }
283
+ function batchMode(value) {
284
+ return value === "none" || value === "list" || value === "window" ? value : void 0;
285
+ }
286
+ /**
287
+ * Read the operator override out of the detection addon's settings. Only a
288
+ * NON-AUTO value counts: the sliders persist `0` and the select persists `''`
289
+ * for "Auto (backend default)", and those must resolve to the runtime's
290
+ * registry value, not to a literal 0 worker threads.
291
+ *
292
+ * The result is deliberately sparse — an empty object means "nothing
293
+ * overridden", which is what lets a per-device pool provision from its own
294
+ * runtime instead of inheriting the node's.
295
+ */
296
+ function readOperatorOverride(settings) {
297
+ const override = {};
298
+ const concurrency = positiveNumber(settings.concurrency);
299
+ if (concurrency !== void 0) override.concurrency = concurrency;
300
+ const numWorkers = positiveNumber(settings.numWorkers);
301
+ if (numWorkers !== void 0) override.numWorkers = numWorkers;
302
+ const mode = batchMode(settings.batchMode);
303
+ if (mode !== void 0) override.batchMode = mode;
304
+ const windowMs = positiveNumber(settings.windowMs);
305
+ if (windowMs !== void 0) override.windowMs = windowMs;
306
+ const maxBatchSize = positiveNumber(settings.maxBatchSize);
307
+ if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
308
+ const numStreams = positiveNumber(settings.numStreams);
309
+ if (numStreams !== void 0) override.numStreams = numStreams;
310
+ const intraOpThreads = positiveNumber(settings.intraOpThreads);
311
+ if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
312
+ return override;
313
+ }
314
+ /** Two overrides carry the same fields with the same values. */
315
+ function overridesEqual(a, b) {
316
+ return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
317
+ }
318
+ //#endregion
231
319
  //#region src/detection-pipeline/engine-store-keys.ts
232
320
  /**
233
321
  * Node-id normalisation for the detection pipeline.
@@ -438,6 +526,122 @@ function resolveDefaultDetectionModel(backend, device, format, getStepDef = getS
438
526
  return getDefaultModelForFormatFromDef(def, format);
439
527
  }
440
528
  //#endregion
529
+ //#region src/detection-pipeline/device-override-mirror.ts
530
+ /**
531
+ * The per-device detection overrides, kept fresh OFF the frame path.
532
+ *
533
+ * ## The read this replaces
534
+ *
535
+ * `runPipeline` resolved a device's overrides by calling
536
+ * `settings.readDeviceStore(deviceId)` on **every inference frame**. Measured
537
+ * on the live hub 2026-08-25: `get detection-pipeline:addon-devices`, 1 143
538
+ * calls a minute at 0.44 rows per call — the disk asked, nineteen times a
539
+ * second, whether an operator has ever changed a threshold.
540
+ *
541
+ * ## Why this is not "a cache in the consumer" (D224)
542
+ *
543
+ * D224 forbids a SECOND authority over a fact that belongs to somebody else:
544
+ * the failure it records is a consumer caching a value whose write path does
545
+ * not run through that consumer, so an operator's own change is the one read
546
+ * the cache gets wrong.
547
+ *
548
+ * This addon OWNS the row. `<addonId>:addon-devices` is detection-pipeline's
549
+ * own store, and the only writers are this addon's `updateDeviceSettings` and
550
+ * its `clearDeviceOverrides` — both of which call {@link invalidate}. A write
551
+ * is therefore its own invalidation, exactly as D224 requires, and the window
552
+ * below governs only the case nobody writes through this process.
553
+ *
554
+ * ## The window, declared
555
+ *
556
+ * {@link DEVICE_OVERRIDE_STALE_MS} is what an operator's change costs on a node
557
+ * that did NOT serve the write — this addon runs per node, and a settings write
558
+ * lands on one of them. Thirty seconds is the trade: it removes 99.9% of the
559
+ * reads (one per device per window instead of one per frame) and it is short
560
+ * enough that "I moved the threshold and nothing happened" is not a support
561
+ * question. Ten minutes — {@link OPERATOR_WRITTEN_STALE_MS}, the constant for a
562
+ * cap whose value only an operator changes — is right for a POLLED reader that
563
+ * costs a camera round trip; this read costs one local SQLite `get`, and the
564
+ * cheap read buys a short window.
565
+ *
566
+ * A stale entry is SERVED and revalidated behind the caller, never awaited: a
567
+ * frame that waits for the settings store is a frame the pipeline dropped, and
568
+ * the reader is by definition about to ask again.
569
+ */
570
+ /**
571
+ * How long a mirrored entry is served before a refresh is started behind the
572
+ * next reader. See the module doc for why it is 30 s and not ten minutes.
573
+ */
574
+ var DEVICE_OVERRIDE_STALE_MS = 3e4;
575
+ /**
576
+ * How long a FAILED refresh is not retried.
577
+ *
578
+ * A refresh that throws leaves `fetchedAt` where it was, so without a cooldown
579
+ * every subsequent frame would start another one — a broken settings store
580
+ * would be hammered at frame rate by the very mechanism built to stop that.
581
+ */
582
+ var DEVICE_OVERRIDE_MISS_COOLDOWN_MS = 5e3;
583
+ var DeviceOverrideMirror = class {
584
+ deps;
585
+ staleMs;
586
+ missCooldownMs;
587
+ entries = /* @__PURE__ */ new Map();
588
+ /** In-flight reads, so a burst of frames on a cold device costs ONE read and
589
+ * not one per frame — the exact failure this class exists to remove. */
590
+ inFlight = /* @__PURE__ */ new Map();
591
+ now;
592
+ constructor(deps, staleMs = DEVICE_OVERRIDE_STALE_MS, missCooldownMs = DEVICE_OVERRIDE_MISS_COOLDOWN_MS) {
593
+ this.deps = deps;
594
+ this.staleMs = staleMs;
595
+ this.missCooldownMs = missCooldownMs;
596
+ this.now = deps.now ?? (() => Date.now());
597
+ }
598
+ /**
599
+ * The device's overrides.
600
+ *
601
+ * COLD awaits — the first frame on a camera has to get the real answer, and a
602
+ * default is exactly what "the operator lowered this threshold" looks like.
603
+ * WARM returns immediately, stale or not, and starts the refresh behind the
604
+ * caller.
605
+ */
606
+ async resolve(deviceId) {
607
+ const entry = this.entries.get(deviceId);
608
+ if (entry === void 0) return this.load(deviceId);
609
+ if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
610
+ return entry.value;
611
+ }
612
+ /** Drop a device's entry. Called by every writer in this process, so an
613
+ * operator's own change is never served from the mirror. */
614
+ invalidate(deviceId) {
615
+ this.entries.delete(deviceId);
616
+ }
617
+ invalidateAll() {
618
+ this.entries.clear();
619
+ }
620
+ async load(deviceId) {
621
+ const pending = this.inFlight.get(deviceId);
622
+ if (pending !== void 0) return pending;
623
+ const run = this.deps.read(deviceId).then((value) => {
624
+ this.entries.set(deviceId, {
625
+ value,
626
+ fetchedAt: this.now()
627
+ });
628
+ return value;
629
+ }).catch((err) => {
630
+ this.deps.onRefreshFailed?.(deviceId, err);
631
+ const existing = this.entries.get(deviceId);
632
+ if (existing !== void 0) {
633
+ existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
634
+ return existing.value;
635
+ }
636
+ return {};
637
+ }).finally(() => {
638
+ this.inFlight.delete(deviceId);
639
+ });
640
+ this.inFlight.set(deviceId, run);
641
+ return run;
642
+ }
643
+ };
644
+ //#endregion
441
645
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
442
646
  /**
443
647
  * SharedInferencePool — TypeScript wrapper for inference_pool.py.
@@ -1980,94 +2184,6 @@ function deviceKeyOf(engine) {
1980
2184
  return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
1981
2185
  }
1982
2186
  //#endregion
1983
- //#region src/detection-pipeline/engine/pool-provisioning.ts
1984
- /** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
1985
- var OVERRIDABLE_KEYS = [
1986
- "concurrency",
1987
- "numWorkers",
1988
- "batchMode",
1989
- "windowMs",
1990
- "maxBatchSize",
1991
- "numStreams",
1992
- "intraOpThreads"
1993
- ];
1994
- /** Backends that are not a runtime of their own and run on onnxruntime. */
1995
- var BACKEND_TO_RUNTIME = {
1996
- onnx: "onnx",
1997
- cpu: "onnx",
1998
- cuda: "onnx",
1999
- openvino: "openvino",
2000
- coreml: "coreml",
2001
- edgetpu: "edgetpu"
2002
- };
2003
- /**
2004
- * The registry runtime a device-pool backend provisions from. Unknown backends
2005
- * floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
2006
- * format, so a pool never comes up unprovisioned.
2007
- */
2008
- function runtimeIdForBackend(backend) {
2009
- return BACKEND_TO_RUNTIME[backend] ?? "onnx";
2010
- }
2011
- /**
2012
- * Resolve the provisioning for a pool running `engine`, applying the precedence
2013
- * documented at the top of this module. `engine.device` is not consulted today
2014
- * (see the module doc) but is part of the signature so a bench-backed
2015
- * per-device row lands here and nowhere else.
2016
- */
2017
- function resolvePoolProvisioning(engine, override = {}) {
2018
- const runtime = runtimeIdForBackend(engine.backend);
2019
- const profile = tuningFor(runtime);
2020
- return {
2021
- runtime,
2022
- overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
2023
- concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
2024
- numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
2025
- batchMode: override.batchMode ?? profile.batchMode,
2026
- windowMs: override.windowMs ?? profile.windowMs,
2027
- maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
2028
- numStreams: override.numStreams ?? profile.numStreams,
2029
- intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
2030
- };
2031
- }
2032
- function positiveNumber(value) {
2033
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
2034
- }
2035
- function batchMode(value) {
2036
- return value === "none" || value === "list" || value === "window" ? value : void 0;
2037
- }
2038
- /**
2039
- * Read the operator override out of the detection addon's settings. Only a
2040
- * NON-AUTO value counts: the sliders persist `0` and the select persists `''`
2041
- * for "Auto (backend default)", and those must resolve to the runtime's
2042
- * registry value, not to a literal 0 worker threads.
2043
- *
2044
- * The result is deliberately sparse — an empty object means "nothing
2045
- * overridden", which is what lets a per-device pool provision from its own
2046
- * runtime instead of inheriting the node's.
2047
- */
2048
- function readOperatorOverride(settings) {
2049
- const override = {};
2050
- const concurrency = positiveNumber(settings.concurrency);
2051
- if (concurrency !== void 0) override.concurrency = concurrency;
2052
- const numWorkers = positiveNumber(settings.numWorkers);
2053
- if (numWorkers !== void 0) override.numWorkers = numWorkers;
2054
- const mode = batchMode(settings.batchMode);
2055
- if (mode !== void 0) override.batchMode = mode;
2056
- const windowMs = positiveNumber(settings.windowMs);
2057
- if (windowMs !== void 0) override.windowMs = windowMs;
2058
- const maxBatchSize = positiveNumber(settings.maxBatchSize);
2059
- if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
2060
- const numStreams = positiveNumber(settings.numStreams);
2061
- if (numStreams !== void 0) override.numStreams = numStreams;
2062
- const intraOpThreads = positiveNumber(settings.intraOpThreads);
2063
- if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
2064
- return override;
2065
- }
2066
- /** Two overrides carry the same fields with the same values. */
2067
- function overridesEqual(a, b) {
2068
- return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
2069
- }
2070
- //#endregion
2071
2187
  //#region src/detection-pipeline/engine/engine-factory.ts
2072
2188
  var BACKEND_TO_POOL_RUNTIME = {
2073
2189
  coreml: "coreml",
@@ -6220,6 +6336,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6220
6336
  readDeviceStore;
6221
6337
  /** Delete all per-device settings for a device (used by `clearDeviceOverrides`). */
6222
6338
  clearDeviceStore;
6339
+ /**
6340
+ * The per-device overrides the FRAME path reads, kept fresh off that path.
6341
+ *
6342
+ * Public so the addon class can drop a device on its own
6343
+ * `updateDeviceSettings` — the write and the mirror have to be the same
6344
+ * process's, or the mirror becomes the second authority D224 forbids.
6345
+ */
6346
+ deviceOverrides;
6223
6347
  constructor(settings, modelsDir, logger, eventBus = null, detectionConfigSchemaSource = null, executorOptions = {}) {
6224
6348
  this.modelsDir = modelsDir;
6225
6349
  this.eventBus = eventBus;
@@ -6231,6 +6355,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6231
6355
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
6232
6356
  const clearDeviceStore = settings.clearDeviceStore;
6233
6357
  this.clearDeviceStore = clearDeviceStore ? (deviceId) => clearDeviceStore.call(settings, deviceId) : async () => void 0;
6358
+ this.deviceOverrides = new DeviceOverrideMirror({
6359
+ read: (deviceId) => this.readDeviceStore(deviceId),
6360
+ onRefreshFailed: (deviceId, err) => this.log.warn("per-device detection overrides refresh failed — serving the last value", {
6361
+ tags: { deviceId },
6362
+ meta: { error: errMsg(err) }
6363
+ })
6364
+ });
6234
6365
  this.currentEngine = ONNX_FLOOR;
6235
6366
  this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
6236
6367
  runtime: this.currentEngine.runtime,
@@ -7415,7 +7546,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7415
7546
  setupMs = performance.now() - wallT0 - decodeMs;
7416
7547
  const isRuntime = Boolean(runtimeFrame || runtimeFrameRef);
7417
7548
  const effectiveDeviceId = input.deviceId ?? 0;
7418
- const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
7549
+ const deviceOverrides = effectiveDeviceId > 0 ? await this.deviceOverrides.resolve(effectiveDeviceId) : {};
7419
7550
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
7420
7551
  const nativeCropProvider = this.buildNativeCropProviderFromRef(input.nativeCropRef) ?? this.buildNativeFaceCropProvider(input.frameHandle);
7421
7552
  let cropZoneBbox;
@@ -8484,7 +8615,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8484
8615
  if (!api) return 0;
8485
8616
  let devices = [];
8486
8617
  try {
8487
- devices = await api.deviceManager.listAll.query({});
8618
+ devices = await api.deviceManager.listAll.query({ projection: "slim" });
8488
8619
  } catch (err) {
8489
8620
  this.log.warn("clearDeviceOverrides: device enumeration failed — skipping per-device clear", { meta: { error: errMsg(err) } });
8490
8621
  return 0;
@@ -8492,6 +8623,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
8492
8623
  let cleared = 0;
8493
8624
  for (const device of devices) try {
8494
8625
  await this.clearDeviceStore(device.id);
8626
+ this.deviceOverrides.invalidate(device.id);
8495
8627
  cleared++;
8496
8628
  } catch (err) {
8497
8629
  this.log.warn("clearDeviceOverrides: failed to clear per-device overrides for device", {
@@ -9049,6 +9181,20 @@ function isProfileSliderKey(key) {
9049
9181
  }
9050
9182
  var DetectionPipelineAddon = class extends BaseAddon {
9051
9183
  provider = null;
9184
+ /**
9185
+ * An operator's own write drops the mirror the frame path reads.
9186
+ *
9187
+ * This override is the half of `device-override-mirror.ts` that keeps it from
9188
+ * being the second authority D224 forbids: `BaseAddon.updateDeviceSettings`
9189
+ * writes THIS addon's `<addonId>:addon-devices` row, so the write and the
9190
+ * mirror are in one process and the write invalidates. Without it the
9191
+ * operator's own change would be the one read the mirror got wrong, which is
9192
+ * the exact failure D224 records.
9193
+ */
9194
+ async updateDeviceSettings(deviceId, patch) {
9195
+ await super.updateDeviceSettings(deviceId, patch);
9196
+ this.provider?.deviceOverrides.invalidate(deviceId);
9197
+ }
9052
9198
  /** Last non-null probed hardware PER NODE — reused when the probe transiently
9053
9199
  * returns null so offered backends / device lists don't collapse. Keyed by
9054
9200
  * node because the hub addon probes other nodes when serving their config.