@camstack/addon-pipeline 1.2.103 → 1.2.105

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 (53) hide show
  1. package/dist/{addon-utils-CLc6yHCN.js → addon-utils-UzUMfVj1.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +1188 -127
  5. package/dist/detection-pipeline/index.mjs +1186 -125
  6. package/dist/{dist-Ccmt3fGJ.mjs → dist-BSWr9Qg8.mjs} +296 -11
  7. package/dist/{dist-C11WuNUP.js → dist-DVMPCqaj.js} +313 -10
  8. package/dist/{event-loop-stall-monitor-Cq_NeC4o.js → event-loop-stall-monitor-BazdZ0g0.js} +82 -88
  9. package/dist/{event-loop-stall-monitor-OJrOMeuu.mjs → event-loop-stall-monitor-BeQHivuY.mjs} +82 -88
  10. package/dist/{lazy-sharp-RxUs6on_.js → lazy-sharp-Du1-XaNP.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +8 -6
  14. package/dist/pipeline-runner/index.mjs +6 -4
  15. package/dist/{process-memory-DOjQ3MgC.js → process-memory-CiNyC-wv.js} +1 -1
  16. package/dist/{process-memory-D0zDmXLI.mjs → process-memory-DFL51gCx.mjs} +1 -1
  17. package/dist/recorder/index.js +660 -130
  18. package/dist/recorder/index.mjs +659 -129
  19. package/dist/remote-restream-BeHi78PZ.mjs +25 -0
  20. package/dist/remote-restream-CO36Sr30.js +36 -0
  21. package/dist/restream-intent-B4BXZra7.mjs +72 -0
  22. package/dist/{remote-restream-BYbAsgUf.js → restream-intent-Cv9x3jmu.js} +30 -30
  23. package/dist/session-decode/decode-worker-child.js +2 -2
  24. package/dist/session-decode/decode-worker-child.mjs +1 -1
  25. package/dist/stream-broker/_stub.js +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BdgcF1lL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-pkzutnza.mjs} +3 -3
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DvZWLWk1.mjs +26 -0
  28. package/dist/stream-broker/{hostInit-Da9wVA2r.mjs → hostInit-B1i1rtBB.mjs} +3 -3
  29. package/dist/stream-broker/index.js +598 -82
  30. package/dist/stream-broker/index.mjs +597 -81
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/dist/{worker-protocol-DDpliBIW.mjs → worker-protocol-Clcy8PvY.mjs} +1 -1
  33. package/dist/{worker-protocol-BePduZVV.js → worker-protocol-pUUt-yhh.js} +1 -1
  34. package/package.json +1 -1
  35. package/python/inference_pool.py +238 -2
  36. package/python/postprocessors/__init__.py +4 -0
  37. package/python/postprocessors/ctc.py +52 -0
  38. package/python/postprocessors/plate_slots.py +180 -0
  39. package/python/postprocessors/test_ctc.py +39 -0
  40. package/python/postprocessors/test_plate_slots.py +217 -0
  41. package/python/postprocessors/test_yolonas.py +83 -0
  42. package/python/postprocessors/yolonas.py +67 -0
  43. package/python/test_inference_pool_backpressure.py +6 -2
  44. package/python/test_inference_pool_coreml_cache.py +12 -4
  45. package/python/test_inference_pool_device_selection.py +12 -4
  46. package/python/test_inference_pool_layout.py +16 -5
  47. package/python/test_inference_pool_memstats.py +6 -2
  48. package/python/test_inference_pool_ov_ppp.py +12 -9
  49. package/python/test_inference_pool_preprocess.py +57 -0
  50. package/python/test_inference_pool_reconfigure.py +122 -0
  51. package/python/test_inference_pool_static_batch.py +426 -0
  52. package/dist/remote-restream-Ci7RXNGb.mjs +0 -66
  53. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DY31bUCj.mjs +0 -26
@@ -8568,12 +8568,25 @@ var RecordingBandSchema = object({
8568
8568
  * segment is ever finalized and the camera records nothing anyway. At the 10 s
8569
8569
  * default segment length, 30 s leaves room for two full segments. `preBufferSec`
8570
8570
  * is retroactive only — it costs nothing at record time, it merely keeps
8571
- * already-written segments — so it is set to one segment length.
8571
+ * already-written segments.
8572
8572
  *
8573
- * An EXPLICIT `0` is left alone: that is an operator statement, not an omission.
8573
+ * ## Why `preBufferSec` is 15 and not one segment length
8574
+ *
8575
+ * It has to cover the broker's RECORDING pre-roll, which is media handed to the
8576
+ * writer from BEFORE the trigger (D192/D193). If this bound were narrower, the
8577
+ * keep gate would delete the very seconds the ring just supplied — two
8578
+ * authorities disagreeing about the same footage, which is the failure D191 was
8579
+ * written against. It is therefore set to `RECORDING_PRE_ROLL_MAX_MS` (15 s),
8580
+ * the widest window the broker can be configured to serve, so an operator
8581
+ * raising the cluster pre-roll can never walk past the gate silently. The
8582
+ * addon-side guard is
8583
+ * `packages/addon-pipeline/src/recorder/addon/__tests__/band-decision.spec.ts`.
8584
+ *
8585
+ * An EXPLICIT `0` is left alone: that is an operator statement, not an omission,
8586
+ * and it is excluded from that invariant by name.
8574
8587
  */
8575
8588
  var DEFAULT_EVENTS_BAND_BUFFER_SEC = {
8576
- preBufferSec: 10,
8589
+ preBufferSec: 15,
8577
8590
  postBufferSec: 30
8578
8591
  };
8579
8592
  DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec * 1e3;
@@ -17906,7 +17919,7 @@ var TrackEnvelopeSchema = object({
17906
17919
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
17907
17920
  * keeps every scalar the list surfaces actually render (ids, class(es),
17908
17921
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
17909
- * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
17922
+ * zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
17910
17923
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
17911
17924
  * `getTrack`. Mirrors the event-store `projection` convention
17912
17925
  * (`getObjectEvents` et al.).
@@ -18201,6 +18214,35 @@ var TrackSchema = object({
18201
18214
  */
18202
18215
  hasFace: boolean().optional(),
18203
18216
  /**
18217
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
18218
+ * face an operator could ASSIGN to an identity.
18219
+ *
18220
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
18221
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
18222
+ * branch, before every gate, and means no more than "a face detector produced
18223
+ * a face detail". This one is stamped at the single moment the gallery row
18224
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
18225
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
18226
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
18227
+ * the crop-store drop. Everything between the detector and that insert can
18228
+ * legitimately refuse the face, so a flag written any earlier promises the
18229
+ * operator something to assign and delivers nothing.
18230
+ *
18231
+ * **Independent of recognition.** A face collected but never auto-matched is
18232
+ * still assignable — it is in fact the face an operator most wants to reach —
18233
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
18234
+ * `subLabel`; this says only that the raw material exists.
18235
+ *
18236
+ * **Set once, never cleared.** A track that produced a gallery row produced
18237
+ * one; deleting the row later is the gallery's business, not this flag's.
18238
+ *
18239
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
18240
+ * before the column omits it, and so does every server that predates the
18241
+ * field. A consumer must test `=== true` and render nothing otherwise —
18242
+ * never infer "no assignable face".
18243
+ */
18244
+ hasEmbeddedFace: boolean().optional(),
18245
+ /**
18204
18246
  * This subject CONTAINS a folded rider — a person the rider-pairing step
18205
18247
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
18206
18248
  * so the passage is tracked once and as a VEHICLE.
@@ -19708,6 +19750,26 @@ var DetailResultSchema = object({
19708
19750
  * the consuming gate treats as accept — never as reject.
19709
19751
  */
19710
19752
  embeddingMagnitude: number().optional(),
19753
+ /**
19754
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
19755
+ * `label`, and for the same reason: the producing node is the only side that
19756
+ * knows which model actually ran, and re-deriving it downstream is exactly
19757
+ * the inference this model exists to forbid.
19758
+ *
19759
+ * Additive and optional, so a runner that predates it degrades to
19760
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
19761
+ * its previous assumption instead of refusing.
19762
+ *
19763
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
19764
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
19765
+ * with nothing on the wire it took the id from the GALLERY, so the gate
19766
+ * compared the gallery against itself and could never fire. That turned the
19767
+ * one guard protecting the face feature space into a tautology, and it is
19768
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
19769
+ * gallery as the catalog comment predicts — it would have scored the new
19770
+ * model's vectors against the old model's samples in one cosine space.
19771
+ */
19772
+ embeddingModelId: string().optional(),
19711
19773
  label: string().optional(),
19712
19774
  /**
19713
19775
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -20725,6 +20787,12 @@ var CameraStatusSchema = object({
20725
20787
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
20726
20788
  fetchedAt: number()
20727
20789
  });
20790
+ var InferenceDeviceExclusionReasonSchema = _enum([
20791
+ "disabled",
20792
+ "unavailable",
20793
+ "cannot-host-camera-root",
20794
+ "accelerator-preferred"
20795
+ ]);
20728
20796
  var NodeInferenceDeviceSchema = object({
20729
20797
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
20730
20798
  key: string(),
@@ -20755,7 +20823,17 @@ var NodeInferenceDeviceSchema = object({
20755
20823
  * available per format; this is the stored selection that becomes the
20756
20824
  * default for EVERY camera landing on this accelerator.
20757
20825
  */
20758
- steps: record(string(), DeviceStepConfigSchema).optional()
20826
+ steps: record(string(), DeviceStepConfigSchema).optional(),
20827
+ /**
20828
+ * `null` when the device IS a camera-root candidate on this node; otherwise
20829
+ * the reason the dispatcher drops it. Computed by the SAME
20830
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
20831
+ * never disagree with the election — deriving it in the UI from
20832
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
20833
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
20834
+ * "an accelerator is serving" predicate).
20835
+ */
20836
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
20759
20837
  });
20760
20838
  var NodeInferenceDevicesSchema = object({
20761
20839
  nodeId: string(),
@@ -21260,6 +21338,7 @@ DeviceType.Camera, method(object({
21260
21338
  isBattery: boolean(),
21261
21339
  reason: _enum([
21262
21340
  "disabled",
21341
+ "offline",
21263
21342
  "sleeping",
21264
21343
  "unreachable",
21265
21344
  "waking"
@@ -21305,9 +21384,17 @@ targets: array(object({
21305
21384
  sleeping: boolean(),
21306
21385
  /** Current device state rendered over the cached frame. State images
21307
21386
  * remain authoritative even when their photographic background is
21308
- * old; null means the link must carry a current camera frame. */
21387
+ * old; null means the link must carry a current camera frame.
21388
+ *
21389
+ * **This is the ONLY source a surface may derive an inactive tile
21390
+ * from.** A client that reads a device's `online`/`disabled` flags and
21391
+ * draws its own verdict will disagree with the pixels it is painting
21392
+ * — the frame comes from here, and so must the sentence over it
21393
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
21394
+ * state. */
21309
21395
  stateReason: _enum([
21310
21396
  "disabled",
21397
+ "offline",
21311
21398
  "sleeping",
21312
21399
  "unreachable",
21313
21400
  "waking"
@@ -24666,7 +24753,12 @@ var ListResultSchema = object({
24666
24753
  probedAt: number()
24667
24754
  });
24668
24755
  var PreferredSchema = LocalInterfaceSchema.nullable();
24669
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24756
+ /**
24757
+ * Candidate base URL for the SDK to race on connect. Order matters —
24758
+ * the SDK should attempt these top-to-bottom with a short per-candidate
24759
+ * timeout (e.g. 1500ms) and cache the winner for the session.
24760
+ */
24761
+ var ConnectionEndpointSchema = object({
24670
24762
  /** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
24671
24763
  label: string(),
24672
24764
  /** Fully-formed base URL with scheme + host + port. */
@@ -24709,7 +24801,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
24709
24801
  * ordering between polls.
24710
24802
  */
24711
24803
  priority: number()
24712
- })).readonly() });
24804
+ });
24805
+ /**
24806
+ * Where the advertised local port came from. Ordered most → least
24807
+ * authoritative, and the whole point of returning it: a client must be able to
24808
+ * tell a FACT about the hub's socket from an echo of its own guess.
24809
+ */
24810
+ var LocalPortSourceEnum = _enum([
24811
+ "server-config",
24812
+ "server-env",
24813
+ "caller-hint",
24814
+ "default"
24815
+ ]);
24816
+ /** The port every LAN/loopback `baseUrl` in the same result was built with. */
24817
+ var AdvertisedLocalPortSchema = object({
24818
+ port: number().int().min(1).max(65535),
24819
+ source: LocalPortSourceEnum
24820
+ });
24821
+ var GetConnectionEndpointsResultSchema = object({
24822
+ endpoints: array(ConnectionEndpointSchema).readonly(),
24823
+ /**
24824
+ * The port the hub built the LAN/loopback URLs with, and where that number
24825
+ * came from.
24826
+ *
24827
+ * Returned rather than merely applied, because "the URL is right" and "the
24828
+ * client can KNOW the URL is right" are different properties. A client that
24829
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
24830
+ * a hub that echoed the port the client sent, so it cannot decide whether to
24831
+ * race the candidate or discard it. With `source` it can: anything but
24832
+ * `caller-hint` is the hub's own socket.
24833
+ *
24834
+ * Absent on hubs predating this field — a client that finds it missing is
24835
+ * talking to an echoing hub and must degrade exactly as it does for
24836
+ * `caller-hint`.
24837
+ */
24838
+ localPort: AdvertisedLocalPortSchema
24839
+ });
24713
24840
  /**
24714
24841
  * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
24715
24842
  * AUTO (resolved from the candidate ranking at send time); `resolved` reports
@@ -24730,8 +24857,13 @@ var AllowedAddressesSchema = object({
24730
24857
  */
24731
24858
  addresses: array(string()).readonly() });
24732
24859
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24733
- /** Local hub HTTP port to use in base URLs. */
24734
- port: number().int().min(1).max(65535),
24860
+ /**
24861
+ * LEGACY HINT — do not send from new code. Kept optional so clients
24862
+ * written against the echoing contract keep working; the hub uses it
24863
+ * only when it cannot read its own port, and says so via
24864
+ * `localPort.source === 'caller-hint'`.
24865
+ */
24866
+ port: number().int().min(1).max(65535).optional(),
24735
24867
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24736
24868
  * candidate. Default `true`. */
24737
24869
  includeLoopback: boolean().optional(),
@@ -36110,6 +36242,153 @@ TimelapseRuleInputSchema.extend({
36110
36242
  createdAt: number(),
36111
36243
  updatedAt: number()
36112
36244
  });
36245
+ /**
36246
+ * A model choice has a SCOPE, and a scope change has a workflow.
36247
+ *
36248
+ * ## The two scopes
36249
+ *
36250
+ * Almost every pipeline step's model is a **node** question: which build runs
36251
+ * fastest on THIS accelerator. Two nodes may legitimately answer differently —
36252
+ * a Coral runs the tflite SSD, a Mac runs the coreml YOLO — because the answers
36253
+ * are compared against nothing. They produce boxes, and a box is a box.
36254
+ *
36255
+ * Two steps are not like that. `face-embedding` and `clip-embedding` produce
36256
+ * **vectors that land in ONE shared index** (`identity_samples`,
36257
+ * `vec_object_clip`) and are ranked against each other by cosine similarity.
36258
+ * Cosine similarity between two encoders is not a weak signal, it is a
36259
+ * meaningless number — and nothing inside a vector can tell you it happened.
36260
+ * So for those two steps the model is a **cluster** question with exactly one
36261
+ * answer, stored in exactly one row.
36262
+ *
36263
+ * That is the same argument [D52](../../../../docs/decisions/adr-0052.md) makes
36264
+ * for the crop convention (`detail-crop.ts`), one level up: the crop decides
36265
+ * WHAT pixels the encoder sees, this decides WHICH encoder sees them. Both are
36266
+ * cluster-wide for the same reason and neither may become per-node.
36267
+ *
36268
+ * ## Why the registry lives here and not in the step catalog
36269
+ *
36270
+ * The scope itself is declared on the step, in
36271
+ * `detection-pipeline/registry/step-definitions.ts` (`StepDefinition.modelScope`)
36272
+ * — that is where a new step declares what kind of choice its model is. But the
36273
+ * catalog lives inside `addon-pipeline`, and three packages that must NOT
36274
+ * import an addon need to know the set: the orchestrator (which renders and
36275
+ * stores the row), post-analysis (which pins the row for a re-embed pass) and
36276
+ * admin-ui (which mirrors it). So the set is mirrored here, and
36277
+ * `scripts/check-cluster-model-scope.ts` fails the build when the two disagree.
36278
+ * A hand-copied key that stops matching is exactly how a setting silently stops
36279
+ * arriving while both sides still look correct.
36280
+ *
36281
+ * ## The defaults ARE today's behaviour
36282
+ *
36283
+ * {@link DEFAULT_CLUSTER_STEP_MODELS} reproduces the catalog `defaultModelId`
36284
+ * of both steps. Declaring the scope therefore changes nothing on the day it
36285
+ * ships: the cluster row starts out saying what every node was already doing.
36286
+ * Changing it is legitimate — that is what the knob is for — but it invalidates
36287
+ * the stored vectors of that surface, and must be followed by a re-embed pass
36288
+ * (`face-reembed-pass.ts`, `embedding-rebuild.ts`).
36289
+ */
36290
+ /**
36291
+ * The cluster-scoped steps. Mirrored from the catalog's `modelScope: 'cluster'`
36292
+ * declarations and guarded by `scripts/check-cluster-model-scope.ts`.
36293
+ */
36294
+ var CLUSTER_MODEL_SCOPED_STEPS = [{
36295
+ stepId: "face-embedding",
36296
+ label: "Face recognition model",
36297
+ defaultModelId: "arcface-r100",
36298
+ indexName: "the enrolled face gallery",
36299
+ options: [
36300
+ {
36301
+ id: "arcface-r100",
36302
+ label: "ArcFace (ResNet34)"
36303
+ },
36304
+ {
36305
+ id: "auraface-r100",
36306
+ label: "AuraFace R100"
36307
+ },
36308
+ {
36309
+ id: "inception-resnet-v1",
36310
+ label: "Inception ResNet V1"
36311
+ }
36312
+ ]
36313
+ }, {
36314
+ stepId: "clip-embedding",
36315
+ label: "Semantic search model",
36316
+ defaultModelId: "mobileclip-s1",
36317
+ indexName: "the object semantic-search index",
36318
+ options: [{
36319
+ id: "mobileclip-s1",
36320
+ label: "MobileCLIP S1"
36321
+ }, {
36322
+ id: "mobileclip-s2",
36323
+ label: "MobileCLIP S2"
36324
+ }]
36325
+ }];
36326
+ /**
36327
+ * The settings key holding one step's cluster model choice.
36328
+ *
36329
+ * Step-qualified so the keys stay unique across the addon's whole schema, which
36330
+ * is what lets {@link pickClusterStepModels} walk every section instead of
36331
+ * trusting the section id.
36332
+ */
36333
+ function clusterModelSettingKey(stepId) {
36334
+ return `clusterModel:${stepId}`;
36335
+ }
36336
+ /** The cluster row when nobody has configured one — today's catalog defaults. */
36337
+ var DEFAULT_CLUSTER_STEP_MODELS = Object.freeze(Object.fromEntries(CLUSTER_MODEL_SCOPED_STEPS.map((s) => [s.stepId, s.defaultModelId])));
36338
+ /** A stored choice is a non-empty model id, or it is not a choice. */
36339
+ var ChosenModelIdSchema = string().min(1);
36340
+ /**
36341
+ * Narrow a FLAT settings record to the cluster row.
36342
+ *
36343
+ * Per-FIELD fallback, deliberately (same rule as `readDetailCropConvention`): a
36344
+ * junk face model must not also discard a valid clip model. An absent, empty or
36345
+ * non-string value resolves to the step's catalog default — the historical
36346
+ * behaviour — never to a blank id, because a blank id downstream becomes
36347
+ * "substitute the format default", which is precisely the substitution this
36348
+ * scope exists to forbid.
36349
+ */
36350
+ function readClusterStepModels(config) {
36351
+ const out = {};
36352
+ for (const step of CLUSTER_MODEL_SCOPED_STEPS) {
36353
+ const parsed = ChosenModelIdSchema.safeParse(config[clusterModelSettingKey(step.stepId)]);
36354
+ out[step.stepId] = parsed.success ? parsed.data : step.defaultModelId;
36355
+ }
36356
+ return out;
36357
+ }
36358
+ function isHydratedField$2(entry) {
36359
+ return typeof entry === "object" && entry !== null && "key" in entry;
36360
+ }
36361
+ /**
36362
+ * Extract the cluster row from an `addon-settings.getGlobalSettings` payload.
36363
+ *
36364
+ * Walks EVERY section rather than looking inside {@link CLUSTER_MODEL_SECTION_ID}
36365
+ * alone: the keys are unique across the addon's schema, and a section rename
36366
+ * must not silently revert the whole cluster to the defaults. A `null` payload
36367
+ * (addon mid-boot) is the defaults.
36368
+ */
36369
+ function pickClusterStepModels(view) {
36370
+ if (view === null) return DEFAULT_CLUSTER_STEP_MODELS;
36371
+ const flat = {};
36372
+ const wanted = new Set(CLUSTER_MODEL_SCOPED_STEPS.map((s) => clusterModelSettingKey(s.stepId)));
36373
+ for (const section of view.sections) for (const entry of section.fields) {
36374
+ if (!isHydratedField$2(entry) || typeof entry.key !== "string") continue;
36375
+ if (wanted.has(entry.key)) flat[entry.key] = entry.value;
36376
+ }
36377
+ return readClusterStepModels(flat);
36378
+ }
36379
+ /**
36380
+ * The cluster model for `stepId`, or `null` when the step is node-scoped.
36381
+ *
36382
+ * `null` is the signal to LEAVE THE CALLER'S RESOLUTION ALONE — it is not "no
36383
+ * model". Returning a default here for a node-scoped step would quietly
36384
+ * override every per-(node,device) selection in the fleet.
36385
+ */
36386
+ function resolveClusterStepModelId(stepId, models) {
36387
+ const step = CLUSTER_MODEL_SCOPED_STEPS.find((s) => s.stepId === stepId);
36388
+ if (step === void 0) return null;
36389
+ const chosen = models[stepId];
36390
+ return chosen !== void 0 && chosen !== "" ? chosen : step.defaultModelId;
36391
+ }
36113
36392
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
36114
36393
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
36115
36394
  /**
@@ -36181,6 +36460,12 @@ function pickDetailCropConvention(view) {
36181
36460
  }
36182
36461
  return readDetailCropConvention(flat);
36183
36462
  }
36463
+ Object.freeze({
36464
+ x: 0,
36465
+ y: 0,
36466
+ w: 1,
36467
+ h: 1
36468
+ });
36184
36469
  DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
36185
36470
  /**
36186
36471
  * Derive the crop rectangle for one parent detection.
@@ -37223,6 +37508,12 @@ Object.defineProperty(exports, "DEFAULT_AUDIO_ANALYZER_CONFIG", {
37223
37508
  return DEFAULT_AUDIO_ANALYZER_CONFIG;
37224
37509
  }
37225
37510
  });
37511
+ Object.defineProperty(exports, "DEFAULT_CLUSTER_STEP_MODELS", {
37512
+ enumerable: true,
37513
+ get: function() {
37514
+ return DEFAULT_CLUSTER_STEP_MODELS;
37515
+ }
37516
+ });
37226
37517
  Object.defineProperty(exports, "DEFAULT_DETAIL_CROP_CONVENTION", {
37227
37518
  enumerable: true,
37228
37519
  get: function() {
@@ -37625,6 +37916,12 @@ Object.defineProperty(exports, "parseProfileBrokerId", {
37625
37916
  return parseProfileBrokerId;
37626
37917
  }
37627
37918
  });
37919
+ Object.defineProperty(exports, "pickClusterStepModels", {
37920
+ enumerable: true,
37921
+ get: function() {
37922
+ return pickClusterStepModels;
37923
+ }
37924
+ });
37628
37925
  Object.defineProperty(exports, "pickDetailCropConvention", {
37629
37926
  enumerable: true,
37630
37927
  get: function() {
@@ -37667,6 +37964,12 @@ Object.defineProperty(exports, "recordingExportCapability", {
37667
37964
  return recordingExportCapability;
37668
37965
  }
37669
37966
  });
37967
+ Object.defineProperty(exports, "resolveClusterStepModelId", {
37968
+ enumerable: true,
37969
+ get: function() {
37970
+ return resolveClusterStepModelId;
37971
+ }
37972
+ });
37670
37973
  Object.defineProperty(exports, "resolveEgressDecodeHwAccel", {
37671
37974
  enumerable: true,
37672
37975
  get: function() {
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-C11WuNUP.js");
1
+ const require_dist = require("./dist-DVMPCqaj.js");
2
2
  let node_perf_hooks = require("node:perf_hooks");
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -737,9 +737,39 @@ var PACKAGE_DETECTION_MODELS = [{
737
737
  }
738
738
  }];
739
739
  var PLATE_OCR_MODELS = [{
740
+ id: "cct-s-v2-global",
741
+ name: "CCT-S v2 Global — License Plate",
742
+ description: "fast-plate-ocr CCT-S v2 — a dedicated license-plate reader (66 regions incl. Germany). 19/21 exact on the cam-617 ground-truth set vs 4/21 for the generic VGG text recognizer, with zero false reads on 18 junk ROIs",
743
+ inputSize: {
744
+ width: 128,
745
+ height: 64
746
+ },
747
+ inputLayout: "nhwc",
748
+ labels: [{
749
+ id: "text",
750
+ name: "Plate Text"
751
+ }],
752
+ preprocessMode: "resize",
753
+ postprocessor: "plate-slots",
754
+ license: "MIT",
755
+ formats: {
756
+ onnx: {
757
+ url: hf("plateRecognition/cct-s-v2-global/onnx/camstack-cct-s-v2-global.onnx"),
758
+ sizeMB: 5.3
759
+ },
760
+ coreml: {
761
+ url: hf("plateRecognition/cct-s-v2-global/coreml/camstack-cct-s-v2-global.mlpackage"),
762
+ sizeMB: 5.1,
763
+ isDirectory: true,
764
+ files: [...MLPACKAGE_FILES],
765
+ runtimes: ["python"]
766
+ },
767
+ openvino: ovFormat(hf("plateRecognition/cct-s-v2-global/openvino/camstack-cct-s-v2-global.xml"), 5)
768
+ }
769
+ }, {
740
770
  id: "vgg-english-g2",
741
771
  name: "VGG English G2",
742
- description: "EasyOCR VGG English G2 — text recognition for license plates and scene text",
772
+ description: "EasyOCR VGG English G2 — generic English text recognition. Superseded as the plate-OCR default by cct-s-v2-global (4/21 vs 19/21 exact on the cam-617 set); kept selectable for non-plate scene text and for any node pinned to it",
743
773
  inputSize: {
744
774
  width: 384,
745
775
  height: 64
@@ -1107,95 +1137,55 @@ var INSTANCE_SEGMENTATION_MODELS = [
1107
1137
  }
1108
1138
  }
1109
1139
  ];
1110
- var CLIP_EMBEDDING_MODELS = [
1111
- {
1112
- id: "mobileclip-s0",
1113
- name: "MobileCLIP S0",
1114
- description: "MobileCLIP S0 — Apple ultra-fast CLIP vision encoder, 512-dim, 256×256 (40 MB ONNX)",
1115
- inputSize: {
1116
- width: 256,
1117
- height: 256
1118
- },
1119
- labels: [{
1120
- id: "embedding",
1121
- name: "CLIP Embedding"
1122
- }],
1123
- preprocessMode: "resize",
1124
- inputNormalization: "none",
1125
- formats: {
1126
- onnx: {
1127
- url: hf("clip/mobileclip-s0/onnx/camstack-mobileclip-s0-vision.onnx"),
1128
- sizeMB: 40
1129
- },
1130
- openvino: ovFormat(hf("clip/mobileclip-s0/openvino/camstack-mobileclip-s0-vision.xml"), 25),
1131
- coreml: {
1132
- url: hf("clip/mobileclip-s0/coreml/camstack-mobileclip-s0-vision.mlpackage"),
1133
- sizeMB: 30,
1134
- isDirectory: true,
1135
- files: [...MLPACKAGE_FILES],
1136
- runtimes: ["python"]
1137
- }
1138
- }
1140
+ var CLIP_EMBEDDING_MODELS = [{
1141
+ id: "mobileclip-s1",
1142
+ name: "MobileCLIP S1",
1143
+ description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (fp16 OpenVINO/CoreML)",
1144
+ inputSize: {
1145
+ width: 256,
1146
+ height: 256
1139
1147
  },
1140
- {
1141
- id: "mobileclip-s1",
1142
- name: "MobileCLIP S1",
1143
- description: "MobileCLIP S1 — Apple balanced CLIP vision encoder, 512-dim, 256×256 (90 MB ONNX)",
1144
- inputSize: {
1145
- width: 256,
1146
- height: 256
1147
- },
1148
- labels: [{
1149
- id: "embedding",
1150
- name: "CLIP Embedding"
1151
- }],
1152
- preprocessMode: "resize",
1153
- inputNormalization: "none",
1154
- formats: {
1155
- onnx: {
1156
- url: hf("clip/mobileclip-s1/onnx/camstack-mobileclip-s1-vision.onnx"),
1157
- sizeMB: 90
1158
- },
1159
- openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
1160
- coreml: {
1161
- url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
1162
- sizeMB: 65,
1163
- isDirectory: true,
1164
- files: [...MLPACKAGE_FILES],
1165
- runtimes: ["python"]
1166
- }
1148
+ labels: [{
1149
+ id: "embedding",
1150
+ name: "CLIP Embedding"
1151
+ }],
1152
+ preprocessMode: "resize",
1153
+ inputNormalization: "none",
1154
+ formats: {
1155
+ openvino: ovFormat(hf("clip/mobileclip-s1/openvino/camstack-mobileclip-s1-vision.xml"), 55),
1156
+ coreml: {
1157
+ url: hf("clip/mobileclip-s1/coreml/camstack-mobileclip-s1-vision.mlpackage"),
1158
+ sizeMB: 65,
1159
+ isDirectory: true,
1160
+ files: [...MLPACKAGE_FILES],
1161
+ runtimes: ["python"]
1167
1162
  }
1163
+ }
1164
+ }, {
1165
+ id: "mobileclip-s2",
1166
+ name: "MobileCLIP S2",
1167
+ description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (fp16 OpenVINO/CoreML)",
1168
+ inputSize: {
1169
+ width: 256,
1170
+ height: 256
1168
1171
  },
1169
- {
1170
- id: "mobileclip-s2",
1171
- name: "MobileCLIP S2",
1172
- description: "MobileCLIP S2 — Apple high-accuracy CLIP vision encoder, 512-dim, 256×256 (150 MB ONNX)",
1173
- inputSize: {
1174
- width: 256,
1175
- height: 256
1176
- },
1177
- labels: [{
1178
- id: "embedding",
1179
- name: "CLIP Embedding"
1180
- }],
1181
- preprocessMode: "resize",
1182
- inputNormalization: "none",
1183
- formats: {
1184
- onnx: {
1185
- url: hf("clip/mobileclip-s2/onnx/camstack-mobileclip-s2-vision.onnx"),
1186
- sizeMB: 150
1187
- },
1188
- openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
1189
- coreml: {
1190
- url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
1191
- sizeMB: 110,
1192
- isDirectory: true,
1193
- files: [...MLPACKAGE_FILES],
1194
- runtimes: ["python"]
1195
- }
1172
+ labels: [{
1173
+ id: "embedding",
1174
+ name: "CLIP Embedding"
1175
+ }],
1176
+ preprocessMode: "resize",
1177
+ inputNormalization: "none",
1178
+ formats: {
1179
+ openvino: ovFormat(hf("clip/mobileclip-s2/openvino/camstack-mobileclip-s2-vision.xml"), 90),
1180
+ coreml: {
1181
+ url: hf("clip/mobileclip-s2/coreml/camstack-mobileclip-s2-vision.mlpackage"),
1182
+ sizeMB: 110,
1183
+ isDirectory: true,
1184
+ files: [...MLPACKAGE_FILES],
1185
+ runtimes: ["python"]
1196
1186
  }
1197
1187
  }
1198
- ];
1188
+ }];
1199
1189
  var AUDIO_CLASSIFIER_MODELS = [{
1200
1190
  id: "yamnet-onnx",
1201
1191
  name: "YAMNet",
@@ -1533,7 +1523,7 @@ var PackageDetectionStep = class extends PipelineStepBase {
1533
1523
  type: "slider",
1534
1524
  key: "minConfidencePackage",
1535
1525
  label: "Min Confidence — Package",
1536
- description: "Drop package predictions below this score. Raise it when static porch furniture (a doormat, a plant pot) is reported as a parcel those sit just above the default.",
1526
+ description: "Drop package predictions below this score. NOTE: raising it does NOT fix a recurring static false positive measured on device 615, the false object (a streetlight light patch) scored up to 0.877 while the one real parcel scored 0.885, so no threshold separates them. The lever that does is the package SIZE ceiling in the camera’s Package detection settings (the false object covers 8–9.5% of the frame, real parcels 1–2%).",
1537
1527
  min: 0,
1538
1528
  max: 1,
1539
1529
  step: .05,
@@ -1560,6 +1550,7 @@ var ClassifierWithMinConfidence = class extends PipelineStepBase {
1560
1550
  var PLATE_OCR_DEFAULT_REGION = "DE";
1561
1551
  var PLATE_OCR_DEFAULT_MIN_TEXT_LENGTH = 3;
1562
1552
  var PLATE_OCR_DEFAULT_MIN_TEXT_CONFIDENCE = 0;
1553
+ var PLATE_OCR_DEFAULT_MIN_CHAR_PROBABILITY = .75;
1563
1554
  /**
1564
1555
  * Plate OCR step — exposes a `rectify` toggle (default ON) that deskews the
1565
1556
  * plate crop (rotation rectification) before the CTC recognizer reads it.
@@ -1647,6 +1638,7 @@ var STEP_FACE_EMBEDDING = new FaceEmbeddingStep({
1647
1638
  labelTier: 2,
1648
1639
  models: [...FACE_EMBEDDING_MODELS],
1649
1640
  defaultModelId: "arcface-r100",
1641
+ modelScope: "cluster",
1650
1642
  defaultConfidence: 0,
1651
1643
  cadence: {
1652
1644
  trigger: "improve",
@@ -1668,6 +1660,7 @@ var STEP_CLIP_EMBEDDING = new PipelineStepBase({
1668
1660
  outputClasses: ["embedding"],
1669
1661
  models: [...CLIP_EMBEDDING_MODELS],
1670
1662
  defaultModelId: "mobileclip-s1",
1663
+ modelScope: "cluster",
1671
1664
  enabledByDefault: true,
1672
1665
  backfillIntoExistingOverrides: true,
1673
1666
  defaultConfidence: 0,
@@ -1703,12 +1696,13 @@ var STEP_PLATE_OCR = new PlateOcrStep({
1703
1696
  outputClasses: ["plate-text"],
1704
1697
  labelTier: 2,
1705
1698
  models: [...PLATE_OCR_MODELS],
1706
- defaultModelId: "vgg-english-g2",
1699
+ defaultModelId: "cct-s-v2-global",
1707
1700
  defaultConfidence: 0,
1708
1701
  charset: [...VGG_G2_CHARSET],
1709
1702
  plateRegion: PLATE_OCR_DEFAULT_REGION,
1710
1703
  minTextLength: PLATE_OCR_DEFAULT_MIN_TEXT_LENGTH,
1711
1704
  minTextConfidence: PLATE_OCR_DEFAULT_MIN_TEXT_CONFIDENCE,
1705
+ minCharProbability: PLATE_OCR_DEFAULT_MIN_CHAR_PROBABILITY,
1712
1706
  cadence: {
1713
1707
  trigger: "once",
1714
1708
  maxPerTrack: 5