@camstack/addon-pipeline 1.2.327 → 1.2.329

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 (44) hide show
  1. package/THIRD_PARTY_MODELS.md +35 -27
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/{default-detection-model-CBIYdAJ5.js → default-detection-model-ChH7Y219.js} +130 -61
  5. package/dist/{default-detection-model-hql6BX9-.mjs → default-detection-model-j0nBpRoB.mjs} +130 -61
  6. package/dist/detection-pipeline/index.js +458 -16
  7. package/dist/detection-pipeline/index.mjs +458 -16
  8. package/dist/{dist-B9LnBPz9.mjs → dist-0HoA3wAi.mjs} +503 -370
  9. package/dist/{dist-BBZwYfSN.js → dist-BlIWhjnu.js} +550 -369
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-mJQBUiEc.js → node-BPpVMnZy.js} +1 -1
  13. package/dist/{node-Ci0E-0VP.mjs → node-HOMWRPzW.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +215 -78
  15. package/dist/pipeline-runner/index.mjs +215 -78
  16. package/dist/{process-memory-DgEfKdtC.js → process-memory-BW06vOIQ.js} +1 -1
  17. package/dist/{process-memory-B63Jlpev.mjs → process-memory-DuF8pa2w.mjs} +1 -1
  18. package/dist/recorder/index.js +2 -2
  19. package/dist/recorder/index.mjs +2 -2
  20. package/dist/{segment-demux-js-DzwHW4Ey.mjs → segment-demux-js-BU2MXpln.mjs} +1 -1
  21. package/dist/{segment-demux-js-C2ufKMu0.js → segment-demux-js-Dl52Zz8w.js} +1 -1
  22. package/dist/session-decode/decode-worker-main.js +13 -21
  23. package/dist/session-decode/decode-worker-main.mjs +13 -21
  24. package/dist/stream-broker/_stub.js +1 -1
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B4fPzS-8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vNfS65In.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DfeHvW4k.mjs +26 -0
  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-Cg4IdtE5.mjs → hostInit-Ciacp783.mjs} +3 -3
  30. package/dist/stream-broker/index.js +4 -3
  31. package/dist/stream-broker/index.mjs +4 -3
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/python/edgetpu_dequant.py +38 -1
  35. package/python/inference_pool.py +69 -138
  36. package/python/postprocessors/test_bird_edgetpu.py +124 -0
  37. package/python/postprocessors/test_yolox_plate_edgetpu.py +126 -0
  38. package/python/postprocessors/test_yunet_edgetpu.py +149 -0
  39. package/python/postprocessors/testdata/bird_classifier_int8_outputs.json +1 -0
  40. package/python/postprocessors/testdata/yolox_plate_int8_outputs.json +1 -0
  41. package/python/postprocessors/testdata/yunet_int8_outputs.json +1 -0
  42. package/python/postprocessors/yolo.py +44 -0
  43. package/python/test_inference_pool_static_batch.py +2 -4
  44. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDGzcgLP.mjs +0 -26
