@camstack/addon-pipeline 1.2.328 → 1.2.330

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 (41) hide show
  1. package/THIRD_PARTY_MODELS.md +2 -2
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/{default-detection-model-1SadEKsW.js → default-detection-model-B6gKGnfx.js} +53 -71
  5. package/dist/{default-detection-model-CMeVTcPZ.mjs → default-detection-model-CPDT_eFT.mjs} +53 -71
  6. package/dist/detection-pipeline/index.js +607 -39
  7. package/dist/detection-pipeline/index.mjs +607 -39
  8. package/dist/{dist-wJO7mXeO.js → dist-CMeICL07.js} +781 -465
  9. package/dist/{dist-Bpub2VcJ.mjs → dist-CuPEyZY2.mjs} +736 -438
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-BjCSgD4M.mjs → node-USq5jYwq.mjs} +1 -1
  13. package/dist/{node-BCrqaejt.js → node-X_0Tka7-.js} +1 -1
  14. package/dist/pipeline-runner/index.js +355 -104
  15. package/dist/pipeline-runner/index.mjs +355 -104
  16. package/dist/{process-memory-BaKQ8u2G.mjs → process-memory-07XYts8V.mjs} +1 -1
  17. package/dist/{process-memory-iUJuTpXs.js → process-memory-o4LFWbBN.js} +1 -1
  18. package/dist/recorder/index.js +7 -7
  19. package/dist/recorder/index.mjs +7 -7
  20. package/dist/{segment-demux-js-C5ez9LYG.mjs → segment-demux-js-Bq8DrH0b.mjs} +2 -2
  21. package/dist/{segment-demux-js-Opx6Hv9G.js → segment-demux-js-FqaQjG7n.js} +2 -2
  22. package/dist/stream-broker/_stub.js +2 -2
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B94LgsV0.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-EYRUYv_r.mjs} +3 -3
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BnP7C_HO.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-CR0--kRv.mjs +26 -0
  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-xOx7AvgW.mjs → hostInit-iGdBpvGc.mjs} +3 -3
  29. package/dist/stream-broker/index.js +4 -4
  30. package/dist/stream-broker/index.mjs +4 -4
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/package.json +1 -1
  33. package/python/edgetpu_dequant.py +38 -1
  34. package/python/inference_pool.py +31 -17
  35. package/python/postprocessors/test_yolox_plate_edgetpu.py +126 -0
  36. package/python/postprocessors/test_yunet_edgetpu.py +149 -0
  37. package/python/postprocessors/testdata/yolox_plate_int8_outputs.json +1 -0
  38. package/python/postprocessors/testdata/yunet_int8_outputs.json +1 -0
  39. package/python/postprocessors/yolo.py +44 -0
  40. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BPWYf4_O.mjs +0 -26
  41. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BpXPNINd.mjs +0 -26
@@ -3,11 +3,11 @@ 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-wJO7mXeO.js");
7
- const require_node = require("../node-BCrqaejt.js");
8
- const require_default_detection_model = require("../default-detection-model-1SadEKsW.js");
6
+ const require_dist = require("../dist-CMeICL07.js");
7
+ const require_node = require("../node-X_0Tka7-.js");
8
+ const require_default_detection_model = require("../default-detection-model-B6gKGnfx.js");
9
9
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
10
- const require_process_memory = require("../process-memory-iUJuTpXs.js");
10
+ const require_process_memory = require("../process-memory-o4LFWbBN.js");
11
11
  let node_os = require("node:os");
12
12
  node_os = require_chunk.__toESM(node_os);
13
13
  let node_fs = require("node:fs");
@@ -759,7 +759,7 @@ var POOL_MODEL_LOAD_BOUNDS = {
759
759
  hardMs: 6e5,
760
760
  why: "A cold iGPU compile of the WHOLE model set measured ~25 s on the N100; one model is 1-3 s (YuNet 1.4 s on a fresh worker, 2026-09-26). 120 s is ~5x the worst measured set. No evidence of a legitimate compile past it; the hard bound gives one 5x more before the worker is recycled.",
761
761
  gilMayBeHeldDuringCompile: false,
762
- gilWhy: "The OpenVINO Python binding is believed to release the GIL around compile_model (gil_scoped_release). Not measured on the hub; the passive recipe in D653 checks it."
762
+ gilWhy: "The OpenVINO Python binding is believed to release the GIL around compile_model (gil_scoped_release). Not yet measured on this node."
763
763
  },
