@camstack/addon-pipeline 1.2.295 → 1.2.297

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 (40) hide show
  1. package/THIRD_PARTY_MODELS.md +8 -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-wNEISVA9.js → default-detection-model-B2n_SFXt.js} +442 -50
  5. package/dist/{default-detection-model-CAUBVbgK.mjs → default-detection-model-D6DypCkI.mjs} +413 -51
  6. package/dist/detection-pipeline/index.js +1516 -269
  7. package/dist/detection-pipeline/index.mjs +1515 -268
  8. package/dist/{dist-CuxSNLKW.mjs → dist-CxHIpsQs.mjs} +834 -398
  9. package/dist/{dist-BsVcf5wO.js → dist-DUqr1zyq.js} +845 -409
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-UFk6I2f6.js → node-Dx6DLQ1j.js} +1 -1
  13. package/dist/{node-Cqb1QiXk.mjs → node-DyeWu78a.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +906 -310
  15. package/dist/pipeline-runner/index.mjs +906 -310
  16. package/dist/{process-memory-D0Nvs9rr.mjs → process-memory-BVR4592X.mjs} +1 -1
  17. package/dist/{process-memory-CJV29sPf.js → process-memory-CMJ3NY-s.js} +1 -1
  18. package/dist/recorder/index.js +4 -6
  19. package/dist/recorder/index.mjs +4 -6
  20. package/dist/{segment-demux-js-DGwmf5hu.js → segment-demux-js-CmGIF_uK.js} +1 -1
  21. package/dist/{segment-demux-js-DIDWw1iE.mjs → segment-demux-js-Dzga-XgE.mjs} +1 -1
  22. package/dist/session-decode/{decode-worker-child.js → decode-worker-main.js} +481 -72
  23. package/dist/session-decode/{decode-worker-child.mjs → decode-worker-main.mjs} +482 -71
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B7nBFqva.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-P71ze8cu.mjs} +2 -2
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CZkpFU-J.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dti8Ex88.mjs +26 -0
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-BKlb3qac.mjs → hostInit-CRlStbz4.mjs} +2 -2
  31. package/dist/stream-broker/index.js +4 -4
  32. package/dist/stream-broker/index.mjs +4 -4
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/{worker-protocol-B2MfQLlu.js → worker-protocol-C-G8qmye.js} +3 -1
  35. package/dist/{worker-protocol-C_W-P_g-.mjs → worker-protocol-D_NzPcnh.mjs} +3 -1
  36. package/package.json +1 -1
  37. package/python/inference_pool.py +422 -64
  38. package/python/test_inference_pool_compile_off_loop.py +414 -0
  39. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCIyBvRa.mjs +0 -26
  40. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DeD_UFdb.mjs +0 -26
