@camstack/addon-pipeline 1.2.102 → 1.2.104

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 (52) hide show
  1. package/dist/{addon-utils-CLc6yHCN.js → addon-utils-DRbRzrDy.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 +844 -110
  5. package/dist/detection-pipeline/index.mjs +842 -108
  6. package/dist/{dist-Ccmt3fGJ.mjs → dist-D9sltFoR.mjs} +230 -6
  7. package/dist/{dist-C11WuNUP.js → dist-gLAVhbvO.js} +247 -5
  8. package/dist/{event-loop-stall-monitor-OJrOMeuu.mjs → event-loop-stall-monitor-BS_lPX6H.mjs} +82 -88
  9. package/dist/{event-loop-stall-monitor-Cq_NeC4o.js → event-loop-stall-monitor-BUhYY39J.js} +82 -88
  10. package/dist/{lazy-sharp-RxUs6on_.js → lazy-sharp-OiAUva0g.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-DY4RHcTj.js} +1 -1
  16. package/dist/{process-memory-D0zDmXLI.mjs → process-memory-Dw7-9jCI.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-ZaqZ17-x.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-BnhMnnKV.mjs +26 -0
  28. package/dist/stream-broker/{hostInit-Da9wVA2r.mjs → hostInit-aZY81RnZ.mjs} +3 -3
  29. package/dist/stream-broker/index.js +628 -92
  30. package/dist/stream-broker/index.mjs +627 -91
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/dist/{worker-protocol-DDpliBIW.mjs → worker-protocol-Bz7-sMZC.mjs} +1 -1
  33. package/dist/{worker-protocol-BePduZVV.js → worker-protocol-D_0q_4Le.js} +1 -1
  34. package/package.json +1 -1
  35. package/python/inference_pool.py +193 -1
  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_static_batch.py +426 -0
  51. package/dist/remote-restream-Ci7RXNGb.mjs +0 -66
  52. 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
@@ -8545,12 +8545,25 @@ var RecordingBandSchema = object({
8545
8545
  * segment is ever finalized and the camera records nothing anyway. At the 10 s
8546
8546
  * default segment length, 30 s leaves room for two full segments. `preBufferSec`
8547
8547
  * is retroactive only — it costs nothing at record time, it merely keeps
8548
- * already-written segments — so it is set to one segment length.
8548
+ * already-written segments.
8549
8549
  *
8550
- * An EXPLICIT `0` is left alone: that is an operator statement, not an omission.
8550
+ * ## Why `preBufferSec` is 15 and not one segment length
8551
+ *
8552
+ * It has to cover the broker's RECORDING pre-roll, which is media handed to the
8553
+ * writer from BEFORE the trigger (D192/D193). If this bound were narrower, the
8554
+ * keep gate would delete the very seconds the ring just supplied — two
8555
+ * authorities disagreeing about the same footage, which is the failure D191 was
8556
+ * written against. It is therefore set to `RECORDING_PRE_ROLL_MAX_MS` (15 s),
8557
+ * the widest window the broker can be configured to serve, so an operator
8558
+ * raising the cluster pre-roll can never walk past the gate silently. The
8559
+ * addon-side guard is
8560
+ * `packages/addon-pipeline/src/recorder/addon/__tests__/band-decision.spec.ts`.
8561
+ *
8562
+ * An EXPLICIT `0` is left alone: that is an operator statement, not an omission,
8563
+ * and it is excluded from that invariant by name.
8551
8564
  */
8552
8565
  var DEFAULT_EVENTS_BAND_BUFFER_SEC = {
8553
- preBufferSec: 10,
8566
+ preBufferSec: 15,
8554
8567
  postBufferSec: 30
8555
8568
  };
8556
8569
  DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec * 1e3;
@@ -17883,7 +17896,7 @@ var TrackEnvelopeSchema = object({
17883
17896
  * `snapshots[]` references — megabytes across a page of tracks. `slim`
17884
17897
  * keeps every scalar the list surfaces actually render (ids, class(es),
17885
17898
  * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
17886
- * zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
17899
+ * zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
17887
17900
  * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
17888
17901
  * `getTrack`. Mirrors the event-store `projection` convention
17889
17902
  * (`getObjectEvents` et al.).
@@ -18178,6 +18191,35 @@ var TrackSchema = object({
18178
18191
  */
18179
18192
  hasFace: boolean().optional(),
18180
18193
  /**
18194
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
18195
+ * face an operator could ASSIGN to an identity.
18196
+ *
18197
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
18198
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
18199
+ * branch, before every gate, and means no more than "a face detector produced
18200
+ * a face detail". This one is stamped at the single moment the gallery row
18201
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
18202
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
18203
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
18204
+ * the crop-store drop. Everything between the detector and that insert can
18205
+ * legitimately refuse the face, so a flag written any earlier promises the
18206
+ * operator something to assign and delivers nothing.
18207
+ *
18208
+ * **Independent of recognition.** A face collected but never auto-matched is
18209
+ * still assignable — it is in fact the face an operator most wants to reach —
18210
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
18211
+ * `subLabel`; this says only that the raw material exists.
18212
+ *
18213
+ * **Set once, never cleared.** A track that produced a gallery row produced
18214
+ * one; deleting the row later is the gallery's business, not this flag's.
18215
+ *
18216
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
18217
+ * before the column omits it, and so does every server that predates the
18218
+ * field. A consumer must test `=== true` and render nothing otherwise —
18219
+ * never infer "no assignable face".
18220
+ */
18221
+ hasEmbeddedFace: boolean().optional(),
18222
+ /**
18181
18223
  * This subject CONTAINS a folded rider — a person the rider-pairing step
18182
18224
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
18183
18225
  * so the passage is tracked once and as a VEHICLE.
@@ -19685,6 +19727,26 @@ var DetailResultSchema = object({
19685
19727
  * the consuming gate treats as accept — never as reject.
19686
19728
  */
19687
19729
  embeddingMagnitude: number().optional(),
19730
+ /**
19731
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
19732
+ * `label`, and for the same reason: the producing node is the only side that
19733
+ * knows which model actually ran, and re-deriving it downstream is exactly
19734
+ * the inference this model exists to forbid.
19735
+ *
19736
+ * Additive and optional, so a runner that predates it degrades to
19737
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
19738
+ * its previous assumption instead of refusing.
19739
+ *
19740
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
19741
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
19742
+ * with nothing on the wire it took the id from the GALLERY, so the gate
19743
+ * compared the gallery against itself and could never fire. That turned the
19744
+ * one guard protecting the face feature space into a tautology, and it is
19745
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
19746
+ * gallery as the catalog comment predicts — it would have scored the new
19747
+ * model's vectors against the old model's samples in one cosine space.
19748
+ */
19749
+ embeddingModelId: string().optional(),
19688
19750
  label: string().optional(),
19689
19751
  /**
19690
19752
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -21237,6 +21299,7 @@ DeviceType.Camera, method(object({
21237
21299
  isBattery: boolean(),
21238
21300
  reason: _enum([
21239
21301
  "disabled",
21302
+ "offline",
21240
21303
  "sleeping",
21241
21304
  "unreachable",
21242
21305
  "waking"
@@ -21282,9 +21345,17 @@ targets: array(object({
21282
21345
  sleeping: boolean(),
21283
21346
  /** Current device state rendered over the cached frame. State images
21284
21347
  * remain authoritative even when their photographic background is
21285
- * old; null means the link must carry a current camera frame. */
21348
+ * old; null means the link must carry a current camera frame.
21349
+ *
21350
+ * **This is the ONLY source a surface may derive an inactive tile
21351
+ * from.** A client that reads a device's `online`/`disabled` flags and
21352
+ * draws its own verdict will disagree with the pixels it is painting
21353
+ * — the frame comes from here, and so must the sentence over it
21354
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
21355
+ * state. */
21286
21356
  stateReason: _enum([
21287
21357
  "disabled",
21358
+ "offline",
21288
21359
  "sleeping",
21289
21360
  "unreachable",
21290
21361
  "waking"
@@ -36087,6 +36158,153 @@ TimelapseRuleInputSchema.extend({
36087
36158
  createdAt: number(),
36088
36159
  updatedAt: number()
36089
36160
  });
36161
+ /**
36162
+ * A model choice has a SCOPE, and a scope change has a workflow.
36163
+ *
36164
+ * ## The two scopes
36165
+ *
36166
+ * Almost every pipeline step's model is a **node** question: which build runs
36167
+ * fastest on THIS accelerator. Two nodes may legitimately answer differently —
36168
+ * a Coral runs the tflite SSD, a Mac runs the coreml YOLO — because the answers
36169
+ * are compared against nothing. They produce boxes, and a box is a box.
36170
+ *
36171
+ * Two steps are not like that. `face-embedding` and `clip-embedding` produce
36172
+ * **vectors that land in ONE shared index** (`identity_samples`,
36173
+ * `vec_object_clip`) and are ranked against each other by cosine similarity.
36174
+ * Cosine similarity between two encoders is not a weak signal, it is a
36175
+ * meaningless number — and nothing inside a vector can tell you it happened.
36176
+ * So for those two steps the model is a **cluster** question with exactly one
36177
+ * answer, stored in exactly one row.
36178
+ *
36179
+ * That is the same argument [D52](../../../../docs/decisions/adr-0052.md) makes
36180
+ * for the crop convention (`detail-crop.ts`), one level up: the crop decides
36181
+ * WHAT pixels the encoder sees, this decides WHICH encoder sees them. Both are
36182
+ * cluster-wide for the same reason and neither may become per-node.
36183
+ *
36184
+ * ## Why the registry lives here and not in the step catalog
36185
+ *
36186
+ * The scope itself is declared on the step, in
36187
+ * `detection-pipeline/registry/step-definitions.ts` (`StepDefinition.modelScope`)
36188
+ * — that is where a new step declares what kind of choice its model is. But the
36189
+ * catalog lives inside `addon-pipeline`, and three packages that must NOT
36190
+ * import an addon need to know the set: the orchestrator (which renders and
36191
+ * stores the row), post-analysis (which pins the row for a re-embed pass) and
36192
+ * admin-ui (which mirrors it). So the set is mirrored here, and
36193
+ * `scripts/check-cluster-model-scope.ts` fails the build when the two disagree.
36194
+ * A hand-copied key that stops matching is exactly how a setting silently stops
36195
+ * arriving while both sides still look correct.
36196
+ *
36197
+ * ## The defaults ARE today's behaviour
36198
+ *
36199
+ * {@link DEFAULT_CLUSTER_STEP_MODELS} reproduces the catalog `defaultModelId`
36200
+ * of both steps. Declaring the scope therefore changes nothing on the day it
36201
+ * ships: the cluster row starts out saying what every node was already doing.
36202
+ * Changing it is legitimate — that is what the knob is for — but it invalidates
36203
+ * the stored vectors of that surface, and must be followed by a re-embed pass
36204
+ * (`face-reembed-pass.ts`, `embedding-rebuild.ts`).
36205
+ */
36206
+ /**
36207
+ * The cluster-scoped steps. Mirrored from the catalog's `modelScope: 'cluster'`
36208
+ * declarations and guarded by `scripts/check-cluster-model-scope.ts`.
36209
+ */
36210
+ var CLUSTER_MODEL_SCOPED_STEPS = [{
36211
+ stepId: "face-embedding",
36212
+ label: "Face recognition model",
36213
+ defaultModelId: "arcface-r100",
36214
+ indexName: "the enrolled face gallery",
36215
+ options: [
36216
+ {
36217
+ id: "arcface-r100",
36218
+ label: "ArcFace (ResNet34)"
36219
+ },
36220
+ {
36221
+ id: "auraface-r100",
36222
+ label: "AuraFace R100"
36223
+ },
36224
+ {
36225
+ id: "inception-resnet-v1",
36226
+ label: "Inception ResNet V1"
36227
+ }
36228
+ ]
36229
+ }, {
36230
+ stepId: "clip-embedding",
36231
+ label: "Semantic search model",
36232
+ defaultModelId: "mobileclip-s1",
36233
+ indexName: "the object semantic-search index",
36234
+ options: [{
36235
+ id: "mobileclip-s1",
36236
+ label: "MobileCLIP S1"
36237
+ }, {
36238
+ id: "mobileclip-s2",
36239
+ label: "MobileCLIP S2"
36240
+ }]
36241
+ }];
36242
+ /**
36243
+ * The settings key holding one step's cluster model choice.
36244
+ *
36245
+ * Step-qualified so the keys stay unique across the addon's whole schema, which
36246
+ * is what lets {@link pickClusterStepModels} walk every section instead of
36247
+ * trusting the section id.
36248
+ */
36249
+ function clusterModelSettingKey(stepId) {
36250
+ return `clusterModel:${stepId}`;
36251
+ }
36252
+ /** The cluster row when nobody has configured one — today's catalog defaults. */
36253
+ var DEFAULT_CLUSTER_STEP_MODELS = Object.freeze(Object.fromEntries(CLUSTER_MODEL_SCOPED_STEPS.map((s) => [s.stepId, s.defaultModelId])));
36254
+ /** A stored choice is a non-empty model id, or it is not a choice. */
36255
+ var ChosenModelIdSchema = string().min(1);
36256
+ /**
36257
+ * Narrow a FLAT settings record to the cluster row.
36258
+ *
36259
+ * Per-FIELD fallback, deliberately (same rule as `readDetailCropConvention`): a
36260
+ * junk face model must not also discard a valid clip model. An absent, empty or
36261
+ * non-string value resolves to the step's catalog default — the historical
36262
+ * behaviour — never to a blank id, because a blank id downstream becomes
36263
+ * "substitute the format default", which is precisely the substitution this
36264
+ * scope exists to forbid.
36265
+ */
36266
+ function readClusterStepModels(config) {
36267
+ const out = {};
36268
+ for (const step of CLUSTER_MODEL_SCOPED_STEPS) {
36269
+ const parsed = ChosenModelIdSchema.safeParse(config[clusterModelSettingKey(step.stepId)]);
36270
+ out[step.stepId] = parsed.success ? parsed.data : step.defaultModelId;
36271
+ }
36272
+ return out;
36273
+ }
36274
+ function isHydratedField$2(entry) {
36275
+ return typeof entry === "object" && entry !== null && "key" in entry;
36276
+ }
36277
+ /**
36278
+ * Extract the cluster row from an `addon-settings.getGlobalSettings` payload.
36279
+ *
36280
+ * Walks EVERY section rather than looking inside {@link CLUSTER_MODEL_SECTION_ID}
36281
+ * alone: the keys are unique across the addon's schema, and a section rename
36282
+ * must not silently revert the whole cluster to the defaults. A `null` payload
36283
+ * (addon mid-boot) is the defaults.
36284
+ */
36285
+ function pickClusterStepModels(view) {
36286
+ if (view === null) return DEFAULT_CLUSTER_STEP_MODELS;
36287
+ const flat = {};
36288
+ const wanted = new Set(CLUSTER_MODEL_SCOPED_STEPS.map((s) => clusterModelSettingKey(s.stepId)));
36289
+ for (const section of view.sections) for (const entry of section.fields) {
36290
+ if (!isHydratedField$2(entry) || typeof entry.key !== "string") continue;
36291
+ if (wanted.has(entry.key)) flat[entry.key] = entry.value;
36292
+ }
36293
+ return readClusterStepModels(flat);
36294
+ }
36295
+ /**
36296
+ * The cluster model for `stepId`, or `null` when the step is node-scoped.
36297
+ *
36298
+ * `null` is the signal to LEAVE THE CALLER'S RESOLUTION ALONE — it is not "no
36299
+ * model". Returning a default here for a node-scoped step would quietly
36300
+ * override every per-(node,device) selection in the fleet.
36301
+ */
36302
+ function resolveClusterStepModelId(stepId, models) {
36303
+ const step = CLUSTER_MODEL_SCOPED_STEPS.find((s) => s.stepId === stepId);
36304
+ if (step === void 0) return null;
36305
+ const chosen = models[stepId];
36306
+ return chosen !== void 0 && chosen !== "" ? chosen : step.defaultModelId;
36307
+ }
36090
36308
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
36091
36309
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
36092
36310
  /**
@@ -36158,6 +36376,12 @@ function pickDetailCropConvention(view) {
36158
36376
  }
36159
36377
  return readDetailCropConvention(flat);
36160
36378
  }
36379
+ Object.freeze({
36380
+ x: 0,
36381
+ y: 0,
36382
+ w: 1,
36383
+ h: 1
36384
+ });
36161
36385
  DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
36162
36386
  /**
36163
36387
  * Derive the crop rectangle for one parent detection.
@@ -37140,4 +37364,4 @@ function enumerateInferenceDevices(hw) {
37140
37364
  return out;
37141
37365
  }
37142
37366
  //#endregion
37143
- export { recordingCapability as $, batteryCapability as A, array as At, egressTranscodeSharingKey as B, EventCategory as Bt, RecordingConfigSchema as C, makeSourceBrokerId as Ct, audioAnalysisCapability as D, selectAssignedProfileSlots as Dt, addonWidgetsSourceCapability as E, parseProfileBrokerId as Et, defineCustomActions as F, number as Ft, mapAudioLabelToMacro as G, enumerateInferenceDevices as H, deriveBatteryPresence as I, object as It, parseProcStatus as J, maskUrlCredentials as K, deriveDetailCropRect as L, record as Lt, createHwAccelCache as M, discriminatedUnion as Mt, customAction as N, lazy as Nt, audioAnalyzerCapability as O, sleep as Ot, defaultDeviceFor as P, literal as Pt, pipelineRunnerCapability as Q, deriveRecordingMode as R, string as Rt, RECORDING_EXPORT_MAX_READ_BYTES as S, makeProfileBrokerId as St, YAMNET_TO_MACRO as T, parseJsonUnknown as Tt, evaluateZoneRules as U, egressTransportFromRequest as V, hfModelUrl as W, pickNativeLeaseOverride as X, pickDetailCropConvention as Y, pipelineExecutorCapability as Z, NativeLeaseSettingsSchema as _, DeviceFeature as _t, COCO_80_LABELS as a, storageEvictableCapability as at, RATE_CONTROL_RELAXED as b, hydrateSchema as bt, DEFAULT_DETAIL_CROP_CONVENTION as c, webrtcSessionCapability as ct, DEVICE_BACKEND_TO_FORMAT as d, Fmp4BoxSplitter as dt, recordingExportCapability as et, EVENT_PAD_MS as f, buildFfmpegArgs as ft, NativeLeaseAdmissionSchema as g, CAM_PROFILE_ORDER as gt, HF_BASE_URL as h, BaseAddon as ht, BatteryStatusSchema as i, runtimeDevices as it, cameraStreamsCapability as j, boolean as jt, audioKindId as k, _enum as kt, DEFAULT_EVENTS_BAND_BUFFER_SEC as l, errMsg as lt, ExportRecordSchema as m, isSoftwareDecode as mt, AUDIO_BACKEND_CHOICES as n, resolvePoolMemoryPolicy as nt, COCO_TO_MACRO as o, streamBrokerCapability as ot, EncodeProfileSchema as p, invocationFromEncodeProfile as pt, motionDetectionCapability as q, AUDIO_MACRO_LABELS as r, resolveRecordingProfiles as rt, DEFAULT_AUDIO_ANALYZER_CONFIG as s, supportedRuntimes as st, APPLE_SA_TO_MACRO as t, resolveEgressDecodeHwAccel as tt, DEFAULT_NATIVE_LEASE_SETTINGS as u, AUDIO_PRESETS as ut, OpsLogEntrySchema as v, DeviceType as vt, RingBuffer as w, nodePin as wt, RATE_CONTROL_TIGHT as x, isEvent as xt, PoolMemoryWatchdog as y, createEvent as yt, detectionPipelineCapability as z, union as zt };
37367
+ export { pipelineExecutorCapability as $, audioKindId as A, selectAssignedProfileSlots as At, detectionPipelineCapability as B, record as Bt, RECORDING_EXPORT_MAX_READ_BYTES as C, hydrateSchema as Ct, addonWidgetsSourceCapability as D, nodePin as Dt, YAMNET_TO_MACRO as E, makeSourceBrokerId as Et, defaultDeviceFor as F, discriminatedUnion as Ft, hfModelUrl as G, egressTransportFromRequest as H, union as Ht, defineCustomActions as I, lazy as It, motionDetectionCapability as J, mapAudioLabelToMacro as K, deriveBatteryPresence as L, literal as Lt, cameraStreamsCapability as M, _enum as Mt, createHwAccelCache as N, array as Nt, audioAnalysisCapability as O, parseJsonUnknown as Ot, customAction as P, boolean as Pt, pickNativeLeaseOverride as Q, deriveDetailCropRect as R, number as Rt, RATE_CONTROL_TIGHT as S, createEvent as St, RingBuffer as T, makeProfileBrokerId as Tt, enumerateInferenceDevices as U, EventCategory as Ut, egressTranscodeSharingKey as V, string as Vt, evaluateZoneRules as W, pickClusterStepModels as X, parseProcStatus as Y, pickDetailCropConvention as Z, NativeLeaseAdmissionSchema as _, isSoftwareDecode as _t, COCO_80_LABELS as a, resolvePoolMemoryPolicy as at, PoolMemoryWatchdog as b, DeviceFeature as bt, DEFAULT_CLUSTER_STEP_MODELS as c, storageEvictableCapability as ct, DEFAULT_NATIVE_LEASE_SETTINGS as d, webrtcSessionCapability as dt, pipelineRunnerCapability as et, DEVICE_BACKEND_TO_FORMAT as f, errMsg as ft, HF_BASE_URL as g, invocationFromEncodeProfile as gt, ExportRecordSchema as h, buildFfmpegArgs as ht, BatteryStatusSchema as i, resolveEgressDecodeHwAccel as it, batteryCapability as j, sleep as jt, audioAnalyzerCapability as k, parseProfileBrokerId as kt, DEFAULT_DETAIL_CROP_CONVENTION as l, streamBrokerCapability as lt, EncodeProfileSchema as m, Fmp4BoxSplitter as mt, AUDIO_BACKEND_CHOICES as n, recordingExportCapability as nt, COCO_TO_MACRO as o, resolveRecordingProfiles as ot, EVENT_PAD_MS as p, AUDIO_PRESETS as pt, maskUrlCredentials as q, AUDIO_MACRO_LABELS as r, resolveClusterStepModelId as rt, DEFAULT_AUDIO_ANALYZER_CONFIG as s, runtimeDevices as st, APPLE_SA_TO_MACRO as t, recordingCapability as tt, DEFAULT_EVENTS_BAND_BUFFER_SEC as u, supportedRuntimes as ut, NativeLeaseSettingsSchema as v, BaseAddon as vt, RecordingConfigSchema as w, isEvent as wt, RATE_CONTROL_RELAXED as x, DeviceType as xt, OpsLogEntrySchema as y, CAM_PROFILE_ORDER as yt, deriveRecordingMode as z, object as zt };
@@ -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
@@ -21260,6 +21322,7 @@ DeviceType.Camera, method(object({
21260
21322
  isBattery: boolean(),
21261
21323
  reason: _enum([
21262
21324
  "disabled",
21325
+ "offline",
21263
21326
  "sleeping",
21264
21327
  "unreachable",
21265
21328
  "waking"
@@ -21305,9 +21368,17 @@ targets: array(object({
21305
21368
  sleeping: boolean(),
21306
21369
  /** Current device state rendered over the cached frame. State images
21307
21370
  * remain authoritative even when their photographic background is
21308
- * old; null means the link must carry a current camera frame. */
21371
+ * old; null means the link must carry a current camera frame.
21372
+ *
21373
+ * **This is the ONLY source a surface may derive an inactive tile
21374
+ * from.** A client that reads a device's `online`/`disabled` flags and
21375
+ * draws its own verdict will disagree with the pixels it is painting
21376
+ * — the frame comes from here, and so must the sentence over it
21377
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
21378
+ * state. */
21309
21379
  stateReason: _enum([
21310
21380
  "disabled",
21381
+ "offline",
21311
21382
  "sleeping",
21312
21383
  "unreachable",
21313
21384
  "waking"
@@ -36110,6 +36181,153 @@ TimelapseRuleInputSchema.extend({
36110
36181
  createdAt: number(),
36111
36182
  updatedAt: number()
36112
36183
  });
36184
+ /**
36185
+ * A model choice has a SCOPE, and a scope change has a workflow.
36186
+ *
36187
+ * ## The two scopes
36188
+ *
36189
+ * Almost every pipeline step's model is a **node** question: which build runs
36190
+ * fastest on THIS accelerator. Two nodes may legitimately answer differently —
36191
+ * a Coral runs the tflite SSD, a Mac runs the coreml YOLO — because the answers
36192
+ * are compared against nothing. They produce boxes, and a box is a box.
36193
+ *
36194
+ * Two steps are not like that. `face-embedding` and `clip-embedding` produce
36195
+ * **vectors that land in ONE shared index** (`identity_samples`,
36196
+ * `vec_object_clip`) and are ranked against each other by cosine similarity.
36197
+ * Cosine similarity between two encoders is not a weak signal, it is a
36198
+ * meaningless number — and nothing inside a vector can tell you it happened.
36199
+ * So for those two steps the model is a **cluster** question with exactly one
36200
+ * answer, stored in exactly one row.
36201
+ *
36202
+ * That is the same argument [D52](../../../../docs/decisions/adr-0052.md) makes
36203
+ * for the crop convention (`detail-crop.ts`), one level up: the crop decides
36204
+ * WHAT pixels the encoder sees, this decides WHICH encoder sees them. Both are
36205
+ * cluster-wide for the same reason and neither may become per-node.
36206
+ *
36207
+ * ## Why the registry lives here and not in the step catalog
36208
+ *
36209
+ * The scope itself is declared on the step, in
36210
+ * `detection-pipeline/registry/step-definitions.ts` (`StepDefinition.modelScope`)
36211
+ * — that is where a new step declares what kind of choice its model is. But the
36212
+ * catalog lives inside `addon-pipeline`, and three packages that must NOT
36213
+ * import an addon need to know the set: the orchestrator (which renders and
36214
+ * stores the row), post-analysis (which pins the row for a re-embed pass) and
36215
+ * admin-ui (which mirrors it). So the set is mirrored here, and
36216
+ * `scripts/check-cluster-model-scope.ts` fails the build when the two disagree.
36217
+ * A hand-copied key that stops matching is exactly how a setting silently stops
36218
+ * arriving while both sides still look correct.
36219
+ *
36220
+ * ## The defaults ARE today's behaviour
36221
+ *
36222
+ * {@link DEFAULT_CLUSTER_STEP_MODELS} reproduces the catalog `defaultModelId`
36223
+ * of both steps. Declaring the scope therefore changes nothing on the day it
36224
+ * ships: the cluster row starts out saying what every node was already doing.
36225
+ * Changing it is legitimate — that is what the knob is for — but it invalidates
36226
+ * the stored vectors of that surface, and must be followed by a re-embed pass
36227
+ * (`face-reembed-pass.ts`, `embedding-rebuild.ts`).
36228
+ */
36229
+ /**
36230
+ * The cluster-scoped steps. Mirrored from the catalog's `modelScope: 'cluster'`
36231
+ * declarations and guarded by `scripts/check-cluster-model-scope.ts`.
36232
+ */
36233
+ var CLUSTER_MODEL_SCOPED_STEPS = [{
36234
+ stepId: "face-embedding",
36235
+ label: "Face recognition model",
36236
+ defaultModelId: "arcface-r100",
36237
+ indexName: "the enrolled face gallery",
36238
+ options: [
36239
+ {
36240
+ id: "arcface-r100",
36241
+ label: "ArcFace (ResNet34)"
36242
+ },
36243
+ {
36244
+ id: "auraface-r100",
36245
+ label: "AuraFace R100"
36246
+ },
36247
+ {
36248
+ id: "inception-resnet-v1",
36249
+ label: "Inception ResNet V1"
36250
+ }
36251
+ ]
36252
+ }, {
36253
+ stepId: "clip-embedding",
36254
+ label: "Semantic search model",
36255
+ defaultModelId: "mobileclip-s1",
36256
+ indexName: "the object semantic-search index",
36257
+ options: [{
36258
+ id: "mobileclip-s1",
36259
+ label: "MobileCLIP S1"
36260
+ }, {
36261
+ id: "mobileclip-s2",
36262
+ label: "MobileCLIP S2"
36263
+ }]
36264
+ }];
36265
+ /**
36266
+ * The settings key holding one step's cluster model choice.
36267
+ *
36268
+ * Step-qualified so the keys stay unique across the addon's whole schema, which
36269
+ * is what lets {@link pickClusterStepModels} walk every section instead of
36270
+ * trusting the section id.
36271
+ */
36272
+ function clusterModelSettingKey(stepId) {
36273
+ return `clusterModel:${stepId}`;
36274
+ }
36275
+ /** The cluster row when nobody has configured one — today's catalog defaults. */
36276
+ var DEFAULT_CLUSTER_STEP_MODELS = Object.freeze(Object.fromEntries(CLUSTER_MODEL_SCOPED_STEPS.map((s) => [s.stepId, s.defaultModelId])));
36277
+ /** A stored choice is a non-empty model id, or it is not a choice. */
36278
+ var ChosenModelIdSchema = string().min(1);
36279
+ /**
36280
+ * Narrow a FLAT settings record to the cluster row.
36281
+ *
36282
+ * Per-FIELD fallback, deliberately (same rule as `readDetailCropConvention`): a
36283
+ * junk face model must not also discard a valid clip model. An absent, empty or
36284
+ * non-string value resolves to the step's catalog default — the historical
36285
+ * behaviour — never to a blank id, because a blank id downstream becomes
36286
+ * "substitute the format default", which is precisely the substitution this
36287
+ * scope exists to forbid.
36288
+ */
36289
+ function readClusterStepModels(config) {
36290
+ const out = {};
36291
+ for (const step of CLUSTER_MODEL_SCOPED_STEPS) {
36292
+ const parsed = ChosenModelIdSchema.safeParse(config[clusterModelSettingKey(step.stepId)]);
36293
+ out[step.stepId] = parsed.success ? parsed.data : step.defaultModelId;
36294
+ }
36295
+ return out;
36296
+ }
36297
+ function isHydratedField$2(entry) {
36298
+ return typeof entry === "object" && entry !== null && "key" in entry;
36299
+ }
36300
+ /**
36301
+ * Extract the cluster row from an `addon-settings.getGlobalSettings` payload.
36302
+ *
36303
+ * Walks EVERY section rather than looking inside {@link CLUSTER_MODEL_SECTION_ID}
36304
+ * alone: the keys are unique across the addon's schema, and a section rename
36305
+ * must not silently revert the whole cluster to the defaults. A `null` payload
36306
+ * (addon mid-boot) is the defaults.
36307
+ */
36308
+ function pickClusterStepModels(view) {
36309
+ if (view === null) return DEFAULT_CLUSTER_STEP_MODELS;
36310
+ const flat = {};
36311
+ const wanted = new Set(CLUSTER_MODEL_SCOPED_STEPS.map((s) => clusterModelSettingKey(s.stepId)));
36312
+ for (const section of view.sections) for (const entry of section.fields) {
36313
+ if (!isHydratedField$2(entry) || typeof entry.key !== "string") continue;
36314
+ if (wanted.has(entry.key)) flat[entry.key] = entry.value;
36315
+ }
36316
+ return readClusterStepModels(flat);
36317
+ }
36318
+ /**
36319
+ * The cluster model for `stepId`, or `null` when the step is node-scoped.
36320
+ *
36321
+ * `null` is the signal to LEAVE THE CALLER'S RESOLUTION ALONE — it is not "no
36322
+ * model". Returning a default here for a node-scoped step would quietly
36323
+ * override every per-(node,device) selection in the fleet.
36324
+ */
36325
+ function resolveClusterStepModelId(stepId, models) {
36326
+ const step = CLUSTER_MODEL_SCOPED_STEPS.find((s) => s.stepId === stepId);
36327
+ if (step === void 0) return null;
36328
+ const chosen = models[stepId];
36329
+ return chosen !== void 0 && chosen !== "" ? chosen : step.defaultModelId;
36330
+ }
36113
36331
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
36114
36332
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
36115
36333
  /**
@@ -36181,6 +36399,12 @@ function pickDetailCropConvention(view) {
36181
36399
  }
36182
36400
  return readDetailCropConvention(flat);
36183
36401
  }
36402
+ Object.freeze({
36403
+ x: 0,
36404
+ y: 0,
36405
+ w: 1,
36406
+ h: 1
36407
+ });
36184
36408
  DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
36185
36409
  /**
36186
36410
  * Derive the crop rectangle for one parent detection.
@@ -37223,6 +37447,12 @@ Object.defineProperty(exports, "DEFAULT_AUDIO_ANALYZER_CONFIG", {
37223
37447
  return DEFAULT_AUDIO_ANALYZER_CONFIG;
37224
37448
  }
37225
37449
  });
37450
+ Object.defineProperty(exports, "DEFAULT_CLUSTER_STEP_MODELS", {
37451
+ enumerable: true,
37452
+ get: function() {
37453
+ return DEFAULT_CLUSTER_STEP_MODELS;
37454
+ }
37455
+ });
37226
37456
  Object.defineProperty(exports, "DEFAULT_DETAIL_CROP_CONVENTION", {
37227
37457
  enumerable: true,
37228
37458
  get: function() {
@@ -37625,6 +37855,12 @@ Object.defineProperty(exports, "parseProfileBrokerId", {
37625
37855
  return parseProfileBrokerId;
37626
37856
  }
37627
37857
  });
37858
+ Object.defineProperty(exports, "pickClusterStepModels", {
37859
+ enumerable: true,
37860
+ get: function() {
37861
+ return pickClusterStepModels;
37862
+ }
37863
+ });
37628
37864
  Object.defineProperty(exports, "pickDetailCropConvention", {
37629
37865
  enumerable: true,
37630
37866
  get: function() {
@@ -37667,6 +37903,12 @@ Object.defineProperty(exports, "recordingExportCapability", {
37667
37903
  return recordingExportCapability;
37668
37904
  }
37669
37905
  });
37906
+ Object.defineProperty(exports, "resolveClusterStepModelId", {
37907
+ enumerable: true,
37908
+ get: function() {
37909
+ return resolveClusterStepModelId;
37910
+ }
37911
+ });
37670
37912
  Object.defineProperty(exports, "resolveEgressDecodeHwAccel", {
37671
37913
  enumerable: true,
37672
37914
  get: function() {