@camstack/types 1.2.158 → 1.2.160

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.
@@ -29,17 +29,29 @@ export type RecordingWeekday = z.infer<typeof RecordingWeekdaySchema>;
29
29
  /**
30
30
  * DERIVED per-camera storage summary — the single field cheap consumers read
31
31
  * (the viewer's status dot, the camera list) instead of walking `bands`:
32
- * - `off` — no band covers the camera (or it is disabled).
33
- * - `events` — every band records around triggers only.
34
- * - `continuous` — at least one band records continuously.
32
+ * - `off` — no band covers the camera (or it is disabled).
33
+ * - `events` — every band records around triggers only.
34
+ * - `continuous` — at least one band records continuously.
35
+ * - `on-device-decision`— the DEVICE decides: recording runs for as long as the
36
+ * camera raises its own `recording-signal` level (a robot that cleans). There
37
+ * is no schedule to author, because there is no hour to program — see
38
+ * {@link RecordingConfigSchema}`.deviceDecision`.
35
39
  *
36
- * NEVER authored: the recorder stamps it from the authoritative `bands` on
37
- * every save (`activeModeForConfig`). Writing it has no effect.
40
+ * NEVER authored: the recorder stamps it from the authoritative intent
41
+ * (`bands` + `deviceDecision`) on every save (`activeModeForConfig`). Writing it
42
+ * has no effect.
43
+ *
44
+ * `on-device-decision` is named for WHO decides, not for how the recording is
45
+ * requested. `on-demand` was rejected: in this repo's vocabulary a "demand" is
46
+ * something the operator makes (the live gate is the recorder's own "demand
47
+ * window"), and a knob whose name suggests the operator starts it while the
48
+ * device actually does is the D62 shape — a control nobody can predict.
38
49
  */
39
50
  export declare const RecordingStorageModeSchema: z.ZodEnum<{
40
51
  off: "off";
41
52
  events: "events";
42
53
  continuous: "continuous";
54
+ "on-device-decision": "on-device-decision";
43
55
  }>;
44
56
  export type RecordingStorageMode = z.infer<typeof RecordingStorageModeSchema>;
45
57
  /**
@@ -72,7 +84,6 @@ export declare const RecordingTriggersSchema: z.ZodObject<{
72
84
  animal: "animal";
73
85
  }>>>;
74
86
  sensorDeviceIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
75
- deviceSignal: z.ZodOptional<z.ZodBoolean>;
76
87
  }, z.core.$strip>;
77
88
  export type RecordingTriggers = z.infer<typeof RecordingTriggersSchema>;
78
89
  /**
@@ -101,7 +112,6 @@ export declare const RecordingBandTriggersSchema: z.ZodObject<{
101
112
  animal: "animal";
102
113
  }>>>;
103
114
  sensorDeviceIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
104
- deviceSignal: z.ZodOptional<z.ZodBoolean>;
105
115
  }, z.core.$strip>;
106
116
  export type RecordingBandTriggers = z.infer<typeof RecordingBandTriggersSchema>;
107
117
  /**
@@ -129,7 +139,6 @@ export declare const RecordingBandSchema: z.ZodObject<{
129
139
  animal: "animal";
130
140
  }>>>;
131
141
  sensorDeviceIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
132
- deviceSignal: z.ZodOptional<z.ZodBoolean>;
133
142
  }, z.core.$strip>>;
134
143
  preBufferSec: z.ZodOptional<z.ZodNumber>;
135
144
  postBufferSec: z.ZodOptional<z.ZodNumber>;
@@ -149,30 +158,44 @@ export type RecordingBand = z.infer<typeof RecordingBandSchema>;
149
158
  * `postBufferSec` must therefore exceed one segment length plus the attach
150
159
  * latency (ffmpeg spawn + RTSP dial), or the window closes before the first
151
160
  * segment is ever finalized and the camera records nothing anyway. At the 10 s
152
- * default segment length, 30 s leaves room for two full segments. `preBufferSec`
153
- * is retroactive only — it costs nothing at record time, it merely keeps
154
- * already-written segments.
155
- *
156
- * ## Why `preBufferSec` is 15 and not one segment length
161
+ * default segment length, 30 s leaves room for two full segments.
157
162
  *
158
- * It has to cover the broker's RECORDING pre-roll, which is media handed to the
159
- * writer from BEFORE the trigger (D192/D193). If this bound were narrower, the
160
- * keep gate would delete the very seconds the ring just supplied — two
161
- * authorities disagreeing about the same footage, which is the failure D191 was
162
- * written against. It is therefore set to `RECORDING_PRE_ROLL_MAX_MS` (15 s),
163
- * the widest window the broker can be configured to serve, so an operator
164
- * raising the cluster pre-roll can never walk past the gate silently. The
165
- * addon-side guard is
166
- * `packages/addon-pipeline/src/recorder/addon/__tests__/band-decision.spec.ts`.
163
+ * ## There is no `preBufferSec` here any more (D381)
167
164
  *
168
- * An EXPLICIT `0` is left alone: that is an operator statement, not an omission,
169
- * and it is excluded from that invariant by name.
165
+ * There used to be, at 15 s, and its whole justification was that it had to
166
+ * cover the widest pre-roll the broker could serve. A number whose only
167
+ * correct value is another subsystem's ceiling is not a setting; it is that
168
+ * ceiling, restated where it can drift. The keep bound is derived from the
169
+ * prebuffer ceiling directly now, in `band-decision.ts`, so an operator
170
+ * raising the prebuffer cannot walk past the gate at all rather than merely
171
+ * being warned not to.
170
172
  */