@@ -61,6 +61,12 @@ under [Non-commercial models](#non-commercial-models).
61
61
  https://github.com/ShiqiYu/libfacedetection.train.
62
62
  Trained on WIDER FACE, whose images are CC BY-NC-ND.
63
63
  - **AuraFace v1** — fal.ai, Apache-2.0. https://huggingface.co/fal/AuraFace-v1
64
+ - **SigLIP 2** — © Google LLC, Apache-2.0 (`Apache-2.0.txt`).
65
+ https://huggingface.co/google/siglip2-base-patch16-224 ·
66
+ https://github.com/google-research/big_vision. CamStack's SigLIP 2 files are
67
+ modified: the vision input scaling `2x - 1` is baked into the graph, the text
68
+ graph slices its input to the model's 64 positions, the tokenizer lower-cases,
69
+ and the towers are converted to ONNX, OpenVINO IR and CoreML FP16.
64
70
  - **EasyOCR** — JaidedAI, Apache-2.0. https://github.com/JaidedAI/EasyOCR
65
71
  - **U²-Net** — Xuebin Qin et al., Apache-2.0. https://github.com/xuebinqin/U-2-Net
66
72
  - **YAMNet** — © Google LLC, Apache-2.0.
@@ -202,6 +208,8 @@ commercial use**:
202
208
  | --- | --- | --- | --- | --- | --- |
203
209
  | `mobileclip-s1`, `mobileclip-s2` (image) | Apple `ml-mobileclip` | MIT / **Apple Machine Learning Research Model License** | per Apple | Model Derivative: converted to OpenVINO IR and CoreML FP16 | Mirror `clip/mobileclip-s1`, `clip/mobileclip-s2` |
204
210
  | `mobileclip-s1-text`, `mobileclip-s2-text` | Apple `ml-mobileclip`, with the CLIP tokenizer | MIT / **Apple Machine Learning Research Model License** | per Apple | Model Derivative: quantised to INT8 ONNX and converted to OpenVINO IR | Mirror `clip/mobileclip-s1`, `clip/mobileclip-s2` |
211
+ | `siglip2-b16-224` (image) | Google SigLIP 2 base patch16-224 (`google/siglip2-base-patch16-224`) | Apache-2.0 / Apache-2.0 | WebLI (Google internal, not released) | input scaling `y = 2x - 1` baked ahead of the patch embedding; converted to OpenVINO IR and CoreML FP16 (an FP32 ONNX is hosted as the verification reference) (`scripts/build-siglip2-model.py`) | Mirror `clip/siglip2` |
212
+ | `siglip2-b16-224-text` | Google SigLIP 2 base patch16-224, with its Gemma tokenizer | Apache-2.0 / Apache-2.0 | WebLI (Google internal, not released) | input sliced to the model's 64 positions in the graph; ONNX weights FP16, OpenVINO IR and CoreML FP16; a `Lowercase` normalizer prepended to the tokenizer | Mirror `clip/siglip2` |
205
213
 
206
214
  ### Segmentation
207
215
 
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-BsVcf5wO.js");
7
- const require_process_memory = require("../process-memory-CJV29sPf.js");
6
+ const require_dist = require("../dist-DUqr1zyq.js");
7
+ const require_process_memory = require("../process-memory-CMJ3NY-s.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
10
10
  let node_path = require("node:path");
@@ -1,6 +1,6 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { E as HF_BASE_URL, G as audioAnalyzerCapability, Rn as EventCategory, W as audioAnalysisCapability, Wt as resolvePoolMemoryPolicy, _n as hydrateSchema, bt as mapAudioLabelToMacro, en as errMsg, f as DEFAULT_AUDIO_ANALYZER_CONFIG, fn as audioChunkBytesPerSample, gn as expandAudioChunkToF32le, j as PoolMemoryWatchdog, n as AUDIO_BACKEND_CHOICES, sn as BaseAddon, xn as nodePin } from "../dist-CuxSNLKW.mjs";
3
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-D0Nvs9rr.mjs";
2
+ import { E as HF_BASE_URL, G as audioAnalyzerCapability, Rn as EventCategory, Ut as resolvePoolMemoryPolicy, W as audioAnalysisCapability, _n as hydrateSchema, bt as mapAudioLabelToMacro, en as errMsg, fn as audioChunkBytesPerSample, gn as expandAudioChunkToF32le, j as PoolMemoryWatchdog, n as AUDIO_BACKEND_CHOICES, p as DEFAULT_AUDIO_ANALYZER_CONFIG, sn as BaseAddon, xn as nodePin } from "../dist-CxHIpsQs.mjs";
3
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-BVR4592X.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import { downloadFile } from "@camstack/system/addon-utils";
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BsVcf5wO.js");
1
+ const require_dist = require("./dist-DUqr1zyq.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/detection-pipeline/pipeline/landmark-precision-gate.ts
4
4
  /**
@@ -1184,55 +1184,84 @@ var INSTANCE_SEGMENTATION_MODELS = [
1184
1184
  }
1185
1185
  }
1186
1186
  ];
1187
- var CLIP_EMBEDDING_MODELS = [{
1188
- id: "mobileclip-s1",
1189
- name: "MobileCLIP S1",
1190
- description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (fp16 OpenVINO/CoreML)",
1191
- inputSize: {
1192
- width: 256,
1193
- height: 256
1187
+ var CLIP_EMBEDDING_MODELS = [
1188
+ {
1189
+ id: "mobileclip-s1",
1190
+ name: "MobileCLIP S1",
1191
+ description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (fp16 OpenVINO/CoreML)",
1192
+ inputSize: {
1193
+ width: 256,
1194
+ height: 256
1195
+ },
1196
+ labels: [{
1197
+ id: "embedding",
1198
+ name: "CLIP Embedding"
1199
+ }],
1200
+ preprocessMode: "resize",
1201
+ inputNormalization: "none",
1202
+ formats: {
1203
+ openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
1204
+ coreml: {
1205
+ url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
1206
+ sizeMB: 65,
1207
+ isDirectory: true,
1208
+ files: [...MLPACKAGE_FILES],
1209
+ runtimes: ["python"]
1210
+ }
1211
+ }
1194
1212
  },
1195
- labels: [{
1196
- id: "embedding",
1197
- name: "CLIP Embedding"
1198
- }],
1199
- preprocessMode: "resize",
1200
- inputNormalization: "none",
1201
- formats: {
1202
- openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
1203
- coreml: {
1204
- url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
1205
- sizeMB: 65,
1206
- isDirectory: true,
1207
- files: [...MLPACKAGE_FILES],
1208
- runtimes: ["python"]
1213
+ {
1214
+ id: "mobileclip-s2",
1215
+ name: "MobileCLIP S2",
1216
+ description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (fp16 OpenVINO/CoreML)",
1217
+ inputSize: {
1218
+ width: 256,
1219
+ height: 256
1220
+ },
1221
+ labels: [{
1222
+ id: "embedding",
1223
+ name: "CLIP Embedding"
1224
+ }],
1225
+ preprocessMode: "resize",
1226
+ inputNormalization: "none",
1227
+ formats: {
1228
+ openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
1229
+ coreml: {
1230
+ url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
1231
+ sizeMB: 110,
1232
+ isDirectory: true,
1233
+ files: [...MLPACKAGE_FILES],
1234
+ runtimes: ["python"]
1235
+ }
1209
1236
  }
1210
- }
1211
- }, {
1212
- id: "mobileclip-s2",
1213
- name: "MobileCLIP S2",
1214
- description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (fp16 OpenVINO/CoreML)",
1215
- inputSize: {
1216
- width: 256,
1217
- height: 256
1218
1237
  },
1219
- labels: [{
1220
- id: "embedding",
1221
- name: "CLIP Embedding"
1222
- }],
1223
- preprocessMode: "resize",
1224
- inputNormalization: "none",
1225
- formats: {
1226
- openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
1227
- coreml: {
1228
- url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
1229
- sizeMB: 110,
1230
- isDirectory: true,
1231
- files: [...MLPACKAGE_FILES],
1232
- runtimes: ["python"]
1238
+ {
1239
+ id: "siglip2-b16-224",
1240
+ name: "SigLIP2 B/16",
1241
+ description: "Google SigLIP2 base, patch 16, 224×224 — Apache-2.0 CLIP vision encoder, 768-dim (fp16 OpenVINO/CoreML)",
1242
+ inputSize: {
1243
+ width: 224,
1244
+ height: 224
1245
+ },
1246
+ labels: [{
1247
+ id: "embedding",
1248
+ name: "CLIP Embedding"
1249
+ }],
1250
+ preprocessMode: "resize",
1251
+ inputNormalization: "none",
1252
+ license: "Apache-2.0",
1253
+ formats: {
1254
+ openvino: ovFormat(hf("clip/siglip2/openvino/camstack-siglip2-b16-224-vision.xml"), 186),
1255
+ coreml: {
1256
+ url: hf("clip/siglip2/coreml/camstack-siglip2-b16-224-vision.mlpackage"),
1257
+ sizeMB: 185,
1258
+ isDirectory: true,
1259
+ files: [...MLPACKAGE_FILES],
1260
+ runtimes: ["python"]
1261
+ }
1233
1262
  }
1234
1263
  }
1235
- }];
1264
+ ];
1236
1265
  var AUDIO_CLASSIFIER_MODELS = [{
1237
1266
  id: "yamnet-onnx",
1238
1267
  name: "YAMNet",
@@ -1626,7 +1655,6 @@ var STEP_FACE_DETECTION = new PipelineStepBase({
1626
1655
  outputClasses: ["face"],
1627
1656
  models: [...FACE_DETECTION_MODELS],
1628
1657
  defaultModelId: "yunet-2023mar",
1629
- defaultModelIdByFormat: { tflite: "scrfd-2.5g" },
1630
1658
  defaultConfidence: .5,
1631
1659
  defaultMinParentScore: .7,
1632
1660
  cadence: {
@@ -1671,7 +1699,7 @@ var STEP_FACE_EMBEDDING = new FaceEmbeddingStep({
1671
1699
  outputClasses: ["identity"],
1672
1700
  labelTier: 2,
1673
1701
  models: [...FACE_EMBEDDING_MODELS],
1674
- defaultModelId: "arcface-r100",
1702
+ defaultModelId: "auraface-r100",
1675
1703
  modelScope: "cluster",
1676
1704
  defaultConfidence: 0,
1677
1705
  cadence: {
@@ -2051,17 +2079,24 @@ var LocalFrameRegistry = class {
2051
2079
  image,
2052
2080
  estimatedBytes,
2053
2081
  refs: 1,
2082
+ consumerRefs: 1,
2054
2083
  releaseNativeOnClose: false
2055
2084
  });
2056
2085
  this.bytes += estimatedBytes;
2057
2086
  this.evictToBounds();
2058
2087
  return this.entries.has(ref.id) ? ref : null;
2059
2088
  }
2060
- acquire(ref) {
2089
+ /**
2090
+ * Take a lease on `ref`. `holder` says why (D656): a reader that resolves a
2091
+ * view now is a `consumer` (the default); a lease PARKED for a read that may
2092
+ * come later is `retention`, and does not keep a draining worker alive.
2093
+ */
2094
+ acquire(ref, holder = "consumer") {
2061
2095
  if (ref.registryId === this.registryId && !this.entries.has(ref.id)) this.staleMissCount += 1;
2062
2096
  const entry = this.getEntry(ref);
2063
2097
  if (!entry) return null;
2064
2098
  entry.refs += 1;
2099
+ if (holder === "consumer") this.addConsumer(entry);
2065
2100
  this.touch(entry);
2066
2101
  let released = false;
2067
2102
  return {
@@ -2069,17 +2104,40 @@ var LocalFrameRegistry = class {
2069
2104
  release: (reason) => {
2070
2105
  if (released) return;
2071
2106
  released = true;
2072
- this.release(ref, reason);
2107
+ this.releaseRef(ref, reason, holder);
2073
2108
  }
2074
2109
  };
2075
2110
  }
2111
+ /** Release the OWNER reference {@link register} took — a consumer reference. */
2076
2112
  release(ref, reason) {
2113
+ this.releaseRef(ref, reason, "consumer");
2114
+ }
2115
+ releaseRef(ref, reason, holder) {
2077
2116
  const entry = this.getEntry(ref);
2078
2117
  if (!entry) return;
2079
2118
  this.releaseCount += 1;
2080
2119
  if (releasesNative(reason)) entry.releaseNativeOnClose = true;
2081
2120
  entry.refs = Math.max(0, entry.refs - 1);
2082
- if (entry.refs === 0) this.drop(entry);
2121
+ if (entry.refs === 0) {
2122
+ this.drop(entry);
2123
+ return;
2124
+ }
2125
+ if (holder === "consumer") this.removeConsumer(entry);
2126
+ }
2127
+ /**
2128
+ * The frame's source learns whether a consumer still holds it, on each
2129
+ * transition only: the last consumer letting go of a frame that stays
2130
+ * PARKED, and a consumer coming back to one. A drop needs no signal — the
2131
+ * image is closed.
2132
+ */
2133
+ addConsumer(entry) {
2134
+ entry.consumerRefs += 1;
2135
+ if (entry.consumerRefs === 1) entry.image.setPendingConsumer?.(true);
2136
+ }
2137
+ removeConsumer(entry) {
2138
+ if (entry.consumerRefs === 0) return;
2139
+ entry.consumerRefs -= 1;
2140
+ if (entry.consumerRefs === 0) entry.image.setPendingConsumer?.(false);
2083
2141
  }
2084
2142
  clear(reason = "detach") {
2085
2143
  for (const entry of [...this.entries.values()]) {
@@ -2178,6 +2236,310 @@ function rootObjectGate(steps) {
2178
2236
  return (steps?.find((s) => s.enabled !== false))?.settings;
2179
2237
  }
2180
2238
  //#endregion
2239
+ //#region src/pipeline-runner/device-jump.ts
2240
+ /**
2241
+ * Step-tree device jump (same-node, phase 1) — the PURE resolution core.
2242
+ *
2243
+ * Given the camera's elected ("effective") inference device and the node's
2244
+ * enabled-device roster, decide which device ONE enrichment step should run on:
2245
+ *
2246
+ * 1. A manual `jumpDeviceKey` override wins when it names a candidate that can
2247
+ * run the step (bypasses the auto choice; operator intent).
2248
+ * 2. Otherwise, if the effective device's format can run the step's model →
2249
+ * NO jump (the step inherits the effective device — the common case).
2250
+ * 3. Otherwise AUTO-jump: among the node's OTHER enabled devices whose format
2251
+ * can run the step (and that are under their `maxSessions` cap), pick the
2252
+ * weighted-least-loaded one — mirroring the balancer's per-device core.
2253
+ * 4. If nothing on the node can run it → `no-candidate` (the caller emits the
2254
+ * loud once-per-(camera,step) warn that replaces today's silent degrade).
2255
+ *
2256
+ * Framework-free + side-effect-free so the whole decision table is unit-testable
2257
+ * without a live runner/catalog. The `canRun(format)` predicate is injected by
2258
+ * the runner (which owns the model catalog); this module never imports it.
2259
+ */
2260
+ /** The `ModelFormat` of a `<backend>:<device>` key, or `undefined` for an unknown backend. */
2261
+ function formatOf(deviceKey) {
2262
+ const colon = deviceKey.indexOf(":");
2263
+ return require_dist.DEVICE_BACKEND_TO_FORMAT[colon >= 0 ? deviceKey.slice(0, colon) : deviceKey];
2264
+ }
2265
+ /** True when the step's model has a build for `deviceKey`'s format. */
2266
+ function canRunOn(deviceKey, canRun) {
2267
+ const format = formatOf(deviceKey);
2268
+ return format !== void 0 && canRun(format, deviceKey);
2269
+ }
2270
+ /** Effective weighted load of a device (session count ÷ weight); lower = preferred. */
2271
+ function weightedLoad(d) {
2272
+ return d.attachedCameras / (d.weight > 0 ? d.weight : 1);
2273
+ }
2274
+ /**
2275
+ * Weighted-least-loaded pick with a deterministic `deviceKey` tie-break —
2276
+ * mirrors the node/device balancer's decision core (least `load/weight`, ties
2277
+ * broken stably). `devices` is assumed non-empty.
2278
+ */
2279
+ function pickWeightedLeastLoaded(devices) {
2280
+ return devices.reduce((best, d) => {
2281
+ const dScore = weightedLoad(d);
2282
+ const bScore = weightedLoad(best);
2283
+ if (dScore < bScore) return d;
2284
+ if (dScore > bScore) return best;
2285
+ return d.deviceKey < best.deviceKey ? d : best;
2286
+ });
2287
+ }
2288
+ /**
2289
+ * Build the `canRun` predicate for ONE enrichment dispatch. The step and its
2290
+ * enabled descendants ride the SAME `runPipeline` call — one pool, one format
2291
+ * (detail-subtree contract: "its inherited children ride the same call → same
2292
+ * device") — so a format can run the dispatch only when EVERY catalog-known
2293
+ * step in the subtree ships a build for it. Gating only the subtree TOP let a
2294
+ * `face-detection` with a tflite build stay on the Coral while its
2295
+ * `face-embedding` child (no tflite build anywhere) doomed the call.
2296
+ * Unknown/custom addonIds (`getStepDef` → null) have no catalog to gate on
2297
+ * and never block. `getStepDef` is injected — this module stays catalog-free.
2298
+ * Each step is judged by `stepModelRunsOnFormat` (`@camstack/types`), the one
2299
+ * rule every placement reads: a legacy stub build counts only when named (D657).
2300
+ */
2301
+ function buildSubtreeCanRun(step, getStepDef) {
2302
+ const judged = [];
2303
+ const walk = (s) => {
2304
+ if (s.enabled === false) return;
2305
+ const def = getStepDef(s.addonId);
2306
+ if (def !== null) judged.push({
2307
+ def,
2308
+ modelId: s.modelId
2309
+ });
2310
+ for (const child of s.children ?? []) walk(child);
2311
+ };
2312
+ walk(step);
2313
+ return (format) => judged.every(({ def, modelId }) => require_dist.stepModelRunsOnFormat(def.models, modelId, format));
2314
+ }
2315
+ /**
2316
+ * Resolve the device one enrichment step runs on. See the module header for the
2317
+ * decision table. Pure — never throws, no I/O.
2318
+ */
2319
+ function resolveStepDevice(input) {
2320
+ const { effectiveDeviceKey, manualJumpDeviceKey, canRun } = input;
2321
+ if (manualJumpDeviceKey !== void 0 && manualJumpDeviceKey !== effectiveDeviceKey) {
2322
+ const target = input.devices.find((d) => d.deviceKey === manualJumpDeviceKey);
2323
+ if (target && canRunOn(target.deviceKey, canRun)) return {
2324
+ kind: "jumped",
2325
+ deviceKey: target.deviceKey,
2326
+ reason: "manual"
2327
+ };
2328
+ }
2329
+ if (canRunOn(effectiveDeviceKey, canRun)) return {
2330
+ kind: "no-jump",
2331
+ deviceKey: effectiveDeviceKey
2332
+ };
2333
+ const runnable = input.devices.filter((d) => d.deviceKey !== effectiveDeviceKey && (d.maxSessions === null || d.attachedCameras < d.maxSessions) && canRunOn(d.deviceKey, canRun));
2334
+ if (runnable.length === 0) return {
2335
+ kind: "no-candidate",
2336
+ deviceKey: effectiveDeviceKey
2337
+ };
2338
+ return {
2339
+ kind: "jumped",
2340
+ deviceKey: pickWeightedLeastLoaded(runnable).deviceKey,
2341
+ reason: "auto"
2342
+ };
2343
+ }
2344
+ /**
2345
+ * Re-provision a jumped subtree for its TARGET device (D650): every step's
2346
+ * `modelId` / `settings` are REPLACED by the target's entry — the orchestrator
2347
+ * computed it as what the step would carry had the camera been elected onto
2348
+ * that device (its per-(node,device) base with the camera's override for it).
2349
+ * A step absent from the map runs with neither, i.e. the target format's
2350
+ * default model. The elected device's provisioning never follows a step across:
2351
+ * it was chosen for a format this subtree does not run on.
2352
+ */
2353
+ function provisionSubtreeForDevice(step, provisioning) {
2354
+ const own = provisioning[step.addonId];
2355
+ const { modelId: _electedModelId, settings: _electedSettings, ...rest } = step;
2356
+ return {
2357
+ ...rest,
2358
+ ...own?.modelId !== void 0 ? { modelId: own.modelId } : {},
2359
+ ...own?.settings !== void 0 ? { settings: own.settings } : {},
2360
+ ...step.children !== void 0 ? { children: step.children.map((c) => provisionSubtreeForDevice(c, provisioning)) } : {}
2361
+ };
2362
+ }
2363
+ /** The subtree as `deviceKey` would run it: the elected device runs it as sent; another takes its roster entry. */
2364
+ function provisionedFor(step, deviceKey, effectiveDeviceKey, roster) {
2365
+ if (deviceKey === effectiveDeviceKey) return step;
2366
+ const provisioning = roster.find((d) => d.deviceKey === deviceKey)?.steps;
2367
+ return provisioning === void 0 ? step : provisionSubtreeForDevice(step, provisioning);
2368
+ }
2369
+ /**
2370
+ * Place ONE detail subtree (D650): which same-node device runs it
2371
+ * ({@link resolveStepDevice}) and, when it jumps, the provisioning it runs
2372
+ * with there ({@link provisionSubtreeForDevice}). Pure — the caller logs.
2373
+ *
2374
+ * A roster entry without `steps` came from an orchestrator that predates D650:
2375
+ * the step keeps what it was sent with, as it did before.
2376
+ */
2377
+ function placeDetailStep(input) {
2378
+ const { step, effectiveDeviceKey, roster } = input;
2379
+ if (!effectiveDeviceKey) return {
2380
+ deviceKey: void 0,
2381
+ step,
2382
+ decision: null,
2383
+ provisioned: false
2384
+ };
2385
+ if (!roster || roster.length < 2) return {
2386
+ deviceKey: effectiveDeviceKey,
2387
+ step,
2388
+ decision: null,
2389
+ provisioned: false
2390
+ };
2391
+ const decision = resolveStepDevice({
2392
+ effectiveDeviceKey,
2393
+ devices: roster.map((d) => ({
2394
+ deviceKey: d.deviceKey,
2395
+ weight: d.weight,
2396
+ maxSessions: d.maxSessions,
2397
+ attachedCameras: input.attachedByKey.get(d.deviceKey) ?? 0
2398
+ })),
2399
+ ...step.jumpDeviceKey !== void 0 ? { manualJumpDeviceKey: step.jumpDeviceKey } : {},
2400
+ canRun: (format, deviceKey) => buildSubtreeCanRun(provisionedFor(step, deviceKey, effectiveDeviceKey, roster), input.getStepDef)(format)
2401
+ });
2402
+ if (decision.kind !== "jumped") return {
2403
+ deviceKey: decision.deviceKey,
2404
+ step,
2405
+ decision,
2406
+ provisioned: false
2407
+ };
2408
+ const provisioning = roster.find((d) => d.deviceKey === decision.deviceKey)?.steps;
2409
+ return {
2410
+ deviceKey: decision.deviceKey,
2411
+ step: provisioning === void 0 ? step : provisionSubtreeForDevice(step, provisioning),
2412
+ decision,
2413
+ provisioned: provisioning !== void 0
2414
+ };
2415
+ }
2416
+ /**
2417
+ * The steps whose models a camera's elected pool loads (D657).
2418
+ *
2419
+ * The frame-plane dispatch executes only root-plane steps; a detail subtree
2420
+ * rides it solely so its model pre-warms the elected pool. That is right only
2421
+ * when the subtree RUNS there. The decision is {@link placeDetailStep}'s — the
2422
+ * one the detail plane takes for the same subtree — so the pool that loads a
2423
+ * model and the pool that runs it cannot disagree:
2424
+ * - no decision (no elected device, or a roster of one) or `no-jump`: kept;
2425
+ * - `jumped` (auto or manual): left out — the detail dispatch loads it on the
2426
+ * TARGET pool, with the target's provisioning;
2427
+ * - `no-candidate`: left out — nothing on the node can load it, so a
2428
+ * pre-warm could only fail (the detail plane still says so, loudly).
2429
+ * Roots are the elected device's job and always stay. Root-plane children
2430
+ * stay and are walked; a detail subtree is judged whole at its top, because
2431
+ * its children ride its call. Pure — the caller logs.
2432
+ */
2433
+ function stepsForElectedPool(steps, isDetailStep, place) {
2434
+ const delegated = [];
2435
+ const walk = (nodes, topLevel) => {
2436
+ const kept = [];
2437
+ for (const step of nodes) {
2438
+ if (!topLevel && step.enabled !== false && isDetailStep(step.addonId)) {
2439
+ const decision = place(step).decision;
2440
+ if (decision === null || decision.kind === "no-jump") kept.push(step);
2441
+ else delegated.push(step.addonId);
2442
+ continue;
2443
+ }
2444
+ kept.push(step.children !== void 0 && step.children.length > 0 ? {
2445
+ ...step,
2446
+ children: walk(step.children, false)
2447
+ } : step);
2448
+ }
2449
+ return kept;
2450
+ };
2451
+ return {
2452
+ steps: walk(steps, true),
2453
+ delegated
2454
+ };
2455
+ }
2456
+ //#endregion
2457
+ //#region src/detection-pipeline/registry/custom-models.ts
2458
+ /**
2459
+ * Group a flat list of custom-model descriptors (as returned by the
2460
+ * `custom-model-registry` collection cap) into a `stepId → entries` map for
2461
+ * the picker / resolution union. Pure; order within a step preserved.
2462
+ */
2463
+ function groupCustomModelsByStep(descriptors) {
2464
+ const byStep = /* @__PURE__ */ new Map();
2465
+ for (const d of descriptors) {
2466
+ const arr = byStep.get(d.stepId) ?? [];
2467
+ arr.push(d.entry);
2468
+ byStep.set(d.stepId, arr);
2469
+ }
2470
+ return byStep;
2471
+ }
2472
+ /**
2473
+ * Union a step's static catalog models with operator-registered custom
2474
+ * models. On an `id` collision the static catalog entry wins — a custom
2475
+ * model can never shadow a built-in one.
2476
+ *
2477
+ * Pure + side-effect-free so it can be unit-tested in isolation and called
2478
+ * from the (free) `buildSchemaSlots` builder without any addon context.
2479
+ */
2480
+ function mergeCustomModels(staticModels, customModels) {
2481
+ const seen = new Set(staticModels.map((m) => m.id));
2482
+ const merged = [...staticModels];
2483
+ for (const m of customModels) {
2484
+ if (seen.has(m.id)) continue;
2485
+ seen.add(m.id);
2486
+ merged.push({
2487
+ ...m,
2488
+ provider: require_dist.inferModelProvider(m)
2489
+ });
2490
+ }
2491
+ return merged;
2492
+ }
2493
+ /**
2494
+ * A step's definition with its custom models merged in (static wins on an id
2495
+ * collision), or `null` for an unknown addonId. Pure.
2496
+ */
2497
+ function mergedStepDefinition(stepId, customByStep) {
2498
+ let def;
2499
+ try {
2500
+ def = getStepDefinition(stepId);
2501
+ } catch {
2502
+ return null;
2503
+ }
2504
+ const custom = customByStep.get(stepId);
2505
+ if (custom === void 0 || custom.length === 0) return def;
2506
+ return {
2507
+ ...def,
2508
+ models: mergeCustomModels(def.models, custom)
2509
+ };
2510
+ }
2511
+ /** TTL-cached read of the custom-model registry, with a synchronous merged view. */
2512
+ var CustomModelCatalog = class {
2513
+ logger;
2514
+ now;
2515
+ byStep = /* @__PURE__ */ new Map();
2516
+ readAtMs = Number.NEGATIVE_INFINITY;
2517
+ constructor(logger, now = Date.now) {
2518
+ this.logger = logger;
2519
+ this.now = now;
2520
+ }
2521
+ /** Re-read the registry when the snapshot is older than the TTL; answer the snapshot. */
2522
+ async refresh(api) {
2523
+ const now = this.now();
2524
+ if (now - this.readAtMs < 5e3) return this.byStep;
2525
+ this.readAtMs = now;
2526
+ if (api === void 0) return this.byStep;
2527
+ try {
2528
+ const providers = await api.addons.listCapabilityProviders.query({ capName: "custom-model-registry" });
2529
+ this.byStep = providers.some((p) => p.isActive) ? groupCustomModelsByStep(await api.customModelRegistry.listModels.query()) : /* @__PURE__ */ new Map();
2530
+ } catch (err) {
2531
+ this.logger.warn("custom-model-registry query failed — keeping the last custom-model snapshot", { meta: { error: err instanceof Error ? err.message : String(err) } });
2532
+ }
2533
+ return this.byStep;
2534
+ }
2535
+ /** The snapshot, synchronously. */
2536
+ current() {
2537
+ return this.byStep;
2538
+ }
2539
+ /** A step's merged definition from the snapshot, synchronously. */
2540
+ stepDefinition = (stepId) => mergedStepDefinition(stepId, this.byStep);
2541
+ };
2542
+ //#endregion
2181
2543
  //#region src/detection-pipeline/default-detection-model.ts
2182
2544
  /** The object-detection step id — the only slot this resolver applies to. */
2183
2545
  var OBJECT_DETECTION_STEP_ID = "object-detection";
@@ -2272,6 +2634,12 @@ Object.defineProperty(exports, "ALL_STEPS", {
2272
2634
  return ALL_STEPS;
2273
2635
  }
2274
2636
  });
2637
+ Object.defineProperty(exports, "CustomModelCatalog", {
2638
+ enumerable: true,
2639
+ get: function() {
2640
+ return CustomModelCatalog;
2641
+ }
2642
+ });
2275
2643
  Object.defineProperty(exports, "OBJECT_DETECTION_STEP_ID", {
2276
2644
  enumerable: true,
2277
2645
  get: function() {
@@ -2284,6 +2652,12 @@ Object.defineProperty(exports, "RETIRED_MODEL_IDS", {
2284
2652
  return RETIRED_MODEL_IDS;
2285
2653
  }
2286
2654
  });
2655
+ Object.defineProperty(exports, "buildSubtreeCanRun", {
2656
+ enumerable: true,
2657
+ get: function() {
2658
+ return buildSubtreeCanRun;
2659
+ }
2660
+ });
2287
2661
  Object.defineProperty(exports, "getDefaultModelForFormat", {
2288
2662
  enumerable: true,
2289
2663
  get: function() {
@@ -2320,6 +2694,18 @@ Object.defineProperty(exports, "macroGateVerdict", {
2320
2694
  return macroGateVerdict;
2321
2695
  }
2322
2696
  });
2697
+ Object.defineProperty(exports, "mergeCustomModels", {
2698
+ enumerable: true,
2699
+ get: function() {
2700
+ return mergeCustomModels;
2701
+ }
2702
+ });
2703
+ Object.defineProperty(exports, "placeDetailStep", {
2704
+ enumerable: true,
2705
+ get: function() {
2706
+ return placeDetailStep;
2707
+ }
2708
+ });
2323
2709
  Object.defineProperty(exports, "resolveDefaultDetectionModel", {
2324
2710
  enumerable: true,
2325
2711
  get: function() {
@@ -2344,3 +2730,9 @@ Object.defineProperty(exports, "rootObjectGate", {
2344
2730
  return rootObjectGate;
2345
2731
  }
2346
2732
  });
2733
+ Object.defineProperty(exports, "stepsForElectedPool", {
2734
+ enumerable: true,
2735
+ get: function() {
2736
+ return stepsForElectedPool;
2737
+ }
2738
+ });