@@ -1,9 +1,9 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { A as PoolMemoryWatchdog, An as sleep, At as overlayClusterStepSettings, Dn as parseJsonUnknown, En as nodePin, Ft as pipelineExecutorCapability, Hn as union, Ht as resolveClusterStepModelId, Jt as runtimeDevices$1, Kt as resolvePoolMemoryPolicy, Mn as array, Mt as pickClusterStepSettings, Rn as object, Sn as hydrateSchema, Un as EventCategory, V as YAMNET_TO_MACRO, Vn as string, Yt as stepModelRunsOnFormat, bt as loadContributionCapability, ct as evaluateZoneRules, d as CLUSTER_MODEL_SCOPED_STEPS, en as supportedRuntimes$1, et as defaultDeviceFor$1, fn as errMsg, h as DEFAULT_CLUSTER_STEP_SETTINGS, it as detectionPipelineCapability, mn as BaseAddon, n as APPLE_SA_TO_MACRO, st as enumerateInferenceDevices, vn as createEvent, y as DEVICE_BACKEND_TO_FORMAT } from "../dist-B9LnBPz9.mjs";
3
- import { s as readProcessCost } from "../node-Ci0E-0VP.mjs";
4
- import { _ as getStep, a as buildSubtreeCanRun, b as landmarkPrecisionVerdict, c as macroGateVerdict, g as getDefaultModelForFormat, h as ALL_STEPS, i as mergeCustomModels, m as ALL_PIPELINE_STEPS, n as resolveDefaultDetectionModel, r as CustomModelCatalog, u as localFrameRegistry, v as getStepDefinition, x as resolveModelDefaultConfidence, y as resolveModelForFormat } from "../default-detection-model-hql6BX9-.mjs";
2
+ import { An as hydrateSchema, Bn as array, Dt as loadContributionCapability, En as createEvent, F as PoolMemoryWatchdog, Fn as parseJsonUnknown, Ht as pipelineExecutorCapability, K as YAMNET_TO_MACRO, Kn as object, Lt as overlayClusterStepSettings, Pn as nodePin, Rn as sleep, Sn as BaseAddon, St as hfModelUrl, T as FILLABLE_STEP_MACRO, Tt as linkedLabelStepId, Xn as union, Yn as string, Yt as resolveClusterStepModelId, Zn as EventCategory, at as defaultDeviceFor$1, b as DEVICE_BACKEND_TO_FORMAT, bn as errMsg, cn as supportedRuntimes$1, d as CLUSTER_MODEL_SCOPED_STEPS, en as resolvePoolMemoryPolicy, ft as enumerateInferenceDevices, h as DEFAULT_CLUSTER_STEP_SETTINGS, lt as detectionPipelineCapability, n as APPLE_SA_TO_MACRO, nn as runtimeDevices$1, pt as evaluateZoneRules, rn as stepModelRunsOnFormat, zt as pickClusterStepSettings } from "../dist-0HoA3wAi.mjs";
3
+ import { s as readProcessCost } from "../node-HOMWRPzW.mjs";
4
+ import { _ as getStep, a as buildSubtreeCanRun, b as landmarkPrecisionVerdict, c as macroGateVerdict, g as getDefaultModelForFormat, h as ALL_STEPS, i as mergeCustomModels, m as ALL_PIPELINE_STEPS, n as resolveDefaultDetectionModel, r as CustomModelCatalog, u as localFrameRegistry, v as getStepDefinition, x as resolveModelDefaultConfidence, y as resolveModelForFormat } from "../default-detection-model-j0nBpRoB.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
6
- import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-B63Jlpev.mjs";
6
+ import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DuF8pa2w.mjs";
7
7
  import * as os from "node:os";
8
8
  import * as fs from "node:fs";
9
9
  import * as path$1 from "node:path";
@@ -2809,6 +2809,8 @@ var PipelineModelManager = class {
2809
2809
  stepId,
2810
2810
  modelId,
2811
2811
  poolIndex: index,
2812
+ path: config.path,
2813
+ ...settings !== void 0 ? { settings } : {},
2812
2814
  lruTick: ++this.lruClock,
2813
2815
  decode
2814
2816
  };
@@ -2816,6 +2818,56 @@ var PipelineModelManager = class {
2816
2818
  return entry;
2817
2819
  }