764
764
  coreml: {
765
765
  softMs: 3e5,
@@ -2816,6 +2816,8 @@ var PipelineModelManager = class {
2816
2816
  stepId,
2817
2817
  modelId,
2818
2818
  poolIndex: index,
2819
+ path: config.path,
2820
+ ...settings !== void 0 ? { settings } : {},
2819
2821
  lruTick: ++this.lruClock,
2820
2822
  decode
2821
2823
  };
@@ -2823,6 +2825,56 @@ var PipelineModelManager = class {
2823
2825
  return entry;
2824
2826
  }
2825
2827
  /**
2828
+ * Reload every resident slot whose model FILE is no longer the one
2829
+ * `buildConfig` names — a Coral pool that switched co-compiled set (D724).
2830
+ * The new slot is loaded BEFORE the old one is unloaded and the entry is
2831
+ * replaced in between, so a lookup always finds a loaded index. Returns the
2832
+ * `(step, model)` pairs reloaded. A failed reload keeps the old slot (it
2833
+ * still serves, from the file it was loaded from) and is logged.
2834
+ */
2835
+ async reloadChangedPaths() {
2836
+ const reloaded = [];
2837
+ for (const [stepId, perStep] of this.loaded) for (const entry of perStep.values()) {
2838
+ const config = this.source.buildConfig(stepId, entry.modelId, entry.settings);
2839
+ if (config.path === entry.path) continue;
2840
+ const index = this.pool.allocateIndex();
2841
+ try {
2842
+ await this.pool.loadModel(index, config);
2843
+ } catch (err) {
2844
+ this.pool.releaseIndex(index);
2845
+ this.log.error("Step variant reload from another model file failed — the old file keeps serving", { meta: {
2846
+ step: stepId,
2847
+ modelId: entry.modelId,
2848
+ deviceKey: this.deviceKey,
2849
+ from: entry.path,
2850
+ to: config.path,
2851
+ error: err instanceof Error ? err.message : String(err)
2852
+ } });
2853
+ continue;
2854
+ }
2855
+ perStep.set(entry.modelId, {
2856
+ ...entry,
2857
+ poolIndex: index,
2858
+ path: config.path,
2859
+ decode: poolDecodeSettingsOf(config)
2860
+ });
2861
+ await this.pool.unloadModel(entry.poolIndex);
2862
+ this.log.info("Step variant reloaded from another model file", { meta: {
2863
+ step: stepId,
2864
+ modelId: entry.modelId,
2865
+ deviceKey: this.deviceKey,
2866
+ from: entry.path,
2867
+ to: config.path,
2868
+ poolIndex: index
2869
+ } });
2870
+ reloaded.push({
2871
+ stepId,
2872
+ modelId: entry.modelId
2873
+ });
2874
+ }
2875
+ return reloaded;
2876
+ }
2877
+ /**
2826
2878
  * Push a retuned decode setting into ONE already-loaded slot.
2827
2879
  *
2828
2880
  * Scope is the point: `reconfigure` names a single pool index, so a slider
@@ -2981,6 +3033,251 @@ function deviceKeyOf(engine) {
2981
3033
  return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
2982
3034
  }
2983
3035
  //#endregion
3036
+ //#region src/detection-pipeline/engine/coral-cocompiled-set.ts
3037
+ var ROOT_DETECTOR_STEP_ID = "object-detection";
3038
+ /** The models a dispatch places on the pool that runs it: every enabled video step. */
3039
+ function placedModelsOf(steps) {
3040
+ return flattenEnabledVideoSteps(steps).map((s) => ({
3041
+ stepId: s.addonId,
3042
+ modelId: s.modelId
3043
+ }));
3044
+ }
3045
+ var sameModel = (a, b) => a.stepId === b.stepId && a.modelId === b.modelId;
3046
+ /**
3047
+ * The set for `placed`, or why there is none.
3048
+ *
3049
+ * 1. A set that reserves the cache for the root detector is eligible only when
3050
+ * the root is placed here, and a rootless set only when it is not: a root
3051
+ * set on a pool without the root wastes the cache the root would have had,
3052
+ * and a rootless set beside a root makes the root swap.
3053
+ * 2. A set must stand for at least two placed models — one model alone is
3054
+ * better off with the whole cache (its separately compiled file).
3055
+ * 3. Most placed models covered wins; then the set with fewest members nobody
3056
+ * placed (they hold cache for nothing); then declaration order.
3057
+ */
3058
+ function selectCoCompiledSet(placed, sets) {
3059
+ if (placed.length === 0) return {
3060
+ kind: "separate",
3061
+ reason: "nothing-placed"
3062
+ };
3063
+ if (placed.length === 1) return {
3064
+ kind: "separate",
3065
+ reason: "single-model"
3066
+ };
3067
+ const hostsRoot = placed.some((p) => p.stepId === ROOT_DETECTOR_STEP_ID);
3068
+ let best = null;
3069
+ for (const set of sets) {
3070
+ if (set.hostsRoot !== hostsRoot) continue;
3071
+ const covered = placed.filter((p) => set.members.some((m) => sameModel(m, p)));
3072
+ if (covered.length < 2) continue;
3073
+ const idle = set.members.length - covered.length;
3074
+ if (best === null || covered.length > best.covered.length || covered.length === best.covered.length && idle < best.idle) best = {
3075
+ set,
3076
+ covered,
3077
+ idle
3078
+ };
3079
+ }
3080
+ if (best === null) return {
3081
+ kind: "separate",
3082
+ reason: "no-set-covers-two"
3083
+ };
3084
+ const { set, covered } = best;
3085
+ return {
3086
+ kind: "set",
3087
+ set,
3088
+ covered,
3089
+ uncovered: placed.filter((p) => !covered.some((c) => sameModel(c, p)))
3090
+ };
3091
+ }
3092
+ /** The member a selection loads `(stepId, modelId)` from, or `null` (its own `formats.tflite`). */
3093
+ function setMemberFor(selection, stepId, modelId) {
3094
+ if (selection.kind !== "set") return null;
3095
+ return selection.set.members.find((m) => m.stepId === stepId && m.modelId === modelId) ?? null;
3096
+ }
3097
+ /**
3098
+ * `entry` with its `tflite` build pointed at the set's file. Everything else —
3099
+ * labels, postprocessor, input contract, licence — is the entry's own: a
3100
+ * member is the same model, compiled together with others.
3101
+ */
3102
+ function withSetBuild(entry, member) {
3103
+ return {
3104
+ ...entry,
3105
+ formats: {
3106
+ ...entry.formats,
3107
+ tflite: {
3108
+ url: member.url,
3109
+ sizeMB: member.sizeMB,
3110
+ runtimes: ["python"]
3111
+ }
3112
+ }
3113
+ };
3114
+ }
3115
+ /** Identity of a selection for change detection: the set AND what it leaves out. */
3116
+ var selectionKey = (s) => s.kind === "set" ? `${s.set.id}|${s.uncovered.map((u) => `${u.stepId}/${u.modelId}`).toSorted().join(",")}` : `separate:${s.reason}`;
3117
+ /**
3118
+ * The placement of ONE Coral pool and the set it loads. Lives exactly as long
3119
+ * as the pool (the factory owns it).
3120
+ */
3121
+ var CoralSetPlanner = class {
3122
+ sets;
3123
+ placed = [];
3124
+ current = {
3125
+ kind: "separate",
3126
+ reason: "nothing-placed"
3127
+ };
3128
+ constructor(sets) {
3129
+ this.sets = sets;
3130
+ }
3131
+ /** Add the models a dispatch places on this pool; say whether the set changed. */
3132
+ note(models) {
3133
+ let grew = false;
3134
+ for (const m of models) {
3135
+ if (this.placed.some((p) => sameModel(p, m))) continue;
3136
+ this.placed.push({
3137
+ stepId: m.stepId,
3138
+ modelId: m.modelId
3139
+ });
3140
+ grew = true;
3141
+ }
3142
+ if (!grew) return { kind: "unchanged" };
3143
+ const next = selectCoCompiledSet(this.placed, this.sets);
3144
+ if (selectionKey(next) === selectionKey(this.current)) {
3145
+ this.current = next;
3146
+ return { kind: "unchanged" };
3147
+ }
3148
+ const from = this.current;
3149
+ this.current = next;
3150
+ return {
3151
+ kind: "changed",
3152
+ from,
3153
+ to: next,
3154
+ placed: [...this.placed]
3155
+ };
3156
+ }
3157
+ selection() {
3158
+ return this.current;
3159
+ }
3160
+ /** `entry` as this pool must load it: from the set when the set stands for it. */
3161
+ entryFor(stepId, entry) {
3162
+ const member = setMemberFor(this.current, stepId, entry.id);
3163
+ return member === null ? entry : withSetBuild(entry, member);
3164
+ }
3165
+ };
3166
+ //#endregion
3167
+ //#region src/detection-pipeline/registry/edgetpu-cocompiled-sets.ts
3168
+ /**
3169
+ * Co-compiled Edge TPU sets — pool-level artifacts, not models (D724).
3170
+ *
3171
+ * A Coral holds ~6.9 MiB of parameter cache. Two separately compiled models
3172
+ * carry two different cache tokens, so every alternation between them on one
3173
+ * Coral flushes the cache and re-streams the incoming model's parameters over
3174
+ * USB (D710). `edgetpu_compiler` given several models at once allocates the
3175
+ * cache between them, in command-line order, under ONE token: loaded from the
3176
+ * same set, they never evict each other — whatever does not fit is streamed on
3177
+ * every invoke of THAT model instead.
3178
+ *
3179
+ * Each member is the SAME model as its catalog entry's `formats.tflite` (same
3180
+ * `modelId`, same int8 graph, same labels and licence record), compiled
3181
+ * together with the other members (D710: a Coral build is a format of the
3182
+ * survivor; the set is where it is placed, not a new model). A Coral pool picks
3183
+ * at most one set from the models placed on it (`coral-cocompiled-set.ts`) and
3184
+ * falls back to the separately compiled `formats.tflite` for everything else.
3185
+ *
3186
+ * Built by `scripts/build-edgetpu-sets.py` (compile order = `members` order =
3187
+ * cache priority) and hosted under `edgetpuSets/<id>/`. Only sets whose
3188
+ * measured cost is acceptable are listed here: a set that would make the root
3189
+ * detector stream parameters on every frame (root + YuNet at 640: the root
3190
+ * keeps 3.34 of its 6.39 MiB, YuNet's activations take the rest) is
3191
+ * deliberately absent.
3192
+ */
3193
+ var HF_REPO = "camstack/camstack-models";
3194
+ var setFile = (setId, file) => require_dist.hfModelUrl(HF_REPO, `edgetpuSets/${setId}/${setId}__${file}`);
3195
+ var SSD = "ssd-mobilenet-v2-coco-edgetpu";
3196
+ var YUNET = "yunet-2023mar";
3197
+ var YOLOX = "yolox-tiny-plate";
3198
+ var BIRD = "bird-classifier";
3199
+ var EDGETPU_COCOMPILED_SETS = [
3200
+ {
3201
+ id: "root-plate-bird",
3202
+ hostsRoot: true,
3203
+ members: [
3204
+ {
3205
+ stepId: "object-detection",
3206
+ modelId: SSD,
3207
+ url: setFile("root-plate-bird", "ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite"),
3208
+ sizeMB: 6.9,
3209
+ onChipMiB: 6.39,
3210
+ streamedMiB: 0
3211
+ },
3212
+ {
3213
+ stepId: "plate-detection",
3214
+ modelId: YOLOX,
3215
+ url: setFile("root-plate-bird", "camstack-yolox-tiny-plate_int8_edgetpu.tflite"),
3216
+ sizeMB: 6.9,
3217
+ onChipMiB: .08,
3218
+ streamedMiB: 5.41
3219
+ },
3220
+ {
3221
+ stepId: "bird-classifier",
3222
+ modelId: BIRD,
3223
+ url: setFile("root-plate-bird", "mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite"),
3224
+ sizeMB: 4.9,
3225
+ onChipMiB: 0,
3226
+ streamedMiB: 4.54
3227
+ }
3228
+ ]
3229
+ },
3230
+ {
3231
+ id: "face-plate-bird",
3232
+ hostsRoot: false,
3233
+ members: [
3234
+ {
3235
+ stepId: "face-detection",
3236
+ modelId: YUNET,
3237
+ url: setFile("face-plate-bird", "camstack-yunet-2023mar_int8_edgetpu.tflite"),
3238
+ sizeMB: .7,
3239
+ onChipMiB: .11,
3240
+ streamedMiB: 0
3241
+ },
3242
+ {
3243
+ stepId: "plate-detection",
3244
+ modelId: YOLOX,
3245
+ url: setFile("face-plate-bird", "camstack-yolox-tiny-plate_int8_edgetpu.tflite"),
3246
+ sizeMB: 6.9,
3247
+ onChipMiB: 3.23,
3248
+ streamedMiB: 2.27
3249
+ },
3250
+ {
3251
+ stepId: "bird-classifier",
3252
+ modelId: BIRD,
3253
+ url: setFile("face-plate-bird", "mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite"),
3254
+ sizeMB: 4.9,
3255
+ onChipMiB: 0,
3256
+ streamedMiB: 4.54
3257
+ }
3258
+ ]
3259
+ },
3260
+ {
3261
+ id: "plate-bird",
3262
+ hostsRoot: false,
3263
+ members: [{
3264
+ stepId: "plate-detection",
3265
+ modelId: YOLOX,
3266
+ url: setFile("plate-bird", "camstack-yolox-tiny-plate_int8_edgetpu.tflite"),
3267
+ sizeMB: 6.9,
3268
+ onChipMiB: 5.55,
3269
+ streamedMiB: 0
3270
+ }, {
3271
+ stepId: "bird-classifier",
3272
+ modelId: BIRD,
3273
+ url: setFile("plate-bird", "mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite"),
3274
+ sizeMB: 4.1,
3275
+ onChipMiB: .92,
3276
+ streamedMiB: 2.8
3277
+ }]
3278
+ }
3279
+ ];
3280
+ //#endregion
2984
3281
  //#region src/detection-pipeline/engine/engine-factory.ts
2985
3282
  var BACKEND_TO_POOL_RUNTIME = {
2986
3283
  coreml: "coreml",
@@ -3074,11 +3371,45 @@ var EngineFactory = class {
3074
3371
  * trying to be.
3075
3372
  */
3076
3373
  resolvedProvisioning;
3374
+ /**
3375
+ * A Coral pool's placement and the co-compiled set it loads (D724); `null`
3376
+ * on every other runtime. Lives exactly as long as this factory — a new
3377
+ * pool starts from nothing placed.
3378
+ */
3379
+ coralSets;
3077
3380
  constructor(opts) {
3078
3381
  this.opts = opts;
3079
3382
  this.log = opts.logger;
3080
3383
  this.deviceKey = deviceKeyOf(opts.engine);
3081
3384
  this.resolvedProvisioning = resolvePoolProvisioning(opts.engine, opts.provisioning);
3385
+ this.coralSets = BACKEND_TO_POOL_RUNTIME[opts.engine.backend] === "edgetpu" ? new CoralSetPlanner(opts.coCompiledSets ?? EDGETPU_COCOMPILED_SETS) : null;
3386
+ }
3387
+ /**
3388
+ * Record the models a dispatch places on this pool. On a Coral pool the
3389
+ * answer says whether the co-compiled set it loads from changed; on any
3390
+ * other pool nothing is placed and nothing changes.
3391
+ */
3392
+ notePlacement(models) {
3393
+ return this.coralSets?.note(models) ?? { kind: "unchanged" };
3394
+ }
3395
+ /** The co-compiled set this pool loads from, or why none (`null`: not a Coral pool). */
3396
+ coralSetSelection() {
3397
+ return this.coralSets?.selection() ?? null;
3398
+ }
3399
+ /**
3400
+ * The catalog entry as THIS pool must download and load it: on a Coral pool
3401
+ * whose set stands for the model, the `tflite` build is the set's file.
3402
+ * Every other entry is returned unchanged.
3403
+ */
3404
+ entryForPool(stepId, entry) {
3405
+ return this.coralSets?.entryFor(stepId, entry) ?? entry;
3406
+ }
3407
+ /**
3408
+ * Reload every resident model whose file this pool no longer names — after
3409
+ * the Coral set changed. The caller has already downloaded the new files.
3410
+ */
3411
+ async reloadChangedModelFiles() {
3412
+ return this.poolManager?.reloadChangedPaths() ?? [];
3082
3413
  }
3083
3414
  /**
3084
3415
  * The provisioning this factory's pool runs with — resolved from the pool's
@@ -3340,6 +3671,7 @@ var EngineFactory = class {
3340
3671
  stepSettings,
3341
3672
  device: this.opts.engine.device,
3342
3673
  resolveCustomModel: this.opts.resolveCustomModel,
3674
+ entryForPool: (sid, entry) => this.entryForPool(sid, entry),
3343
3675
  onClassMapDrop: ({ stepId: sid, modelId: mid, unmappedLabels }) => {
3344
3676
  const key = `${sid}::${mid}`;
3345
3677
  if (this.classMapDropLogged.has(key)) return;
@@ -3365,8 +3697,9 @@ var EngineFactory = class {
3365
3697
  function buildPoolModelConfigForStep(inputs) {
3366
3698
  const { stepId, modelId, poolRuntime, modelsDir } = inputs;
3367
3699
  const def = require_default_detection_model.getStepDefinition(stepId);
3368
- const modelEntry = def.models.find((m) => m.id === modelId) ?? inputs.resolveCustomModel?.(stepId, modelId);
3369
- if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog (static or custom registry)`);
3700
+ const resolvedEntry = def.models.find((m) => m.id === modelId) ?? inputs.resolveCustomModel?.(stepId, modelId);
3701
+ if (!resolvedEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog (static or custom registry)`);
3702
+ const modelEntry = inputs.entryForPool?.(stepId, resolvedEntry) ?? resolvedEntry;
3370
3703
  const format = RUNTIME_TO_FORMAT[poolRuntime];
3371
3704
  if (!format) throw new Error(`No format mapping for pool runtime "${poolRuntime}"`);
3372
3705
  const dropWarning = collectClassMapDropWarning(def, modelEntry);
@@ -5450,6 +5783,26 @@ var PLATE_OCR_CROP_POLICIES = {
5450
5783
  }
5451
5784
  };
5452
5785
  /**
5786
+ * The margin around a plate box drawn by the ROOT model (D730) — a Frigate+
5787
+ * `license_plate` read in place of `plate-detection`'s own box.
5788
+ *
5789
+ * The margin is a property of the box's detector (see above), and this
5790
+ * detector's boxes have never been measured against the plate readers: 0.15 is
5791
+ * the value that served a TIGHT detector box (the deleted `yolov8n-plate`,
5792
+ * 7/7 Kia reads at 0.13–0.16), on the assumption that Frigate+ boxes are
5793
+ * tight as well. It is uncalibrated and needs measuring on a ground-truth set
5794
+ * of root-supplied plates before it is trusted; until then it applies to every
5795
+ * reader alike, replacing the reader's own detector-specific margin.
5796
+ */
5797
+ var ROOT_SUPPLIED_PLATE_MARGIN_FRACTION = .15;
5798
+ /**
5799
+ * The margin a reader cuts around its plate box: the reader's own, unless the
5800
+ * box was drawn by the root model.
5801
+ */
5802
+ function plateOcrMarginFraction(policy, boxFromRoot) {
5803
+ return boxFromRoot ? ROOT_SUPPLIED_PLATE_MARGIN_FRACTION : policy.marginFraction;
5804
+ }
5805
+ /**
5453
5806
  * The crop policy for a step child, or `null` when the child is not a plate
5454
5807
  * reader at all (a face embedder, a classifier — nothing here applies to them).
5455
5808
  */
@@ -5479,6 +5832,79 @@ function expandBbox(bbox, marginFraction, imageWidth, imageHeight) {
5479
5832
  ];
5480
5833
  }
5481
5834
  //#endregion
5835
+ //#region src/detection-pipeline/pipeline/root-child-boxes.ts
5836
+ /**
5837
+ * Root child boxes (D730) — a root model's `license_plate` / `face` box made
5838
+ * into a DETAIL of the vehicle / person that contains it.
5839
+ *
5840
+ * The labels are deliberately absent from the root's classMap, so they never
5841
+ * make a first-level detection and therefore never a track (D683). When the
5842
+ * root declares a CROP supply for them, the executor keeps the boxes aside
5843
+ * instead of discarding them, and once the frame's first-level detections are
5844
+ * final this attaches each to its containing parent (`containingParentIndex`).
5845
+ * Whether the camera USES them is the runner's decision
5846
+ * (`pipeline-runner/root-supply.ts`); a box nothing contains is discarded.
5847
+ *
5848
+ * Pure.
5849
+ */
5850
+ /** The crop supply a raw root class belongs to, if the root declares one. */
5851
+ function cropSupplyForLabel(supplies, rawClass) {
5852
+ for (const supply of supplies ?? []) if (supply.kind === "crop" && supply.rootLabels.includes(rawClass)) return supply;
5853
+ }
5854
+ function toBox(b) {
5855
+ return {
5856
+ x: b[0],
5857
+ y: b[1],
5858
+ w: b[2] - b[0],
5859
+ h: b[3] - b[1]
5860
+ };
5861
+ }
5862
+ /**
5863
+ * Attach each candidate to the smallest first-level parent of its supply's
5864
+ * macro that contains it. Never mutates the parents.
5865
+ */
5866
+ function attachRootChildBoxes(candidates, parents, modelId, idGen) {
5867
+ const details = [];
5868
+ const uncontained = [];
5869
+ for (const candidate of candidates) {
5870
+ const eligible = parents.filter((p) => p.kind === "first-level" && p.macroClass === candidate.supply.parentMacro);
5871
+ const index = require_dist.containingParentIndex(toBox(candidate.bbox), eligible.map((p) => toBox(p.bbox)));
5872
+ const parent = index >= 0 ? eligible[index] : void 0;
5873
+ if (parent === void 0) {
5874
+ uncontained.push(candidate);
5875
+ continue;
5876
+ }
5877
+ const stepId = candidate.supply.stepId;
5878
+ details.push({
5879
+ id: idGen.next(),
5880
+ kind: "detail",
5881
+ macroClass: candidate.supply.childClass,
5882
+ originalClass: candidate.rawClass,
5883
+ score: candidate.score,
5884
+ bbox: [
5885
+ candidate.bbox[0],
5886
+ candidate.bbox[1],
5887
+ candidate.bbox[2],
5888
+ candidate.bbox[3]
5889
+ ],
5890
+ parentId: parent.id,
5891
+ rootSupplied: {
5892
+ stepId,
5893
+ modelId
5894
+ },
5895
+ ownLabels: [],
5896
+ alternateLabels: {},
5897
+ stepLatencyMs: {},
5898
+ modelIds: { [stepId]: modelId },
5899
+ extractMode: "full-frame"
5900
+ });
5901
+ }
5902
+ return {
5903
+ details,
5904
+ uncontained
5905
+ };
5906
+ }
5907
+ //#endregion
5482
5908
  //#region src/detection-pipeline/pipeline/plate-warp.ts
5483
5909
  /**
5484
5910
  * Plate-crop WARP — perspective rectification of a licence-plate ROI to a
@@ -6035,6 +6461,7 @@ function buildFrameResult(input) {
6035
6461
  } : {},
6036
6462
  labels,
6037
6463
  ...m.parentId !== void 0 ? { parentId: m.parentId } : {},
6464
+ ...m.rootSupplied !== void 0 ? { rootSupplied: m.rootSupplied } : {},
6038
6465
  ...embedding !== void 0 ? {
6039
6466
  embedding,
6040
6467
  ...embeddingModelId !== void 0 ? { embeddingModelId } : {}
@@ -6136,33 +6563,35 @@ function toMutableRootDetection(det, rootStep, idGen, rootStepLatencyMs) {
6136
6563
  //#endregion
6137
6564
  //#region src/detection-pipeline/pipeline/linked-label.ts
6138
6565
  /**
6139
- * The detector's linked label (D715): when the model that RAN declares it
6140
- * fills a step, a detection whose raw class names that step's effect carries
6141
- * the effect as a `ScoredLabel` attributed to `detector-link:<stepId>` — in
6142
- * the step's own vocabulary and tier, so tier 1 means one thing whichever
6566
+ * The detector's linked label (D715, D730): when the model that RAN declares a
6567
+ * LABEL supply for a step, a detection whose raw class names that step's effect
6568
+ * carries the effect as a `ScoredLabel` attributed to `detector-link:<stepId>`
6569
+ * — in the step's own vocabulary and tier, so tier 1 means one thing whichever
6143
6570
  * writer produced it.
6144
6571
  *
6145
6572
  * Written at score 0 here, always: the executor cannot see the camera's whole
6146
6573
  * step tree (a detail step placed on another pool is pruned from the dispatch),
6147
- * so it cannot know who the writer is. The runner, which holds the attach
6148
- * config, raises the score to the detection's own when the detector is
6149
- * authoritative (`pipeline-runner/label-link.ts`). Score 0 is the fallback any
6150
- * scored answer of the step outranks.
6574
+ * so it cannot know whether the camera USES the supply. The runner, which holds
6575
+ * the attach config, decides with `resolveRootSupply` (`pipeline-runner/root-supply.ts`):
6576
+ * effective ⇒ the label is raised to the detection's score, or dropped when the
6577
+ * step's class list switched it off; otherwise it stays the score-0 fallback
6578
+ * any scored answer of the step outranks.
6151
6579
  */
6152
6580
  /** The linked labels one mapped root detection carries. Pure. */
6153
- function linkedLabelsFor(fills, rawClass, macroClass, modelId) {
6154
- if (fills === void 0 || fills.length === 0) return [];
6581
+ function linkedLabelsFor(supplies, rawClass, macroClass, modelId) {
6582
+ if (supplies === void 0 || supplies.length === 0) return [];
6155
6583
  const out = [];
6156
- for (const fill of fills) {
6157
- if (require_dist.FILLABLE_STEP_MACRO[fill.stepId] !== macroClass) continue;
6158
- const effect = fill.labelToEffect[rawClass];
6584
+ for (const supply of supplies) {
6585
+ if (supply.kind !== "label") continue;
6586
+ if (require_dist.LABEL_SUPPLY_MACRO[supply.stepId] !== macroClass) continue;
6587
+ const effect = supply.labelToEffect[rawClass];
6159
6588
  if (effect === void 0 || effect === macroClass) continue;
6160
- const tier = require_default_detection_model.getStepDefinition(fill.stepId).labelTier;
6589
+ const tier = require_default_detection_model.getStepDefinition(supply.stepId).labelTier;
6161
6590
  out.push({
6162
6591
  label: effect,
6163
6592
  score: 0,
6164
6593
  ...tier !== void 0 ? { tier } : {},
6165
- stepId: require_dist.linkedLabelStepId(fill.stepId),
6594
+ stepId: require_dist.linkedLabelStepId(supply.stepId),
6166
6595
  modelId
6167
6596
  });
6168
6597
  }
@@ -6391,10 +6820,19 @@ var PipelineExecutor = class {
6391
6820
  const rootView = rootInputViewProvider ? await rootInputViewProvider(rootStep, rootCrop) : null;
6392
6821
  const cropPrep = !rootView && rootCrop !== void 0 ? await this.prepareCropZoneInput(rootCrop, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
6393
6822
  const rootStart = Date.now();
6394
- const rawRootOutput = await this.executeStep(rootStep, rootView?.input ?? cropPrep?.input ?? withInferenceDeviceId(rootInput, deviceId), rootView?.width ?? cropPrep?.width ?? imageWidth, rootView?.height ?? cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg, rootShed);
6823
+ const supplied = runOpts?.suppliedRoot?.stepId === rootStep.stepId ? runOpts.suppliedRoot : void 0;
6824
+ const rawRootOutput = supplied ? {
6825
+ kind: "detections",
6826
+ detections: supplied.detections.map((d) => ({
6827
+ class: rootStep.definition.outputClasses[0] ?? rootStep.stepId,
6828
+ score: d.score,
6829
+ bbox: d.bbox,
6830
+ ...d.landmarks !== void 0 ? { landmarks: d.landmarks } : {}
6831
+ }))
6832
+ } : await this.executeStep(rootStep, rootView?.input ?? cropPrep?.input ?? withInferenceDeviceId(rootInput, deviceId), rootView?.width ?? cropPrep?.width ?? imageWidth, rootView?.height ?? cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg, rootShed);
6395
6833
  const rootMs = Date.now() - rootStart;
6396
6834
  if (rawRootOutput === null) continue;
6397
- const rootOutput = rootView && rawRootOutput.kind === "detections" ? reprojectViewDetections(rawRootOutput, rootView.geometry) : cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
6835
+ const rootOutput = supplied ? rawRootOutput : rootView && rawRootOutput.kind === "detections" ? reprojectViewDetections(rawRootOutput, rootView.geometry) : cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
6398
6836
  if (rootOutput.kind !== "detections") {
6399
6837
  if (isEnrichmentOutput(rootOutput)) {
6400
6838
  const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
@@ -6416,8 +6854,33 @@ var PipelineExecutor = class {
6416
6854
  }
6417
6855
  continue;
6418
6856
  }
6857
+ const rootChildCandidates = [];
6858
+ const firstLevelStart = firstLevel.length;
6419
6859
  for (const det of rootOutput.detections) {
6420
- const mutable = toMutableRootDetection(det, rootStep, idGen, rootMs);
6860
+ const cropSupply = cropSupplyForLabel(rootStep.rootSupplies, det.class);
6861
+ if (cropSupply !== void 0) {
6862
+ rootChildCandidates.push({
6863
+ rawClass: det.class,
6864
+ score: det.score,
6865
+ bbox: [
6866
+ det.bbox[0],
6867
+ det.bbox[1],
6868
+ det.bbox[2],
6869
+ det.bbox[3]
6870
+ ],
6871
+ supply: cropSupply
6872
+ });
6873
+ continue;
6874
+ }
6875
+ const seeded = toMutableRootDetection(det, rootStep, idGen, rootMs);
6876
+ const mutable = supplied ? {
6877
+ ...seeded,
6878
+ rootSupplied: {
6879
+ stepId: rootStep.stepId,
6880
+ modelId: supplied.modelId
6881
+ },
6882
+ modelIds: { [rootStep.stepId]: supplied.modelId }
6883
+ } : seeded;
6421
6884
  if (rootStep.effectiveClassMap) {
6422
6885
  const mapped = rootStep.effectiveClassMap.mapping[det.class];
6423
6886
  if (mapped) {
@@ -6438,7 +6901,7 @@ var PipelineExecutor = class {
6438
6901
  continue;
6439
6902
  }
6440
6903
  }
6441
- for (const linked of linkedLabelsFor(rootStep.fillsClassifierEffects, det.class, mutable.macroClass, rootStep.modelId)) mutable.ownLabels.push(linked);
6904
+ for (const linked of linkedLabelsFor(rootStep.rootSupplies, det.class, mutable.macroClass, rootStep.modelId)) mutable.ownLabels.push(linked);
6442
6905
  const filterVerdict = this.macroFilterVerdict(mutable.macroClass, mutable.score, rootStep.settings);
6443
6906
  if (filterVerdict !== "pass") {
6444
6907
  (discarded ??= []).push({
@@ -6520,6 +6983,21 @@ var PipelineExecutor = class {
6520
6983
  }
6521
6984
  firstLevel.push(mutable);
6522
6985
  }
6986
+ if (rootChildCandidates.length > 0) {
6987
+ const attached = attachRootChildBoxes(rootChildCandidates, firstLevel.slice(firstLevelStart), rootStep.modelId, idGen);
6988
+ details.push(...attached.details);
6989
+ for (const c of attached.uncontained) (discarded ??= []).push({
6990
+ bbox: [
6991
+ c.bbox[0],
6992
+ c.bbox[1],
6993
+ c.bbox[2],
6994
+ c.bbox[3]
6995
+ ],
6996
+ macroClass: c.supply.childClass,
6997
+ score: c.score,
6998
+ reason: "class-filter"
6999
+ });
7000
+ }
6523
7001
  }
6524
7002
  const totalMs = Date.now() - startMs;
6525
7003
  return {
@@ -6787,7 +7265,8 @@ var PipelineExecutor = class {
6787
7265
  }
6788
7266
  } else {
6789
7267
  const cropPolicy = resolvePlateOcrCropPolicy(resolveEffectivePostprocessor(child.definition, child.modelId));
6790
- const cropBbox = cropPolicy ? expandBbox(parentDetection.bbox, cropPolicy.marginFraction, imageWidth, imageHeight) : parentDetection.bbox;
7268
+ const marginFraction = cropPolicy ? plateOcrMarginFraction(cropPolicy, parentDetection.rootSupplied !== void 0) : 0;
7269
+ const cropBbox = cropPolicy ? expandBbox(parentDetection.bbox, marginFraction, imageWidth, imageHeight) : parentDetection.bbox;
6791
7270
  const nativeChild = nativeCropProvider && child.children.length === 0 ? await nativeChildCrop(nativeCropProvider, cropBbox, imageWidth, imageHeight, NATIVE_CHILD_CROP_MAX_WIDTH) : null;
6792
7271
  if (nativeChild) {
6793
7272
  cropJpegBuf = nativeChild.jpeg;
@@ -6810,7 +7289,8 @@ var PipelineExecutor = class {
6810
7289
  plateLongSidePx: Math.round(plateLongSidePx),
6811
7290
  plateShortSidePx: Math.round(Math.min(cropW, cropH)),
6812
7291
  minPlateLongSidePx: cropPolicy.minLongSidePx,
6813
- marginFraction: cropPolicy.marginFraction,
7292
+ marginFraction,
7293
+ boxFrom: parentDetection.rootSupplied !== void 0 ? "root-model" : "step",
6814
7294
  source: nativeChild ? "native" : "detection-tile"
6815
7295
  }
6816
7296
  });
@@ -7008,9 +7488,18 @@ function buildNode(step, getEngine, resolveModel, onMissingVariant, resolved) {
7008
7488
  children,
7009
7489
  ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {},
7010
7490
  ...effectiveClassMap !== void 0 ? { effectiveClassMap } : {},
7011
- ...modelEntry?.fillsClassifierEffects !== void 0 ? { fillsClassifierEffects: modelEntry.fillsClassifierEffects } : {}
7491
+ ...rootSuppliesField(definition, modelEntry)
7012
7492
  };
7013
7493
  }
7494
+ /** A root step's supplies, omitted when it declares none (and for every child step). */
7495
+ function rootSuppliesField(definition, modelEntry) {
7496
+ if (definition.inputClasses !== null) return {};
7497
+ const supplies = require_dist.rootSuppliesOf(modelEntry, {
7498
+ labels: definition.labels ?? [],
7499
+ ...definition.classMap !== void 0 ? { classMap: definition.classMap } : {}
7500
+ });
7501
+ return supplies.length > 0 ? { rootSupplies: supplies } : {};
7502
+ }
7014
7503
  /**
7015
7504
  * Walk the step's declared `getConfigSchema()` and collect every field
7016
7505
  * with a primitive `default` value into a plain record. Group fields
@@ -9156,7 +9645,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9156
9645
  execution = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
9157
9646
  traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
9158
9647
  plane: input.plane,
9159
- occasion: input.replay === true ? "replay" : "live"
9648
+ occasion: input.replay === true ? "replay" : "live",
9649
+ ...input.suppliedRoot !== void 0 ? { suppliedRoot: input.suppliedRoot } : {}
9160
9650
  }, nativeCropProvider, cropZoneBbox, rootInputViewProvider);
9161
9651
  executionSucceeded = true;
9162
9652
  } finally {
@@ -9432,6 +9922,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9432
9922
  */
9433
9923
  async loadDispatchModels(steps, factory, format, context, policy) {
9434
9924
  const failures = [];
9925
+ if (factory !== null) await this.noteCoralPlacement(steps, factory, format, context);
9435
9926
  const walk = async (nodes, root) => {
9436
9927
  for (const step of nodes) {
9437
9928
  if (!step.enabled || step.slot === "audio-classifier") continue;
@@ -9456,6 +9947,69 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9456
9947
  await walk(steps, true);
9457
9948
  return failures;
9458
9949
  }
9950
+ /**
9951
+ * Tell a Coral pool which models this dispatch places on it, and act on a
9952
+ * change of the co-compiled set it loads from (D724): download the set's
9953
+ * files for the models already resident and reload them — queued behind the
9954
+ * pool's other loads, so nothing loads from the old file meanwhile. Logged
9955
+ * once per change, tagged with the camera whose dispatch moved it.
9956
+ */
9957
+ async noteCoralPlacement(steps, factory, format, context) {
9958
+ const change = factory.notePlacement(placedModelsOf(steps));
9959
+ if (change.kind === "unchanged") return;
9960
+ const deviceKey = context.deviceKey ?? deviceKeyOf(this.currentEngine);
9961
+ const tags = context.deviceId !== void 0 ? { tags: { deviceId: context.deviceId } } : {};
9962
+ const placed = change.placed.map((p) => `${p.stepId}/${p.modelId}`);
9963
+ if (change.to.kind === "set") this.log.info("Coral pool loads a co-compiled set — the models placed on it share the parameter cache", {
9964
+ ...tags,
9965
+ meta: {
9966
+ deviceKey,
9967
+ set: change.to.set.id,
9968
+ placed,
9969
+ fromSet: change.to.covered.map((p) => `${p.stepId}/${p.modelId}`),
9970
+ separatelyCompiled: change.to.uncovered.map((p) => `${p.stepId}/${p.modelId}`),
9971
+ previous: change.from.kind === "set" ? change.from.set.id : null
9972
+ }
9973
+ });
9974
+ else if (change.to.reason === "no-set-covers-two") this.log.warn("Coral pool has no co-compiled set for the models placed on it — each is compiled alone, so they evict each other from the parameter cache on every alternation", {
9975
+ ...tags,
9976
+ meta: {
9977
+ deviceKey,
9978
+ placed,
9979
+ previous: change.from.kind === "set" ? change.from.set.id : null,
9980
+ hint: "keep the extra step off this Coral with its jumpDeviceKey, or place a combination a published set covers"
9981
+ }
9982
+ });
9983
+ const pool = factory;
9984
+ const previous = this.modelLoadTail.get(pool) ?? Promise.resolve();
9985
+ const tail = (async () => {
9986
+ await previous;
9987
+ for (const loaded of factory.listLoaded()) {
9988
+ const resolved = await this.resolveModelEntry(loaded.stepId, loaded.modelId);
9989
+ if (resolved === void 0) continue;
9990
+ const entry = factory.entryForPool(loaded.stepId, resolved);
9991
+ if (!(0, _camstack_system_addon_utils.isModelDownloaded)(this.modelsDir, entry, format)) await this.downloadWithRetry(entry, format, 3);
9992
+ }
9993
+ const reloaded = await factory.reloadChangedModelFiles();
9994
+ if (reloaded.length > 0) this.log.info("Coral pool reloaded its resident models from the new set", {
9995
+ ...tags,
9996
+ meta: {
9997
+ deviceKey,
9998
+ reloaded: reloaded.map((r) => `${r.stepId}/${r.modelId}`)
9999
+ }
10000
+ });
10001
+ })().catch((err) => {
10002
+ this.log.error("Coral pool could not move its resident models to the new set — they keep serving from the old files", {
10003
+ ...tags,
10004
+ meta: {
10005
+ deviceKey,
10006
+ error: require_dist.errMsg(err)
10007
+ }
10008
+ });
10009
+ });
10010
+ this.modelLoadTail.set(pool, tail);
10011
+ await tail;
10012
+ }
9459
10013
  /** Ensure all models needed by steps are downloaded and loaded in the engine pool.
9460
10014
  * `factory`/`format` default to the node's engine; a per-device dispatch passes
9461
10015
  * that device's factory + format (Phase 2 multi-device). `context` names the
@@ -9553,7 +10107,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9553
10107
  const refusal = this.loadRefusal(pool, deviceKey, models);
9554
10108
  if (refusal !== null) throw refusal;
9555
10109
  try {
9556
- await this.downloadNeededModels(needed, format);
10110
+ await this.downloadNeededModels(needed, format, factory);
9557
10111
  this.log.info("Loading additional models for benchmark", { meta: {
9558
10112
  count: needed.length,
9559
10113
  models,
@@ -9617,16 +10171,21 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9617
10171
  });
9618
10172
  }
9619
10173
  /** Download any model of `needed` missing in `format` (the device's). */
9620
- async downloadNeededModels(needed, format) {
10174
+ async downloadNeededModels(needed, format, factory = null) {
9621
10175
  for (const step of needed) try {
9622
- await this.ensureStepModelArtifact(step, format);
10176
+ await this.ensureStepModelArtifact(step, format, factory);
9623
10177
  } catch (err) {
9624
10178
  throw new StepModelArtifactError(step.addonId, step.modelId, err);
9625
10179
  }
9626
10180
  }
9627
- /** Make one step's model artifact present on disk for `format`, or say why it cannot be. */
9628
- async ensureStepModelArtifact(step, format) {
9629
- const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
10181
+ /**
10182
+ * Make one step's model artifact present on disk for `format`, or say why it
10183
+ * cannot be. The entry is the one `factory`'s pool will LOAD — on a Coral
10184
+ * pool, a co-compiled set's file when the set stands for the model (D724).
10185
+ */
10186
+ async ensureStepModelArtifact(step, format, factory = null) {
10187
+ const resolved = await this.resolveModelEntry(step.addonId, step.modelId);
10188
+ const modelEntry = resolved && factory !== null ? factory.entryForPool(step.addonId, resolved) : resolved;
9630
10189
  if (modelEntry && !(0, _camstack_system_addon_utils.isModelDownloaded)(this.modelsDir, modelEntry, format)) {
9631
10190
  if (modelEntry.formats[format] === void 0) throw new Error(`Model "${modelEntry.id}" has no ${format} format build (available: ${Object.keys(modelEntry.formats).join(", ") || "none"}) — not retrying a permanent format mismatch`);
9632
10191
  if (modelEntry.formats[format]?.url.startsWith("camstack-local://") === true) throw new Error(`Custom model "${modelEntry.id}" (${format}) is not present in this node's models directory — distribute it to this node from Model Studio before selecting it here`);
@@ -10648,7 +11207,7 @@ function buildSchemaSlots(formats, modelsDir, customByStep) {
10648
11207
  ...m.legacy !== void 0 ? { legacy: m.legacy } : {},
10649
11208
  provider: m.provider ?? "camstack",
10650
11209
  defaultConfidence: require_default_detection_model.resolveModelDefaultConfidence(step, m),
10651
- ...m.fillsClassifierEffects !== void 0 ? { fillsClassifierEffects: m.fillsClassifierEffects } : {}
11210
+ ...modelSuppliesField(step, m)
10652
11211
  })),
10653
11212
  defaultModelId: step.defaultModelId,
10654
11213
  ...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},
@@ -10784,6 +11343,15 @@ function isDetailPlaneStep(addonId) {
10784
11343
  return false;
10785
11344
  }
10786
11345
  }
11346
+ /** A root model's supplies for the schema, omitted when it supplies nothing. */
11347
+ function modelSuppliesField(step, model) {
11348
+ if (step.inputClasses !== null) return {};
11349
+ const supplies = require_dist.rootSuppliesOf(model, {
11350
+ labels: step.labels ?? [],
11351
+ ...step.classMap !== void 0 ? { classMap: step.classMap } : {}
11352
+ });
11353
+ return supplies.length > 0 ? { suppliesSteps: supplies } : {};
11354
+ }
10787
11355
  /**
10788
11356
  * What a dispatch runs after its per-step loads (D657).
10789
11357
  *
@@ -11032,7 +11600,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
11032
11600
  type: "select",
11033
11601
  key: "batchMode",
11034
11602
  label: "Batch dispatch mode",
11035
- description: "How the Python pool GROUPS predict requests — it never changes a model's compiled batch SHAPE, which is derived from (engine, device) at load and is not configurable (D202). `none` = one call per frame. `window` = per-model accumulator coalesces concurrent calls within Window (ms). Auto uses the matrix-sweep default for the current backend. Applies to THIS node only.",
11603
+ description: "How the Python pool GROUPS predict requests — it never changes a model's compiled batch SHAPE, which is derived from (engine, device) at load and is not configurable. `none` = one call per frame. `window` = per-model accumulator coalesces concurrent calls within Window (ms). Auto uses the matrix-sweep default for the current backend. Applies to THIS node only.",
11036
11604
  options: [
11037
11605
  {
11038
11606
  value: "",
@@ -11076,7 +11644,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
11076
11644
  type: "slider",
11077
11645
  key: "maxBatchSize",
11078
11646
  label: "Max batch size",
11079
- description: "Cap on how many requests are GROUPED into one batched predict call. Not the model's compiled batch shape — that is derived from (engine, device) at load and is not configurable (D202); a stacked tensor is only ever built for an export that declares a flexible batch axis itself. Auto = backend default. Applies to THIS node only.",
11647
+ description: "Cap on how many requests are GROUPED into one batched predict call. Not the model's compiled batch shape — that is derived from (engine, device) at load and is not configurable; a stacked tensor is only ever built for an export that declares a flexible batch axis itself. Auto = backend default. Applies to THIS node only.",
11080
11648
  min: 1,
11081
11649
  max: 16,
11082
11650
  step: 1,