@camstack/addon-post-analysis 1.2.50 → 1.2.52

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.
@@ -9178,6 +9178,69 @@ var StreamFormatSchema = _enum([
9178
9178
  "mjpeg",
9179
9179
  "rtsp"
9180
9180
  ]);
9181
+ /** A container `produceEventMedia` can emit. */
9182
+ var EventMediaKindSchema = _enum(["mp4", "gif"]);
9183
+ /**
9184
+ * One produced artifact, referenced by HANDLE.
9185
+ *
9186
+ * Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
9187
+ * method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
9188
+ * [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
9189
+ * on demand, compressed, by handle). `bytes` is here so a caller can decide
9190
+ * whether it wants the fetch at all.
9191
+ */
9192
+ var EventMediaArtifactSchema = object({
9193
+ kind: EventMediaKindSchema,
9194
+ /** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
9195
+ handle: string(),
9196
+ /**
9197
+ * The node holding the bytes — the ROUTING key for `fetchEventMedia`.
9198
+ *
9199
+ * `stream-broker` is a singleton cap and an unpinned call never leaves the
9200
+ * hub, so a handle produced on an agent's broker would be redeemed against
9201
+ * the hub's store and come back `null`. Same contract, same field name and
9202
+ * the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
9203
+ * lives and the consumer pins to it.
9204
+ */
9205
+ nodeId: string(),
9206
+ mime: string(),
9207
+ bytes: number().int(),
9208
+ width: number().int(),
9209
+ height: number().int()
9210
+ });
9211
+ /**
9212
+ * What a production actually covered — the answer to the only question an
9213
+ * operator asks about a notification clip.
9214
+ *
9215
+ * `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
9216
+ * so a caller can state "this clip starts 4.1 s before the event" instead of
9217
+ * inferring it from a duration. A production whose `fromTs` is later than the
9218
+ * event is a production with no pre-roll, and that is exactly the defect this
9219
+ * method exists to make visible rather than plausible.
9220
+ */
9221
+ var EventMediaCoverageSchema = object({
9222
+ fromTs: number(),
9223
+ toTs: number(),
9224
+ /** Encoded packets in the muxed window. */
9225
+ packets: number().int()
9226
+ });
9227
+ /**
9228
+ * The result of ONE cut, in every container the caller asked for.
9229
+ *
9230
+ * Every artifact in `media` came out of the SAME window of the SAME rendition —
9231
+ * that is the whole reason this is one method rather than one call per format.
9232
+ * A consumer attaching a gif and a video can no longer show two different
9233
+ * moments, because it never chose two sources.
9234
+ */
9235
+ var EventMediaProductionSchema = object({
9236
+ media: array(EventMediaArtifactSchema).readonly(),
9237
+ coverage: EventMediaCoverageSchema,
9238
+ /** The rendition actually cut from — what the default or the fallback chose. */
9239
+ profile: CamProfileSchema,
9240
+ /** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
9241
+ * source, a downscale, or a playback rate other than 1). */
9242
+ video: _enum(["copy", "encode"])
9243
+ });
9181
9244
  var RtspRestreamEntrySchema = object({
9182
9245
  brokerId: string(),
9183
9246
  url: string(),
@@ -9577,6 +9640,34 @@ method(object({
9577
9640
  }), {
9578
9641
  kind: "mutation",
9579
9642
  auth: "admin"
9643
+ }), method(object({
9644
+ deviceId: number(),
9645
+ /** Absent = the largest H.264 rendition at or below 1080p, which is
9646
+ * also the one that can be copied. Falls back to whatever the ring
9647
+ * actually retained, and the answer says which. */
9648
+ profile: CamProfileSchema.optional(),
9649
+ aroundMs: number(),
9650
+ preSeconds: number().min(0).max(20).default(4),
9651
+ postSeconds: number().min(0).max(20).default(6),
9652
+ kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
9653
+ /** GIF geometry. The video keeps the source's own. */
9654
+ gifMaxWidth: number().int().min(120).max(1280).default(640),
9655
+ gifFps: number().int().min(1).max(15).default(8),
9656
+ /**
9657
+ * Playback rate, applied to EVERY container so they stay one clip.
9658
+ * `1` is real time and is what allows the copy branch.
9659
+ */
9660
+ speed: number().min(1).max(8).default(1)
9661
+ }), EventMediaProductionSchema, {
9662
+ kind: "mutation",
9663
+ auth: "admin"
9664
+ }), method(object({ handle: string() }), object({
9665
+ base64: string(),
9666
+ mime: string(),
9667
+ bytes: number().int()
9668
+ }).nullable(), {
9669
+ kind: "mutation",
9670
+ auth: "admin"
9580
9671
  }), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
9581
9672
  probed: boolean(),
9582
9673
  summary: string()
@@ -15701,6 +15792,60 @@ var TrackFlagsSchema = object({
15701
15792
  * `trained` without a re-fetch. */
15702
15793
  retrainStatus: RetrainStatusSchema
15703
15794
  });
15795
+ union([literal(1), literal(2)]);
15796
+ /**
15797
+ * WHO decided a label, and when. Carried per tier so a value can be traced to
15798
+ * the step and model that produced it — which is what makes the write rule
15799
+ * arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
15800
+ * and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
15801
+ *
15802
+ * `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
15803
+ * `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
15804
+ * `migration:4g` for a value the 4g migration moved from the single-slot era —
15805
+ * that value has no provenance, and the write rule lets ANY properly-attributed
15806
+ * write of the same tier replace it regardless of score.
15807
+ */
15808
+ var LabelAttributionSchema = object({
15809
+ stepId: string(),
15810
+ modelId: string().optional(),
15811
+ decidedAt: number()
15812
+ });
15813
+ /**
15814
+ * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
15815
+ * `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
15816
+ * track and its events always answer the same question the same way.
15817
+ *
15818
+ * Two scalar columns, not an array: every consumer wants "the coarse one" or
15819
+ * "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
15820
+ * is tier 2, and each carries its own score + attribution.
15821
+ *
15822
+ * **Reading it.** What a human should be shown is `subLabel ?? label` — the
15823
+ * finest thing known. Before 4g the single `label` column held the finest
15824
+ * value, so a consumer that has not been updated reads the tier-1 slot and
15825
+ * shows nothing on a species-only row; that is why the migration puts every
15826
+ * pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
15827
+ * and why the read surfaces were changed in the same train.
15828
+ *
15829
+ * **Writing it.** The slots are independent, which is the whole point: a
15830
+ * tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
15831
+ * migratorius`), so fineness cannot regress by construction. Within a tier the
15832
+ * higher score wins. One rule, one implementation — see
15833
+ * `pipeline/label-tier.ts` in addon-post-analysis.
15834
+ */
15835
+ var TieredLabelFields = {
15836
+ /** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
15837
+ label: string().optional(),
15838
+ /** Confidence of the tier-1 value, as reported by the deciding step. */
15839
+ labelScore: number().optional(),
15840
+ /** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
15841
+ labelMeta: LabelAttributionSchema.optional(),
15842
+ /** Tier 2 — the instance. See {@link LabelTierSchema}. */
15843
+ subLabel: string().optional(),
15844
+ /** Confidence of the tier-2 value, as reported by the deciding step. */
15845
+ subLabelScore: number().optional(),
15846
+ /** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
15847
+ subLabelMeta: LabelAttributionSchema.optional()
15848
+ };
15704
15849
  /** Per-camera slice of a training-export estimate. */
15705
15850
  var TrainingExportDeviceTotalsSchema = object({
15706
15851
  deviceId: number(),
@@ -15725,7 +15870,7 @@ var TrackSchema = object({
15725
15870
  trackId: string(),
15726
15871
  deviceId: number(),
15727
15872
  className: string(),
15728
- label: string().optional(),
15873
+ ...TieredLabelFields,
15729
15874
  producingDeviceName: string().optional(),
15730
15875
  /** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
15731
15876
  source: TrackSourceSchema.optional(),
@@ -15838,7 +15983,7 @@ var ObjectEventSchema = object({
15838
15983
  /** Omitted in slim projection. */
15839
15984
  trackId: string().optional(),
15840
15985
  className: string(),
15841
- label: string().optional(),
15986
+ ...TieredLabelFields,
15842
15987
  /** Omitted in slim projection. */
15843
15988
  confidence: number().optional(),
15844
15989
  /** Heavy JSON — omitted in slim projection. */
@@ -15919,6 +16064,173 @@ var MediaFileSchema = object({
15919
16064
  * stored blob and a `?variant=thumb` rendering without fetching either.
15920
16065
  */
15921
16066
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
16067
+ /**
16068
+ * The MACRO tier of an annotation — a CLOSED set.
16069
+ *
16070
+ * This is what the exported detector predicts, so a typo here is a new class
16071
+ * with one example in it. `label` and `subLabel` are open strings by contrast:
16072
+ * the whole point of the page is teaching the model things it does not know
16073
+ * yet, and constraining that vocabulary would make it useless.
16074
+ *
16075
+ * A macro class is NEVER a label. The provider refuses a write whose `label` or
16076
+ * `subLabel` is one of these values, in any casing, because once `person`
16077
+ * exists in both tiers "every person box" stops being answerable without
16078
+ * knowing every string anyone ever typed — and the damage is retroactive.
16079
+ */
16080
+ var RetrainMacroClassSchema = _enum([
16081
+ "person",
16082
+ "vehicle",
16083
+ "animal",
16084
+ "package",
16085
+ "face",
16086
+ "plate"
16087
+ ]);
16088
+ /** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
16089
+ var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
16090
+ /** Did a human draw this box, or did the assist propose it? */
16091
+ var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
16092
+ /** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
16093
+ var RetrainBboxSchema = object({
16094
+ x: number(),
16095
+ y: number(),
16096
+ w: number(),
16097
+ h: number()
16098
+ });
16099
+ /**
16100
+ * One annotated subject.
16101
+ *
16102
+ * `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
16103
+ * (letterboxed root / zone-cropped package / subject-cropped classifier) are
16104
+ * derived from it at export and never stored — storing them is how one feature
16105
+ * space ends up holding two crops of the same subject (D52).
16106
+ */
16107
+ var RetrainAnnotationSchema = object({
16108
+ id: string(),
16109
+ trackId: string(),
16110
+ deviceId: number(),
16111
+ /** The COPY in retrain storage — never the source track's media key. */
16112
+ mediaKey: string(),
16113
+ bbox: RetrainBboxSchema,
16114
+ macroClass: RetrainMacroClassSchema,
16115
+ label: string().optional(),
16116
+ subLabel: string().optional(),
16117
+ kind: RetrainAnnotationKindSchema,
16118
+ source: RetrainAnnotationSourceSchema,
16119
+ /** Which model proposed this box — or, on a `model_error`, drew the phantom. */
16120
+ assistModelId: string().optional(),
16121
+ assistScore: number().optional(),
16122
+ exportedInBatch: string().optional(),
16123
+ createdAt: number()
16124
+ });
16125
+ /** The write form — the server owns `id`, `createdAt` and the frame binding. */
16126
+ var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
16127
+ id: true,
16128
+ trackId: true,
16129
+ deviceId: true,
16130
+ mediaKey: true,
16131
+ createdAt: true,
16132
+ exportedInBatch: true
16133
+ });
16134
+ /** A track sitting in `staging`, with everything the worklist needs to rank it. */
16135
+ var RetrainTrackSchema = object({
16136
+ trackId: string(),
16137
+ deviceId: number(),
16138
+ className: string(),
16139
+ label: string().optional(),
16140
+ firstSeen: number(),
16141
+ lastSeen: number(),
16142
+ /** How many frames the dataset already holds from this track. */
16143
+ frameCount: number().int(),
16144
+ /** How many subjects have been annotated on those frames. `0` with
16145
+ * `frameCount: 0` is exactly "staging, still to work". */
16146
+ annotationCount: number().int()
16147
+ });
16148
+ /** A frame the picker may offer — an index row, no blob was read to produce it. */
16149
+ var RetrainFrameCandidateSchema = object({
16150
+ mediaKey: string(),
16151
+ kind: MediaFileKindEnum,
16152
+ timestamp: number(),
16153
+ sizeBytes: number().int(),
16154
+ /** A copy of this original already exists — selecting it is free and cannot
16155
+ * fail, whatever became of the original. */
16156
+ copied: boolean()
16157
+ });
16158
+ /** A frame the dataset OWNS: bytes copied at selection time. */
16159
+ var RetrainFrameSchema = object({
16160
+ frameId: string(),
16161
+ deviceId: number(),
16162
+ trackId: string(),
16163
+ /** Provenance only. It may already point at nothing — that is expected. */
16164
+ sourceMediaKey: string(),
16165
+ sourceKind: MediaFileKindEnum,
16166
+ sizeBytes: number().int(),
16167
+ width: number().int(),
16168
+ height: number().int(),
16169
+ copiedAt: number()
16170
+ });
16171
+ /** Why a copy-on-select could not be honoured — named, never a silent skip. */
16172
+ var RetrainCopyRefusalSchema = _enum([
16173
+ "source-missing",
16174
+ "unreadable-image",
16175
+ "write-failed"
16176
+ ]);
16177
+ var RetrainFrameSelectionSchema = object({
16178
+ copied: array(RetrainFrameSchema).readonly(),
16179
+ refused: array(object({
16180
+ sourceMediaKey: string(),
16181
+ reason: RetrainCopyRefusalSchema
16182
+ })).readonly()
16183
+ });
16184
+ var RetrainFrameListSchema = object({
16185
+ candidates: array(RetrainFrameCandidateSchema).readonly(),
16186
+ copies: array(RetrainFrameSchema).readonly(),
16187
+ /** What the page pre-selects — the native key frame when one survives. */
16188
+ autoPickMediaKey: string().optional()
16189
+ });
16190
+ /** What the operator asked the assist to look for. */
16191
+ var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
16192
+ kind: literal("package"),
16193
+ zone: RetrainBboxSchema.optional()
16194
+ }), object({
16195
+ kind: literal("objects"),
16196
+ modelId: string(),
16197
+ minScore: number().optional()
16198
+ })]);
16199
+ /**
16200
+ * The assist's answer — a discriminated union, because "the model saw nothing"
16201
+ * and "this node cannot run that model" lead to different next moves and a
16202
+ * nullable result cannot tell them apart.
16203
+ */
16204
+ var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
16205
+ kind: literal("proposed"),
16206
+ modelId: string(),
16207
+ stepId: string(),
16208
+ minScore: number(),
16209
+ /** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
16210
+ proposals: array(RetrainAnnotationDraftSchema).readonly(),
16211
+ /** Returned by the runner but removed by the threshold. */
16212
+ belowThreshold: number().int()
16213
+ }), object({
16214
+ kind: literal("refused"),
16215
+ /** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
16216
+ reason: string(),
16217
+ detail: string().optional()
16218
+ })]);
16219
+ /** The outcome of a lifecycle move owned by the retrain page. */
16220
+ var RetrainTransitionResultSchema = object({
16221
+ trackId: string(),
16222
+ /** Where the track ended up, whatever happened. */
16223
+ retrainStatus: RetrainStatusSchema,
16224
+ /** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
16225
+ changed: boolean(),
16226
+ reason: _enum([
16227
+ "unknown-track",
16228
+ "no-frames-copied",
16229
+ "not-staging",
16230
+ "not-trained",
16231
+ "unchanged"
16232
+ ]).optional()
16233
+ });
15922
16234
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
15923
16235
  var MAX_EVENT_QUERY_LIMIT = 5e3;
15924
16236
  var DeviceEventQueryInput = object({
@@ -15973,7 +16285,7 @@ var KeyEventSchema = object({
15973
16285
  /** Track start time (firstSeen). */
15974
16286
  timestamp: number(),
15975
16287
  className: string(),
15976
- label: string().optional(),
16288
+ ...TieredLabelFields,
15977
16289
  importance: number(),
15978
16290
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15979
16291
  bestEventId: string(),
@@ -16464,6 +16776,201 @@ var pipelineAnalyticsCapability = {
16464
16776
  kind: "query",
16465
16777
  auth: "admin"
16466
16778
  }),
16779
+ /**
16780
+ * The staging worklist for one camera, or for every camera that has one.
16781
+ *
16782
+ * Fetched ON DEMAND, over the staging set only — the page never scans
16783
+ * history, because making the working set small is the entire purpose of
16784
+ * the mark. Each row carries how many frames the dataset already holds from
16785
+ * the track and how many subjects were annotated on them, so
16786
+ * `frameCount: 0` reads as "still to work" without a second call per track.
16787
+ *
16788
+ * `auth: 'admin'`, unlike the viewer-level mark itself: marking a track is
16789
+ * curation you do while looking at it, but building the training set the
16790
+ * fleet's models are fine-tuned on is not.
16791
+ */
16792
+ listRetrainStaging: method(object({
16793
+ /** Empty ⇒ every camera that has staging tracks. A LIST, not a single
16794
+ * `deviceId`, deliberately: `deviceId` would make this device-bound and
16795
+ * route it at one camera's owner, and "every camera" would stop being
16796
+ * expressible at all. */
16797
+ deviceIds: array(number()).optional(),
16798
+ limit: number().int().min(1).max(500).optional()
16799
+ }), array(RetrainTrackSchema).readonly(), {
16800
+ kind: "query",
16801
+ auth: "admin"
16802
+ }),
16803
+ /**
16804
+ * What a track can contribute, and what it already has.
16805
+ *
16806
+ * `candidates` are the track's whole, unannotated frames — index rows only,
16807
+ * so this is cheap. `copies` are the frames already inside the dataset, and
16808
+ * a candidate whose copy exists is marked `copied: true`: selecting it again
16809
+ * is free and CANNOT fail, whatever became of the original.
16810
+ *
16811
+ * A crop, a thumbnail and `fullFrameBoxed` are never candidates. The last
16812
+ * one matters most: it has the model's own rectangle burned into the pixels,
16813
+ * and a detector trained on it learns to find a green line.
16814
+ */
16815
+ listRetrainFrames: method(object({ trackId: string() }), RetrainFrameListSchema, {
16816
+ kind: "query",
16817
+ auth: "admin"
16818
+ }),
16819
+ /**
16820
+ * COPY-ON-SELECT — the write that makes `trained` safe to evict.
16821
+ *
16822
+ * Selecting a frame copies its bytes into retrain storage immediately: not
16823
+ * a reference, not a lease. Once the copy exists the dataset no longer
16824
+ * depends on the track's media, which is exactly what lets D81 hand a
16825
+ * `trained` track back to retention.
16826
+ *
16827
+ * The order inside is load-bearing and is pinned by a test: an EXISTING
16828
+ * copy is returned without touching the source, so an original that
16829
+ * evaporated blocks the selection of THAT ORIGINAL and never the copy
16830
+ * already taken. Every refusal comes back named — a dropped selection is
16831
+ * never silent, on the wire or in the log.
16832
+ */
16833
+ selectRetrainFrames: method(object({
16834
+ deviceId: number(),
16835
+ trackId: string(),
16836
+ mediaKeys: array(string()).min(1)
16837
+ }), RetrainFrameSelectionSchema, {
16838
+ kind: "mutation",
16839
+ auth: "admin"
16840
+ }),
16841
+ /** Un-select a frame: its annotations go first, then the copy and its blob.
16842
+ * Deliberately destructive and deliberately explicit — it is the only way
16843
+ * a frame leaves the dataset before export. */
16844
+ deselectRetrainFrame: method(object({
16845
+ deviceId: number(),
16846
+ trackId: string(),
16847
+ frameId: string()
16848
+ }), object({
16849
+ removed: boolean(),
16850
+ removedAnnotations: number().int()
16851
+ }), {
16852
+ kind: "mutation",
16853
+ auth: "admin"
16854
+ }),
16855
+ /**
16856
+ * The pixels of ONE copied frame, base64.
16857
+ *
16858
+ * Through the cap rather than a data plane because it is genuinely one
16859
+ * frame at a time, on demand, at human speed — the shape D9/D18 permit
16860
+ * (what they forbid is frames crossing a boundary at frame RATE). The
16861
+ * annotation canvas needs the image and its exact dimensions in the same
16862
+ * answer: a canvas that places a normalised box against a size it guessed
16863
+ * draws every box in the wrong place.
16864
+ */
16865
+ getRetrainFrameImage: method(object({ frameId: string() }), object({
16866
+ base64: string(),
16867
+ width: number().int(),
16868
+ height: number().int()
16869
+ }), {
16870
+ kind: "query",
16871
+ auth: "admin"
16872
+ }),
16873
+ /**
16874
+ * Ask the pipeline what it sees, as a PROPOSAL.
16875
+ *
16876
+ * Runs through `pipelineRunner.runStatelessStep` on the COPIED frame, and
16877
+ * every box comes back as a draft with `source: 'assist'` plus the model and
16878
+ * score that produced it. The operator confirms, edits, adds and deletes;
16879
+ * nothing is stored until `saveRetrainAnnotations`.
16880
+ *
16881
+ * For packages the request is `rfdetr-package` on the ZONE CROP at 0.35 —
16882
+ * never the whole frame, where a package detector at that threshold proposes
16883
+ * furniture. A package request with no zone is REFUSED rather than widened,
16884
+ * because the silent widening would look like a bad model for as long as
16885
+ * nobody checked which rectangle it ran on.
16886
+ */
16887
+ proposeRetrainAnnotations: method(object({
16888
+ deviceId: number(),
16889
+ trackId: string(),
16890
+ frameId: string(),
16891
+ subject: RetrainAssistSubjectSchema,
16892
+ /** Which node runs it. Absent ⇒ wherever an unowned call lands. */
16893
+ nodeId: string().optional()
16894
+ }), RetrainAssistResultSchema, {
16895
+ kind: "mutation",
16896
+ auth: "admin"
16897
+ }),
16898
+ /** Every annotation on a track, oldest first. */
16899
+ listRetrainAnnotations: method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
16900
+ kind: "query",
16901
+ auth: "admin"
16902
+ }),
16903
+ /**
16904
+ * Replace EVERY annotation on one frame with the supplied set.
16905
+ *
16906
+ * Whole-frame replacement, not per-box upsert: the unit of ground truth is
16907
+ * the frame, and "the operator deleted a box" must be the same durable
16908
+ * outcome as "the operator never drew it". A per-box patch would let a frame
16909
+ * keep a box the operator removed on a surface that only knew about the
16910
+ * boxes it sent.
16911
+ *
16912
+ * Refuses a macro class typed into `label` or `subLabel` — the tiers are
16913
+ * separate and the guard is at the WRITE, because a mixed taxonomy cannot
16914
+ * be un-mixed by reading it.
16915
+ */
16916
+ saveRetrainAnnotations: method(object({
16917
+ deviceId: number(),
16918
+ trackId: string(),
16919
+ frameId: string(),
16920
+ annotations: array(RetrainAnnotationDraftSchema)
16921
+ }), array(RetrainAnnotationSchema).readonly(), {
16922
+ kind: "mutation",
16923
+ auth: "admin"
16924
+ }),
16925
+ /**
16926
+ * Finish with a track: `staging → trained`. **The only writer of that
16927
+ * state** — D81 shipped the column with it deliberately unreachable.
16928
+ *
16929
+ * Refuses a track the dataset holds no copies from. `trained` un-pins the
16930
+ * track's media, so completing without a copy is a delete order for material
16931
+ * nothing ever extracted anything from; that refusal IS the safety argument
16932
+ * of D81, expressed as a precondition.
16933
+ */
16934
+ completeRetrainTrack: method(object({
16935
+ deviceId: number(),
16936
+ trackId: string()
16937
+ }), RetrainTransitionResultSchema, {
16938
+ kind: "mutation",
16939
+ auth: "admin"
16940
+ }),
16941
+ /**
16942
+ * The deliberate return: `trained → staging`, for the rare case.
16943
+ *
16944
+ * The generic `setTrackFlags` toggle refuses this in both directions by
16945
+ * design (D81) — re-staging from a checkbox is how the same material gets
16946
+ * annotated twice under two ground truths. Doing it here means the operator
16947
+ * is looking at the annotations that already exist while they decide, and
16948
+ * those annotations are LEFT ALONE: "put this back" must not be a
16949
+ * destructive act wearing a navigational name.
16950
+ */
16951
+ restageRetrainTrack: method(object({
16952
+ deviceId: number(),
16953
+ trackId: string()
16954
+ }), RetrainTransitionResultSchema, {
16955
+ kind: "mutation",
16956
+ auth: "admin"
16957
+ }),
16958
+ /**
16959
+ * Where to download the ANNOTATED dataset.
16960
+ *
16961
+ * The sibling of `getTrainingExportUrl` and deliberately not the same
16962
+ * archive: that one streams a marked track's stored media verbatim, this one
16963
+ * streams the retrain COPIES plus an `annotations.json` carrying, for every
16964
+ * subject, the canonical full-frame box AND the geometry derived for each
16965
+ * model shape (letterboxed root / zone-cropped package / subject-cropped
16966
+ * classifier). Derived at export, never stored — one box in, three shapes
16967
+ * out, so two crops of the same subject can never end up in one feature
16968
+ * space (D52).
16969
+ */
16970
+ getRetrainExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
16971
+ kind: "query",
16972
+ auth: "admin"
16973
+ }),
16467
16974
  getEventMedia: method(object({
16468
16975
  eventId: string(),
16469
16976
  kind: MediaFileKindEnum.optional()
@@ -17128,6 +17635,22 @@ var DetailResultSchema = object({
17128
17635
  bbox: NativeCropBboxSchema.optional(),
17129
17636
  embedding: string().optional(),
17130
17637
  label: string().optional(),
17638
+ /**
17639
+ * The tier `label` occupies, copied VERBATIM from the producing step's
17640
+ * `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
17641
+ *
17642
+ * It rides the wire rather than being resolved by the consumer because the
17643
+ * declaration lives with the step definition, which only the executing node
17644
+ * has: post-analysis holds no step registry, and re-deriving the tier from
17645
+ * `className` there would be exactly the inference this model exists to
17646
+ * forbid. A `label` that arrives WITHOUT this field is refused by the write
17647
+ * rule and logged (`label tier undeclared`) — an older runner therefore
17648
+ * stops enriching rather than guessing, which is why addon-pipeline is
17649
+ * deployed BEFORE addon-post-analysis.
17650
+ */
17651
+ labelTier: union([literal(1), literal(2)]).optional(),
17652
+ /** Model that produced `label` — carried into the tier's attribution. */
17653
+ labelModelId: string().optional(),
17131
17654
  alignedCropJpeg: string().optional(),
17132
17655
  /** Face short side (px) measured on the NATIVE crop surface. The `bbox`
17133
17656
  * above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
@@ -30077,6 +30600,12 @@ Object.freeze({
30077
30600
  addonId: null,
30078
30601
  access: "delete"
30079
30602
  },
30603
+ "pipelineAnalytics.completeRetrainTrack": {
30604
+ capName: "pipeline-analytics",
30605
+ capScope: "device",
30606
+ addonId: null,
30607
+ access: "create"
30608
+ },
30080
30609
  "pipelineAnalytics.deleteDeviceEvents": {
30081
30610
  capName: "pipeline-analytics",
30082
30611
  capScope: "device",
@@ -30089,6 +30618,12 @@ Object.freeze({
30089
30618
  addonId: null,
30090
30619
  access: "delete"
30091
30620
  },
30621
+ "pipelineAnalytics.deselectRetrainFrame": {
30622
+ capName: "pipeline-analytics",
30623
+ capScope: "device",
30624
+ addonId: null,
30625
+ access: "create"
30626
+ },
30092
30627
  "pipelineAnalytics.getActiveTracks": {
30093
30628
  capName: "pipeline-analytics",
30094
30629
  capScope: "device",
@@ -30149,6 +30684,18 @@ Object.freeze({
30149
30684
  addonId: null,
30150
30685
  access: "view"
30151
30686
  },
30687
+ "pipelineAnalytics.getRetrainExportUrl": {
30688
+ capName: "pipeline-analytics",
30689
+ capScope: "device",
30690
+ addonId: null,
30691
+ access: "view"
30692
+ },
30693
+ "pipelineAnalytics.getRetrainFrameImage": {
30694
+ capName: "pipeline-analytics",
30695
+ capScope: "device",
30696
+ addonId: null,
30697
+ access: "view"
30698
+ },
30152
30699
  "pipelineAnalytics.getSensorEvents": {
30153
30700
  capName: "pipeline-analytics",
30154
30701
  capScope: "device",
@@ -30203,6 +30750,24 @@ Object.freeze({
30203
30750
  addonId: null,
30204
30751
  access: "view"
30205
30752
  },
30753
+ "pipelineAnalytics.listRetrainAnnotations": {
30754
+ capName: "pipeline-analytics",
30755
+ capScope: "device",
30756
+ addonId: null,
30757
+ access: "view"
30758
+ },
30759
+ "pipelineAnalytics.listRetrainFrames": {
30760
+ capName: "pipeline-analytics",
30761
+ capScope: "device",
30762
+ addonId: null,
30763
+ access: "view"
30764
+ },
30765
+ "pipelineAnalytics.listRetrainStaging": {
30766
+ capName: "pipeline-analytics",
30767
+ capScope: "device",
30768
+ addonId: null,
30769
+ access: "view"
30770
+ },
30206
30771
  "pipelineAnalytics.listTrackMedia": {
30207
30772
  capName: "pipeline-analytics",
30208
30773
  capScope: "device",
@@ -30215,6 +30780,12 @@ Object.freeze({
30215
30780
  addonId: null,
30216
30781
  access: "view"
30217
30782
  },
30783
+ "pipelineAnalytics.proposeRetrainAnnotations": {
30784
+ capName: "pipeline-analytics",
30785
+ capScope: "device",
30786
+ addonId: null,
30787
+ access: "create"
30788
+ },
30218
30789
  "pipelineAnalytics.pruneEvents": {
30219
30790
  capName: "pipeline-analytics",
30220
30791
  capScope: "device",
@@ -30245,12 +30816,30 @@ Object.freeze({
30245
30816
  addonId: null,
30246
30817
  access: "create"
30247
30818
  },
30819
+ "pipelineAnalytics.restageRetrainTrack": {
30820
+ capName: "pipeline-analytics",
30821
+ capScope: "device",
30822
+ addonId: null,
30823
+ access: "create"
30824
+ },
30825
+ "pipelineAnalytics.saveRetrainAnnotations": {
30826
+ capName: "pipeline-analytics",
30827
+ capScope: "device",
30828
+ addonId: null,
30829
+ access: "create"
30830
+ },
30248
30831
  "pipelineAnalytics.searchObjectEvents": {
30249
30832
  capName: "pipeline-analytics",
30250
30833
  capScope: "device",
30251
30834
  addonId: null,
30252
30835
  access: "view"
30253
30836
  },
30837
+ "pipelineAnalytics.selectRetrainFrames": {
30838
+ capName: "pipeline-analytics",
30839
+ capScope: "device",
30840
+ addonId: null,
30841
+ access: "create"
30842
+ },
30254
30843
  "pipelineAnalytics.setTrackFlags": {
30255
30844
  capName: "pipeline-analytics",
30256
30845
  capScope: "device",
@@ -31643,6 +32232,12 @@ Object.freeze({
31643
32232
  addonId: null,
31644
32233
  access: "create"
31645
32234
  },
32235
+ "streamBroker.fetchEventMedia": {
32236
+ capName: "stream-broker",
32237
+ capScope: "system",
32238
+ addonId: null,
32239
+ access: "create"
32240
+ },
31646
32241
  "streamBroker.getAllRtspEntries": {
31647
32242
  capName: "stream-broker",
31648
32243
  capScope: "system",
@@ -31727,6 +32322,12 @@ Object.freeze({
31727
32322
  addonId: null,
31728
32323
  access: "create"
31729
32324
  },
32325
+ "streamBroker.produceEventMedia": {
32326
+ capName: "stream-broker",
32327
+ capScope: "system",
32328
+ addonId: null,
32329
+ access: "create"
32330
+ },
31730
32331
  "streamBroker.publishCameraStream": {
31731
32332
  capName: "stream-broker",
31732
32333
  capScope: "system",
@@ -32778,6 +33379,12 @@ Object.defineProperty(exports, "EventCategory", {
32778
33379
  return EventCategory;
32779
33380
  }
32780
33381
  });
33382
+ Object.defineProperty(exports, "LabelAttributionSchema", {
33383
+ enumerable: true,
33384
+ get: function() {
33385
+ return LabelAttributionSchema;
33386
+ }
33387
+ });
32781
33388
  Object.defineProperty(exports, "MACRO_LABELS", {
32782
33389
  enumerable: true,
32783
33390
  get: function() {