2818
2820
  /**
2821
+ * Reload every resident slot whose model FILE is no longer the one
2822
+ * `buildConfig` names — a Coral pool that switched co-compiled set (D724).
2823
+ * The new slot is loaded BEFORE the old one is unloaded and the entry is
2824
+ * replaced in between, so a lookup always finds a loaded index. Returns the
2825
+ * `(step, model)` pairs reloaded. A failed reload keeps the old slot (it
2826
+ * still serves, from the file it was loaded from) and is logged.
2827
+ */
2828
+ async reloadChangedPaths() {
2829
+ const reloaded = [];
2830
+ for (const [stepId, perStep] of this.loaded) for (const entry of perStep.values()) {
2831
+ const config = this.source.buildConfig(stepId, entry.modelId, entry.settings);
2832
+ if (config.path === entry.path) continue;
2833
+ const index = this.pool.allocateIndex();
2834
+ try {
2835
+ await this.pool.loadModel(index, config);
2836
+ } catch (err) {
2837
+ this.pool.releaseIndex(index);
2838
+ this.log.error("Step variant reload from another model file failed — the old file keeps serving", { meta: {
2839
+ step: stepId,
2840
+ modelId: entry.modelId,
2841
+ deviceKey: this.deviceKey,
2842
+ from: entry.path,
2843
+ to: config.path,
2844
+ error: err instanceof Error ? err.message : String(err)
2845
+ } });
2846
+ continue;
2847
+ }
2848
+ perStep.set(entry.modelId, {
2849
+ ...entry,
2850
+ poolIndex: index,
2851
+ path: config.path,
2852
+ decode: poolDecodeSettingsOf(config)
2853
+ });
2854
+ await this.pool.unloadModel(entry.poolIndex);
2855
+ this.log.info("Step variant reloaded from another model file", { meta: {
2856
+ step: stepId,
2857
+ modelId: entry.modelId,
2858
+ deviceKey: this.deviceKey,
2859
+ from: entry.path,
2860
+ to: config.path,
2861
+ poolIndex: index
2862
+ } });
2863
+ reloaded.push({
2864
+ stepId,
2865
+ modelId: entry.modelId
2866
+ });
2867
+ }
2868
+ return reloaded;
2869
+ }
2870
+ /**
2819
2871
  * Push a retuned decode setting into ONE already-loaded slot.
2820
2872
  *
2821
2873
  * Scope is the point: `reconfigure` names a single pool index, so a slider
@@ -2974,6 +3026,251 @@ function deviceKeyOf(engine) {
2974
3026
  return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
2975
3027
  }
2976
3028
  //#endregion
3029
+ //#region src/detection-pipeline/engine/coral-cocompiled-set.ts
3030
+ var ROOT_DETECTOR_STEP_ID = "object-detection";
3031
+ /** The models a dispatch places on the pool that runs it: every enabled video step. */
3032
+ function placedModelsOf(steps) {
3033
+ return flattenEnabledVideoSteps(steps).map((s) => ({
3034
+ stepId: s.addonId,
3035
+ modelId: s.modelId
3036
+ }));
3037
+ }
3038
+ var sameModel = (a, b) => a.stepId === b.stepId && a.modelId === b.modelId;
3039
+ /**
3040
+ * The set for `placed`, or why there is none.
3041
+ *
3042
+ * 1. A set that reserves the cache for the root detector is eligible only when
3043
+ * the root is placed here, and a rootless set only when it is not: a root
3044
+ * set on a pool without the root wastes the cache the root would have had,
3045
+ * and a rootless set beside a root makes the root swap.
3046
+ * 2. A set must stand for at least two placed models — one model alone is
3047
+ * better off with the whole cache (its separately compiled file).
3048
+ * 3. Most placed models covered wins; then the set with fewest members nobody
3049
+ * placed (they hold cache for nothing); then declaration order.
3050
+ */
3051
+ function selectCoCompiledSet(placed, sets) {
3052
+ if (placed.length === 0) return {
3053
+ kind: "separate",
3054
+ reason: "nothing-placed"
3055
+ };
3056
+ if (placed.length === 1) return {
3057
+ kind: "separate",
3058
+ reason: "single-model"
3059
+ };
3060
+ const hostsRoot = placed.some((p) => p.stepId === ROOT_DETECTOR_STEP_ID);
3061
+ let best = null;
3062
+ for (const set of sets) {
3063
+ if (set.hostsRoot !== hostsRoot) continue;
3064
+ const covered = placed.filter((p) => set.members.some((m) => sameModel(m, p)));
3065
+ if (covered.length < 2) continue;
3066
+ const idle = set.members.length - covered.length;
3067
+ if (best === null || covered.length > best.covered.length || covered.length === best.covered.length && idle < best.idle) best = {
3068
+ set,
3069
+ covered,
3070
+ idle
3071
+ };
3072
+ }
3073
+ if (best === null) return {
3074
+ kind: "separate",
3075
+ reason: "no-set-covers-two"
3076
+ };
3077
+ const { set, covered } = best;
3078
+ return {
3079
+ kind: "set",
3080
+ set,
3081
+ covered,
3082
+ uncovered: placed.filter((p) => !covered.some((c) => sameModel(c, p)))
3083
+ };
3084
+ }
3085
+ /** The member a selection loads `(stepId, modelId)` from, or `null` (its own `formats.tflite`). */
3086
+ function setMemberFor(selection, stepId, modelId) {
3087
+ if (selection.kind !== "set") return null;
3088
+ return selection.set.members.find((m) => m.stepId === stepId && m.modelId === modelId) ?? null;
3089
+ }
3090
+ /**
3091
+ * `entry` with its `tflite` build pointed at the set's file. Everything else —
3092
+ * labels, postprocessor, input contract, licence — is the entry's own: a
3093
+ * member is the same model, compiled together with others.
3094
+ */
3095
+ function withSetBuild(entry, member) {
3096
+ return {
3097
+ ...entry,
3098
+ formats: {
3099
+ ...entry.formats,
3100
+ tflite: {
3101
+ url: member.url,
3102
+ sizeMB: member.sizeMB,
3103
+ runtimes: ["python"]
3104
+ }
3105
+ }
3106
+ };
3107
+ }
3108
+ /** Identity of a selection for change detection: the set AND what it leaves out. */
3109
+ var selectionKey = (s) => s.kind === "set" ? `${s.set.id}|${s.uncovered.map((u) => `${u.stepId}/${u.modelId}`).toSorted().join(",")}` : `separate:${s.reason}`;
3110
+ /**
3111
+ * The placement of ONE Coral pool and the set it loads. Lives exactly as long
3112
+ * as the pool (the factory owns it).
3113
+ */
3114
+ var CoralSetPlanner = class {
3115
+ sets;
3116
+ placed = [];
3117
+ current = {
3118
+ kind: "separate",
3119
+ reason: "nothing-placed"
3120
+ };
3121
+ constructor(sets) {
3122
+ this.sets = sets;
3123
+ }
3124
+ /** Add the models a dispatch places on this pool; say whether the set changed. */
3125
+ note(models) {
3126
+ let grew = false;
3127
+ for (const m of models) {
3128
+ if (this.placed.some((p) => sameModel(p, m))) continue;
3129
+ this.placed.push({
3130
+ stepId: m.stepId,
3131
+ modelId: m.modelId
3132
+ });
3133
+ grew = true;
3134
+ }
3135
+ if (!grew) return { kind: "unchanged" };
3136
+ const next = selectCoCompiledSet(this.placed, this.sets);
3137
+ if (selectionKey(next) === selectionKey(this.current)) {
3138
+ this.current = next;
3139
+ return { kind: "unchanged" };
3140
+ }
3141
+ const from = this.current;
3142
+ this.current = next;
3143
+ return {
3144
+ kind: "changed",
3145
+ from,
3146
+ to: next,
3147
+ placed: [...this.placed]
3148
+ };
3149
+ }
3150
+ selection() {
3151
+ return this.current;
3152
+ }
3153
+ /** `entry` as this pool must load it: from the set when the set stands for it. */
3154
+ entryFor(stepId, entry) {
3155
+ const member = setMemberFor(this.current, stepId, entry.id);
3156
+ return member === null ? entry : withSetBuild(entry, member);
3157
+ }
3158
+ };
3159
+ //#endregion
3160
+ //#region src/detection-pipeline/registry/edgetpu-cocompiled-sets.ts
3161
+ /**
3162
+ * Co-compiled Edge TPU sets — pool-level artifacts, not models (D724).
3163
+ *
3164
+ * A Coral holds ~6.9 MiB of parameter cache. Two separately compiled models
3165
+ * carry two different cache tokens, so every alternation between them on one
3166
+ * Coral flushes the cache and re-streams the incoming model's parameters over
3167
+ * USB (D710). `edgetpu_compiler` given several models at once allocates the
3168
+ * cache between them, in command-line order, under ONE token: loaded from the
3169
+ * same set, they never evict each other — whatever does not fit is streamed on
3170
+ * every invoke of THAT model instead.
3171
+ *
3172
+ * Each member is the SAME model as its catalog entry's `formats.tflite` (same
3173
+ * `modelId`, same int8 graph, same labels and licence record), compiled
3174
+ * together with the other members (D710: a Coral build is a format of the
3175
+ * survivor; the set is where it is placed, not a new model). A Coral pool picks
3176
+ * at most one set from the models placed on it (`coral-cocompiled-set.ts`) and
3177
+ * falls back to the separately compiled `formats.tflite` for everything else.
3178
+ *
3179
+ * Built by `scripts/build-edgetpu-sets.py` (compile order = `members` order =
3180
+ * cache priority) and hosted under `edgetpuSets/<id>/`. Only sets whose
3181
+ * measured cost is acceptable are listed here: a set that would make the root
3182
+ * detector stream parameters on every frame (root + YuNet at 640: the root
3183
+ * keeps 3.34 of its 6.39 MiB, YuNet's activations take the rest) is
3184
+ * deliberately absent.
3185
+ */
3186
+ var HF_REPO = "camstack/camstack-models";
3187
+ var setFile = (setId, file) => hfModelUrl(HF_REPO, `edgetpuSets/${setId}/${setId}__${file}`);
3188
+ var SSD = "ssd-mobilenet-v2-coco-edgetpu";
3189
+ var YUNET = "yunet-2023mar";
3190
+ var YOLOX = "yolox-tiny-plate";
3191
+ var BIRD = "bird-classifier";
3192
+ var EDGETPU_COCOMPILED_SETS = [
3193
+ {
3194
+ id: "root-plate-bird",
3195
+ hostsRoot: true,
3196
+ members: [
3197
+ {
3198
+ stepId: "object-detection",
3199
+ modelId: SSD,
3200
+ url: setFile("root-plate-bird", "ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite"),
3201
+ sizeMB: 6.9,
3202
+ onChipMiB: 6.39,
3203
+ streamedMiB: 0
3204
+ },
3205
+ {
3206
+ stepId: "plate-detection",
3207
+ modelId: YOLOX,
3208
+ url: setFile("root-plate-bird", "camstack-yolox-tiny-plate_int8_edgetpu.tflite"),
3209
+ sizeMB: 6.9,
3210
+ onChipMiB: .08,
3211
+ streamedMiB: 5.41
3212
+ },
3213
+ {
3214
+ stepId: "bird-classifier",
3215
+ modelId: BIRD,
3216
+ url: setFile("root-plate-bird", "mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite"),
3217
+ sizeMB: 4.9,
3218
+ onChipMiB: 0,
3219
+ streamedMiB: 4.54
3220
+ }
3221
+ ]
3222
+ },
3223
+ {
3224
+ id: "face-plate-bird",
3225
+ hostsRoot: false,
3226
+ members: [
3227
+ {
3228
+ stepId: "face-detection",
3229
+ modelId: YUNET,
3230
+ url: setFile("face-plate-bird", "camstack-yunet-2023mar_int8_edgetpu.tflite"),
3231
+ sizeMB: .7,
3232
+ onChipMiB: .11,
3233
+ streamedMiB: 0
3234
+ },
3235
+ {
3236
+ stepId: "plate-detection",
3237
+ modelId: YOLOX,
3238
+ url: setFile("face-plate-bird", "camstack-yolox-tiny-plate_int8_edgetpu.tflite"),
3239
+ sizeMB: 6.9,
3240
+ onChipMiB: 3.23,
3241
+ streamedMiB: 2.27
3242
+ },
3243
+ {
3244
+ stepId: "bird-classifier",
3245
+ modelId: BIRD,
3246
+ url: setFile("face-plate-bird", "mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite"),
3247
+ sizeMB: 4.9,
3248
+ onChipMiB: 0,
3249
+ streamedMiB: 4.54
3250
+ }
3251
+ ]
3252
+ },
3253
+ {
3254
+ id: "plate-bird",
3255
+ hostsRoot: false,
3256
+ members: [{
3257
+ stepId: "plate-detection",
3258
+ modelId: YOLOX,
3259
+ url: setFile("plate-bird", "camstack-yolox-tiny-plate_int8_edgetpu.tflite"),
3260
+ sizeMB: 6.9,
3261
+ onChipMiB: 5.55,
3262
+ streamedMiB: 0
3263
+ }, {
3264
+ stepId: "bird-classifier",
3265
+ modelId: BIRD,
3266
+ url: setFile("plate-bird", "mobilenet_v2_1.0_224_inat_bird_quant_edgetpu.tflite"),
3267
+ sizeMB: 4.1,
3268
+ onChipMiB: .92,
3269
+ streamedMiB: 2.8
3270
+ }]
3271
+ }
3272
+ ];
3273
+ //#endregion
2977
3274
  //#region src/detection-pipeline/engine/engine-factory.ts