171
173
  export declare const DEFAULT_EVENTS_BAND_BUFFER_SEC: {
172
- readonly preBufferSec: 15;
173
174
  readonly postBufferSec: 30;
174
175
  };
175
176
  export type DefaultEventsBandBufferSec = typeof DEFAULT_EVENTS_BAND_BUFFER_SEC;
177
+ /**
178
+ * Drop the RETIRED band `preBufferSec` from a persisted recording config.
179
+ *
180
+ * Explicit, and it reports what it removed, because the alternative is a
181
+ * silent strip: `RecordingBandSchema` is a plain `z.object`, so the key
182
+ * survives a parse and disappears on the next save with no line anywhere. An
183
+ * operator who had typed a number would find it gone and nothing would say
184
+ * when or why.
185
+ *
186
+ * Nothing is carried forward: the value it held was always required to be at
187
+ * least the broker's widest pre-roll, and the gate is now derived from exactly
188
+ * that ceiling. Every configuration that was legal therefore keeps a keep-gate
189
+ * at least as wide as the one it had — including an explicit `0`, which widens.
190
+ * That is deliberate and it is the only behaviour change: a band that kept
191
+ * nothing before its trigger now keeps up to the prebuffer ceiling of
192
+ * already-written footage, which is media that exists either way.
193
+ */
194
+ export declare function stripRetiredBandPreBufferSec(config: RecordingConfig): {
195
+ readonly config: RecordingConfig;
196
+ /** Index of every band a value was removed from. Empty = nothing to do. */
197
+ readonly strippedBands: readonly number[];
198
+ };
176
199
  /**
177
200
  * Adjacent recorded ranges closer than this belong to the SAME videoclip visit.
178
201
  *
@@ -223,6 +246,7 @@ export declare const RecordingConfigSchema: z.ZodObject<{
223
246
  off: "off";
224
247
  events: "events";
225
248
  continuous: "continuous";
249
+ "on-device-decision": "on-device-decision";
226
250
  }>>;
227
251
  profiles: z.ZodOptional<z.ZodArray<z.ZodEnum<{
228
252
  high: "high";
@@ -247,19 +271,19 @@ export declare const RecordingConfigSchema: z.ZodObject<{
247
271
  animal: "animal";
248
272
  }>>>;
249
273
  sensorDeviceIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
250
- deviceSignal: z.ZodOptional<z.ZodBoolean>;
251
274
  }, z.core.$strip>>;
252
275
  preBufferSec: z.ZodOptional<z.ZodNumber>;
253
276
  postBufferSec: z.ZodOptional<z.ZodNumber>;
254
277
  }, z.core.$strip>>>;
278
+ deviceDecision: z.ZodOptional<z.ZodBoolean>;
255
279
  retention: z.ZodOptional<z.ZodObject<{
256
280
  maxAgeDays: z.ZodOptional<z.ZodNumber>;
257
281
  maxSizeGb: z.ZodOptional<z.ZodNumber>;
258
282
  }, z.core.$strip>>;
259
283
  }, z.core.$strict>;
260
284
  export type RecordingConfig = z.infer<typeof RecordingConfigSchema>;
261
- /** The bands + enabled flag a mode is derived from — all {@link deriveRecordingMode} reads. */
262
- export type RecordingModeSource = Pick<RecordingConfig, 'enabled' | 'bands'>;
285
+ /** The authored intent a mode is derived from — all {@link deriveRecordingMode} reads. */
286
+ export type RecordingModeSource = Pick<RecordingConfig, 'enabled' | 'bands' | 'deviceDecision'>;
263
287
  /**
264
288
  * Derive the {@link RecordingStorageModeSchema} summary from the authoritative
265
289
  * bands: `continuous` when any band records continuously, `events` when a band
@@ -289,3 +313,36 @@ export declare const DEFAULT_RECORDING_PROFILES: readonly CamProfile[];
289
313
  * still records something). Empty assigned → empty result.
290
314
  */
