@camstack/addon-post-analysis 1.2.49 → 1.2.51

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.
@@ -1,3 +1,4 @@
1
+ import "node:crypto";
1
2
  //#region ../types/dist/event-category-41fKf-q9.mjs
2
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
4
  EventCategory["SystemBoot"] = "system.boot";
@@ -6460,7 +6461,20 @@ var BrokerStatsSchema = object({
6460
6461
  sampleRate: number(),
6461
6462
  channels: number(),
6462
6463
  supported: boolean()
6463
- }).nullable().optional()
6464
+ }).nullable().optional(),
6465
+ /**
6466
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
6467
+ * distributing none of the device's audio, on live or recording.
6468
+ *
6469
+ * Present so a silent camera can be told apart from a broken one on the
6470
+ * stream panel itself, without cross-referencing the switch group: a
6471
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
6472
+ * working exactly as asked. `audioMutedDropped` counts the audio units
6473
+ * thrown away since the current dial — it is how you confirm from stats
6474
+ * alone that the mute is on the packet path and not merely persisted.
6475
+ */
6476
+ audioMuted: boolean().optional(),
6477
+ audioMutedDropped: number().optional()
6464
6478
  });
6465
6479
  /**
6466
6480
  * Exporter-facing "profile restream" entry. Returned by
@@ -7122,6 +7136,473 @@ function sleep(ms) {
7122
7136
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7123
7137
  }
7124
7138
  //#endregion
7139
+ //#region ../types/dist/fmp4-box-splitter-B53u9-Nu.mjs
7140
+ var AUDIO_ENCODER_BY_CODEC = {
7141
+ opus: "libopus",
7142
+ aac: "aac",
7143
+ pcmu: "pcm_mulaw",
7144
+ pcma: "pcm_alaw"
7145
+ };
7146
+ /**
7147
+ * Camera-microphone audio, per codec. Lives HERE rather than in
7148
+ * `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
7149
+ * on these types); it is re-exported from there, which is where to read it.
7150
+ *
7151
+ * Every source in this repo is a mono camera mic. The former broker preset
7152
+ * encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
7153
+ * that is the value this consolidation changed.
7154
+ */
7155
+ var AUDIO_PRESETS = {
7156
+ aac: {
7157
+ kind: "encode",
7158
+ codec: "aac",
7159
+ bitrateKbps: 128,
7160
+ sampleRateHz: 48e3,
7161
+ channels: 1
7162
+ },
7163
+ opus: {
7164
+ kind: "encode",
7165
+ codec: "opus",
7166
+ bitrateKbps: 64,
7167
+ sampleRateHz: 48e3,
7168
+ channels: 1
7169
+ },
7170
+ pcmu: {
7171
+ kind: "encode",
7172
+ codec: "pcmu",
7173
+ sampleRateHz: 8e3,
7174
+ channels: 1
7175
+ }
7176
+ };
7177
+ /** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
7178
+ function logBannerArgs(level) {
7179
+ return [
7180
+ "-hide_banner",
7181
+ "-loglevel",
7182
+ level
7183
+ ];
7184
+ }
7185
+ /** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
7186
+ function isSoftwareDecode(decodeHwAccel) {
7187
+ return !decodeHwAccel || decodeHwAccel === "none" || decodeHwAccel === "copy";
7188
+ }
7189
+ /**
7190
+ * Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
7191
+ * appended to this list by a caller — that is the whole point of the function.
7192
+ */
7193
+ function buildInputArgs(input, decodeHwAccel) {
7194
+ const args = [];
7195
+ if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
7196
+ if (input.extraArgs?.length) args.push(...input.extraArgs);
7197
+ if (input.analyzeDurationUs !== void 0) args.push("-analyzeduration", String(input.analyzeDurationUs));
7198
+ if (input.probeSizeBytes !== void 0) args.push("-probesize", String(input.probeSizeBytes));
7199
+ if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
7200
+ if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
7201
+ args.push("-i", input.url);
7202
+ return args;
7203
+ }
7204
+ /** The `-vf` filter args, or `[]` when a consumer `-vf` already claims the slot. */
7205
+ function buildVideoFilterArgs(scale, outputArgs) {
7206
+ if (!scale) return [];
7207
+ if (outputArgs.some((a) => a === "-vf")) return [];
7208
+ if (scale.mode === "exact") return ["-vf", `scale=${scale.width}:${scale.height}`];
7209
+ return ["-vf", `scale='min(${scale.width},iw)':'min(${scale.height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2`];
7210
+ }
7211
+ /** Rate-control args for an encode plan. */
7212
+ function buildRateControlArgs(video) {
7213
+ const kbps = video.bitrateKbps;
7214
+ if (kbps === void 0) return [];
7215
+ const rc = video.rateControl ?? {
7216
+ kind: "cap",
7217
+ vbvSeconds: 2
7218
+ };
7219
+ const bufsize = Math.max(1, Math.round(kbps * rc.vbvSeconds));
7220
+ return [
7221
+ ...rc.kind === "cbr" ? ["-b:v", `${kbps}k`] : [],
7222
+ "-maxrate",
7223
+ `${kbps}k`,
7224
+ "-bufsize",
7225
+ `${bufsize}k`
7226
+ ];
7227
+ }
7228
+ /** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
7229
+ function buildVideoArgs(video, outputArgs) {
7230
+ if (video.kind === "copy") return [
7231
+ "-c:v",
7232
+ "copy",
7233
+ ...video.bitstreamFilter ? ["-bsf:v", video.bitstreamFilter] : []
7234
+ ];
7235
+ const args = [
7236
+ ...buildVideoFilterArgs(video.scale, outputArgs),
7237
+ "-c:v",
7238
+ video.encoder
7239
+ ];
7240
+ if (video.preset !== void 0) args.push("-preset", video.preset);
7241
+ if (video.tune !== void 0) args.push("-tune", video.tune);
7242
+ if (video.profile !== void 0) args.push("-profile:v", video.profile);
7243
+ if (video.level !== void 0) args.push("-level", video.level);
7244
+ if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
7245
+ if (video.fps !== void 0) args.push("-r", String(video.fps));
7246
+ if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
7247
+ if (video.forceKeyFramesSeconds !== void 0) args.push("-force_key_frames", `expr:gte(t,n_forced*${video.forceKeyFramesSeconds})`);
7248
+ if (video.bf !== void 0) args.push("-bf", String(video.bf));
7249
+ args.push(...buildRateControlArgs(video));
7250
+ if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
7251
+ return args;
7252
+ }
7253
+ /** The whole audio block, after `-i`. */
7254
+ function buildAudioArgs(audio) {
7255
+ if (audio.kind === "none") return ["-an"];
7256
+ if (audio.kind === "copy") return ["-c:a", "copy"];
7257
+ const args = [];
7258
+ if (audio.filter !== void 0) args.push("-af", audio.filter);
7259
+ args.push("-c:a", AUDIO_ENCODER_BY_CODEC[audio.codec]);
7260
+ if (audio.application !== void 0) args.push("-application", audio.application);
7261
+ if (audio.frameDurationMs !== void 0) args.push("-frame_duration", String(audio.frameDurationMs));
7262
+ if (audio.globalHeader === true) args.push("-flags", "+global_header");
7263
+ if (audio.sampleRateHz !== void 0) args.push("-ar", String(audio.sampleRateHz));
7264
+ if (audio.bitrateKbps !== void 0) args.push("-b:a", `${audio.bitrateKbps}k`);
7265
+ if (audio.vbvBufferKbits !== void 0) args.push("-bufsize", `${audio.vbvBufferKbits}k`);
7266
+ if (audio.channels !== void 0) args.push("-ac", String(audio.channels));
7267
+ return args;
7268
+ }
7269
+ /** RTP output-leg args (`-payload_type`, `-ssrc`, `-sdp_file`, `-f rtp <url>`). */
7270
+ function buildRtpOutputArgs(out) {
7271
+ const args = [];
7272
+ if (out.payloadType !== void 0) args.push("-payload_type", String(out.payloadType));
7273
+ if (out.ssrc !== void 0) args.push("-ssrc", String(out.ssrc));
7274
+ if (out.sdpFile !== void 0) args.push("-sdp_file", out.sdpFile);
7275
+ args.push("-f", "rtp", out.url);
7276
+ return args;
7277
+ }
7278
+ /** `true` when the sink is a raw elementary bytestream that cannot mux audio. */
7279
+ function isElementaryVideoSink(sink) {
7280
+ return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
7281
+ }
7282
+ /**
7283
+ * The fragmented-MP4 muxer flags, in the order the recorder has proven them
7284
+ * (`recorder/addon/ffmpeg-args.ts` passes the same `movflags` string through
7285
+ * `-segment_format_options`, across every vendor in the fleet):
7286
+ *
7287
+ * - `frag_keyframe` — cut a fragment at each key frame, so every fragment
7288
+ * opens on a sync sample. HKSV's whole requirement.
7289
+ * - `empty_moov` — write `ftyp`+`moov` up front with no samples in it, which
7290
+ * is what makes the head a standalone INITIALISATION segment.
7291
+ * - `default_base_moof` — fragment offsets are self-relative, so a fragment is
7292
+ * demuxable without the bytes that preceded it. D31's byte-range read path
7293
+ * depends on exactly this property of the recorder's segments.
7294
+ */
7295
+ var FMP4_MOVFLAGS = "+frag_keyframe+empty_moov+default_base_moof";
7296
+ /**
7297
+ * The terminal sink args for every non-`rtp-outputs` sink. Exhaustive over the
7298
+ * union so a new member cannot fall through to `['-f', container, 'pipe:1']`,
7299
+ * which is what a plain `container` read would have done for `mp4` — a valid
7300
+ * argv that writes a NON-fragmented, unseekable-to-a-pipe MP4 and produces one
7301
+ * unusable byte stream.
7302
+ */
7303
+ function buildStdoutOrRtspSinkArgs(sink) {
7304
+ if (sink.kind === "rtsp-listen") return [
7305
+ "-f",
7306
+ "rtsp",
7307
+ "-rtsp_transport",
7308
+ "tcp",
7309
+ "-rtsp_flags",
7310
+ "listen",
7311
+ sink.url
7312
+ ];
7313
+ if (sink.kind === "rtp-outputs") return [];
7314
+ return sink.container === "mp4" ? buildFmp4SinkArgs(sink) : [
7315
+ "-f",
7316
+ sink.container,
7317
+ "pipe:1"
7318
+ ];
7319
+ }
7320
+ /**
7321
+ * How far BELOW the negotiated fragment length `-min_frag_duration` is set.
7322
+ *
7323
+ * `-min_frag_duration` refuses to cut before that much media has accumulated,
7324
+ * and then waits for the next key frame. Set to exactly `fragmentMs`, the
7325
+ * commonest camera configuration in existence — a key-frame grid EQUAL to the
7326
+ * requested fragment length — lands the deadline on the same instant as the key
7327
+ * frame, loses the race, and skips to the following one: **every fragment comes
7328
+ * out at twice the requested length.**
7329
+ *
7330
+ * Measured on the live fleet 2026-08-07, camera 615, `-c:v copy` (D84):
7331
+ *
7332
+ * | slot | GOP | `-min_frag_duration` | median gap |
7333
+ * | --- | --- | --- | --- |
7334
+ * | 1280×720 | 40 f @ 10 fps = 4.0 s | 4000 ms | **7944 ms** |
7335
+ * | 1280×720 | 40 f @ 10 fps = 4.0 s | 3600 ms | 3973 ms |
7336
+ * | 3840×2160 | 100 f @ 25 fps = 4.0 s | 4000 ms | 8042 ms |
7337
+ * | 3840×2160 | 100 f @ 25 fps = 4.0 s | 3600 ms | 3998 ms |
7338
+ *
7339
+ * A doubled fragment is not a cosmetic overshoot: HKSV requires every fragment
7340
+ * to be no longer than the length the controller SELECTED, so the shipped-but-
7341
+ * inert phase-1 sink would have violated the contract on its first real clip.
7342
+ *
7343
+ * 10 % is chosen against the two failures either side of it. Too small and
7344
+ * ordinary jitter (measured spread 3953-4096 ms) re-loses the race; too large
7345
+ * and a source with a key frame slightly EARLY than the grid gets cut there,
7346
+ * yielding a short fragment for no reason.
7347
+ */
7348
+ var FMP4_MIN_FRAG_MARGIN = .9;
7349
+ /** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
7350
+ function buildFmp4SinkArgs(sink) {
7351
+ return [
7352
+ "-movflags",
7353
+ FMP4_MOVFLAGS,
7354
+ "-min_frag_duration",
7355
+ String(Math.max(0, Math.round(sink.fragmentMs * FMP4_MIN_FRAG_MARGIN * 1e3))),
7356
+ "-f",
7357
+ "mp4",
7358
+ "pipe:1"
7359
+ ];
7360
+ }
7361
+ /**
7362
+ * A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
7363
+ * audio optional so a source with no audio skips it instead of failing the
7364
+ * whole invocation.
7365
+ */
7366
+ function buildAudioSidecarArgs(sidecar) {
7367
+ return [
7368
+ "-map",
7369
+ "0:a:0?",
7370
+ ...buildAudioArgs(sidecar.codec === "pcma" ? {
7371
+ kind: "encode",
7372
+ codec: "pcma",
7373
+ sampleRateHz: 8e3,
7374
+ channels: 1
7375
+ } : AUDIO_PRESETS[sidecar.codec]),
7376
+ ...buildRtpOutputArgs({
7377
+ url: sidecar.rtpUrl,
7378
+ sdpFile: sidecar.sdpFile
7379
+ })
7380
+ ];
7381
+ }
7382
+ /**
7383
+ * Assemble the full ffmpeg argument list. Layout:
7384
+ *
7385
+ * -hide_banner -loglevel <level>
7386
+ * [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
7387
+ * [<input.extraArgs>] │
7388
+ * [-fflags <flag>…] │
7389
+ * [-rtsp_transport tcp] │
7390
+ * -i <url> ─┘
7391
+ * <video block> <threads> <audio block> ─┐ OUTPUT options.
7392
+ * <consumer outputArgs verbatim> │
7393
+ * <sink> ─┘ terminal
7394
+ */
7395
+ function buildFfmpegArgs(inv) {
7396
+ const head = [...logBannerArgs(inv.logLevel), ...buildInputArgs(inv.input, inv.decodeHwAccel)];
7397
+ const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
7398
+ if (inv.sink.kind === "rtp-outputs") {
7399
+ const videoLeg = inv.sink.video ? [
7400
+ "-an",
7401
+ "-map",
7402
+ "0:v:0",
7403
+ ...buildVideoArgs(inv.video, inv.outputArgs),
7404
+ ...threadArgs,
7405
+ ...inv.outputArgs,
7406
+ ...buildRtpOutputArgs(inv.sink.video)
7407
+ ] : [];
7408
+ const audioLeg = inv.sink.audio ? [
7409
+ "-vn",
7410
+ "-map",
7411
+ "0:a:0?",
7412
+ ...buildAudioArgs(inv.audio),
7413
+ ...buildRtpOutputArgs(inv.sink.audio)
7414
+ ] : [];
7415
+ return [
7416
+ ...head,
7417
+ ...videoLeg,
7418
+ ...audioLeg
7419
+ ];
7420
+ }
7421
+ const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
7422
+ const sinkArgs = buildStdoutOrRtspSinkArgs(inv.sink);
7423
+ return [
7424
+ ...head,
7425
+ ...buildVideoArgs(inv.video, inv.outputArgs),
7426
+ ...threadArgs,
7427
+ ...audioArgs,
7428
+ ...inv.outputArgs,
7429
+ ...sinkArgs,
7430
+ ...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
7431
+ ];
7432
+ }
7433
+ var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
7434
+ /** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
7435
+ var BOX_HEADER_BYTES = 8;
7436
+ var LARGE_BOX_HEADER_BYTES = 16;
7437
+ var Fmp4BoxSplitter = class {
7438
+ maxUnitBytes;
7439
+ /** Bytes of the CURRENT unit plus any partial box after it. */
7440
+ buffer = new Uint8Array(0);
7441
+ /** Where the current unit starts inside {@link buffer}. */
7442
+ unitStart = 0;
7443
+ /** Where the box scanner has reached inside {@link buffer}. */
7444
+ cursor = 0;
7445
+ state = "init";
7446
+ nextSequence = 0;
7447
+ faultReason = null;
7448
+ interstitial = /* @__PURE__ */ new Set();
7449
+ constructor(options = {}) {
7450
+ this.maxUnitBytes = options.maxUnitBytes ?? DEFAULT_MAX_UNIT_BYTES;
7451
+ }
7452
+ /**
7453
+ * Non-null once the stream cannot be split. The splitter emits nothing
7454
+ * further, so a caller polls this to kill the child rather than watching a
7455
+ * silent stall — a fragmenter that quietly stops producing looks exactly like
7456
+ * a camera with no motion.
7457
+ */
7458
+ get fault() {
7459
+ return this.faultReason;
7460
+ }
7461
+ /** Bytes currently held. The memory bound, observable rather than asserted. */
7462
+ get pendingBytes() {
7463
+ return this.buffer.length - this.unitStart;
7464
+ }
7465
+ /**
7466
+ * Top-level box types seen BETWEEN fragments and discarded — `mfra`, `free`,
7467
+ * a stray `sidx`. Reported rather than dropped in silence: they are legal and
7468
+ * useless to a fragment consumer, but a type nobody expected showing up here
7469
+ * is the first symptom of a muxer that is not writing what we think it is.
7470
+ */
7471
+ get discardedInterstitialTypes() {
7472
+ return [...this.interstitial];
7473
+ }
7474
+ /**
7475
+ * Feed bytes; get back whatever units completed. Returns `[]` once faulted.
7476
+ */
7477
+ push(chunk) {
7478
+ if (this.faultReason !== null || chunk.length === 0) return [];
7479
+ this.append(chunk);
7480
+ if (this.pendingBytes > this.maxUnitBytes) return this.fail(`a single fMP4 unit exceeded ${this.maxUnitBytes} bytes — this stream is not fragmented`);
7481
+ return this.drainBoxes();
7482
+ }
7483
+ append(chunk) {
7484
+ if (this.buffer.length === 0) {
7485
+ this.buffer = chunk.slice();
7486
+ return;
7487
+ }
7488
+ const next = new Uint8Array(this.buffer.length + chunk.length);
7489
+ next.set(this.buffer, 0);
7490
+ next.set(chunk, this.buffer.length);
7491
+ this.buffer = next;
7492
+ }
7493
+ /** Consume every COMPLETE top-level box now in the buffer. */
7494
+ drainBoxes() {
7495
+ const units = [];
7496
+ for (;;) {
7497
+ const header = this.readHeader();
7498
+ if (this.faultReason !== null) return units;
7499
+ if (header === null) break;
7500
+ if (this.cursor + header.totalBytes > this.buffer.length) break;
7501
+ const boxStart = this.cursor;
7502
+ const boxEnd = boxStart + header.totalBytes;
7503
+ this.cursor = boxEnd;
7504
+ const unit = this.consumeBox(header.type, boxStart, boxEnd);
7505
+ if (this.faultReason !== null) return units;
7506
+ if (unit !== null) units.push(unit);
7507
+ }
7508
+ this.compact();
7509
+ return units;
7510
+ }
7511
+ /**
7512
+ * Apply one box to the state machine. Returns a unit when this box CLOSED
7513
+ * one, `null` otherwise.
7514
+ */
7515
+ consumeBox(type, boxStart, boxEnd) {
7516
+ if (this.state === "init") {
7517
+ if (type !== "moof") return null;
7518
+ if (boxStart === this.unitStart) {
7519
+ this.fail("a moof arrived before any initialisation box — there is no ftyp/moov to send");
7520
+ return null;
7521
+ }
7522
+ const init = this.emit("init", this.unitStart, boxStart);
7523
+ this.unitStart = boxStart;
7524
+ this.state = "fragment";
7525
+ return init;
7526
+ }
7527
+ if (this.state === "idle") {
7528
+ if (type !== "moof") {
7529
+ this.interstitial.add(type);
7530
+ this.unitStart = boxEnd;
7531
+ return null;
7532
+ }
7533
+ this.unitStart = boxStart;
7534
+ this.state = "fragment";
7535
+ return null;
7536
+ }
7537
+ if (type !== "mdat") return null;
7538
+ const fragment = this.emit("fragment", this.unitStart, boxEnd);
7539
+ this.unitStart = boxEnd;
7540
+ this.state = "idle";
7541
+ return fragment;
7542
+ }
7543
+ /**
7544
+ * Parse the header at {@link cursor}, or `null` when too few bytes have
7545
+ * arrived to know. Faults on a size the splitter cannot honour.
7546
+ */
7547
+ readHeader() {
7548
+ const available = this.buffer.length - this.cursor;
7549
+ if (available < BOX_HEADER_BYTES) return null;
7550
+ const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
7551
+ const size = view.getUint32(this.cursor);
7552
+ const type = String.fromCharCode(this.buffer[this.cursor + 4] ?? 0, this.buffer[this.cursor + 5] ?? 0, this.buffer[this.cursor + 6] ?? 0, this.buffer[this.cursor + 7] ?? 0);
7553
+ if (size === 0) {
7554
+ this.fail(`box "${type}" declares size 0 (to EOF) — an unbounded box cannot be fragmented`);
7555
+ return null;
7556
+ }
7557
+ if (size === 1) {
7558
+ if (available < LARGE_BOX_HEADER_BYTES) return null;
7559
+ const large = view.getBigUint64(this.cursor + BOX_HEADER_BYTES);
7560
+ if (large > BigInt(this.maxUnitBytes)) {
7561
+ this.fail(`box "${type}" declares ${large} bytes, over the ${this.maxUnitBytes} byte bound`);
7562
+ return null;
7563
+ }
7564
+ return {
7565
+ type,
7566
+ totalBytes: Number(large)
7567
+ };
7568
+ }
7569
+ if (size < BOX_HEADER_BYTES) {
7570
+ this.fail(`box "${type}" declares an impossible size of ${size} bytes`);
7571
+ return null;
7572
+ }
7573
+ return {
7574
+ type,
7575
+ totalBytes: size
7576
+ };
7577
+ }
7578
+ emit(kind, start, end) {
7579
+ const sequence = this.nextSequence;
7580
+ this.nextSequence += 1;
7581
+ return {
7582
+ kind,
7583
+ data: this.buffer.slice(start, end),
7584
+ sequence
7585
+ };
7586
+ }
7587
+ /**
7588
+ * Drop everything already emitted or discarded. Without this the buffer is
7589
+ * the whole stream and the process dies in hours, not minutes.
7590
+ */
7591
+ compact() {
7592
+ if (this.unitStart === 0) return;
7593
+ this.buffer = this.buffer.slice(this.unitStart);
7594
+ this.cursor -= this.unitStart;
7595
+ this.unitStart = 0;
7596
+ }
7597
+ fail(reason) {
7598
+ this.faultReason = reason;
7599
+ this.buffer = new Uint8Array(0);
7600
+ this.unitStart = 0;
7601
+ this.cursor = 0;
7602
+ return [];
7603
+ }
7604
+ };
7605
+ //#endregion
7125
7606
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7126
7607
  /**
7127
7608
  import { errMsg } from '@camstack/types'
@@ -7291,6 +7772,19 @@ object({
7291
7772
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7292
7773
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7293
7774
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7775
+ * | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
7776
+ *
7777
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
7778
+ *
7779
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
7780
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
7781
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
7782
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
7783
+ * instant, vendor-independent and reversible without touching the camera, and
7784
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
7785
+ * D62 forbids a second switch that *disagrees* with the first; these two
7786
+ * cannot disagree, because neither reads the other's store — the camera holds
7787
+ * one, the broker holds the other, and each reports its own fact.
7294
7788
  *
7295
7789
  * ## The two switches whose authority is not on this server
7296
7790
  *
@@ -7352,6 +7846,7 @@ var CameraSwitchIdSchema = _enum([
7352
7846
  "object-detection",
7353
7847
  "privacy-mask",
7354
7848
  "device-audio",
7849
+ "broker-audio",
7355
7850
  "audio-analysis",
7356
7851
  "recording",
7357
7852
  "notifications"
@@ -7377,7 +7872,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7377
7872
  object({
7378
7873
  kind: literal("camera-mask"),
7379
7874
  capName: string()
7380
- })
7875
+ }),
7876
+ object({ kind: literal("broker-audio-mute") })
7381
7877
  ]);
7382
7878
  /**
7383
7879
  * Why a switch is not offered for this camera. Rendered instead of the
@@ -9599,7 +10095,25 @@ method(object({
9599
10095
  }), _void(), {
9600
10096
  kind: "mutation",
9601
10097
  auth: "admin"
9602
- }), method(object({ brokerId: string() }), boolean()), object({
10098
+ }), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
10099
+ muted: boolean(),
10100
+ /**
10101
+ * How many live non-derived brokers currently hold the mute. Purely
10102
+ * diagnostic: `muted` is the policy and is authoritative on its own
10103
+ * (it applies to brokers that do not exist yet), while this says
10104
+ * whether anything is presently being silenced.
10105
+ */
10106
+ appliedBrokers: number().int().nonnegative()
10107
+ })), method(object({
10108
+ deviceId: number().int(),
10109
+ muted: boolean()
10110
+ }), object({
10111
+ muted: boolean(),
10112
+ appliedBrokers: number().int().nonnegative()
10113
+ }), {
10114
+ kind: "mutation",
10115
+ auth: "admin"
10116
+ }), object({
9603
10117
  deviceId: number().int().nonnegative(),
9604
10118
  camStreamId: string(),
9605
10119
  profile: CamProfileSchema
@@ -15555,6 +16069,30 @@ var TrackSourceSchema = _enum([
15555
16069
  "audio"
15556
16070
  ]);
15557
16071
  /**
16072
+ * Where a track sits in the RETRAIN lifecycle (D81).
16073
+ *
16074
+ * - `none` — never marked, or un-marked. Evictable.
16075
+ * - `staging` — the operator wants this track as training material and has not
16076
+ * finished with it. **This is the only state retention holds**: the track and
16077
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
16078
+ * the device's age window.
16079
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
16080
+ * were COPIED into the retrain dataset at selection time, so the dataset no
16081
+ * longer depends on the track's media and the track becomes EVICTABLE again.
16082
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
16083
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
16084
+ *
16085
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
16086
+ * the store's filter language has only positive equality and `whereIn` — no
16087
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
16088
+ * would make the entire pre-column history immortal in one deploy.
16089
+ */
16090
+ var RetrainStatusSchema = _enum([
16091
+ "none",
16092
+ "staging",
16093
+ "trained"
16094
+ ]);
16095
+ /**
15558
16096
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
15559
16097
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
15560
16098
  * so the two surfaces cannot drift.
@@ -15564,18 +16102,31 @@ var TrackSourceSchema = _enum([
15564
16102
  * columns existed read as absent, and a consumer that needs a boolean should say
15565
16103
  * `flag === true`, not `flag !== false`.
15566
16104
  *
15567
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
15568
- * operator curation, and the behaviour they drive will be specified separately.
15569
- * In particular a `markForTrain` track is NOT pinned against retention — see
15570
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
16105
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
16106
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
16107
+ * `true` moves `none → staging`, writing `false` moves `staging → none`, and a
16108
+ * `trained` track reports `false` while refusing both writes. The boolean is
16109
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
16110
+ * "never marked" from "already trained" must read `retrainStatus`.
16111
+ *
16112
+ * `debug` does NOT pin; it is attention, not durability.
15571
16113
  */
15572
16114
  var TrackFlagFields = {
15573
- /** Operator marked this track as training material. */
16115
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
16116
+ * `'staging'`. */
15574
16117
  markForTrain: boolean().optional(),
15575
16118
  /** Operator marked this track for diagnostic attention. */
15576
16119
  debug: boolean().optional()
15577
16120
  };
15578
16121
  /**
16122
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
16123
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
16124
+ * write patch, and the status is not something the toggle sets — it is what the
16125
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
16126
+ * always present on a persisted row (the column default materialises `'none'`).
16127
+ */
16128
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
16129
+ /**
15579
16130
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
15580
16131
  * one flag can never clear the other — the toggles are independent and are
15581
16132
  * driven from three surfaces that do not know about each other.
@@ -15589,13 +16140,92 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
15589
16140
  var TrackFlagsSchema = object({
15590
16141
  trackId: string(),
15591
16142
  markForTrain: boolean(),
15592
- debug: boolean()
16143
+ debug: boolean(),
16144
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
16145
+ * a track row) because this shape is only ever produced by the write body,
16146
+ * which always knows it — and a surface that has just written needs to render
16147
+ * `trained` without a re-fetch. */
16148
+ retrainStatus: RetrainStatusSchema
16149
+ });
16150
+ union([literal(1), literal(2)]);
16151
+ /**
16152
+ * WHO decided a label, and when. Carried per tier so a value can be traced to
16153
+ * the step and model that produced it — which is what makes the write rule
16154
+ * arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
16155
+ * and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
16156
+ *
16157
+ * `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
16158
+ * `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
16159
+ * `migration:4g` for a value the 4g migration moved from the single-slot era —
16160
+ * that value has no provenance, and the write rule lets ANY properly-attributed
16161
+ * write of the same tier replace it regardless of score.
16162
+ */
16163
+ var LabelAttributionSchema = object({
16164
+ stepId: string(),
16165
+ modelId: string().optional(),
16166
+ decidedAt: number()
16167
+ });
16168
+ /**
16169
+ * The TIERED label model (roadmap 4g), spread into `TrackSchema` and
16170
+ * `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
16171
+ * track and its events always answer the same question the same way.
16172
+ *
16173
+ * Two scalar columns, not an array: every consumer wants "the coarse one" or
16174
+ * "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
16175
+ * is tier 2, and each carries its own score + attribution.
16176
+ *
16177
+ * **Reading it.** What a human should be shown is `subLabel ?? label` — the
16178
+ * finest thing known. Before 4g the single `label` column held the finest
16179
+ * value, so a consumer that has not been updated reads the tier-1 slot and
16180
+ * shows nothing on a species-only row; that is why the migration puts every
16181
+ * pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
16182
+ * and why the read surfaces were changed in the same train.
16183
+ *
16184
+ * **Writing it.** The slots are independent, which is the whole point: a
16185
+ * tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
16186
+ * migratorius`), so fineness cannot regress by construction. Within a tier the
16187
+ * higher score wins. One rule, one implementation — see
16188
+ * `pipeline/label-tier.ts` in addon-post-analysis.
16189
+ */
16190
+ var TieredLabelFields = {
16191
+ /** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
16192
+ label: string().optional(),
16193
+ /** Confidence of the tier-1 value, as reported by the deciding step. */
16194
+ labelScore: number().optional(),
16195
+ /** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
16196
+ labelMeta: LabelAttributionSchema.optional(),
16197
+ /** Tier 2 — the instance. See {@link LabelTierSchema}. */
16198
+ subLabel: string().optional(),
16199
+ /** Confidence of the tier-2 value, as reported by the deciding step. */
16200
+ subLabelScore: number().optional(),
16201
+ /** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
16202
+ subLabelMeta: LabelAttributionSchema.optional()
16203
+ };
16204
+ /** Per-camera slice of a training-export estimate. */
16205
+ var TrainingExportDeviceTotalsSchema = object({
16206
+ deviceId: number(),
16207
+ tracks: number().int(),
16208
+ files: number().int(),
16209
+ bytes: number().int()
16210
+ });
16211
+ /**
16212
+ * What a training export WOULD contain. Computed from media index rows only —
16213
+ * no blob is read to produce this.
16214
+ */
16215
+ var TrainingExportSummarySchema = object({
16216
+ generatedAt: number(),
16217
+ trackCount: number().int(),
16218
+ fileCount: number().int(),
16219
+ byteCount: number().int(),
16220
+ /** More marked tracks exist than a single pass carries. */
16221
+ truncated: boolean(),
16222
+ devices: array(TrainingExportDeviceTotalsSchema).readonly()
15593
16223
  });
15594
16224
  var TrackSchema = object({
15595
16225
  trackId: string(),
15596
16226
  deviceId: number(),
15597
16227
  className: string(),
15598
- label: string().optional(),
16228
+ ...TieredLabelFields,
15599
16229
  producingDeviceName: string().optional(),
15600
16230
  /** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
15601
16231
  source: TrackSourceSchema.optional(),
@@ -15634,7 +16264,8 @@ var TrackSchema = object({
15634
16264
  * Populated from the persisted envelope columns on historical reads;
15635
16265
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
15636
16266
  envelope: TrackEnvelopeSchema.optional(),
15637
- ...TrackFlagFields
16267
+ ...TrackFlagFields,
16268
+ ...TrackRetrainFields
15638
16269
  });
15639
16270
  var BaseEventFields = {
15640
16271
  id: string(),
@@ -15707,7 +16338,7 @@ var ObjectEventSchema = object({
15707
16338
  /** Omitted in slim projection. */
15708
16339
  trackId: string().optional(),
15709
16340
  className: string(),
15710
- label: string().optional(),
16341
+ ...TieredLabelFields,
15711
16342
  /** Omitted in slim projection. */
15712
16343
  confidence: number().optional(),
15713
16344
  /** Heavy JSON — omitted in slim projection. */
@@ -15788,6 +16419,173 @@ var MediaFileSchema = object({
15788
16419
  * stored blob and a `?variant=thumb` rendering without fetching either.
15789
16420
  */
15790
16421
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
16422
+ /**
16423
+ * The MACRO tier of an annotation — a CLOSED set.
16424
+ *
16425
+ * This is what the exported detector predicts, so a typo here is a new class
16426
+ * with one example in it. `label` and `subLabel` are open strings by contrast:
16427
+ * the whole point of the page is teaching the model things it does not know
16428
+ * yet, and constraining that vocabulary would make it useless.
16429
+ *
16430
+ * A macro class is NEVER a label. The provider refuses a write whose `label` or
16431
+ * `subLabel` is one of these values, in any casing, because once `person`
16432
+ * exists in both tiers "every person box" stops being answerable without
16433
+ * knowing every string anyone ever typed — and the damage is retroactive.
16434
+ */
16435
+ var RetrainMacroClassSchema = _enum([
16436
+ "person",
16437
+ "vehicle",
16438
+ "animal",
16439
+ "package",
16440
+ "face",
16441
+ "plate"
16442
+ ]);
16443
+ /** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
16444
+ var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
16445
+ /** Did a human draw this box, or did the assist propose it? */
16446
+ var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
16447
+ /** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
16448
+ var RetrainBboxSchema = object({
16449
+ x: number(),
16450
+ y: number(),
16451
+ w: number(),
16452
+ h: number()
16453
+ });
16454
+ /**
16455
+ * One annotated subject.
16456
+ *
16457
+ * `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
16458
+ * (letterboxed root / zone-cropped package / subject-cropped classifier) are
16459
+ * derived from it at export and never stored — storing them is how one feature
16460
+ * space ends up holding two crops of the same subject (D52).
16461
+ */
16462
+ var RetrainAnnotationSchema = object({
16463
+ id: string(),
16464
+ trackId: string(),
16465
+ deviceId: number(),
16466
+ /** The COPY in retrain storage — never the source track's media key. */
16467
+ mediaKey: string(),
16468
+ bbox: RetrainBboxSchema,
16469
+ macroClass: RetrainMacroClassSchema,
16470
+ label: string().optional(),
16471
+ subLabel: string().optional(),
16472
+ kind: RetrainAnnotationKindSchema,
16473
+ source: RetrainAnnotationSourceSchema,
16474
+ /** Which model proposed this box — or, on a `model_error`, drew the phantom. */
16475
+ assistModelId: string().optional(),
16476
+ assistScore: number().optional(),
16477
+ exportedInBatch: string().optional(),
16478
+ createdAt: number()
16479
+ });
16480
+ /** The write form — the server owns `id`, `createdAt` and the frame binding. */
16481
+ var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
16482
+ id: true,
16483
+ trackId: true,
16484
+ deviceId: true,
16485
+ mediaKey: true,
16486
+ createdAt: true,
16487
+ exportedInBatch: true
16488
+ });
16489
+ /** A track sitting in `staging`, with everything the worklist needs to rank it. */
16490
+ var RetrainTrackSchema = object({
16491
+ trackId: string(),
16492
+ deviceId: number(),
16493
+ className: string(),
16494
+ label: string().optional(),
16495
+ firstSeen: number(),
16496
+ lastSeen: number(),
16497
+ /** How many frames the dataset already holds from this track. */
16498
+ frameCount: number().int(),
16499
+ /** How many subjects have been annotated on those frames. `0` with
16500
+ * `frameCount: 0` is exactly "staging, still to work". */
16501
+ annotationCount: number().int()
16502
+ });
16503
+ /** A frame the picker may offer — an index row, no blob was read to produce it. */
16504
+ var RetrainFrameCandidateSchema = object({
16505
+ mediaKey: string(),
16506
+ kind: MediaFileKindEnum,
16507
+ timestamp: number(),
16508
+ sizeBytes: number().int(),
16509
+ /** A copy of this original already exists — selecting it is free and cannot
16510
+ * fail, whatever became of the original. */
16511
+ copied: boolean()
16512
+ });
16513
+ /** A frame the dataset OWNS: bytes copied at selection time. */
16514
+ var RetrainFrameSchema = object({
16515
+ frameId: string(),
16516
+ deviceId: number(),
16517
+ trackId: string(),
16518
+ /** Provenance only. It may already point at nothing — that is expected. */
16519
+ sourceMediaKey: string(),
16520
+ sourceKind: MediaFileKindEnum,
16521
+ sizeBytes: number().int(),
16522
+ width: number().int(),
16523
+ height: number().int(),
16524
+ copiedAt: number()
16525
+ });
16526
+ /** Why a copy-on-select could not be honoured — named, never a silent skip. */
16527
+ var RetrainCopyRefusalSchema = _enum([
16528
+ "source-missing",
16529
+ "unreadable-image",
16530
+ "write-failed"
16531
+ ]);
16532
+ var RetrainFrameSelectionSchema = object({
16533
+ copied: array(RetrainFrameSchema).readonly(),
16534
+ refused: array(object({
16535
+ sourceMediaKey: string(),
16536
+ reason: RetrainCopyRefusalSchema
16537
+ })).readonly()
16538
+ });
16539
+ var RetrainFrameListSchema = object({
16540
+ candidates: array(RetrainFrameCandidateSchema).readonly(),
16541
+ copies: array(RetrainFrameSchema).readonly(),
16542
+ /** What the page pre-selects — the native key frame when one survives. */
16543
+ autoPickMediaKey: string().optional()
16544
+ });
16545
+ /** What the operator asked the assist to look for. */
16546
+ var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
16547
+ kind: literal("package"),
16548
+ zone: RetrainBboxSchema.optional()
16549
+ }), object({
16550
+ kind: literal("objects"),
16551
+ modelId: string(),
16552
+ minScore: number().optional()
16553
+ })]);
16554
+ /**
16555
+ * The assist's answer — a discriminated union, because "the model saw nothing"
16556
+ * and "this node cannot run that model" lead to different next moves and a
16557
+ * nullable result cannot tell them apart.
16558
+ */
16559
+ var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
16560
+ kind: literal("proposed"),
16561
+ modelId: string(),
16562
+ stepId: string(),
16563
+ minScore: number(),
16564
+ /** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
16565
+ proposals: array(RetrainAnnotationDraftSchema).readonly(),
16566
+ /** Returned by the runner but removed by the threshold. */
16567
+ belowThreshold: number().int()
16568
+ }), object({
16569
+ kind: literal("refused"),
16570
+ /** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
16571
+ reason: string(),
16572
+ detail: string().optional()
16573
+ })]);
16574
+ /** The outcome of a lifecycle move owned by the retrain page. */
16575
+ var RetrainTransitionResultSchema = object({
16576
+ trackId: string(),
16577
+ /** Where the track ended up, whatever happened. */
16578
+ retrainStatus: RetrainStatusSchema,
16579
+ /** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
16580
+ changed: boolean(),
16581
+ reason: _enum([
16582
+ "unknown-track",
16583
+ "no-frames-copied",
16584
+ "not-staging",
16585
+ "not-trained",
16586
+ "unchanged"
16587
+ ]).optional()
16588
+ });
15791
16589
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
15792
16590
  var MAX_EVENT_QUERY_LIMIT = 5e3;
15793
16591
  var DeviceEventQueryInput = object({
@@ -15842,13 +16640,14 @@ var KeyEventSchema = object({
15842
16640
  /** Track start time (firstSeen). */
15843
16641
  timestamp: number(),
15844
16642
  className: string(),
15845
- label: string().optional(),
16643
+ ...TieredLabelFields,
15846
16644
  importance: number(),
15847
16645
  /** Highest-confidence ObjectEvent id for the track (empty when none). */
15848
16646
  bestEventId: string(),
15849
16647
  /** Track lifetime in ms (lastSeen - firstSeen). */
15850
16648
  windowMs: number().optional(),
15851
- ...TrackFlagFields
16649
+ ...TrackFlagFields,
16650
+ ...TrackRetrainFields
15852
16651
  });
15853
16652
  object({
15854
16653
  trackId: string(),
@@ -16210,11 +17009,29 @@ var pipelineAnalyticsCapability = {
16210
17009
  *
16211
17010
  * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
16212
17011
  * authenticated non-admin surface and two of the three call sites are
16213
- * there. Revisit if a flag ever gains an effect that costs storage —
16214
- * `deleteTracks` next door is admin for exactly that reason.
17012
+ * there. The note that used to sit here said to revisit this the day a flag
17013
+ * gained an effect that costs storage, and D81 is that day — `markForTrain`
17014
+ * now pins. It STAYS protected, and the reason is that the alternative
17015
+ * makes the feature pointless: marking a track is something you do while
17016
+ * looking at it, on the surface you were already looking at it on, and that
17017
+ * surface is the viewer. What the storage cost gets instead is a BOUND — a
17018
+ * per-device pin budget enforced in the body, refusing a new pin past the
17019
+ * limit while always allowing un-marking. `deleteTracks` next door is still
17020
+ * admin, because destroying evidence and preserving it are not symmetric.
16215
17021
  *
16216
- * Returns the RESOLVED state of both flags (absent → `false`) so a caller
16217
- * can drive its toggle without a re-fetch. Rejects an unknown track.
17022
+ * `markForTrain` writes the retrain LIFECYCLE, not a boolean column: `true`
17023
+ * is `none → staging`, `false` is `staging → none`. A track already
17024
+ * `trained` refuses BOTH — its frames are copies inside the retrain dataset
17025
+ * and re-staging it from a generic toggle is how the same material gets
17026
+ * annotated twice under two ground truths. Returning a trained track to
17027
+ * staging is a deliberate action of the retrain page, which is also the only
17028
+ * thing that produces `trained` in the first place.
17029
+ *
17030
+ * Returns the RESOLVED state of both flags (absent → `false`) plus the
17031
+ * `retrainStatus` they were derived from, so a caller can drive its toggle
17032
+ * — and render a `trained` badge — without a re-fetch. Rejects an unknown
17033
+ * track, a new staging mark on a device already holding its full budget, and
17034
+ * any `markForTrain` write against a trained track.
16218
17035
  */
16219
17036
  setTrackFlags: method(object({
16220
17037
  /** Log/audit scope only — the trackId is globally unique on its own. */
@@ -16278,6 +17095,237 @@ var pipelineAnalyticsCapability = {
16278
17095
  kind: "query",
16279
17096
  auth: "admin"
16280
17097
  }),
17098
+ /**
17099
+ * The CHEAP QUESTION, asked before any media moves: how big is the dataset
17100
+ * the marked (`markForTrain`) tracks would produce?
17101
+ *
17102
+ * Answered from media INDEX rows only — key, kind, size, timestamp — so it
17103
+ * costs ~2 KB of reads per track and no blob reads at all. The measured harm
17104
+ * behind D56 was a bulk pass that read and base64'd every blob a track owned
17105
+ * before deciding anything, taking hub-main to 82 s busy out of 120; an
17106
+ * export is that same I/O shape, so it inherits the same discipline: know
17107
+ * the size, then decide.
17108
+ *
17109
+ * `truncated` reports that more marked tracks exist than one pass carries.
17110
+ * Empty `deviceIds` ⇒ every device that has marked tracks.
17111
+ */
17112
+ getTrainingExportSummary: method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
17113
+ kind: "query",
17114
+ auth: "admin"
17115
+ }),
17116
+ /**
17117
+ * Where to download the dataset archive.
17118
+ *
17119
+ * The BYTES do not come back through this cap — they come from the returned
17120
+ * data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
17121
+ * archive base64'd through a unary RPC envelope would be held whole in
17122
+ * memory twice on a hub this repo has already OOM'd once (D9/D18 are the
17123
+ * same lesson about frames). `getDownloadUrl` on `recordingExport` is the
17124
+ * precedent, and this follows it deliberately.
17125
+ *
17126
+ * The archive contains a `manifest.json` FIRST, then the stored media
17127
+ * VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
17128
+ * model is run: a training set's pixels must be the pixels the pipeline saw.
17129
+ */
17130
+ getTrainingExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
17131
+ kind: "query",
17132
+ auth: "admin"
17133
+ }),
17134
+ /**
17135
+ * The staging worklist for one camera, or for every camera that has one.
17136
+ *
17137
+ * Fetched ON DEMAND, over the staging set only — the page never scans
17138
+ * history, because making the working set small is the entire purpose of
17139
+ * the mark. Each row carries how many frames the dataset already holds from
17140
+ * the track and how many subjects were annotated on them, so
17141
+ * `frameCount: 0` reads as "still to work" without a second call per track.
17142
+ *
17143
+ * `auth: 'admin'`, unlike the viewer-level mark itself: marking a track is
17144
+ * curation you do while looking at it, but building the training set the
17145
+ * fleet's models are fine-tuned on is not.
17146
+ */
17147
+ listRetrainStaging: method(object({
17148
+ /** Empty ⇒ every camera that has staging tracks. A LIST, not a single
17149
+ * `deviceId`, deliberately: `deviceId` would make this device-bound and
17150
+ * route it at one camera's owner, and "every camera" would stop being
17151
+ * expressible at all. */
17152
+ deviceIds: array(number()).optional(),
17153
+ limit: number().int().min(1).max(500).optional()
17154
+ }), array(RetrainTrackSchema).readonly(), {
17155
+ kind: "query",
17156
+ auth: "admin"
17157
+ }),
17158
+ /**
17159
+ * What a track can contribute, and what it already has.
17160
+ *
17161
+ * `candidates` are the track's whole, unannotated frames — index rows only,
17162
+ * so this is cheap. `copies` are the frames already inside the dataset, and
17163
+ * a candidate whose copy exists is marked `copied: true`: selecting it again
17164
+ * is free and CANNOT fail, whatever became of the original.
17165
+ *
17166
+ * A crop, a thumbnail and `fullFrameBoxed` are never candidates. The last
17167
+ * one matters most: it has the model's own rectangle burned into the pixels,
17168
+ * and a detector trained on it learns to find a green line.
17169
+ */
17170
+ listRetrainFrames: method(object({ trackId: string() }), RetrainFrameListSchema, {
17171
+ kind: "query",
17172
+ auth: "admin"
17173
+ }),
17174
+ /**
17175
+ * COPY-ON-SELECT — the write that makes `trained` safe to evict.
17176
+ *
17177
+ * Selecting a frame copies its bytes into retrain storage immediately: not
17178
+ * a reference, not a lease. Once the copy exists the dataset no longer
17179
+ * depends on the track's media, which is exactly what lets D81 hand a
17180
+ * `trained` track back to retention.
17181
+ *
17182
+ * The order inside is load-bearing and is pinned by a test: an EXISTING
17183
+ * copy is returned without touching the source, so an original that
17184
+ * evaporated blocks the selection of THAT ORIGINAL and never the copy
17185
+ * already taken. Every refusal comes back named — a dropped selection is
17186
+ * never silent, on the wire or in the log.
17187
+ */
17188
+ selectRetrainFrames: method(object({
17189
+ deviceId: number(),
17190
+ trackId: string(),
17191
+ mediaKeys: array(string()).min(1)
17192
+ }), RetrainFrameSelectionSchema, {
17193
+ kind: "mutation",
17194
+ auth: "admin"
17195
+ }),
17196
+ /** Un-select a frame: its annotations go first, then the copy and its blob.
17197
+ * Deliberately destructive and deliberately explicit — it is the only way
17198
+ * a frame leaves the dataset before export. */
17199
+ deselectRetrainFrame: method(object({
17200
+ deviceId: number(),
17201
+ trackId: string(),
17202
+ frameId: string()
17203
+ }), object({
17204
+ removed: boolean(),
17205
+ removedAnnotations: number().int()
17206
+ }), {
17207
+ kind: "mutation",
17208
+ auth: "admin"
17209
+ }),
17210
+ /**
17211
+ * The pixels of ONE copied frame, base64.
17212
+ *
17213
+ * Through the cap rather than a data plane because it is genuinely one
17214
+ * frame at a time, on demand, at human speed — the shape D9/D18 permit
17215
+ * (what they forbid is frames crossing a boundary at frame RATE). The
17216
+ * annotation canvas needs the image and its exact dimensions in the same
17217
+ * answer: a canvas that places a normalised box against a size it guessed
17218
+ * draws every box in the wrong place.
17219
+ */
17220
+ getRetrainFrameImage: method(object({ frameId: string() }), object({
17221
+ base64: string(),
17222
+ width: number().int(),
17223
+ height: number().int()
17224
+ }), {
17225
+ kind: "query",
17226
+ auth: "admin"
17227
+ }),
17228
+ /**
17229
+ * Ask the pipeline what it sees, as a PROPOSAL.
17230
+ *
17231
+ * Runs through `pipelineRunner.runStatelessStep` on the COPIED frame, and
17232
+ * every box comes back as a draft with `source: 'assist'` plus the model and
17233
+ * score that produced it. The operator confirms, edits, adds and deletes;
17234
+ * nothing is stored until `saveRetrainAnnotations`.
17235
+ *
17236
+ * For packages the request is `rfdetr-package` on the ZONE CROP at 0.35 —
17237
+ * never the whole frame, where a package detector at that threshold proposes
17238
+ * furniture. A package request with no zone is REFUSED rather than widened,
17239
+ * because the silent widening would look like a bad model for as long as
17240
+ * nobody checked which rectangle it ran on.
17241
+ */
17242
+ proposeRetrainAnnotations: method(object({
17243
+ deviceId: number(),
17244
+ trackId: string(),
17245
+ frameId: string(),
17246
+ subject: RetrainAssistSubjectSchema,
17247
+ /** Which node runs it. Absent ⇒ wherever an unowned call lands. */
17248
+ nodeId: string().optional()
17249
+ }), RetrainAssistResultSchema, {
17250
+ kind: "mutation",
17251
+ auth: "admin"
17252
+ }),
17253
+ /** Every annotation on a track, oldest first. */
17254
+ listRetrainAnnotations: method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
17255
+ kind: "query",
17256
+ auth: "admin"
17257
+ }),
17258
+ /**
17259
+ * Replace EVERY annotation on one frame with the supplied set.
17260
+ *
17261
+ * Whole-frame replacement, not per-box upsert: the unit of ground truth is
17262
+ * the frame, and "the operator deleted a box" must be the same durable
17263
+ * outcome as "the operator never drew it". A per-box patch would let a frame
17264
+ * keep a box the operator removed on a surface that only knew about the
17265
+ * boxes it sent.
17266
+ *
17267
+ * Refuses a macro class typed into `label` or `subLabel` — the tiers are
17268
+ * separate and the guard is at the WRITE, because a mixed taxonomy cannot
17269
+ * be un-mixed by reading it.
17270
+ */
17271
+ saveRetrainAnnotations: method(object({
17272
+ deviceId: number(),
17273
+ trackId: string(),
17274
+ frameId: string(),
17275
+ annotations: array(RetrainAnnotationDraftSchema)
17276
+ }), array(RetrainAnnotationSchema).readonly(), {
17277
+ kind: "mutation",
17278
+ auth: "admin"
17279
+ }),
17280
+ /**
17281
+ * Finish with a track: `staging → trained`. **The only writer of that
17282
+ * state** — D81 shipped the column with it deliberately unreachable.
17283
+ *
17284
+ * Refuses a track the dataset holds no copies from. `trained` un-pins the
17285
+ * track's media, so completing without a copy is a delete order for material
17286
+ * nothing ever extracted anything from; that refusal IS the safety argument
17287
+ * of D81, expressed as a precondition.
17288
+ */
17289
+ completeRetrainTrack: method(object({
17290
+ deviceId: number(),
17291
+ trackId: string()
17292
+ }), RetrainTransitionResultSchema, {
17293
+ kind: "mutation",
17294
+ auth: "admin"
17295
+ }),
17296
+ /**
17297
+ * The deliberate return: `trained → staging`, for the rare case.
17298
+ *
17299
+ * The generic `setTrackFlags` toggle refuses this in both directions by
17300
+ * design (D81) — re-staging from a checkbox is how the same material gets
17301
+ * annotated twice under two ground truths. Doing it here means the operator
17302
+ * is looking at the annotations that already exist while they decide, and
17303
+ * those annotations are LEFT ALONE: "put this back" must not be a
17304
+ * destructive act wearing a navigational name.
17305
+ */
17306
+ restageRetrainTrack: method(object({
17307
+ deviceId: number(),
17308
+ trackId: string()
17309
+ }), RetrainTransitionResultSchema, {
17310
+ kind: "mutation",
17311
+ auth: "admin"
17312
+ }),
17313
+ /**
17314
+ * Where to download the ANNOTATED dataset.
17315
+ *
17316
+ * The sibling of `getTrainingExportUrl` and deliberately not the same
17317
+ * archive: that one streams a marked track's stored media verbatim, this one
17318
+ * streams the retrain COPIES plus an `annotations.json` carrying, for every
17319
+ * subject, the canonical full-frame box AND the geometry derived for each
17320
+ * model shape (letterboxed root / zone-cropped package / subject-cropped
17321
+ * classifier). Derived at export, never stored — one box in, three shapes
17322
+ * out, so two crops of the same subject can never end up in one feature
17323
+ * space (D52).
17324
+ */
17325
+ getRetrainExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
17326
+ kind: "query",
17327
+ auth: "admin"
17328
+ }),
16281
17329
  getEventMedia: method(object({
16282
17330
  eventId: string(),
16283
17331
  kind: MediaFileKindEnum.optional()
@@ -16942,6 +17990,22 @@ var DetailResultSchema = object({
16942
17990
  bbox: NativeCropBboxSchema.optional(),
16943
17991
  embedding: string().optional(),
16944
17992
  label: string().optional(),
17993
+ /**
17994
+ * The tier `label` occupies, copied VERBATIM from the producing step's
17995
+ * `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
17996
+ *
17997
+ * It rides the wire rather than being resolved by the consumer because the
17998
+ * declaration lives with the step definition, which only the executing node
17999
+ * has: post-analysis holds no step registry, and re-deriving the tier from
18000
+ * `className` there would be exactly the inference this model exists to
18001
+ * forbid. A `label` that arrives WITHOUT this field is refused by the write
18002
+ * rule and logged (`label tier undeclared`) — an older runner therefore
18003
+ * stops enriching rather than guessing, which is why addon-pipeline is
18004
+ * deployed BEFORE addon-post-analysis.
18005
+ */
18006
+ labelTier: union([literal(1), literal(2)]).optional(),
18007
+ /** Model that produced `label` — carried into the tier's attribution. */
18008
+ labelModelId: string().optional(),
16945
18009
  alignedCropJpeg: string().optional(),
16946
18010
  /** Face short side (px) measured on the NATIVE crop surface. The `bbox`
16947
18011
  * above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
@@ -23110,6 +24174,173 @@ DeviceType.Camera, method(object({
23110
24174
  status: OsdStatusSchema
23111
24175
  });
23112
24176
  /**
24177
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
24178
+ *
24179
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
24180
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
24181
+ * comes from, and it must not — a driver that grew a "show the temperature
24182
+ * here" feature would grow it once per vendor.
24183
+ *
24184
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
24185
+ * which value feeds the slot, how it is formatted, and under which
24186
+ * conditions it is shown at all. One addon renders every binding on every
24187
+ * camera, so a new source costs zero driver code.
24188
+ *
24189
+ * Three deliberate choices, each with a rejected alternative:
24190
+ *
24191
+ * 1. A source is `(capName, valuePath)` over the kernel's device
24192
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
24193
+ * cap-keyed slice a device publishes is bindable the day the cap
24194
+ * ships. The rejected alternative (one enum member per source, with
24195
+ * a resolver branch each) is what makes "add the humidity too" a
24196
+ * code change.
24197
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
24198
+ * notification centre's condition vocabulary — rather than a parallel
24199
+ * model. An operator who has learned one condition editor has learned
24200
+ * both.
24201
+ * 3. Because the renderer's facts are device STATE and not a detection
24202
+ * record, only a SUBSET of that vocabulary can be answered here.
24203
+ * `setSlotBinding` REJECTS the rest at write time (see
24204
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
24205
+ * condition that can never be true renders a permanently blank
24206
+ * overlay, and a blank overlay looks exactly like a broken camera.
24207
+ */
24208
+ /** Where a slot's value comes from. */
24209
+ var OsdSourceSchema = discriminatedUnion("kind", [
24210
+ object({
24211
+ kind: literal("static"),
24212
+ text: string().max(64)
24213
+ }),
24214
+ object({
24215
+ kind: literal("clock"),
24216
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
24217
+ pattern: string().min(1).max(32).default("HH:mm"),
24218
+ /** IANA zone. Omitted = the server's zone. */
24219
+ timezone: string().min(1).max(64).optional()
24220
+ }),
24221
+ object({
24222
+ kind: literal("device-state"),
24223
+ deviceId: number().int().optional(),
24224
+ capName: string().min(1).max(64),
24225
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
24226
+ valuePath: string().min(1).max(64)
24227
+ })
24228
+ ]);
24229
+ var OsdSlotBindingSchema = object({
24230
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
24231
+ enabled: boolean().default(true),
24232
+ source: OsdSourceSchema,
24233
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
24234
+ template: string().max(96).default("${value}"),
24235
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
24236
+ maxCharacters: number().int().min(4).max(64).optional(),
24237
+ /**
24238
+ * Decimal places for a numeric value. `0` yields an integer — the
24239
+ * documented workaround for firmwares that reject `.` in overlay text.
24240
+ */
24241
+ maxDecimals: number().int().min(0).max(4).default(1),
24242
+ /** Appended via `${unit}`. The state mirror does not carry units. */
24243
+ unitLabel: string().max(8).optional(),
24244
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
24245
+ valueMap: record(string(), string()).optional(),
24246
+ /** Time windows in which the slot is shown. Absent = always. */
24247
+ schedule: NcScheduleSchema.optional(),
24248
+ /**
24249
+ * Display gate, in the notification centre's condition vocabulary.
24250
+ * Only the keys reported by `getConditionSupport` are accepted.
24251
+ */
24252
+ conditions: NcConditionsSchema.optional(),
24253
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
24254
+ fallbackText: string().max(64).default("")
24255
+ });
24256
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
24257
+ var OsdSlotViewSchema = object({
24258
+ slotId: string(),
24259
+ kind: OsdOverlayKindEnum,
24260
+ /** Firmware refuses text edits (a timestamp, the channel name). */
24261
+ readOnly: boolean(),
24262
+ cameraEnabled: boolean(),
24263
+ cameraText: string().optional(),
24264
+ binding: OsdSlotBindingSchema.nullable()
24265
+ });
24266
+ /**
24267
+ * What happened to one slot on one render pass. `unchanged` exists so the
24268
+ * operator can tell "we are driving this and the value is steady" from
24269
+ * "we never got there" — and so the loop can prove it is not rewriting
24270
+ * identical text to the camera every tick.
24271
+ */
24272
+ var OsdRenderOutcomeEnum = _enum([
24273
+ "written",
24274
+ "unchanged",
24275
+ "gated",
24276
+ "unreadable",
24277
+ "disabled",
24278
+ "unbound",
24279
+ "failed"
24280
+ ]);
24281
+ var OsdRenderResultSchema = object({
24282
+ slotId: string(),
24283
+ outcome: OsdRenderOutcomeEnum,
24284
+ /** The text the slot should carry. Empty = the slot is switched off. */
24285
+ text: string(),
24286
+ /** Why, whenever the outcome is not a plain write. Never silent. */
24287
+ reason: string().optional()
24288
+ });
24289
+ var OsdSourceValueTypeEnum = _enum([
24290
+ "number",
24291
+ "boolean",
24292
+ "string",
24293
+ "enum"
24294
+ ]);
24295
+ /**
24296
+ * One bindable value, derived from a cap's `runtimeState` schema — never
24297
+ * hand-listed. The editor renders from this, so a cap that ships a new
24298
+ * state field becomes bindable with no UI change.
24299
+ */
24300
+ var OsdSourceOptionSchema = object({
24301
+ deviceId: number().int(),
24302
+ deviceName: string(),
24303
+ capName: string(),
24304
+ valuePath: string(),
24305
+ label: string(),
24306
+ valueType: OsdSourceValueTypeEnum,
24307
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
24308
+ enumValues: array(string()).readonly().optional()
24309
+ });
24310
+ method(object({ deviceId: number().int() }), object({
24311
+ supported: boolean(),
24312
+ slots: array(OsdSlotViewSchema)
24313
+ }), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
24314
+ supported: array(string()),
24315
+ catalog: array(NcConditionDescriptorSchema)
24316
+ }), { auth: "admin" }), method(object({
24317
+ deviceId: number().int(),
24318
+ slotId: string().min(1),
24319
+ binding: OsdSlotBindingSchema
24320
+ }), object({
24321
+ slot: OsdSlotViewSchema,
24322
+ render: OsdRenderResultSchema
24323
+ }), {
24324
+ kind: "mutation",
24325
+ auth: "admin"
24326
+ }), method(object({
24327
+ deviceId: number().int(),
24328
+ slotId: string().min(1)
24329
+ }), object({ success: literal(true) }), {
24330
+ kind: "mutation",
24331
+ auth: "admin"
24332
+ }), method(object({
24333
+ deviceId: number().int(),
24334
+ slotId: string().min(1),
24335
+ binding: OsdSlotBindingSchema.optional()
24336
+ }), OsdRenderResultSchema, {
24337
+ kind: "mutation",
24338
+ auth: "admin"
24339
+ }), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
24340
+ kind: "mutation",
24341
+ auth: "admin"
24342
+ });
24343
+ /**
23113
24344
  * Feeder connectivity / power status — mirrors the HA petkit device-status
23114
24345
  * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
23115
24346
  * `on_batteries` (running on battery backup). `null` until first reported.
@@ -29610,6 +30841,48 @@ Object.freeze({
29610
30841
  addonId: null,
29611
30842
  access: "create"
29612
30843
  },
30844
+ "osdManager.clearSlotBinding": {
30845
+ capName: "osd-manager",
30846
+ capScope: "system",
30847
+ addonId: null,
30848
+ access: "delete"
30849
+ },
30850
+ "osdManager.getConditionSupport": {
30851
+ capName: "osd-manager",
30852
+ capScope: "system",
30853
+ addonId: null,
30854
+ access: "view"
30855
+ },
30856
+ "osdManager.getDeviceOsd": {
30857
+ capName: "osd-manager",
30858
+ capScope: "system",
30859
+ addonId: null,
30860
+ access: "view"
30861
+ },
30862
+ "osdManager.getSourceCatalog": {
30863
+ capName: "osd-manager",
30864
+ capScope: "system",
30865
+ addonId: null,
30866
+ access: "view"
30867
+ },
30868
+ "osdManager.previewSlot": {
30869
+ capName: "osd-manager",
30870
+ capScope: "system",
30871
+ addonId: null,
30872
+ access: "create"
30873
+ },
30874
+ "osdManager.renderDevice": {
30875
+ capName: "osd-manager",
30876
+ capScope: "system",
30877
+ addonId: null,
30878
+ access: "create"
30879
+ },
30880
+ "osdManager.setSlotBinding": {
30881
+ capName: "osd-manager",
30882
+ capScope: "system",
30883
+ addonId: null,
30884
+ access: "create"
30885
+ },
29613
30886
  "petFeeder.callPet": {
29614
30887
  capName: "pet-feeder",
29615
30888
  capScope: "device",
@@ -29682,6 +30955,12 @@ Object.freeze({
29682
30955
  addonId: null,
29683
30956
  access: "delete"
29684
30957
  },
30958
+ "pipelineAnalytics.completeRetrainTrack": {
30959
+ capName: "pipeline-analytics",
30960
+ capScope: "device",
30961
+ addonId: null,
30962
+ access: "create"
30963
+ },
29685
30964
  "pipelineAnalytics.deleteDeviceEvents": {
29686
30965
  capName: "pipeline-analytics",
29687
30966
  capScope: "device",
@@ -29694,6 +30973,12 @@ Object.freeze({
29694
30973
  addonId: null,
29695
30974
  access: "delete"
29696
30975
  },
30976
+ "pipelineAnalytics.deselectRetrainFrame": {
30977
+ capName: "pipeline-analytics",
30978
+ capScope: "device",
30979
+ addonId: null,
30980
+ access: "create"
30981
+ },
29697
30982
  "pipelineAnalytics.getActiveTracks": {
29698
30983
  capName: "pipeline-analytics",
29699
30984
  capScope: "device",
@@ -29754,6 +31039,18 @@ Object.freeze({
29754
31039
  addonId: null,
29755
31040
  access: "view"
29756
31041
  },
31042
+ "pipelineAnalytics.getRetrainExportUrl": {
31043
+ capName: "pipeline-analytics",
31044
+ capScope: "device",
31045
+ addonId: null,
31046
+ access: "view"
31047
+ },
31048
+ "pipelineAnalytics.getRetrainFrameImage": {
31049
+ capName: "pipeline-analytics",
31050
+ capScope: "device",
31051
+ addonId: null,
31052
+ access: "view"
31053
+ },
29757
31054
  "pipelineAnalytics.getSensorEvents": {
29758
31055
  capName: "pipeline-analytics",
29759
31056
  capScope: "device",
@@ -29772,6 +31069,18 @@ Object.freeze({
29772
31069
  addonId: null,
29773
31070
  access: "view"
29774
31071
  },
31072
+ "pipelineAnalytics.getTrainingExportSummary": {
31073
+ capName: "pipeline-analytics",
31074
+ capScope: "device",
31075
+ addonId: null,
31076
+ access: "view"
31077
+ },
31078
+ "pipelineAnalytics.getTrainingExportUrl": {
31079
+ capName: "pipeline-analytics",
31080
+ capScope: "device",
31081
+ addonId: null,
31082
+ access: "view"
31083
+ },
29775
31084
  "pipelineAnalytics.listEventKinds": {
29776
31085
  capName: "pipeline-analytics",
29777
31086
  capScope: "device",
@@ -29796,6 +31105,24 @@ Object.freeze({
29796
31105
  addonId: null,
29797
31106
  access: "view"
29798
31107
  },
31108
+ "pipelineAnalytics.listRetrainAnnotations": {
31109
+ capName: "pipeline-analytics",
31110
+ capScope: "device",
31111
+ addonId: null,
31112
+ access: "view"
31113
+ },
31114
+ "pipelineAnalytics.listRetrainFrames": {
31115
+ capName: "pipeline-analytics",
31116
+ capScope: "device",
31117
+ addonId: null,
31118
+ access: "view"
31119
+ },
31120
+ "pipelineAnalytics.listRetrainStaging": {
31121
+ capName: "pipeline-analytics",
31122
+ capScope: "device",
31123
+ addonId: null,
31124
+ access: "view"
31125
+ },
29799
31126
  "pipelineAnalytics.listTrackMedia": {
29800
31127
  capName: "pipeline-analytics",
29801
31128
  capScope: "device",
@@ -29808,6 +31135,12 @@ Object.freeze({
29808
31135
  addonId: null,
29809
31136
  access: "view"
29810
31137
  },
31138
+ "pipelineAnalytics.proposeRetrainAnnotations": {
31139
+ capName: "pipeline-analytics",
31140
+ capScope: "device",
31141
+ addonId: null,
31142
+ access: "create"
31143
+ },
29811
31144
  "pipelineAnalytics.pruneEvents": {
29812
31145
  capName: "pipeline-analytics",
29813
31146
  capScope: "device",
@@ -29838,12 +31171,30 @@ Object.freeze({
29838
31171
  addonId: null,
29839
31172
  access: "create"
29840
31173
  },
31174
+ "pipelineAnalytics.restageRetrainTrack": {
31175
+ capName: "pipeline-analytics",
31176
+ capScope: "device",
31177
+ addonId: null,
31178
+ access: "create"
31179
+ },
31180
+ "pipelineAnalytics.saveRetrainAnnotations": {
31181
+ capName: "pipeline-analytics",
31182
+ capScope: "device",
31183
+ addonId: null,
31184
+ access: "create"
31185
+ },
29841
31186
  "pipelineAnalytics.searchObjectEvents": {
29842
31187
  capName: "pipeline-analytics",
29843
31188
  capScope: "device",
29844
31189
  addonId: null,
29845
31190
  access: "view"
29846
31191
  },
31192
+ "pipelineAnalytics.selectRetrainFrames": {
31193
+ capName: "pipeline-analytics",
31194
+ capScope: "device",
31195
+ addonId: null,
31196
+ access: "create"
31197
+ },
29847
31198
  "pipelineAnalytics.setTrackFlags": {
29848
31199
  capName: "pipeline-analytics",
29849
31200
  capScope: "device",
@@ -31248,6 +32599,12 @@ Object.freeze({
31248
32599
  addonId: null,
31249
32600
  access: "view"
31250
32601
  },
32602
+ "streamBroker.getDeviceAudioMute": {
32603
+ capName: "stream-broker",
32604
+ capScope: "system",
32605
+ addonId: null,
32606
+ access: "view"
32607
+ },
31251
32608
  "streamBroker.getPreBufferInfo": {
31252
32609
  capName: "stream-broker",
31253
32610
  capScope: "system",
@@ -31368,6 +32725,12 @@ Object.freeze({
31368
32725
  addonId: null,
31369
32726
  access: "create"
31370
32727
  },
32728
+ "streamBroker.setDeviceAudioMute": {
32729
+ capName: "stream-broker",
32730
+ capScope: "system",
32731
+ addonId: null,
32732
+ access: "create"
32733
+ },
31371
32734
  "streamBroker.setPreBufferDuration": {
31372
32735
  capName: "stream-broker",
31373
32736
  capScope: "system",
@@ -32015,6 +33378,66 @@ Object.freeze({
32015
33378
  "smtp-provider": "email"
32016
33379
  });
32017
33380
  new Set(["devices", "classes"]);
33381
+ var WEEKDAY_TO_DAY = {
33382
+ Sun: 0,
33383
+ Mon: 1,
33384
+ Tue: 2,
33385
+ Wed: 3,
33386
+ Thu: 4,
33387
+ Fri: 5,
33388
+ Sat: 6
33389
+ };
33390
+ /** Resolve (weekday, minute-of-day) of `atMs` in the schedule's timezone.
33391
+ * An invalid/unknown IANA name falls back to the host timezone. */
33392
+ function localDayMinute(atMs, timezone) {
33393
+ const d = new Date(atMs);
33394
+ if (timezone !== void 0) try {
33395
+ const parts = new Intl.DateTimeFormat("en-US", {
33396
+ timeZone: timezone,
33397
+ weekday: "short",
33398
+ hour: "numeric",
33399
+ minute: "numeric",
33400
+ hourCycle: "h23"
33401
+ }).formatToParts(d);
33402
+ let weekday;
33403
+ let hour;
33404
+ let minute;
33405
+ for (const p of parts) if (p.type === "weekday") weekday = p.value;
33406
+ else if (p.type === "hour") hour = Number(p.value);
33407
+ else if (p.type === "minute") minute = Number(p.value);
33408
+ const day = weekday !== void 0 ? WEEKDAY_TO_DAY[weekday] : void 0;
33409
+ if (day !== void 0 && hour !== void 0 && minute !== void 0) return {
33410
+ day,
33411
+ minute: hour * 60 + minute
33412
+ };
33413
+ } catch {}
33414
+ return {
33415
+ day: d.getDay(),
33416
+ minute: d.getHours() * 60 + d.getMinutes()
33417
+ };
33418
+ }
33419
+ /**
33420
+ * Is the schedule active at `atMs`? No schedule = always active. Windows
33421
+ * are OR'd; a window with `startMinute > endMinute` crosses midnight (it
33422
+ * starts on a listed day and spills into the next). `invert` flips the
33423
+ * result (active OUTSIDE the windows).
33424
+ */
33425
+ function isScheduleActive(schedule, atMs) {
33426
+ if (schedule === void 0) return true;
33427
+ const { day, minute } = localDayMinute(atMs, schedule.timezone);
33428
+ const prevDay = (day + 6) % 7;
33429
+ let inside = false;
33430
+ for (const w of schedule.windows) if (w.startMinute <= w.endMinute) {
33431
+ if (w.days.includes(day) && minute >= w.startMinute && minute < w.endMinute) {
33432
+ inside = true;
33433
+ break;
33434
+ }
33435
+ } else if (w.days.includes(day) && minute >= w.startMinute || w.days.includes(prevDay) && minute < w.endMinute) {
33436
+ inside = true;
33437
+ break;
33438
+ }
33439
+ return schedule.invert === true ? !inside : inside;
33440
+ }
32018
33441
  /**
32019
33442
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
32020
33443
  *
@@ -32257,4 +33680,4 @@ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
32257
33680
  DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
32258
33681
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
32259
33682
  //#endregion
32260
- export { object as $, hfModelUrl as A, errMsg as B, buildEventKindDescriptor as C, embeddingEncoderCapability as D, defineCustomActions as E, readDeviceStateFrom as F, isDeviceScopedCap as G, DeviceType as H, subKindsOf as I, _enum as J, nodePin as K, vectorDimFromBase64 as L, notificationRulesCapability as M, pipelineAnalyticsCapability as N, encodeVectorBase64 as O, plateGalleryCapability as P, number as Q, videoclipsCapability as R, audioMetricsCapability as S, customAction as T, createEvent as U, BaseAddon as V, hydrateSchema as W, boolean as X, array as Y, literal as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, addonWidgetsSourceCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, record as et, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, kebabToCamel as j, faceGalleryCapability as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, unknown as nt, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, sleep as q, EVENT_KIND_BY_CAP as r, EventCategory as rt, NC_TAXONOMY as s, BaseDevice as t, string as tt, NcRulePatchSchema as u, TimelapseRuleSchema as v, cosineSimilarity as w, alarmPanelCapability as x, TrackSourceSchema as y, zoneAnalyticsCapability as z };
33683
+ export { sleep as $, encodeVectorBase64 as A, vectorDimFromBase64 as B, alarmPanelCapability as C, customAction as D, cosineSimilarity as E, notificationRulesCapability as F, buildFfmpegArgs as G, zoneAnalyticsCapability as H, pipelineAnalyticsCapability as I, DeviceType as J, isSoftwareDecode as K, plateGalleryCapability as L, hfModelUrl as M, isScheduleActive as N, defineCustomActions as O, kebabToCamel as P, nodePin as Q, readDeviceStateFrom as R, addonWidgetsSourceCapability as S, buildEventKindDescriptor as T, errMsg as U, videoclipsCapability as V, Fmp4BoxSplitter as W, hydrateSchema as X, createEvent as Y, isDeviceScopedCap as Z, OpsLogEntrySchema as _, LabelAttributionSchema as a, object as at, TimelapseRuleSchema as b, NC_TAXONOMY as c, unknown as ct, NcRulePatchSchema as d, _enum as et, NcRuleSchema as f, NcTaxonomySchema as g, NcSnoozeSuppressedSchema as h, EVENT_PAD_MS as i, number as it, faceGalleryCapability as j, embeddingEncoderCapability as k, NcConditionDescriptorSchema as l, EventCategory as lt, NcSnoozeSchema as m, DEFAULT_EVENT_COLOR as n, boolean as nt, MACRO_LABELS as o, record as ot, NcSnoozeInputSchema as p, BaseAddon as q, EVENT_KIND_BY_CAP as r, literal as rt, NC_CONDITION_CATALOG as s, string as st, BaseDevice as t, array as tt, NcRuleInputSchema as u, RetrainStatusSchema as v, audioMetricsCapability as w, TrackSourceSchema as x, TimelapseRuleInputSchema as y, subKindsOf as z };