2978
3275
  var BACKEND_TO_POOL_RUNTIME = {
2979
3276
  coreml: "coreml",
@@ -3067,11 +3364,45 @@ var EngineFactory = class {
3067
3364
  * trying to be.
3068
3365
  */
3069
3366
  resolvedProvisioning;
3367
+ /**
3368
+ * A Coral pool's placement and the co-compiled set it loads (D724); `null`
3369
+ * on every other runtime. Lives exactly as long as this factory — a new
3370
+ * pool starts from nothing placed.
3371
+ */
3372
+ coralSets;
3070
3373
  constructor(opts) {
3071
3374
  this.opts = opts;
3072
3375
  this.log = opts.logger;
3073
3376
  this.deviceKey = deviceKeyOf(opts.engine);
3074
3377
  this.resolvedProvisioning = resolvePoolProvisioning(opts.engine, opts.provisioning);
3378
+ this.coralSets = BACKEND_TO_POOL_RUNTIME[opts.engine.backend] === "edgetpu" ? new CoralSetPlanner(opts.coCompiledSets ?? EDGETPU_COCOMPILED_SETS) : null;
3379
+ }
3380
+ /**
3381
+ * Record the models a dispatch places on this pool. On a Coral pool the
3382
+ * answer says whether the co-compiled set it loads from changed; on any
3383
+ * other pool nothing is placed and nothing changes.
3384
+ */
3385
+ notePlacement(models) {
3386
+ return this.coralSets?.note(models) ?? { kind: "unchanged" };
3387
+ }
3388
+ /** The co-compiled set this pool loads from, or why none (`null`: not a Coral pool). */
3389
+ coralSetSelection() {
3390
+ return this.coralSets?.selection() ?? null;
3391
+ }
3392
+ /**
3393
+ * The catalog entry as THIS pool must download and load it: on a Coral pool
3394
+ * whose set stands for the model, the `tflite` build is the set's file.
3395
+ * Every other entry is returned unchanged.
3396
+ */
3397
+ entryForPool(stepId, entry) {
3398
+ return this.coralSets?.entryFor(stepId, entry) ?? entry;
3399
+ }
3400
+ /**
3401
+ * Reload every resident model whose file this pool no longer names — after
3402
+ * the Coral set changed. The caller has already downloaded the new files.
3403
+ */
3404
+ async reloadChangedModelFiles() {
3405
+ return this.poolManager?.reloadChangedPaths() ?? [];
3075
3406
  }
3076
3407
  /**
3077
3408
  * The provisioning this factory's pool runs with — resolved from the pool's
@@ -3333,6 +3664,7 @@ var EngineFactory = class {
3333
3664
  stepSettings,
3334
3665
  device: this.opts.engine.device,
3335
3666
  resolveCustomModel: this.opts.resolveCustomModel,
3667
+ entryForPool: (sid, entry) => this.entryForPool(sid, entry),
3336
3668
  onClassMapDrop: ({ stepId: sid, modelId: mid, unmappedLabels }) => {
3337
3669
  const key = `${sid}::${mid}`;
3338
3670
  if (this.classMapDropLogged.has(key)) return;
@@ -3358,8 +3690,9 @@ var EngineFactory = class {
3358
3690
  function buildPoolModelConfigForStep(inputs) {
3359
3691
  const { stepId, modelId, poolRuntime, modelsDir } = inputs;
3360
3692
  const def = getStepDefinition(stepId);
3361
- const modelEntry = def.models.find((m) => m.id === modelId) ?? inputs.resolveCustomModel?.(stepId, modelId);
3362
- if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog (static or custom registry)`);
3693
+ const resolvedEntry = def.models.find((m) => m.id === modelId) ?? inputs.resolveCustomModel?.(stepId, modelId);
3694
+ if (!resolvedEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog (static or custom registry)`);
3695
+ const modelEntry = inputs.entryForPool?.(stepId, resolvedEntry) ?? resolvedEntry;
3363
3696
  const format = RUNTIME_TO_FORMAT[poolRuntime];
3364
3697
  if (!format) throw new Error(`No format mapping for pool runtime "${poolRuntime}"`);
3365
3698
  const dropWarning = collectClassMapDropWarning(def, modelEntry);
@@ -5437,7 +5770,7 @@ var PLATE_OCR_CROP_POLICIES = {
5437
5770
  minLongSidePx: 85
5438
5771
  },
5439
5772
  "plate-slots": {
5440
- marginFraction: .15,
5773
+ marginFraction: .05,
5441
5774
  rectify: false,
5442
5775
  minLongSidePx: 70
5443
5776
  }
@@ -6127,6 +6460,41 @@ function toMutableRootDetection(det, rootStep, idGen, rootStepLatencyMs) {
6127
6460
  };
6128
6461
  }
6129
6462
  //#endregion
6463
+ //#region src/detection-pipeline/pipeline/linked-label.ts
6464
+ /**
6465
+ * The detector's linked label (D715): when the model that RAN declares it
6466
+ * fills a step, a detection whose raw class names that step's effect carries
6467
+ * the effect as a `ScoredLabel` attributed to `detector-link:<stepId>` — in
6468
+ * the step's own vocabulary and tier, so tier 1 means one thing whichever
6469
+ * writer produced it.
6470
+ *
6471
+ * Written at score 0 here, always: the executor cannot see the camera's whole
6472
+ * step tree (a detail step placed on another pool is pruned from the dispatch),
6473
+ * so it cannot know who the writer is. The runner, which holds the attach
6474
+ * config, raises the score to the detection's own when the detector is
6475
+ * authoritative (`pipeline-runner/label-link.ts`). Score 0 is the fallback any
6476
+ * scored answer of the step outranks.
6477
+ */
6478
+ /** The linked labels one mapped root detection carries. Pure. */
6479
+ function linkedLabelsFor(fills, rawClass, macroClass, modelId) {
6480
+ if (fills === void 0 || fills.length === 0) return [];
6481
+ const out = [];
6482
+ for (const fill of fills) {
6483
+ if (FILLABLE_STEP_MACRO[fill.stepId] !== macroClass) continue;
6484
+ const effect = fill.labelToEffect[rawClass];
6485
+ if (effect === void 0 || effect === macroClass) continue;
6486
+ const tier = getStepDefinition(fill.stepId).labelTier;
6487
+ out.push({
6488
+ label: effect,
6489
+ score: 0,
6490
+ ...tier !== void 0 ? { tier } : {},
6491
+ stepId: linkedLabelStepId(fill.stepId),
6492
+ modelId
6493
+ });
6494
+ }
6495
+ return out;
6496
+ }
6497
+ //#endregion
6130
6498
  //#region src/detection-pipeline/pipeline/executor.ts
6131
6499
  /**
6132
6500
  * Default minimum short-side (px, source-frame resolution) of a face bbox for
@@ -6396,6 +6764,7 @@ var PipelineExecutor = class {
6396
6764
  continue;
6397
6765
  }
6398
6766
  }
6767
+ for (const linked of linkedLabelsFor(rootStep.fillsClassifierEffects, det.class, mutable.macroClass, rootStep.modelId)) mutable.ownLabels.push(linked);
6399
6768
  const filterVerdict = this.macroFilterVerdict(mutable.macroClass, mutable.score, rootStep.settings);
6400
6769
  if (filterVerdict !== "pass") {
6401
6770
  (discarded ??= []).push({
@@ -6964,7 +7333,8 @@ function buildNode(step, getEngine, resolveModel, onMissingVariant, resolved) {
6964
7333
  enabled: step.enabled,
6965
7334
  children,
6966
7335
  ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {},
6967
- ...effectiveClassMap !== void 0 ? { effectiveClassMap } : {}
7336
+ ...effectiveClassMap !== void 0 ? { effectiveClassMap } : {},
7337
+ ...modelEntry?.fillsClassifierEffects !== void 0 ? { fillsClassifierEffects: modelEntry.fillsClassifierEffects } : {}
6968
7338
  };
6969
7339
  }
6970
7340
  /**
@@ -7750,7 +8120,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
7750
8120
  * The rebuild is not cheap — `ensureModelsForSteps` loads EVERY enabled step
7751
8121
  * into the fresh factory, which on the hub is seven models
7752
8122
  * (yolov9m-320-int8, scrfd-2.5g, arcface-r100, yolov8n-plate,
7753
- * vgg_english_g2, mobileclip-s1-vision, yolov8n-package). The hub log shows
8123
+ * vgg_english_g2, mobileclip-s1-vision, yolov8n-package — the set of that
8124
+ * date; all but the root detector have since been replaced, D703/D719).
8125
+ * The hub log shows
7754
8126
  * the cycle plainly: `COMPILE ×7` → shed within 7-15s → `EVICT` ~2.5 min
7755
8127
  * later → repeat. So the first seconds of a motion event — precisely when
7756
8128
  * detection matters — were spent recompiling models that had just been
@@ -9386,6 +9758,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9386
9758
  */
9387
9759
  async loadDispatchModels(steps, factory, format, context, policy) {
9388
9760
  const failures = [];
9761
+ if (factory !== null) await this.noteCoralPlacement(steps, factory, format, context);
9389
9762
  const walk = async (nodes, root) => {
9390
9763
  for (const step of nodes) {
9391
9764
  if (!step.enabled || step.slot === "audio-classifier") continue;
@@ -9410,6 +9783,69 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9410
9783
  await walk(steps, true);
9411
9784
  return failures;
9412
9785
  }
9786
+ /**
9787
+ * Tell a Coral pool which models this dispatch places on it, and act on a
9788
+ * change of the co-compiled set it loads from (D724): download the set's
9789
+ * files for the models already resident and reload them — queued behind the
9790
+ * pool's other loads, so nothing loads from the old file meanwhile. Logged
9791
+ * once per change, tagged with the camera whose dispatch moved it.
9792
+ */
9793
+ async noteCoralPlacement(steps, factory, format, context) {
9794
+ const change = factory.notePlacement(placedModelsOf(steps));
9795
+ if (change.kind === "unchanged") return;
9796
+ const deviceKey = context.deviceKey ?? deviceKeyOf(this.currentEngine);
9797
+ const tags = context.deviceId !== void 0 ? { tags: { deviceId: context.deviceId } } : {};
9798
+ const placed = change.placed.map((p) => `${p.stepId}/${p.modelId}`);
9799
+ if (change.to.kind === "set") this.log.info("Coral pool loads a co-compiled set — the models placed on it share the parameter cache", {
9800
+ ...tags,
9801
+ meta: {
9802
+ deviceKey,
9803
+ set: change.to.set.id,
9804
+ placed,
9805
+ fromSet: change.to.covered.map((p) => `${p.stepId}/${p.modelId}`),
9806
+ separatelyCompiled: change.to.uncovered.map((p) => `${p.stepId}/${p.modelId}`),
9807
+ previous: change.from.kind === "set" ? change.from.set.id : null
9808
+ }
9809
+ });
9810
+ 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", {
9811
+ ...tags,
9812
+ meta: {
9813
+ deviceKey,
9814
+ placed,
9815
+ previous: change.from.kind === "set" ? change.from.set.id : null,
9816
+ hint: "keep the extra step off this Coral with its jumpDeviceKey, or place a combination a published set covers"
9817
+ }
9818
+ });
9819
+ const pool = factory;
9820
+ const previous = this.modelLoadTail.get(pool) ?? Promise.resolve();
9821
+ const tail = (async () => {
9822
+ await previous;
9823
+ for (const loaded of factory.listLoaded()) {
9824
+ const resolved = await this.resolveModelEntry(loaded.stepId, loaded.modelId);
9825
+ if (resolved === void 0) continue;
9826
+ const entry = factory.entryForPool(loaded.stepId, resolved);
9827
+ if (!isModelDownloaded(this.modelsDir, entry, format)) await this.downloadWithRetry(entry, format, 3);
9828
+ }
9829
+ const reloaded = await factory.reloadChangedModelFiles();
9830
+ if (reloaded.length > 0) this.log.info("Coral pool reloaded its resident models from the new set", {
9831
+ ...tags,
9832
+ meta: {
9833
+ deviceKey,
9834
+ reloaded: reloaded.map((r) => `${r.stepId}/${r.modelId}`)
9835
+ }
9836
+ });
9837
+ })().catch((err) => {
9838
+ this.log.error("Coral pool could not move its resident models to the new set — they keep serving from the old files", {
9839
+ ...tags,
9840
+ meta: {
9841
+ deviceKey,
9842
+ error: errMsg(err)
9843
+ }
9844
+ });
9845
+ });
9846
+ this.modelLoadTail.set(pool, tail);
9847
+ await tail;
9848
+ }
9413
9849
  /** Ensure all models needed by steps are downloaded and loaded in the engine pool.
9414
9850
  * `factory`/`format` default to the node's engine; a per-device dispatch passes
9415
9851
  * that device's factory + format (Phase 2 multi-device). `context` names the
@@ -9507,7 +9943,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9507
9943
  const refusal = this.loadRefusal(pool, deviceKey, models);
9508
9944
  if (refusal !== null) throw refusal;
9509
9945
  try {
9510
- await this.downloadNeededModels(needed, format);
9946
+ await this.downloadNeededModels(needed, format, factory);
9511
9947
  this.log.info("Loading additional models for benchmark", { meta: {
9512
9948
  count: needed.length,
9513
9949
  models,
@@ -9571,16 +10007,21 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
9571
10007
  });
9572
10008
  }
9573
10009
  /** Download any model of `needed` missing in `format` (the device's). */
9574
- async downloadNeededModels(needed, format) {
10010
+ async downloadNeededModels(needed, format, factory = null) {
9575
10011
  for (const step of needed) try {
9576
- await this.ensureStepModelArtifact(step, format);
10012
+ await this.ensureStepModelArtifact(step, format, factory);
9577
10013
  } catch (err) {
9578
10014
  throw new StepModelArtifactError(step.addonId, step.modelId, err);
9579
10015
  }
9580
10016
  }
9581
- /** Make one step's model artifact present on disk for `format`, or say why it cannot be. */
9582
- async ensureStepModelArtifact(step, format) {
9583
- const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
10017
+ /**
10018
+ * Make one step's model artifact present on disk for `format`, or say why it
10019
+ * cannot be. The entry is the one `factory`'s pool will LOAD — on a Coral
10020
+ * pool, a co-compiled set's file when the set stands for the model (D724).
10021
+ */
10022
+ async ensureStepModelArtifact(step, format, factory = null) {
10023
+ const resolved = await this.resolveModelEntry(step.addonId, step.modelId);
10024
+ const modelEntry = resolved && factory !== null ? factory.entryForPool(step.addonId, resolved) : resolved;
9584
10025
  if (modelEntry && !isModelDownloaded(this.modelsDir, modelEntry, format)) {
9585
10026
  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`);
9586
10027
  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`);
@@ -10601,7 +11042,8 @@ function buildSchemaSlots(formats, modelsDir, customByStep) {
10601
11042
  ...m.group ? { group: m.group } : {},
10602
11043
  ...m.legacy !== void 0 ? { legacy: m.legacy } : {},
10603
11044
  provider: m.provider ?? "camstack",
10604
- defaultConfidence: resolveModelDefaultConfidence(step, m)
11045
+ defaultConfidence: resolveModelDefaultConfidence(step, m),
11046
+ ...m.fillsClassifierEffects !== void 0 ? { fillsClassifierEffects: m.fillsClassifierEffects } : {}
10605
11047
  })),
10606
11048
  defaultModelId: step.defaultModelId,
10607
11049
  ...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},