291
315
  export declare function resolveRecordingProfiles(assigned: readonly CamProfile[], override: readonly CamProfile[] | undefined): CamProfile[];
316
+ /** What {@link migrateLegacyDeviceSignalConfig} did to one stored row. */
317
+ export interface LegacyDeviceSignalMigration {
318
+ /** The row to persist/parse. The SAME reference when nothing was legacy. */
319
+ readonly value: unknown;
320
+ /** The config now says `deviceDecision: true` — the mode was recovered. */
321
+ readonly migrated: boolean;
322
+ /**
323
+ * The legacy key was on a band that ALSO listened to something else (motion,
324
+ * a sensor …), so the key was dropped and the band kept. The camera keeps its
325
+ * other triggers and LOSES the device signal until an operator picks the new
326
+ * mode — the honest outcome: folding a mixed band into a mode would silently
327
+ * delete a schedule the operator authored.
328
+ */
329
+ readonly strippedOnly: boolean;
330
+ }
331
+ /**
332
+ * Turn a config stored under D371 (`bands[].triggers.deviceSignal: true`) into
333
+ * a D380 one (`deviceDecision: true`, no bands). Pure, total and IDEMPOTENT —
334
+ * a row with no legacy key comes back as the SAME reference, `migrated: false`.
335
+ *
336
+ * ## Why the schema cannot be left to do this
337
+ *
338
+ * `RecordingTriggersSchema` is a non-strict `z.object`, so the retired key is
339
+ * SILENTLY STRIPPED on read. The camera whose only trigger it was would parse
340
+ * cleanly into an `events` band that lists nothing — "enabled, and records
341
+ * nothing, forever", the exact shape `eventsBandCanEverDemand` exists to shout
342
+ * about, reached without a single line in the log. On the reference hub that is
343
+ * camera 4374, the one camera an operator had configured by hand.
344
+ *
345
+ * `enabled` is carried through untouched: a camera the operator had switched
346
+ * OFF comes out of a migration still off (D62).
347
+ */
348
+ export declare function migrateLegacyDeviceSignalConfig(raw: unknown): LegacyDeviceSignalMigration;
@@ -3859,7 +3859,7 @@ function createDeviceProxy(api, binding, opts) {
3859
3859
  * can answer "may a rule actuate this?" without importing the schema barrel
3860
3860
  * (~144MB RSS per runner, D28).
3861
3861
  *
3862
- * Coverage: 82 device-scoped capabilities.
3862
+ * Coverage: 83 device-scoped capabilities.
3863
3863
  */
3864
3864
  var DEVICE_SCOPED_CAPS = new Set([
3865
3865
  "accessories",
@@ -3924,6 +3924,7 @@ var DEVICE_SCOPED_CAPS = new Set([
3924
3924
  "ptz",
3925
3925
  "ptz-autotrack",
3926
3926
  "reboot",
3927
+ "recording-signal",
3927
3928
  "scene-monitor",
3928
3929
  "script-runner",
3929
3930
  "smoke",
@@ -3859,7 +3859,7 @@ function createDeviceProxy(api, binding, opts) {
3859
3859
  * can answer "may a rule actuate this?" without importing the schema barrel
3860
3860
  * (~144MB RSS per runner, D28).
3861
3861
  *
3862
- * Coverage: 82 device-scoped capabilities.
3862
+ * Coverage: 83 device-scoped capabilities.
3863
3863
  */
3864
3864
  var DEVICE_SCOPED_CAPS = new Set([
3865
3865
  "accessories",
@@ -3924,6 +3924,7 @@ var DEVICE_SCOPED_CAPS = new Set([
3924
3924
  "ptz",
3925
3925
  "ptz-autotrack",
3926
3926
  "reboot",
3927
+ "recording-signal",
3927
3928
  "scene-monitor",
3928
3929
  "script-runner",
3929
3930
  "smoke",
@@ -16,6 +16,12 @@ export interface SpatialDetection {
16
16
  * measured at native resolution is not the thing they exist to kill.
17
17
  */
18
18
  readonly nativeRecovery?: NativeRecoveryEvidence;
19
+ /**
20
+ * Mirrored from {@link ObjectDetection.birthEvidence} when this detection
21
+ * enters the tracker, so a birth the tracker mints carries the pixels that
22
+ * produced it all the way to the confirmation gate (D379).
23
+ */
24
+ readonly birthEvidence?: BirthEvidence;
19
25
  }
20
26
  export interface CropInput {
21
27
  readonly frame: FrameInput;
@@ -230,6 +236,12 @@ export interface DetectionBase {
230
236
  * {@link NativeRecoveryEvidence}.
231
237
  */
232
238
  readonly nativeRecovery?: NativeRecoveryEvidence;
239
+ /**
240
+ * The box's own pixels, cut by the runner at the instant of detection, for a
241
+ * box it had not seen recently. See {@link BirthEvidence} — this is what the
242
+ * confirmation gate looks at instead of asking for pixels afterwards.
243
+ */
244
+ readonly birthEvidence?: BirthEvidence;
233
245
  }
234
246
  /**
235
247
  * What a native-resolution second pass ALREADY measured about a box.
@@ -255,6 +267,64 @@ export interface NativeRecoveryEvidence {
255
267
  /** Side of the compressed view the model actually saw, in pixels. */
256
268
  readonly viewPx: number;
257
269
  }
270
+ /**
271
+ * The pixels of a candidate birth, CUT AT THE INSTANT IT WAS DETECTED.
272
+ *
273
+ * ## Why this exists (D379)
274
+ *
275
+ * The confirmation gate asks one question of a new track: re-run the detector
276
+ * on a crop of the box and see whether the subject is really there. Until now
277
+ * it asked for those pixels AFTERWARDS, by `FrameHandle`, across a process
278
+ * boundary — and measured on the live hub over three hours that request fails
279
+ * 7 452 times, 99.9% of them `worker-lease-gone`, at a median handle age of
280
+ * 1 347 ms and misses recorded as early as 254 ms. The frame is not evicted
281
+ * late; it is very often never retained at all (`leaseAdmitted:9` against
282
+ * `leaseMarks:297` on one measured worker, `leaseReleasedEarly:288`). 1 191 of
283
+ * 1 883 gate verdicts in that window (63%) never saw a pixel.
284
+ *
285
+ * So the crop is cut where the pixels are already in hand — in the runner, on
286
+ * the frame the detection was measured on — and RIDES the detection. No store,
287
+ * no handle, no lease, no round trip, and above all no OTHER INSTANT: a subject
288
+ * that has moved is not in a later frame, and its absence there is not evidence
289
+ * it was never here.
290
+ *
291
+ * This is the same discipline Scrypted enforces structurally: its decoded frame
292
+ * is invalidated the moment the `for await` body returns
293
+ * (`plugins/python-codecs/src/libav.py:105-112`), so every second look — face
294
+ * crop, embedding, retained detection image — is taken IN BAND
295
+ * (`plugins/objectdetector/src/main.ts:512-520`).
296
+ *
297
+ * ## What it is NOT
298
+ *
299
+ * Not a detail/CLIP crop, and nothing here calls `deriveDetailCropRect` (D52):
300
+ * this rectangle is the detection box padded for a CLASSIFICATION question and
301
+ * is never fed to an embedder, never stored as media, never indexed. It is also
302
+ * not a frame: it is one small JPEG of one box, cut only for a box the runner
303
+ * has not seen recently, so it never crosses a boundary at frame rate (D9/D18).
304
+ */
305
+ export interface BirthEvidence {
306
+ /** Base64 JPEG of {@link rect}, cut from the frame this detection was measured on. */
307
+ readonly jpegBase64: string;
308
+ /**
309
+ * The rectangle the JPEG covers, in ABSOLUTE pixels of the analysis frame —
310
+ * the same space as `DetectionBoundingBox`. The gate needs it to put a
311
+ * crop-space re-detection back into the frame (secondary promotion), and it
312
+ * is the padded box, never the bare one.
313
+ */
314
+ readonly rect: {
315
+ readonly x: number;
316
+ readonly y: number;
317
+ readonly width: number;
318
+ readonly height: number;
319
+ };
320
+ /**
321
+ * Which raster it was cut from. `analysis` is the ≤640 detection frame the
322
+ * runner holds in its own address space — the only raster that is present,
323
+ * for free, at the instant of detection. A future native rung would add a
324
+ * value here rather than change the meaning of this one.
325
+ */
326
+ readonly tier: 'analysis';
327
+ }
258
328
  /** Object detection (first-level person/vehicle/animal OR detail face/plate). */
259
329
  export interface ObjectDetection extends DetectionBase {
260
330
  readonly kind: 'first-level' | 'detail';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/types",
3
- "version": "1.2.158",
3
+ "version": "1.2.160",
4
4
  "description": "Shared types, interfaces, and model catalogs for the CamStack detection ecosystem",
5
5
  "keywords": [
6
6
  "camstack",