@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.
- package/dist/{dist-BaqsyWbd.js → dist-CnSzKo7x.js} +1477 -19
- package/dist/{dist-DYOxxL_e.mjs → dist-DTeVOZBt.mjs} +1442 -19
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +2 -1
- package/dist/node-CnEKzHgS.js +653 -0
- package/dist/node-pIpcQ_aN.mjs +649 -0
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B-HELo_d.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D-6lLgLp.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CiIJt_s8.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-JSo2V9IT.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-wlxZFMRT.mjs → hostInit-DPOdauxs.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +11560 -7502
- package/dist/pipeline-analytics/index.mjs +11560 -7502
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/node-Bjc_xs4R.mjs +0 -147
- package/dist/node-CdY4IvEy.js +0 -151
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
19
19
|
value: mod,
|
|
20
20
|
enumerable: true
|
|
21
21
|
}) : target, mod));
|
|
22
|
-
|
|
22
|
+
require("node:crypto");
|
|
23
23
|
//#region ../types/dist/event-category-41fKf-q9.mjs
|
|
24
24
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
25
25
|
EventCategory["SystemBoot"] = "system.boot";
|
|
@@ -6482,7 +6482,20 @@ var BrokerStatsSchema = object({
|
|
|
6482
6482
|
sampleRate: number(),
|
|
6483
6483
|
channels: number(),
|
|
6484
6484
|
supported: boolean()
|
|
6485
|
-
}).nullable().optional()
|
|
6485
|
+
}).nullable().optional(),
|
|
6486
|
+
/**
|
|
6487
|
+
* BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
|
|
6488
|
+
* distributing none of the device's audio, on live or recording.
|
|
6489
|
+
*
|
|
6490
|
+
* Present so a silent camera can be told apart from a broken one on the
|
|
6491
|
+
* stream panel itself, without cross-referencing the switch group: a
|
|
6492
|
+
* broker holding an `audio` track descriptor while `audioMuted` is true is
|
|
6493
|
+
* working exactly as asked. `audioMutedDropped` counts the audio units
|
|
6494
|
+
* thrown away since the current dial — it is how you confirm from stats
|
|
6495
|
+
* alone that the mute is on the packet path and not merely persisted.
|
|
6496
|
+
*/
|
|
6497
|
+
audioMuted: boolean().optional(),
|
|
6498
|
+
audioMutedDropped: number().optional()
|
|
6486
6499
|
});
|
|
6487
6500
|
/**
|
|
6488
6501
|
* Exporter-facing "profile restream" entry. Returned by
|
|
@@ -7144,6 +7157,473 @@ function sleep(ms) {
|
|
|
7144
7157
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7145
7158
|
}
|
|
7146
7159
|
//#endregion
|
|
7160
|
+
//#region ../types/dist/fmp4-box-splitter-B53u9-Nu.mjs
|
|
7161
|
+
var AUDIO_ENCODER_BY_CODEC = {
|
|
7162
|
+
opus: "libopus",
|
|
7163
|
+
aac: "aac",
|
|
7164
|
+
pcmu: "pcm_mulaw",
|
|
7165
|
+
pcma: "pcm_alaw"
|
|
7166
|
+
};
|
|
7167
|
+
/**
|
|
7168
|
+
* Camera-microphone audio, per codec. Lives HERE rather than in
|
|
7169
|
+
* `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
|
|
7170
|
+
* on these types); it is re-exported from there, which is where to read it.
|
|
7171
|
+
*
|
|
7172
|
+
* Every source in this repo is a mono camera mic. The former broker preset
|
|
7173
|
+
* encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
|
|
7174
|
+
* that is the value this consolidation changed.
|
|
7175
|
+
*/
|
|
7176
|
+
var AUDIO_PRESETS = {
|
|
7177
|
+
aac: {
|
|
7178
|
+
kind: "encode",
|
|
7179
|
+
codec: "aac",
|
|
7180
|
+
bitrateKbps: 128,
|
|
7181
|
+
sampleRateHz: 48e3,
|
|
7182
|
+
channels: 1
|
|
7183
|
+
},
|
|
7184
|
+
opus: {
|
|
7185
|
+
kind: "encode",
|
|
7186
|
+
codec: "opus",
|
|
7187
|
+
bitrateKbps: 64,
|
|
7188
|
+
sampleRateHz: 48e3,
|
|
7189
|
+
channels: 1
|
|
7190
|
+
},
|
|
7191
|
+
pcmu: {
|
|
7192
|
+
kind: "encode",
|
|
7193
|
+
codec: "pcmu",
|
|
7194
|
+
sampleRateHz: 8e3,
|
|
7195
|
+
channels: 1
|
|
7196
|
+
}
|
|
7197
|
+
};
|
|
7198
|
+
/** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
|
|
7199
|
+
function logBannerArgs(level) {
|
|
7200
|
+
return [
|
|
7201
|
+
"-hide_banner",
|
|
7202
|
+
"-loglevel",
|
|
7203
|
+
level
|
|
7204
|
+
];
|
|
7205
|
+
}
|
|
7206
|
+
/** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
|
|
7207
|
+
function isSoftwareDecode(decodeHwAccel) {
|
|
7208
|
+
return !decodeHwAccel || decodeHwAccel === "none" || decodeHwAccel === "copy";
|
|
7209
|
+
}
|
|
7210
|
+
/**
|
|
7211
|
+
* Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
|
|
7212
|
+
* appended to this list by a caller — that is the whole point of the function.
|
|
7213
|
+
*/
|
|
7214
|
+
function buildInputArgs(input, decodeHwAccel) {
|
|
7215
|
+
const args = [];
|
|
7216
|
+
if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
|
|
7217
|
+
if (input.extraArgs?.length) args.push(...input.extraArgs);
|
|
7218
|
+
if (input.analyzeDurationUs !== void 0) args.push("-analyzeduration", String(input.analyzeDurationUs));
|
|
7219
|
+
if (input.probeSizeBytes !== void 0) args.push("-probesize", String(input.probeSizeBytes));
|
|
7220
|
+
if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
|
|
7221
|
+
if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
|
|
7222
|
+
args.push("-i", input.url);
|
|
7223
|
+
return args;
|
|
7224
|
+
}
|
|
7225
|
+
/** The `-vf` filter args, or `[]` when a consumer `-vf` already claims the slot. */
|
|
7226
|
+
function buildVideoFilterArgs(scale, outputArgs) {
|
|
7227
|
+
if (!scale) return [];
|
|
7228
|
+
if (outputArgs.some((a) => a === "-vf")) return [];
|
|
7229
|
+
if (scale.mode === "exact") return ["-vf", `scale=${scale.width}:${scale.height}`];
|
|
7230
|
+
return ["-vf", `scale='min(${scale.width},iw)':'min(${scale.height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2`];
|
|
7231
|
+
}
|
|
7232
|
+
/** Rate-control args for an encode plan. */
|
|
7233
|
+
function buildRateControlArgs(video) {
|
|
7234
|
+
const kbps = video.bitrateKbps;
|
|
7235
|
+
if (kbps === void 0) return [];
|
|
7236
|
+
const rc = video.rateControl ?? {
|
|
7237
|
+
kind: "cap",
|
|
7238
|
+
vbvSeconds: 2
|
|
7239
|
+
};
|
|
7240
|
+
const bufsize = Math.max(1, Math.round(kbps * rc.vbvSeconds));
|
|
7241
|
+
return [
|
|
7242
|
+
...rc.kind === "cbr" ? ["-b:v", `${kbps}k`] : [],
|
|
7243
|
+
"-maxrate",
|
|
7244
|
+
`${kbps}k`,
|
|
7245
|
+
"-bufsize",
|
|
7246
|
+
`${bufsize}k`
|
|
7247
|
+
];
|
|
7248
|
+
}
|
|
7249
|
+
/** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
|
|
7250
|
+
function buildVideoArgs(video, outputArgs) {
|
|
7251
|
+
if (video.kind === "copy") return [
|
|
7252
|
+
"-c:v",
|
|
7253
|
+
"copy",
|
|
7254
|
+
...video.bitstreamFilter ? ["-bsf:v", video.bitstreamFilter] : []
|
|
7255
|
+
];
|
|
7256
|
+
const args = [
|
|
7257
|
+
...buildVideoFilterArgs(video.scale, outputArgs),
|
|
7258
|
+
"-c:v",
|
|
7259
|
+
video.encoder
|
|
7260
|
+
];
|
|
7261
|
+
if (video.preset !== void 0) args.push("-preset", video.preset);
|
|
7262
|
+
if (video.tune !== void 0) args.push("-tune", video.tune);
|
|
7263
|
+
if (video.profile !== void 0) args.push("-profile:v", video.profile);
|
|
7264
|
+
if (video.level !== void 0) args.push("-level", video.level);
|
|
7265
|
+
if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
|
|
7266
|
+
if (video.fps !== void 0) args.push("-r", String(video.fps));
|
|
7267
|
+
if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
|
|
7268
|
+
if (video.forceKeyFramesSeconds !== void 0) args.push("-force_key_frames", `expr:gte(t,n_forced*${video.forceKeyFramesSeconds})`);
|
|
7269
|
+
if (video.bf !== void 0) args.push("-bf", String(video.bf));
|
|
7270
|
+
args.push(...buildRateControlArgs(video));
|
|
7271
|
+
if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
|
|
7272
|
+
return args;
|
|
7273
|
+
}
|
|
7274
|
+
/** The whole audio block, after `-i`. */
|
|
7275
|
+
function buildAudioArgs(audio) {
|
|
7276
|
+
if (audio.kind === "none") return ["-an"];
|
|
7277
|
+
if (audio.kind === "copy") return ["-c:a", "copy"];
|
|
7278
|
+
const args = [];
|
|
7279
|
+
if (audio.filter !== void 0) args.push("-af", audio.filter);
|
|
7280
|
+
args.push("-c:a", AUDIO_ENCODER_BY_CODEC[audio.codec]);
|
|
7281
|
+
if (audio.application !== void 0) args.push("-application", audio.application);
|
|
7282
|
+
if (audio.frameDurationMs !== void 0) args.push("-frame_duration", String(audio.frameDurationMs));
|
|
7283
|
+
if (audio.globalHeader === true) args.push("-flags", "+global_header");
|
|
7284
|
+
if (audio.sampleRateHz !== void 0) args.push("-ar", String(audio.sampleRateHz));
|
|
7285
|
+
if (audio.bitrateKbps !== void 0) args.push("-b:a", `${audio.bitrateKbps}k`);
|
|
7286
|
+
if (audio.vbvBufferKbits !== void 0) args.push("-bufsize", `${audio.vbvBufferKbits}k`);
|
|
7287
|
+
if (audio.channels !== void 0) args.push("-ac", String(audio.channels));
|
|
7288
|
+
return args;
|
|
7289
|
+
}
|
|
7290
|
+
/** RTP output-leg args (`-payload_type`, `-ssrc`, `-sdp_file`, `-f rtp <url>`). */
|
|
7291
|
+
function buildRtpOutputArgs(out) {
|
|
7292
|
+
const args = [];
|
|
7293
|
+
if (out.payloadType !== void 0) args.push("-payload_type", String(out.payloadType));
|
|
7294
|
+
if (out.ssrc !== void 0) args.push("-ssrc", String(out.ssrc));
|
|
7295
|
+
if (out.sdpFile !== void 0) args.push("-sdp_file", out.sdpFile);
|
|
7296
|
+
args.push("-f", "rtp", out.url);
|
|
7297
|
+
return args;
|
|
7298
|
+
}
|
|
7299
|
+
/** `true` when the sink is a raw elementary bytestream that cannot mux audio. */
|
|
7300
|
+
function isElementaryVideoSink(sink) {
|
|
7301
|
+
return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
|
|
7302
|
+
}
|
|
7303
|
+
/**
|
|
7304
|
+
* The fragmented-MP4 muxer flags, in the order the recorder has proven them
|
|
7305
|
+
* (`recorder/addon/ffmpeg-args.ts` passes the same `movflags` string through
|
|
7306
|
+
* `-segment_format_options`, across every vendor in the fleet):
|
|
7307
|
+
*
|
|
7308
|
+
* - `frag_keyframe` — cut a fragment at each key frame, so every fragment
|
|
7309
|
+
* opens on a sync sample. HKSV's whole requirement.
|
|
7310
|
+
* - `empty_moov` — write `ftyp`+`moov` up front with no samples in it, which
|
|
7311
|
+
* is what makes the head a standalone INITIALISATION segment.
|
|
7312
|
+
* - `default_base_moof` — fragment offsets are self-relative, so a fragment is
|
|
7313
|
+
* demuxable without the bytes that preceded it. D31's byte-range read path
|
|
7314
|
+
* depends on exactly this property of the recorder's segments.
|
|
7315
|
+
*/
|
|
7316
|
+
var FMP4_MOVFLAGS = "+frag_keyframe+empty_moov+default_base_moof";
|
|
7317
|
+
/**
|
|
7318
|
+
* The terminal sink args for every non-`rtp-outputs` sink. Exhaustive over the
|
|
7319
|
+
* union so a new member cannot fall through to `['-f', container, 'pipe:1']`,
|
|
7320
|
+
* which is what a plain `container` read would have done for `mp4` — a valid
|
|
7321
|
+
* argv that writes a NON-fragmented, unseekable-to-a-pipe MP4 and produces one
|
|
7322
|
+
* unusable byte stream.
|
|
7323
|
+
*/
|
|
7324
|
+
function buildStdoutOrRtspSinkArgs(sink) {
|
|
7325
|
+
if (sink.kind === "rtsp-listen") return [
|
|
7326
|
+
"-f",
|
|
7327
|
+
"rtsp",
|
|
7328
|
+
"-rtsp_transport",
|
|
7329
|
+
"tcp",
|
|
7330
|
+
"-rtsp_flags",
|
|
7331
|
+
"listen",
|
|
7332
|
+
sink.url
|
|
7333
|
+
];
|
|
7334
|
+
if (sink.kind === "rtp-outputs") return [];
|
|
7335
|
+
return sink.container === "mp4" ? buildFmp4SinkArgs(sink) : [
|
|
7336
|
+
"-f",
|
|
7337
|
+
sink.container,
|
|
7338
|
+
"pipe:1"
|
|
7339
|
+
];
|
|
7340
|
+
}
|
|
7341
|
+
/**
|
|
7342
|
+
* How far BELOW the negotiated fragment length `-min_frag_duration` is set.
|
|
7343
|
+
*
|
|
7344
|
+
* `-min_frag_duration` refuses to cut before that much media has accumulated,
|
|
7345
|
+
* and then waits for the next key frame. Set to exactly `fragmentMs`, the
|
|
7346
|
+
* commonest camera configuration in existence — a key-frame grid EQUAL to the
|
|
7347
|
+
* requested fragment length — lands the deadline on the same instant as the key
|
|
7348
|
+
* frame, loses the race, and skips to the following one: **every fragment comes
|
|
7349
|
+
* out at twice the requested length.**
|
|
7350
|
+
*
|
|
7351
|
+
* Measured on the live fleet 2026-08-07, camera 615, `-c:v copy` (D84):
|
|
7352
|
+
*
|
|
7353
|
+
* | slot | GOP | `-min_frag_duration` | median gap |
|
|
7354
|
+
* | --- | --- | --- | --- |
|
|
7355
|
+
* | 1280×720 | 40 f @ 10 fps = 4.0 s | 4000 ms | **7944 ms** |
|
|
7356
|
+
* | 1280×720 | 40 f @ 10 fps = 4.0 s | 3600 ms | 3973 ms |
|
|
7357
|
+
* | 3840×2160 | 100 f @ 25 fps = 4.0 s | 4000 ms | 8042 ms |
|
|
7358
|
+
* | 3840×2160 | 100 f @ 25 fps = 4.0 s | 3600 ms | 3998 ms |
|
|
7359
|
+
*
|
|
7360
|
+
* A doubled fragment is not a cosmetic overshoot: HKSV requires every fragment
|
|
7361
|
+
* to be no longer than the length the controller SELECTED, so the shipped-but-
|
|
7362
|
+
* inert phase-1 sink would have violated the contract on its first real clip.
|
|
7363
|
+
*
|
|
7364
|
+
* 10 % is chosen against the two failures either side of it. Too small and
|
|
7365
|
+
* ordinary jitter (measured spread 3953-4096 ms) re-loses the race; too large
|
|
7366
|
+
* and a source with a key frame slightly EARLY than the grid gets cut there,
|
|
7367
|
+
* yielding a short fragment for no reason.
|
|
7368
|
+
*/
|
|
7369
|
+
var FMP4_MIN_FRAG_MARGIN = .9;
|
|
7370
|
+
/** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
|
|
7371
|
+
function buildFmp4SinkArgs(sink) {
|
|
7372
|
+
return [
|
|
7373
|
+
"-movflags",
|
|
7374
|
+
FMP4_MOVFLAGS,
|
|
7375
|
+
"-min_frag_duration",
|
|
7376
|
+
String(Math.max(0, Math.round(sink.fragmentMs * FMP4_MIN_FRAG_MARGIN * 1e3))),
|
|
7377
|
+
"-f",
|
|
7378
|
+
"mp4",
|
|
7379
|
+
"pipe:1"
|
|
7380
|
+
];
|
|
7381
|
+
}
|
|
7382
|
+
/**
|
|
7383
|
+
* A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
|
|
7384
|
+
* audio optional so a source with no audio skips it instead of failing the
|
|
7385
|
+
* whole invocation.
|
|
7386
|
+
*/
|
|
7387
|
+
function buildAudioSidecarArgs(sidecar) {
|
|
7388
|
+
return [
|
|
7389
|
+
"-map",
|
|
7390
|
+
"0:a:0?",
|
|
7391
|
+
...buildAudioArgs(sidecar.codec === "pcma" ? {
|
|
7392
|
+
kind: "encode",
|
|
7393
|
+
codec: "pcma",
|
|
7394
|
+
sampleRateHz: 8e3,
|
|
7395
|
+
channels: 1
|
|
7396
|
+
} : AUDIO_PRESETS[sidecar.codec]),
|
|
7397
|
+
...buildRtpOutputArgs({
|
|
7398
|
+
url: sidecar.rtpUrl,
|
|
7399
|
+
sdpFile: sidecar.sdpFile
|
|
7400
|
+
})
|
|
7401
|
+
];
|
|
7402
|
+
}
|
|
7403
|
+
/**
|
|
7404
|
+
* Assemble the full ffmpeg argument list. Layout:
|
|
7405
|
+
*
|
|
7406
|
+
* -hide_banner -loglevel <level>
|
|
7407
|
+
* [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
|
|
7408
|
+
* [<input.extraArgs>] │
|
|
7409
|
+
* [-fflags <flag>…] │
|
|
7410
|
+
* [-rtsp_transport tcp] │
|
|
7411
|
+
* -i <url> ─┘
|
|
7412
|
+
* <video block> <threads> <audio block> ─┐ OUTPUT options.
|
|
7413
|
+
* <consumer outputArgs verbatim> │
|
|
7414
|
+
* <sink> ─┘ terminal
|
|
7415
|
+
*/
|
|
7416
|
+
function buildFfmpegArgs(inv) {
|
|
7417
|
+
const head = [...logBannerArgs(inv.logLevel), ...buildInputArgs(inv.input, inv.decodeHwAccel)];
|
|
7418
|
+
const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
|
|
7419
|
+
if (inv.sink.kind === "rtp-outputs") {
|
|
7420
|
+
const videoLeg = inv.sink.video ? [
|
|
7421
|
+
"-an",
|
|
7422
|
+
"-map",
|
|
7423
|
+
"0:v:0",
|
|
7424
|
+
...buildVideoArgs(inv.video, inv.outputArgs),
|
|
7425
|
+
...threadArgs,
|
|
7426
|
+
...inv.outputArgs,
|
|
7427
|
+
...buildRtpOutputArgs(inv.sink.video)
|
|
7428
|
+
] : [];
|
|
7429
|
+
const audioLeg = inv.sink.audio ? [
|
|
7430
|
+
"-vn",
|
|
7431
|
+
"-map",
|
|
7432
|
+
"0:a:0?",
|
|
7433
|
+
...buildAudioArgs(inv.audio),
|
|
7434
|
+
...buildRtpOutputArgs(inv.sink.audio)
|
|
7435
|
+
] : [];
|
|
7436
|
+
return [
|
|
7437
|
+
...head,
|
|
7438
|
+
...videoLeg,
|
|
7439
|
+
...audioLeg
|
|
7440
|
+
];
|
|
7441
|
+
}
|
|
7442
|
+
const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
|
|
7443
|
+
const sinkArgs = buildStdoutOrRtspSinkArgs(inv.sink);
|
|
7444
|
+
return [
|
|
7445
|
+
...head,
|
|
7446
|
+
...buildVideoArgs(inv.video, inv.outputArgs),
|
|
7447
|
+
...threadArgs,
|
|
7448
|
+
...audioArgs,
|
|
7449
|
+
...inv.outputArgs,
|
|
7450
|
+
...sinkArgs,
|
|
7451
|
+
...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
|
|
7452
|
+
];
|
|
7453
|
+
}
|
|
7454
|
+
var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
|
|
7455
|
+
/** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
|
|
7456
|
+
var BOX_HEADER_BYTES = 8;
|
|
7457
|
+
var LARGE_BOX_HEADER_BYTES = 16;
|
|
7458
|
+
var Fmp4BoxSplitter = class {
|
|
7459
|
+
maxUnitBytes;
|
|
7460
|
+
/** Bytes of the CURRENT unit plus any partial box after it. */
|
|
7461
|
+
buffer = new Uint8Array(0);
|
|
7462
|
+
/** Where the current unit starts inside {@link buffer}. */
|
|
7463
|
+
unitStart = 0;
|
|
7464
|
+
/** Where the box scanner has reached inside {@link buffer}. */
|
|
7465
|
+
cursor = 0;
|
|
7466
|
+
state = "init";
|
|
7467
|
+
nextSequence = 0;
|
|
7468
|
+
faultReason = null;
|
|
7469
|
+
interstitial = /* @__PURE__ */ new Set();
|
|
7470
|
+
constructor(options = {}) {
|
|
7471
|
+
this.maxUnitBytes = options.maxUnitBytes ?? DEFAULT_MAX_UNIT_BYTES;
|
|
7472
|
+
}
|
|
7473
|
+
/**
|
|
7474
|
+
* Non-null once the stream cannot be split. The splitter emits nothing
|
|
7475
|
+
* further, so a caller polls this to kill the child rather than watching a
|
|
7476
|
+
* silent stall — a fragmenter that quietly stops producing looks exactly like
|
|
7477
|
+
* a camera with no motion.
|
|
7478
|
+
*/
|
|
7479
|
+
get fault() {
|
|
7480
|
+
return this.faultReason;
|
|
7481
|
+
}
|
|
7482
|
+
/** Bytes currently held. The memory bound, observable rather than asserted. */
|
|
7483
|
+
get pendingBytes() {
|
|
7484
|
+
return this.buffer.length - this.unitStart;
|
|
7485
|
+
}
|
|
7486
|
+
/**
|
|
7487
|
+
* Top-level box types seen BETWEEN fragments and discarded — `mfra`, `free`,
|
|
7488
|
+
* a stray `sidx`. Reported rather than dropped in silence: they are legal and
|
|
7489
|
+
* useless to a fragment consumer, but a type nobody expected showing up here
|
|
7490
|
+
* is the first symptom of a muxer that is not writing what we think it is.
|
|
7491
|
+
*/
|
|
7492
|
+
get discardedInterstitialTypes() {
|
|
7493
|
+
return [...this.interstitial];
|
|
7494
|
+
}
|
|
7495
|
+
/**
|
|
7496
|
+
* Feed bytes; get back whatever units completed. Returns `[]` once faulted.
|
|
7497
|
+
*/
|
|
7498
|
+
push(chunk) {
|
|
7499
|
+
if (this.faultReason !== null || chunk.length === 0) return [];
|
|
7500
|
+
this.append(chunk);
|
|
7501
|
+
if (this.pendingBytes > this.maxUnitBytes) return this.fail(`a single fMP4 unit exceeded ${this.maxUnitBytes} bytes — this stream is not fragmented`);
|
|
7502
|
+
return this.drainBoxes();
|
|
7503
|
+
}
|
|
7504
|
+
append(chunk) {
|
|
7505
|
+
if (this.buffer.length === 0) {
|
|
7506
|
+
this.buffer = chunk.slice();
|
|
7507
|
+
return;
|
|
7508
|
+
}
|
|
7509
|
+
const next = new Uint8Array(this.buffer.length + chunk.length);
|
|
7510
|
+
next.set(this.buffer, 0);
|
|
7511
|
+
next.set(chunk, this.buffer.length);
|
|
7512
|
+
this.buffer = next;
|
|
7513
|
+
}
|
|
7514
|
+
/** Consume every COMPLETE top-level box now in the buffer. */
|
|
7515
|
+
drainBoxes() {
|
|
7516
|
+
const units = [];
|
|
7517
|
+
for (;;) {
|
|
7518
|
+
const header = this.readHeader();
|
|
7519
|
+
if (this.faultReason !== null) return units;
|
|
7520
|
+
if (header === null) break;
|
|
7521
|
+
if (this.cursor + header.totalBytes > this.buffer.length) break;
|
|
7522
|
+
const boxStart = this.cursor;
|
|
7523
|
+
const boxEnd = boxStart + header.totalBytes;
|
|
7524
|
+
this.cursor = boxEnd;
|
|
7525
|
+
const unit = this.consumeBox(header.type, boxStart, boxEnd);
|
|
7526
|
+
if (this.faultReason !== null) return units;
|
|
7527
|
+
if (unit !== null) units.push(unit);
|
|
7528
|
+
}
|
|
7529
|
+
this.compact();
|
|
7530
|
+
return units;
|
|
7531
|
+
}
|
|
7532
|
+
/**
|
|
7533
|
+
* Apply one box to the state machine. Returns a unit when this box CLOSED
|
|
7534
|
+
* one, `null` otherwise.
|
|
7535
|
+
*/
|
|
7536
|
+
consumeBox(type, boxStart, boxEnd) {
|
|
7537
|
+
if (this.state === "init") {
|
|
7538
|
+
if (type !== "moof") return null;
|
|
7539
|
+
if (boxStart === this.unitStart) {
|
|
7540
|
+
this.fail("a moof arrived before any initialisation box — there is no ftyp/moov to send");
|
|
7541
|
+
return null;
|
|
7542
|
+
}
|
|
7543
|
+
const init = this.emit("init", this.unitStart, boxStart);
|
|
7544
|
+
this.unitStart = boxStart;
|
|
7545
|
+
this.state = "fragment";
|
|
7546
|
+
return init;
|
|
7547
|
+
}
|
|
7548
|
+
if (this.state === "idle") {
|
|
7549
|
+
if (type !== "moof") {
|
|
7550
|
+
this.interstitial.add(type);
|
|
7551
|
+
this.unitStart = boxEnd;
|
|
7552
|
+
return null;
|
|
7553
|
+
}
|
|
7554
|
+
this.unitStart = boxStart;
|
|
7555
|
+
this.state = "fragment";
|
|
7556
|
+
return null;
|
|
7557
|
+
}
|
|
7558
|
+
if (type !== "mdat") return null;
|
|
7559
|
+
const fragment = this.emit("fragment", this.unitStart, boxEnd);
|
|
7560
|
+
this.unitStart = boxEnd;
|
|
7561
|
+
this.state = "idle";
|
|
7562
|
+
return fragment;
|
|
7563
|
+
}
|
|
7564
|
+
/**
|
|
7565
|
+
* Parse the header at {@link cursor}, or `null` when too few bytes have
|
|
7566
|
+
* arrived to know. Faults on a size the splitter cannot honour.
|
|
7567
|
+
*/
|
|
7568
|
+
readHeader() {
|
|
7569
|
+
const available = this.buffer.length - this.cursor;
|
|
7570
|
+
if (available < BOX_HEADER_BYTES) return null;
|
|
7571
|
+
const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
|
|
7572
|
+
const size = view.getUint32(this.cursor);
|
|
7573
|
+
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);
|
|
7574
|
+
if (size === 0) {
|
|
7575
|
+
this.fail(`box "${type}" declares size 0 (to EOF) — an unbounded box cannot be fragmented`);
|
|
7576
|
+
return null;
|
|
7577
|
+
}
|
|
7578
|
+
if (size === 1) {
|
|
7579
|
+
if (available < LARGE_BOX_HEADER_BYTES) return null;
|
|
7580
|
+
const large = view.getBigUint64(this.cursor + BOX_HEADER_BYTES);
|
|
7581
|
+
if (large > BigInt(this.maxUnitBytes)) {
|
|
7582
|
+
this.fail(`box "${type}" declares ${large} bytes, over the ${this.maxUnitBytes} byte bound`);
|
|
7583
|
+
return null;
|
|
7584
|
+
}
|
|
7585
|
+
return {
|
|
7586
|
+
type,
|
|
7587
|
+
totalBytes: Number(large)
|
|
7588
|
+
};
|
|
7589
|
+
}
|
|
7590
|
+
if (size < BOX_HEADER_BYTES) {
|
|
7591
|
+
this.fail(`box "${type}" declares an impossible size of ${size} bytes`);
|
|
7592
|
+
return null;
|
|
7593
|
+
}
|
|
7594
|
+
return {
|
|
7595
|
+
type,
|
|
7596
|
+
totalBytes: size
|
|
7597
|
+
};
|
|
7598
|
+
}
|
|
7599
|
+
emit(kind, start, end) {
|
|
7600
|
+
const sequence = this.nextSequence;
|
|
7601
|
+
this.nextSequence += 1;
|
|
7602
|
+
return {
|
|
7603
|
+
kind,
|
|
7604
|
+
data: this.buffer.slice(start, end),
|
|
7605
|
+
sequence
|
|
7606
|
+
};
|
|
7607
|
+
}
|
|
7608
|
+
/**
|
|
7609
|
+
* Drop everything already emitted or discarded. Without this the buffer is
|
|
7610
|
+
* the whole stream and the process dies in hours, not minutes.
|
|
7611
|
+
*/
|
|
7612
|
+
compact() {
|
|
7613
|
+
if (this.unitStart === 0) return;
|
|
7614
|
+
this.buffer = this.buffer.slice(this.unitStart);
|
|
7615
|
+
this.cursor -= this.unitStart;
|
|
7616
|
+
this.unitStart = 0;
|
|
7617
|
+
}
|
|
7618
|
+
fail(reason) {
|
|
7619
|
+
this.faultReason = reason;
|
|
7620
|
+
this.buffer = new Uint8Array(0);
|
|
7621
|
+
this.unitStart = 0;
|
|
7622
|
+
this.cursor = 0;
|
|
7623
|
+
return [];
|
|
7624
|
+
}
|
|
7625
|
+
};
|
|
7626
|
+
//#endregion
|
|
7147
7627
|
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
7148
7628
|
/**
|
|
7149
7629
|
import { errMsg } from '@camstack/types'
|
|
@@ -7313,6 +7793,19 @@ object({
|
|
|
7313
7793
|
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
7314
7794
|
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
7315
7795
|
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
7796
|
+
* | `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 |
|
|
7797
|
+
*
|
|
7798
|
+
* ## `device-audio` and `broker-audio` are two functions, not two knobs
|
|
7799
|
+
*
|
|
7800
|
+
* They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
|
|
7801
|
+
* `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
|
|
7802
|
+
* genuinely stops, it survives CamStack entirely, and it costs a multi-second
|
|
7803
|
+
* encoder restart on every flip. `broker-audio` writes THIS server, so it is
|
|
7804
|
+
* instant, vendor-independent and reversible without touching the camera, and
|
|
7805
|
+
* a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
|
|
7806
|
+
* D62 forbids a second switch that *disagrees* with the first; these two
|
|
7807
|
+
* cannot disagree, because neither reads the other's store — the camera holds
|
|
7808
|
+
* one, the broker holds the other, and each reports its own fact.
|
|
7316
7809
|
*
|
|
7317
7810
|
* ## The two switches whose authority is not on this server
|
|
7318
7811
|
*
|
|
@@ -7374,6 +7867,7 @@ var CameraSwitchIdSchema = _enum([
|
|
|
7374
7867
|
"object-detection",
|
|
7375
7868
|
"privacy-mask",
|
|
7376
7869
|
"device-audio",
|
|
7870
|
+
"broker-audio",
|
|
7377
7871
|
"audio-analysis",
|
|
7378
7872
|
"recording",
|
|
7379
7873
|
"notifications"
|
|
@@ -7399,7 +7893,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7399
7893
|
object({
|
|
7400
7894
|
kind: literal("camera-mask"),
|
|
7401
7895
|
capName: string()
|
|
7402
|
-
})
|
|
7896
|
+
}),
|
|
7897
|
+
object({ kind: literal("broker-audio-mute") })
|
|
7403
7898
|
]);
|
|
7404
7899
|
/**
|
|
7405
7900
|
* Why a switch is not offered for this camera. Rendered instead of the
|
|
@@ -9621,7 +10116,25 @@ method(object({
|
|
|
9621
10116
|
}), _void(), {
|
|
9622
10117
|
kind: "mutation",
|
|
9623
10118
|
auth: "admin"
|
|
9624
|
-
}), method(object({ brokerId: string() }), boolean()), object({
|
|
10119
|
+
}), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
|
|
10120
|
+
muted: boolean(),
|
|
10121
|
+
/**
|
|
10122
|
+
* How many live non-derived brokers currently hold the mute. Purely
|
|
10123
|
+
* diagnostic: `muted` is the policy and is authoritative on its own
|
|
10124
|
+
* (it applies to brokers that do not exist yet), while this says
|
|
10125
|
+
* whether anything is presently being silenced.
|
|
10126
|
+
*/
|
|
10127
|
+
appliedBrokers: number().int().nonnegative()
|
|
10128
|
+
})), method(object({
|
|
10129
|
+
deviceId: number().int(),
|
|
10130
|
+
muted: boolean()
|
|
10131
|
+
}), object({
|
|
10132
|
+
muted: boolean(),
|
|
10133
|
+
appliedBrokers: number().int().nonnegative()
|
|
10134
|
+
}), {
|
|
10135
|
+
kind: "mutation",
|
|
10136
|
+
auth: "admin"
|
|
10137
|
+
}), object({
|
|
9625
10138
|
deviceId: number().int().nonnegative(),
|
|
9626
10139
|
camStreamId: string(),
|
|
9627
10140
|
profile: CamProfileSchema
|
|
@@ -15577,6 +16090,30 @@ var TrackSourceSchema = _enum([
|
|
|
15577
16090
|
"audio"
|
|
15578
16091
|
]);
|
|
15579
16092
|
/**
|
|
16093
|
+
* Where a track sits in the RETRAIN lifecycle (D81).
|
|
16094
|
+
*
|
|
16095
|
+
* - `none` — never marked, or un-marked. Evictable.
|
|
16096
|
+
* - `staging` — the operator wants this track as training material and has not
|
|
16097
|
+
* finished with it. **This is the only state retention holds**: the track and
|
|
16098
|
+
* everything it owns (object events, crops, keyframes, CLIP vector) survive
|
|
16099
|
+
* the device's age window.
|
|
16100
|
+
* - `trained` — the retrain page has taken what it needed. The frames it chose
|
|
16101
|
+
* were COPIED into the retrain dataset at selection time, so the dataset no
|
|
16102
|
+
* longer depends on the track's media and the track becomes EVICTABLE again.
|
|
16103
|
+
* Terminal for the plain `markForTrain` toggle: returning it to `staging` is
|
|
16104
|
+
* a deliberate action of the retrain page, not a side effect of a checkbox.
|
|
16105
|
+
*
|
|
16106
|
+
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
|
|
16107
|
+
* the store's filter language has only positive equality and `whereIn` — no
|
|
16108
|
+
* negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
|
|
16109
|
+
* would make the entire pre-column history immortal in one deploy.
|
|
16110
|
+
*/
|
|
16111
|
+
var RetrainStatusSchema = _enum([
|
|
16112
|
+
"none",
|
|
16113
|
+
"staging",
|
|
16114
|
+
"trained"
|
|
16115
|
+
]);
|
|
16116
|
+
/**
|
|
15580
16117
|
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
15581
16118
|
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
15582
16119
|
* so the two surfaces cannot drift.
|
|
@@ -15586,18 +16123,31 @@ var TrackSourceSchema = _enum([
|
|
|
15586
16123
|
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
15587
16124
|
* `flag === true`, not `flag !== false`.
|
|
15588
16125
|
*
|
|
15589
|
-
*
|
|
15590
|
-
*
|
|
15591
|
-
*
|
|
15592
|
-
* `
|
|
16126
|
+
* `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
|
|
16127
|
+
* it is exactly `retrainStatus === 'staging'`, in both directions. Writing
|
|
16128
|
+
* `true` moves `none → staging`, writing `false` moves `staging → none`, and a
|
|
16129
|
+
* `trained` track reports `false` while refusing both writes. The boolean is
|
|
16130
|
+
* kept because three surfaces drive a toggle off it; anything that needs to tell
|
|
16131
|
+
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16132
|
+
*
|
|
16133
|
+
* `debug` does NOT pin; it is attention, not durability.
|
|
15593
16134
|
*/
|
|
15594
16135
|
var TrackFlagFields = {
|
|
15595
|
-
/** Operator marked this track as training material.
|
|
16136
|
+
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16137
|
+
* `'staging'`. */
|
|
15596
16138
|
markForTrain: boolean().optional(),
|
|
15597
16139
|
/** Operator marked this track for diagnostic attention. */
|
|
15598
16140
|
debug: boolean().optional()
|
|
15599
16141
|
};
|
|
15600
16142
|
/**
|
|
16143
|
+
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
16144
|
+
* Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
|
|
16145
|
+
* write patch, and the status is not something the toggle sets — it is what the
|
|
16146
|
+
* toggle's boolean is derived from. Absent on an in-RAM track never touched;
|
|
16147
|
+
* always present on a persisted row (the column default materialises `'none'`).
|
|
16148
|
+
*/
|
|
16149
|
+
var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
|
|
16150
|
+
/**
|
|
15601
16151
|
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
15602
16152
|
* one flag can never clear the other — the toggles are independent and are
|
|
15603
16153
|
* driven from three surfaces that do not know about each other.
|
|
@@ -15611,13 +16161,92 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
|
|
|
15611
16161
|
var TrackFlagsSchema = object({
|
|
15612
16162
|
trackId: string(),
|
|
15613
16163
|
markForTrain: boolean(),
|
|
15614
|
-
debug: boolean()
|
|
16164
|
+
debug: boolean(),
|
|
16165
|
+
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16166
|
+
* a track row) because this shape is only ever produced by the write body,
|
|
16167
|
+
* which always knows it — and a surface that has just written needs to render
|
|
16168
|
+
* `trained` without a re-fetch. */
|
|
16169
|
+
retrainStatus: RetrainStatusSchema
|
|
16170
|
+
});
|
|
16171
|
+
union([literal(1), literal(2)]);
|
|
16172
|
+
/**
|
|
16173
|
+
* WHO decided a label, and when. Carried per tier so a value can be traced to
|
|
16174
|
+
* the step and model that produced it — which is what makes the write rule
|
|
16175
|
+
* arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
|
|
16176
|
+
* and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
|
|
16177
|
+
*
|
|
16178
|
+
* `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
|
|
16179
|
+
* `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
|
|
16180
|
+
* `migration:4g` for a value the 4g migration moved from the single-slot era —
|
|
16181
|
+
* that value has no provenance, and the write rule lets ANY properly-attributed
|
|
16182
|
+
* write of the same tier replace it regardless of score.
|
|
16183
|
+
*/
|
|
16184
|
+
var LabelAttributionSchema = object({
|
|
16185
|
+
stepId: string(),
|
|
16186
|
+
modelId: string().optional(),
|
|
16187
|
+
decidedAt: number()
|
|
16188
|
+
});
|
|
16189
|
+
/**
|
|
16190
|
+
* The TIERED label model (roadmap 4g), spread into `TrackSchema` and
|
|
16191
|
+
* `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
|
|
16192
|
+
* track and its events always answer the same question the same way.
|
|
16193
|
+
*
|
|
16194
|
+
* Two scalar columns, not an array: every consumer wants "the coarse one" or
|
|
16195
|
+
* "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
|
|
16196
|
+
* is tier 2, and each carries its own score + attribution.
|
|
16197
|
+
*
|
|
16198
|
+
* **Reading it.** What a human should be shown is `subLabel ?? label` — the
|
|
16199
|
+
* finest thing known. Before 4g the single `label` column held the finest
|
|
16200
|
+
* value, so a consumer that has not been updated reads the tier-1 slot and
|
|
16201
|
+
* shows nothing on a species-only row; that is why the migration puts every
|
|
16202
|
+
* pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
|
|
16203
|
+
* and why the read surfaces were changed in the same train.
|
|
16204
|
+
*
|
|
16205
|
+
* **Writing it.** The slots are independent, which is the whole point: a
|
|
16206
|
+
* tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
|
|
16207
|
+
* migratorius`), so fineness cannot regress by construction. Within a tier the
|
|
16208
|
+
* higher score wins. One rule, one implementation — see
|
|
16209
|
+
* `pipeline/label-tier.ts` in addon-post-analysis.
|
|
16210
|
+
*/
|
|
16211
|
+
var TieredLabelFields = {
|
|
16212
|
+
/** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
|
|
16213
|
+
label: string().optional(),
|
|
16214
|
+
/** Confidence of the tier-1 value, as reported by the deciding step. */
|
|
16215
|
+
labelScore: number().optional(),
|
|
16216
|
+
/** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
|
|
16217
|
+
labelMeta: LabelAttributionSchema.optional(),
|
|
16218
|
+
/** Tier 2 — the instance. See {@link LabelTierSchema}. */
|
|
16219
|
+
subLabel: string().optional(),
|
|
16220
|
+
/** Confidence of the tier-2 value, as reported by the deciding step. */
|
|
16221
|
+
subLabelScore: number().optional(),
|
|
16222
|
+
/** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
|
|
16223
|
+
subLabelMeta: LabelAttributionSchema.optional()
|
|
16224
|
+
};
|
|
16225
|
+
/** Per-camera slice of a training-export estimate. */
|
|
16226
|
+
var TrainingExportDeviceTotalsSchema = object({
|
|
16227
|
+
deviceId: number(),
|
|
16228
|
+
tracks: number().int(),
|
|
16229
|
+
files: number().int(),
|
|
16230
|
+
bytes: number().int()
|
|
16231
|
+
});
|
|
16232
|
+
/**
|
|
16233
|
+
* What a training export WOULD contain. Computed from media index rows only —
|
|
16234
|
+
* no blob is read to produce this.
|
|
16235
|
+
*/
|
|
16236
|
+
var TrainingExportSummarySchema = object({
|
|
16237
|
+
generatedAt: number(),
|
|
16238
|
+
trackCount: number().int(),
|
|
16239
|
+
fileCount: number().int(),
|
|
16240
|
+
byteCount: number().int(),
|
|
16241
|
+
/** More marked tracks exist than a single pass carries. */
|
|
16242
|
+
truncated: boolean(),
|
|
16243
|
+
devices: array(TrainingExportDeviceTotalsSchema).readonly()
|
|
15615
16244
|
});
|
|
15616
16245
|
var TrackSchema = object({
|
|
15617
16246
|
trackId: string(),
|
|
15618
16247
|
deviceId: number(),
|
|
15619
16248
|
className: string(),
|
|
15620
|
-
|
|
16249
|
+
...TieredLabelFields,
|
|
15621
16250
|
producingDeviceName: string().optional(),
|
|
15622
16251
|
/** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
|
|
15623
16252
|
source: TrackSourceSchema.optional(),
|
|
@@ -15656,7 +16285,8 @@ var TrackSchema = object({
|
|
|
15656
16285
|
* Populated from the persisted envelope columns on historical reads;
|
|
15657
16286
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
15658
16287
|
envelope: TrackEnvelopeSchema.optional(),
|
|
15659
|
-
...TrackFlagFields
|
|
16288
|
+
...TrackFlagFields,
|
|
16289
|
+
...TrackRetrainFields
|
|
15660
16290
|
});
|
|
15661
16291
|
var BaseEventFields = {
|
|
15662
16292
|
id: string(),
|
|
@@ -15729,7 +16359,7 @@ var ObjectEventSchema = object({
|
|
|
15729
16359
|
/** Omitted in slim projection. */
|
|
15730
16360
|
trackId: string().optional(),
|
|
15731
16361
|
className: string(),
|
|
15732
|
-
|
|
16362
|
+
...TieredLabelFields,
|
|
15733
16363
|
/** Omitted in slim projection. */
|
|
15734
16364
|
confidence: number().optional(),
|
|
15735
16365
|
/** Heavy JSON — omitted in slim projection. */
|
|
@@ -15810,6 +16440,173 @@ var MediaFileSchema = object({
|
|
|
15810
16440
|
* stored blob and a `?variant=thumb` rendering without fetching either.
|
|
15811
16441
|
*/
|
|
15812
16442
|
var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
|
|
16443
|
+
/**
|
|
16444
|
+
* The MACRO tier of an annotation — a CLOSED set.
|
|
16445
|
+
*
|
|
16446
|
+
* This is what the exported detector predicts, so a typo here is a new class
|
|
16447
|
+
* with one example in it. `label` and `subLabel` are open strings by contrast:
|
|
16448
|
+
* the whole point of the page is teaching the model things it does not know
|
|
16449
|
+
* yet, and constraining that vocabulary would make it useless.
|
|
16450
|
+
*
|
|
16451
|
+
* A macro class is NEVER a label. The provider refuses a write whose `label` or
|
|
16452
|
+
* `subLabel` is one of these values, in any casing, because once `person`
|
|
16453
|
+
* exists in both tiers "every person box" stops being answerable without
|
|
16454
|
+
* knowing every string anyone ever typed — and the damage is retroactive.
|
|
16455
|
+
*/
|
|
16456
|
+
var RetrainMacroClassSchema = _enum([
|
|
16457
|
+
"person",
|
|
16458
|
+
"vehicle",
|
|
16459
|
+
"animal",
|
|
16460
|
+
"package",
|
|
16461
|
+
"face",
|
|
16462
|
+
"plate"
|
|
16463
|
+
]);
|
|
16464
|
+
/** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
|
|
16465
|
+
var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
|
|
16466
|
+
/** Did a human draw this box, or did the assist propose it? */
|
|
16467
|
+
var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
|
|
16468
|
+
/** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
|
|
16469
|
+
var RetrainBboxSchema = object({
|
|
16470
|
+
x: number(),
|
|
16471
|
+
y: number(),
|
|
16472
|
+
w: number(),
|
|
16473
|
+
h: number()
|
|
16474
|
+
});
|
|
16475
|
+
/**
|
|
16476
|
+
* One annotated subject.
|
|
16477
|
+
*
|
|
16478
|
+
* `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
|
|
16479
|
+
* (letterboxed root / zone-cropped package / subject-cropped classifier) are
|
|
16480
|
+
* derived from it at export and never stored — storing them is how one feature
|
|
16481
|
+
* space ends up holding two crops of the same subject (D52).
|
|
16482
|
+
*/
|
|
16483
|
+
var RetrainAnnotationSchema = object({
|
|
16484
|
+
id: string(),
|
|
16485
|
+
trackId: string(),
|
|
16486
|
+
deviceId: number(),
|
|
16487
|
+
/** The COPY in retrain storage — never the source track's media key. */
|
|
16488
|
+
mediaKey: string(),
|
|
16489
|
+
bbox: RetrainBboxSchema,
|
|
16490
|
+
macroClass: RetrainMacroClassSchema,
|
|
16491
|
+
label: string().optional(),
|
|
16492
|
+
subLabel: string().optional(),
|
|
16493
|
+
kind: RetrainAnnotationKindSchema,
|
|
16494
|
+
source: RetrainAnnotationSourceSchema,
|
|
16495
|
+
/** Which model proposed this box — or, on a `model_error`, drew the phantom. */
|
|
16496
|
+
assistModelId: string().optional(),
|
|
16497
|
+
assistScore: number().optional(),
|
|
16498
|
+
exportedInBatch: string().optional(),
|
|
16499
|
+
createdAt: number()
|
|
16500
|
+
});
|
|
16501
|
+
/** The write form — the server owns `id`, `createdAt` and the frame binding. */
|
|
16502
|
+
var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
|
|
16503
|
+
id: true,
|
|
16504
|
+
trackId: true,
|
|
16505
|
+
deviceId: true,
|
|
16506
|
+
mediaKey: true,
|
|
16507
|
+
createdAt: true,
|
|
16508
|
+
exportedInBatch: true
|
|
16509
|
+
});
|
|
16510
|
+
/** A track sitting in `staging`, with everything the worklist needs to rank it. */
|
|
16511
|
+
var RetrainTrackSchema = object({
|
|
16512
|
+
trackId: string(),
|
|
16513
|
+
deviceId: number(),
|
|
16514
|
+
className: string(),
|
|
16515
|
+
label: string().optional(),
|
|
16516
|
+
firstSeen: number(),
|
|
16517
|
+
lastSeen: number(),
|
|
16518
|
+
/** How many frames the dataset already holds from this track. */
|
|
16519
|
+
frameCount: number().int(),
|
|
16520
|
+
/** How many subjects have been annotated on those frames. `0` with
|
|
16521
|
+
* `frameCount: 0` is exactly "staging, still to work". */
|
|
16522
|
+
annotationCount: number().int()
|
|
16523
|
+
});
|
|
16524
|
+
/** A frame the picker may offer — an index row, no blob was read to produce it. */
|
|
16525
|
+
var RetrainFrameCandidateSchema = object({
|
|
16526
|
+
mediaKey: string(),
|
|
16527
|
+
kind: MediaFileKindEnum,
|
|
16528
|
+
timestamp: number(),
|
|
16529
|
+
sizeBytes: number().int(),
|
|
16530
|
+
/** A copy of this original already exists — selecting it is free and cannot
|
|
16531
|
+
* fail, whatever became of the original. */
|
|
16532
|
+
copied: boolean()
|
|
16533
|
+
});
|
|
16534
|
+
/** A frame the dataset OWNS: bytes copied at selection time. */
|
|
16535
|
+
var RetrainFrameSchema = object({
|
|
16536
|
+
frameId: string(),
|
|
16537
|
+
deviceId: number(),
|
|
16538
|
+
trackId: string(),
|
|
16539
|
+
/** Provenance only. It may already point at nothing — that is expected. */
|
|
16540
|
+
sourceMediaKey: string(),
|
|
16541
|
+
sourceKind: MediaFileKindEnum,
|
|
16542
|
+
sizeBytes: number().int(),
|
|
16543
|
+
width: number().int(),
|
|
16544
|
+
height: number().int(),
|
|
16545
|
+
copiedAt: number()
|
|
16546
|
+
});
|
|
16547
|
+
/** Why a copy-on-select could not be honoured — named, never a silent skip. */
|
|
16548
|
+
var RetrainCopyRefusalSchema = _enum([
|
|
16549
|
+
"source-missing",
|
|
16550
|
+
"unreadable-image",
|
|
16551
|
+
"write-failed"
|
|
16552
|
+
]);
|
|
16553
|
+
var RetrainFrameSelectionSchema = object({
|
|
16554
|
+
copied: array(RetrainFrameSchema).readonly(),
|
|
16555
|
+
refused: array(object({
|
|
16556
|
+
sourceMediaKey: string(),
|
|
16557
|
+
reason: RetrainCopyRefusalSchema
|
|
16558
|
+
})).readonly()
|
|
16559
|
+
});
|
|
16560
|
+
var RetrainFrameListSchema = object({
|
|
16561
|
+
candidates: array(RetrainFrameCandidateSchema).readonly(),
|
|
16562
|
+
copies: array(RetrainFrameSchema).readonly(),
|
|
16563
|
+
/** What the page pre-selects — the native key frame when one survives. */
|
|
16564
|
+
autoPickMediaKey: string().optional()
|
|
16565
|
+
});
|
|
16566
|
+
/** What the operator asked the assist to look for. */
|
|
16567
|
+
var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
|
|
16568
|
+
kind: literal("package"),
|
|
16569
|
+
zone: RetrainBboxSchema.optional()
|
|
16570
|
+
}), object({
|
|
16571
|
+
kind: literal("objects"),
|
|
16572
|
+
modelId: string(),
|
|
16573
|
+
minScore: number().optional()
|
|
16574
|
+
})]);
|
|
16575
|
+
/**
|
|
16576
|
+
* The assist's answer — a discriminated union, because "the model saw nothing"
|
|
16577
|
+
* and "this node cannot run that model" lead to different next moves and a
|
|
16578
|
+
* nullable result cannot tell them apart.
|
|
16579
|
+
*/
|
|
16580
|
+
var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
|
|
16581
|
+
kind: literal("proposed"),
|
|
16582
|
+
modelId: string(),
|
|
16583
|
+
stepId: string(),
|
|
16584
|
+
minScore: number(),
|
|
16585
|
+
/** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
|
|
16586
|
+
proposals: array(RetrainAnnotationDraftSchema).readonly(),
|
|
16587
|
+
/** Returned by the runner but removed by the threshold. */
|
|
16588
|
+
belowThreshold: number().int()
|
|
16589
|
+
}), object({
|
|
16590
|
+
kind: literal("refused"),
|
|
16591
|
+
/** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
|
|
16592
|
+
reason: string(),
|
|
16593
|
+
detail: string().optional()
|
|
16594
|
+
})]);
|
|
16595
|
+
/** The outcome of a lifecycle move owned by the retrain page. */
|
|
16596
|
+
var RetrainTransitionResultSchema = object({
|
|
16597
|
+
trackId: string(),
|
|
16598
|
+
/** Where the track ended up, whatever happened. */
|
|
16599
|
+
retrainStatus: RetrainStatusSchema,
|
|
16600
|
+
/** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
|
|
16601
|
+
changed: boolean(),
|
|
16602
|
+
reason: _enum([
|
|
16603
|
+
"unknown-track",
|
|
16604
|
+
"no-frames-copied",
|
|
16605
|
+
"not-staging",
|
|
16606
|
+
"not-trained",
|
|
16607
|
+
"unchanged"
|
|
16608
|
+
]).optional()
|
|
16609
|
+
});
|
|
15813
16610
|
var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
|
|
15814
16611
|
var MAX_EVENT_QUERY_LIMIT = 5e3;
|
|
15815
16612
|
var DeviceEventQueryInput = object({
|
|
@@ -15864,13 +16661,14 @@ var KeyEventSchema = object({
|
|
|
15864
16661
|
/** Track start time (firstSeen). */
|
|
15865
16662
|
timestamp: number(),
|
|
15866
16663
|
className: string(),
|
|
15867
|
-
|
|
16664
|
+
...TieredLabelFields,
|
|
15868
16665
|
importance: number(),
|
|
15869
16666
|
/** Highest-confidence ObjectEvent id for the track (empty when none). */
|
|
15870
16667
|
bestEventId: string(),
|
|
15871
16668
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
15872
16669
|
windowMs: number().optional(),
|
|
15873
|
-
...TrackFlagFields
|
|
16670
|
+
...TrackFlagFields,
|
|
16671
|
+
...TrackRetrainFields
|
|
15874
16672
|
});
|
|
15875
16673
|
object({
|
|
15876
16674
|
trackId: string(),
|
|
@@ -16232,11 +17030,29 @@ var pipelineAnalyticsCapability = {
|
|
|
16232
17030
|
*
|
|
16233
17031
|
* `auth: 'protected'` (the default), NOT `admin`: the viewer is an
|
|
16234
17032
|
* authenticated non-admin surface and two of the three call sites are
|
|
16235
|
-
* there.
|
|
16236
|
-
*
|
|
17033
|
+
* there. The note that used to sit here said to revisit this the day a flag
|
|
17034
|
+
* gained an effect that costs storage, and D81 is that day — `markForTrain`
|
|
17035
|
+
* now pins. It STAYS protected, and the reason is that the alternative
|
|
17036
|
+
* makes the feature pointless: marking a track is something you do while
|
|
17037
|
+
* looking at it, on the surface you were already looking at it on, and that
|
|
17038
|
+
* surface is the viewer. What the storage cost gets instead is a BOUND — a
|
|
17039
|
+
* per-device pin budget enforced in the body, refusing a new pin past the
|
|
17040
|
+
* limit while always allowing un-marking. `deleteTracks` next door is still
|
|
17041
|
+
* admin, because destroying evidence and preserving it are not symmetric.
|
|
16237
17042
|
*
|
|
16238
|
-
*
|
|
16239
|
-
*
|
|
17043
|
+
* `markForTrain` writes the retrain LIFECYCLE, not a boolean column: `true`
|
|
17044
|
+
* is `none → staging`, `false` is `staging → none`. A track already
|
|
17045
|
+
* `trained` refuses BOTH — its frames are copies inside the retrain dataset
|
|
17046
|
+
* and re-staging it from a generic toggle is how the same material gets
|
|
17047
|
+
* annotated twice under two ground truths. Returning a trained track to
|
|
17048
|
+
* staging is a deliberate action of the retrain page, which is also the only
|
|
17049
|
+
* thing that produces `trained` in the first place.
|
|
17050
|
+
*
|
|
17051
|
+
* Returns the RESOLVED state of both flags (absent → `false`) plus the
|
|
17052
|
+
* `retrainStatus` they were derived from, so a caller can drive its toggle
|
|
17053
|
+
* — and render a `trained` badge — without a re-fetch. Rejects an unknown
|
|
17054
|
+
* track, a new staging mark on a device already holding its full budget, and
|
|
17055
|
+
* any `markForTrain` write against a trained track.
|
|
16240
17056
|
*/
|
|
16241
17057
|
setTrackFlags: method(object({
|
|
16242
17058
|
/** Log/audit scope only — the trackId is globally unique on its own. */
|
|
@@ -16300,6 +17116,237 @@ var pipelineAnalyticsCapability = {
|
|
|
16300
17116
|
kind: "query",
|
|
16301
17117
|
auth: "admin"
|
|
16302
17118
|
}),
|
|
17119
|
+
/**
|
|
17120
|
+
* The CHEAP QUESTION, asked before any media moves: how big is the dataset
|
|
17121
|
+
* the marked (`markForTrain`) tracks would produce?
|
|
17122
|
+
*
|
|
17123
|
+
* Answered from media INDEX rows only — key, kind, size, timestamp — so it
|
|
17124
|
+
* costs ~2 KB of reads per track and no blob reads at all. The measured harm
|
|
17125
|
+
* behind D56 was a bulk pass that read and base64'd every blob a track owned
|
|
17126
|
+
* before deciding anything, taking hub-main to 82 s busy out of 120; an
|
|
17127
|
+
* export is that same I/O shape, so it inherits the same discipline: know
|
|
17128
|
+
* the size, then decide.
|
|
17129
|
+
*
|
|
17130
|
+
* `truncated` reports that more marked tracks exist than one pass carries.
|
|
17131
|
+
* Empty `deviceIds` ⇒ every device that has marked tracks.
|
|
17132
|
+
*/
|
|
17133
|
+
getTrainingExportSummary: method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
|
|
17134
|
+
kind: "query",
|
|
17135
|
+
auth: "admin"
|
|
17136
|
+
}),
|
|
17137
|
+
/**
|
|
17138
|
+
* Where to download the dataset archive.
|
|
17139
|
+
*
|
|
17140
|
+
* The BYTES do not come back through this cap — they come from the returned
|
|
17141
|
+
* data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
|
|
17142
|
+
* archive base64'd through a unary RPC envelope would be held whole in
|
|
17143
|
+
* memory twice on a hub this repo has already OOM'd once (D9/D18 are the
|
|
17144
|
+
* same lesson about frames). `getDownloadUrl` on `recordingExport` is the
|
|
17145
|
+
* precedent, and this follows it deliberately.
|
|
17146
|
+
*
|
|
17147
|
+
* The archive contains a `manifest.json` FIRST, then the stored media
|
|
17148
|
+
* VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
|
|
17149
|
+
* model is run: a training set's pixels must be the pixels the pipeline saw.
|
|
17150
|
+
*/
|
|
17151
|
+
getTrainingExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
17152
|
+
kind: "query",
|
|
17153
|
+
auth: "admin"
|
|
17154
|
+
}),
|
|
17155
|
+
/**
|
|
17156
|
+
* The staging worklist for one camera, or for every camera that has one.
|
|
17157
|
+
*
|
|
17158
|
+
* Fetched ON DEMAND, over the staging set only — the page never scans
|
|
17159
|
+
* history, because making the working set small is the entire purpose of
|
|
17160
|
+
* the mark. Each row carries how many frames the dataset already holds from
|
|
17161
|
+
* the track and how many subjects were annotated on them, so
|
|
17162
|
+
* `frameCount: 0` reads as "still to work" without a second call per track.
|
|
17163
|
+
*
|
|
17164
|
+
* `auth: 'admin'`, unlike the viewer-level mark itself: marking a track is
|
|
17165
|
+
* curation you do while looking at it, but building the training set the
|
|
17166
|
+
* fleet's models are fine-tuned on is not.
|
|
17167
|
+
*/
|
|
17168
|
+
listRetrainStaging: method(object({
|
|
17169
|
+
/** Empty ⇒ every camera that has staging tracks. A LIST, not a single
|
|
17170
|
+
* `deviceId`, deliberately: `deviceId` would make this device-bound and
|
|
17171
|
+
* route it at one camera's owner, and "every camera" would stop being
|
|
17172
|
+
* expressible at all. */
|
|
17173
|
+
deviceIds: array(number()).optional(),
|
|
17174
|
+
limit: number().int().min(1).max(500).optional()
|
|
17175
|
+
}), array(RetrainTrackSchema).readonly(), {
|
|
17176
|
+
kind: "query",
|
|
17177
|
+
auth: "admin"
|
|
17178
|
+
}),
|
|
17179
|
+
/**
|
|
17180
|
+
* What a track can contribute, and what it already has.
|
|
17181
|
+
*
|
|
17182
|
+
* `candidates` are the track's whole, unannotated frames — index rows only,
|
|
17183
|
+
* so this is cheap. `copies` are the frames already inside the dataset, and
|
|
17184
|
+
* a candidate whose copy exists is marked `copied: true`: selecting it again
|
|
17185
|
+
* is free and CANNOT fail, whatever became of the original.
|
|
17186
|
+
*
|
|
17187
|
+
* A crop, a thumbnail and `fullFrameBoxed` are never candidates. The last
|
|
17188
|
+
* one matters most: it has the model's own rectangle burned into the pixels,
|
|
17189
|
+
* and a detector trained on it learns to find a green line.
|
|
17190
|
+
*/
|
|
17191
|
+
listRetrainFrames: method(object({ trackId: string() }), RetrainFrameListSchema, {
|
|
17192
|
+
kind: "query",
|
|
17193
|
+
auth: "admin"
|
|
17194
|
+
}),
|
|
17195
|
+
/**
|
|
17196
|
+
* COPY-ON-SELECT — the write that makes `trained` safe to evict.
|
|
17197
|
+
*
|
|
17198
|
+
* Selecting a frame copies its bytes into retrain storage immediately: not
|
|
17199
|
+
* a reference, not a lease. Once the copy exists the dataset no longer
|
|
17200
|
+
* depends on the track's media, which is exactly what lets D81 hand a
|
|
17201
|
+
* `trained` track back to retention.
|
|
17202
|
+
*
|
|
17203
|
+
* The order inside is load-bearing and is pinned by a test: an EXISTING
|
|
17204
|
+
* copy is returned without touching the source, so an original that
|
|
17205
|
+
* evaporated blocks the selection of THAT ORIGINAL and never the copy
|
|
17206
|
+
* already taken. Every refusal comes back named — a dropped selection is
|
|
17207
|
+
* never silent, on the wire or in the log.
|
|
17208
|
+
*/
|
|
17209
|
+
selectRetrainFrames: method(object({
|
|
17210
|
+
deviceId: number(),
|
|
17211
|
+
trackId: string(),
|
|
17212
|
+
mediaKeys: array(string()).min(1)
|
|
17213
|
+
}), RetrainFrameSelectionSchema, {
|
|
17214
|
+
kind: "mutation",
|
|
17215
|
+
auth: "admin"
|
|
17216
|
+
}),
|
|
17217
|
+
/** Un-select a frame: its annotations go first, then the copy and its blob.
|
|
17218
|
+
* Deliberately destructive and deliberately explicit — it is the only way
|
|
17219
|
+
* a frame leaves the dataset before export. */
|
|
17220
|
+
deselectRetrainFrame: method(object({
|
|
17221
|
+
deviceId: number(),
|
|
17222
|
+
trackId: string(),
|
|
17223
|
+
frameId: string()
|
|
17224
|
+
}), object({
|
|
17225
|
+
removed: boolean(),
|
|
17226
|
+
removedAnnotations: number().int()
|
|
17227
|
+
}), {
|
|
17228
|
+
kind: "mutation",
|
|
17229
|
+
auth: "admin"
|
|
17230
|
+
}),
|
|
17231
|
+
/**
|
|
17232
|
+
* The pixels of ONE copied frame, base64.
|
|
17233
|
+
*
|
|
17234
|
+
* Through the cap rather than a data plane because it is genuinely one
|
|
17235
|
+
* frame at a time, on demand, at human speed — the shape D9/D18 permit
|
|
17236
|
+
* (what they forbid is frames crossing a boundary at frame RATE). The
|
|
17237
|
+
* annotation canvas needs the image and its exact dimensions in the same
|
|
17238
|
+
* answer: a canvas that places a normalised box against a size it guessed
|
|
17239
|
+
* draws every box in the wrong place.
|
|
17240
|
+
*/
|
|
17241
|
+
getRetrainFrameImage: method(object({ frameId: string() }), object({
|
|
17242
|
+
base64: string(),
|
|
17243
|
+
width: number().int(),
|
|
17244
|
+
height: number().int()
|
|
17245
|
+
}), {
|
|
17246
|
+
kind: "query",
|
|
17247
|
+
auth: "admin"
|
|
17248
|
+
}),
|
|
17249
|
+
/**
|
|
17250
|
+
* Ask the pipeline what it sees, as a PROPOSAL.
|
|
17251
|
+
*
|
|
17252
|
+
* Runs through `pipelineRunner.runStatelessStep` on the COPIED frame, and
|
|
17253
|
+
* every box comes back as a draft with `source: 'assist'` plus the model and
|
|
17254
|
+
* score that produced it. The operator confirms, edits, adds and deletes;
|
|
17255
|
+
* nothing is stored until `saveRetrainAnnotations`.
|
|
17256
|
+
*
|
|
17257
|
+
* For packages the request is `rfdetr-package` on the ZONE CROP at 0.35 —
|
|
17258
|
+
* never the whole frame, where a package detector at that threshold proposes
|
|
17259
|
+
* furniture. A package request with no zone is REFUSED rather than widened,
|
|
17260
|
+
* because the silent widening would look like a bad model for as long as
|
|
17261
|
+
* nobody checked which rectangle it ran on.
|
|
17262
|
+
*/
|
|
17263
|
+
proposeRetrainAnnotations: method(object({
|
|
17264
|
+
deviceId: number(),
|
|
17265
|
+
trackId: string(),
|
|
17266
|
+
frameId: string(),
|
|
17267
|
+
subject: RetrainAssistSubjectSchema,
|
|
17268
|
+
/** Which node runs it. Absent ⇒ wherever an unowned call lands. */
|
|
17269
|
+
nodeId: string().optional()
|
|
17270
|
+
}), RetrainAssistResultSchema, {
|
|
17271
|
+
kind: "mutation",
|
|
17272
|
+
auth: "admin"
|
|
17273
|
+
}),
|
|
17274
|
+
/** Every annotation on a track, oldest first. */
|
|
17275
|
+
listRetrainAnnotations: method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
|
|
17276
|
+
kind: "query",
|
|
17277
|
+
auth: "admin"
|
|
17278
|
+
}),
|
|
17279
|
+
/**
|
|
17280
|
+
* Replace EVERY annotation on one frame with the supplied set.
|
|
17281
|
+
*
|
|
17282
|
+
* Whole-frame replacement, not per-box upsert: the unit of ground truth is
|
|
17283
|
+
* the frame, and "the operator deleted a box" must be the same durable
|
|
17284
|
+
* outcome as "the operator never drew it". A per-box patch would let a frame
|
|
17285
|
+
* keep a box the operator removed on a surface that only knew about the
|
|
17286
|
+
* boxes it sent.
|
|
17287
|
+
*
|
|
17288
|
+
* Refuses a macro class typed into `label` or `subLabel` — the tiers are
|
|
17289
|
+
* separate and the guard is at the WRITE, because a mixed taxonomy cannot
|
|
17290
|
+
* be un-mixed by reading it.
|
|
17291
|
+
*/
|
|
17292
|
+
saveRetrainAnnotations: method(object({
|
|
17293
|
+
deviceId: number(),
|
|
17294
|
+
trackId: string(),
|
|
17295
|
+
frameId: string(),
|
|
17296
|
+
annotations: array(RetrainAnnotationDraftSchema)
|
|
17297
|
+
}), array(RetrainAnnotationSchema).readonly(), {
|
|
17298
|
+
kind: "mutation",
|
|
17299
|
+
auth: "admin"
|
|
17300
|
+
}),
|
|
17301
|
+
/**
|
|
17302
|
+
* Finish with a track: `staging → trained`. **The only writer of that
|
|
17303
|
+
* state** — D81 shipped the column with it deliberately unreachable.
|
|
17304
|
+
*
|
|
17305
|
+
* Refuses a track the dataset holds no copies from. `trained` un-pins the
|
|
17306
|
+
* track's media, so completing without a copy is a delete order for material
|
|
17307
|
+
* nothing ever extracted anything from; that refusal IS the safety argument
|
|
17308
|
+
* of D81, expressed as a precondition.
|
|
17309
|
+
*/
|
|
17310
|
+
completeRetrainTrack: method(object({
|
|
17311
|
+
deviceId: number(),
|
|
17312
|
+
trackId: string()
|
|
17313
|
+
}), RetrainTransitionResultSchema, {
|
|
17314
|
+
kind: "mutation",
|
|
17315
|
+
auth: "admin"
|
|
17316
|
+
}),
|
|
17317
|
+
/**
|
|
17318
|
+
* The deliberate return: `trained → staging`, for the rare case.
|
|
17319
|
+
*
|
|
17320
|
+
* The generic `setTrackFlags` toggle refuses this in both directions by
|
|
17321
|
+
* design (D81) — re-staging from a checkbox is how the same material gets
|
|
17322
|
+
* annotated twice under two ground truths. Doing it here means the operator
|
|
17323
|
+
* is looking at the annotations that already exist while they decide, and
|
|
17324
|
+
* those annotations are LEFT ALONE: "put this back" must not be a
|
|
17325
|
+
* destructive act wearing a navigational name.
|
|
17326
|
+
*/
|
|
17327
|
+
restageRetrainTrack: method(object({
|
|
17328
|
+
deviceId: number(),
|
|
17329
|
+
trackId: string()
|
|
17330
|
+
}), RetrainTransitionResultSchema, {
|
|
17331
|
+
kind: "mutation",
|
|
17332
|
+
auth: "admin"
|
|
17333
|
+
}),
|
|
17334
|
+
/**
|
|
17335
|
+
* Where to download the ANNOTATED dataset.
|
|
17336
|
+
*
|
|
17337
|
+
* The sibling of `getTrainingExportUrl` and deliberately not the same
|
|
17338
|
+
* archive: that one streams a marked track's stored media verbatim, this one
|
|
17339
|
+
* streams the retrain COPIES plus an `annotations.json` carrying, for every
|
|
17340
|
+
* subject, the canonical full-frame box AND the geometry derived for each
|
|
17341
|
+
* model shape (letterboxed root / zone-cropped package / subject-cropped
|
|
17342
|
+
* classifier). Derived at export, never stored — one box in, three shapes
|
|
17343
|
+
* out, so two crops of the same subject can never end up in one feature
|
|
17344
|
+
* space (D52).
|
|
17345
|
+
*/
|
|
17346
|
+
getRetrainExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
17347
|
+
kind: "query",
|
|
17348
|
+
auth: "admin"
|
|
17349
|
+
}),
|
|
16303
17350
|
getEventMedia: method(object({
|
|
16304
17351
|
eventId: string(),
|
|
16305
17352
|
kind: MediaFileKindEnum.optional()
|
|
@@ -16964,6 +18011,22 @@ var DetailResultSchema = object({
|
|
|
16964
18011
|
bbox: NativeCropBboxSchema.optional(),
|
|
16965
18012
|
embedding: string().optional(),
|
|
16966
18013
|
label: string().optional(),
|
|
18014
|
+
/**
|
|
18015
|
+
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
18016
|
+
* `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
|
|
18017
|
+
*
|
|
18018
|
+
* It rides the wire rather than being resolved by the consumer because the
|
|
18019
|
+
* declaration lives with the step definition, which only the executing node
|
|
18020
|
+
* has: post-analysis holds no step registry, and re-deriving the tier from
|
|
18021
|
+
* `className` there would be exactly the inference this model exists to
|
|
18022
|
+
* forbid. A `label` that arrives WITHOUT this field is refused by the write
|
|
18023
|
+
* rule and logged (`label tier undeclared`) — an older runner therefore
|
|
18024
|
+
* stops enriching rather than guessing, which is why addon-pipeline is
|
|
18025
|
+
* deployed BEFORE addon-post-analysis.
|
|
18026
|
+
*/
|
|
18027
|
+
labelTier: union([literal(1), literal(2)]).optional(),
|
|
18028
|
+
/** Model that produced `label` — carried into the tier's attribution. */
|
|
18029
|
+
labelModelId: string().optional(),
|
|
16967
18030
|
alignedCropJpeg: string().optional(),
|
|
16968
18031
|
/** Face short side (px) measured on the NATIVE crop surface. The `bbox`
|
|
16969
18032
|
* above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
|
|
@@ -23132,6 +24195,173 @@ DeviceType.Camera, method(object({
|
|
|
23132
24195
|
status: OsdStatusSchema
|
|
23133
24196
|
});
|
|
23134
24197
|
/**
|
|
24198
|
+
* `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
|
|
24199
|
+
*
|
|
24200
|
+
* The `osd` cap is the firmware contract: it probes a camera's overlay
|
|
24201
|
+
* SLOTS and writes literal text into one. It has no idea WHERE that text
|
|
24202
|
+
* comes from, and it must not — a driver that grew a "show the temperature
|
|
24203
|
+
* here" feature would grow it once per vendor.
|
|
24204
|
+
*
|
|
24205
|
+
* This cap owns the other half: a per-(camera, slot) BINDING that says
|
|
24206
|
+
* which value feeds the slot, how it is formatted, and under which
|
|
24207
|
+
* conditions it is shown at all. One addon renders every binding on every
|
|
24208
|
+
* camera, so a new source costs zero driver code.
|
|
24209
|
+
*
|
|
24210
|
+
* Three deliberate choices, each with a rejected alternative:
|
|
24211
|
+
*
|
|
24212
|
+
* 1. A source is `(capName, valuePath)` over the kernel's device
|
|
24213
|
+
* runtime-state mirror — NOT a closed enum of source kinds. Every
|
|
24214
|
+
* cap-keyed slice a device publishes is bindable the day the cap
|
|
24215
|
+
* ships. The rejected alternative (one enum member per source, with
|
|
24216
|
+
* a resolver branch each) is what makes "add the humidity too" a
|
|
24217
|
+
* code change.
|
|
24218
|
+
* 2. The display gate reuses `NcConditionsSchema` verbatim — the
|
|
24219
|
+
* notification centre's condition vocabulary — rather than a parallel
|
|
24220
|
+
* model. An operator who has learned one condition editor has learned
|
|
24221
|
+
* both.
|
|
24222
|
+
* 3. Because the renderer's facts are device STATE and not a detection
|
|
24223
|
+
* record, only a SUBSET of that vocabulary can be answered here.
|
|
24224
|
+
* `setSlotBinding` REJECTS the rest at write time (see
|
|
24225
|
+
* `getConditionSupport`). It does not accept-then-fail-closed: a
|
|
24226
|
+
* condition that can never be true renders a permanently blank
|
|
24227
|
+
* overlay, and a blank overlay looks exactly like a broken camera.
|
|
24228
|
+
*/
|
|
24229
|
+
/** Where a slot's value comes from. */
|
|
24230
|
+
var OsdSourceSchema = discriminatedUnion("kind", [
|
|
24231
|
+
object({
|
|
24232
|
+
kind: literal("static"),
|
|
24233
|
+
text: string().max(64)
|
|
24234
|
+
}),
|
|
24235
|
+
object({
|
|
24236
|
+
kind: literal("clock"),
|
|
24237
|
+
/** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
|
|
24238
|
+
pattern: string().min(1).max(32).default("HH:mm"),
|
|
24239
|
+
/** IANA zone. Omitted = the server's zone. */
|
|
24240
|
+
timezone: string().min(1).max(64).optional()
|
|
24241
|
+
}),
|
|
24242
|
+
object({
|
|
24243
|
+
kind: literal("device-state"),
|
|
24244
|
+
deviceId: number().int().optional(),
|
|
24245
|
+
capName: string().min(1).max(64),
|
|
24246
|
+
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
24247
|
+
valuePath: string().min(1).max(64)
|
|
24248
|
+
})
|
|
24249
|
+
]);
|
|
24250
|
+
var OsdSlotBindingSchema = object({
|
|
24251
|
+
/** Off = the manager stops driving this slot. It does NOT clear it. */
|
|
24252
|
+
enabled: boolean().default(true),
|
|
24253
|
+
source: OsdSourceSchema,
|
|
24254
|
+
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
24255
|
+
template: string().max(96).default("${value}"),
|
|
24256
|
+
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
24257
|
+
maxCharacters: number().int().min(4).max(64).optional(),
|
|
24258
|
+
/**
|
|
24259
|
+
* Decimal places for a numeric value. `0` yields an integer — the
|
|
24260
|
+
* documented workaround for firmwares that reject `.` in overlay text.
|
|
24261
|
+
*/
|
|
24262
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
24263
|
+
/** Appended via `${unit}`. The state mirror does not carry units. */
|
|
24264
|
+
unitLabel: string().max(8).optional(),
|
|
24265
|
+
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
24266
|
+
valueMap: record(string(), string()).optional(),
|
|
24267
|
+
/** Time windows in which the slot is shown. Absent = always. */
|
|
24268
|
+
schedule: NcScheduleSchema.optional(),
|
|
24269
|
+
/**
|
|
24270
|
+
* Display gate, in the notification centre's condition vocabulary.
|
|
24271
|
+
* Only the keys reported by `getConditionSupport` are accepted.
|
|
24272
|
+
*/
|
|
24273
|
+
conditions: NcConditionsSchema.optional(),
|
|
24274
|
+
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
24275
|
+
fallbackText: string().max(64).default("")
|
|
24276
|
+
});
|
|
24277
|
+
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
24278
|
+
var OsdSlotViewSchema = object({
|
|
24279
|
+
slotId: string(),
|
|
24280
|
+
kind: OsdOverlayKindEnum,
|
|
24281
|
+
/** Firmware refuses text edits (a timestamp, the channel name). */
|
|
24282
|
+
readOnly: boolean(),
|
|
24283
|
+
cameraEnabled: boolean(),
|
|
24284
|
+
cameraText: string().optional(),
|
|
24285
|
+
binding: OsdSlotBindingSchema.nullable()
|
|
24286
|
+
});
|
|
24287
|
+
/**
|
|
24288
|
+
* What happened to one slot on one render pass. `unchanged` exists so the
|
|
24289
|
+
* operator can tell "we are driving this and the value is steady" from
|
|
24290
|
+
* "we never got there" — and so the loop can prove it is not rewriting
|
|
24291
|
+
* identical text to the camera every tick.
|
|
24292
|
+
*/
|
|
24293
|
+
var OsdRenderOutcomeEnum = _enum([
|
|
24294
|
+
"written",
|
|
24295
|
+
"unchanged",
|
|
24296
|
+
"gated",
|
|
24297
|
+
"unreadable",
|
|
24298
|
+
"disabled",
|
|
24299
|
+
"unbound",
|
|
24300
|
+
"failed"
|
|
24301
|
+
]);
|
|
24302
|
+
var OsdRenderResultSchema = object({
|
|
24303
|
+
slotId: string(),
|
|
24304
|
+
outcome: OsdRenderOutcomeEnum,
|
|
24305
|
+
/** The text the slot should carry. Empty = the slot is switched off. */
|
|
24306
|
+
text: string(),
|
|
24307
|
+
/** Why, whenever the outcome is not a plain write. Never silent. */
|
|
24308
|
+
reason: string().optional()
|
|
24309
|
+
});
|
|
24310
|
+
var OsdSourceValueTypeEnum = _enum([
|
|
24311
|
+
"number",
|
|
24312
|
+
"boolean",
|
|
24313
|
+
"string",
|
|
24314
|
+
"enum"
|
|
24315
|
+
]);
|
|
24316
|
+
/**
|
|
24317
|
+
* One bindable value, derived from a cap's `runtimeState` schema — never
|
|
24318
|
+
* hand-listed. The editor renders from this, so a cap that ships a new
|
|
24319
|
+
* state field becomes bindable with no UI change.
|
|
24320
|
+
*/
|
|
24321
|
+
var OsdSourceOptionSchema = object({
|
|
24322
|
+
deviceId: number().int(),
|
|
24323
|
+
deviceName: string(),
|
|
24324
|
+
capName: string(),
|
|
24325
|
+
valuePath: string(),
|
|
24326
|
+
label: string(),
|
|
24327
|
+
valueType: OsdSourceValueTypeEnum,
|
|
24328
|
+
/** Present for `enum`; the editor offers these as `valueMap` keys. */
|
|
24329
|
+
enumValues: array(string()).readonly().optional()
|
|
24330
|
+
});
|
|
24331
|
+
method(object({ deviceId: number().int() }), object({
|
|
24332
|
+
supported: boolean(),
|
|
24333
|
+
slots: array(OsdSlotViewSchema)
|
|
24334
|
+
}), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
|
|
24335
|
+
supported: array(string()),
|
|
24336
|
+
catalog: array(NcConditionDescriptorSchema)
|
|
24337
|
+
}), { auth: "admin" }), method(object({
|
|
24338
|
+
deviceId: number().int(),
|
|
24339
|
+
slotId: string().min(1),
|
|
24340
|
+
binding: OsdSlotBindingSchema
|
|
24341
|
+
}), object({
|
|
24342
|
+
slot: OsdSlotViewSchema,
|
|
24343
|
+
render: OsdRenderResultSchema
|
|
24344
|
+
}), {
|
|
24345
|
+
kind: "mutation",
|
|
24346
|
+
auth: "admin"
|
|
24347
|
+
}), method(object({
|
|
24348
|
+
deviceId: number().int(),
|
|
24349
|
+
slotId: string().min(1)
|
|
24350
|
+
}), object({ success: literal(true) }), {
|
|
24351
|
+
kind: "mutation",
|
|
24352
|
+
auth: "admin"
|
|
24353
|
+
}), method(object({
|
|
24354
|
+
deviceId: number().int(),
|
|
24355
|
+
slotId: string().min(1),
|
|
24356
|
+
binding: OsdSlotBindingSchema.optional()
|
|
24357
|
+
}), OsdRenderResultSchema, {
|
|
24358
|
+
kind: "mutation",
|
|
24359
|
+
auth: "admin"
|
|
24360
|
+
}), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
|
|
24361
|
+
kind: "mutation",
|
|
24362
|
+
auth: "admin"
|
|
24363
|
+
});
|
|
24364
|
+
/**
|
|
23135
24365
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
23136
24366
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
23137
24367
|
* `on_batteries` (running on battery backup). `null` until first reported.
|
|
@@ -29632,6 +30862,48 @@ Object.freeze({
|
|
|
29632
30862
|
addonId: null,
|
|
29633
30863
|
access: "create"
|
|
29634
30864
|
},
|
|
30865
|
+
"osdManager.clearSlotBinding": {
|
|
30866
|
+
capName: "osd-manager",
|
|
30867
|
+
capScope: "system",
|
|
30868
|
+
addonId: null,
|
|
30869
|
+
access: "delete"
|
|
30870
|
+
},
|
|
30871
|
+
"osdManager.getConditionSupport": {
|
|
30872
|
+
capName: "osd-manager",
|
|
30873
|
+
capScope: "system",
|
|
30874
|
+
addonId: null,
|
|
30875
|
+
access: "view"
|
|
30876
|
+
},
|
|
30877
|
+
"osdManager.getDeviceOsd": {
|
|
30878
|
+
capName: "osd-manager",
|
|
30879
|
+
capScope: "system",
|
|
30880
|
+
addonId: null,
|
|
30881
|
+
access: "view"
|
|
30882
|
+
},
|
|
30883
|
+
"osdManager.getSourceCatalog": {
|
|
30884
|
+
capName: "osd-manager",
|
|
30885
|
+
capScope: "system",
|
|
30886
|
+
addonId: null,
|
|
30887
|
+
access: "view"
|
|
30888
|
+
},
|
|
30889
|
+
"osdManager.previewSlot": {
|
|
30890
|
+
capName: "osd-manager",
|
|
30891
|
+
capScope: "system",
|
|
30892
|
+
addonId: null,
|
|
30893
|
+
access: "create"
|
|
30894
|
+
},
|
|
30895
|
+
"osdManager.renderDevice": {
|
|
30896
|
+
capName: "osd-manager",
|
|
30897
|
+
capScope: "system",
|
|
30898
|
+
addonId: null,
|
|
30899
|
+
access: "create"
|
|
30900
|
+
},
|
|
30901
|
+
"osdManager.setSlotBinding": {
|
|
30902
|
+
capName: "osd-manager",
|
|
30903
|
+
capScope: "system",
|
|
30904
|
+
addonId: null,
|
|
30905
|
+
access: "create"
|
|
30906
|
+
},
|
|
29635
30907
|
"petFeeder.callPet": {
|
|
29636
30908
|
capName: "pet-feeder",
|
|
29637
30909
|
capScope: "device",
|
|
@@ -29704,6 +30976,12 @@ Object.freeze({
|
|
|
29704
30976
|
addonId: null,
|
|
29705
30977
|
access: "delete"
|
|
29706
30978
|
},
|
|
30979
|
+
"pipelineAnalytics.completeRetrainTrack": {
|
|
30980
|
+
capName: "pipeline-analytics",
|
|
30981
|
+
capScope: "device",
|
|
30982
|
+
addonId: null,
|
|
30983
|
+
access: "create"
|
|
30984
|
+
},
|
|
29707
30985
|
"pipelineAnalytics.deleteDeviceEvents": {
|
|
29708
30986
|
capName: "pipeline-analytics",
|
|
29709
30987
|
capScope: "device",
|
|
@@ -29716,6 +30994,12 @@ Object.freeze({
|
|
|
29716
30994
|
addonId: null,
|
|
29717
30995
|
access: "delete"
|
|
29718
30996
|
},
|
|
30997
|
+
"pipelineAnalytics.deselectRetrainFrame": {
|
|
30998
|
+
capName: "pipeline-analytics",
|
|
30999
|
+
capScope: "device",
|
|
31000
|
+
addonId: null,
|
|
31001
|
+
access: "create"
|
|
31002
|
+
},
|
|
29719
31003
|
"pipelineAnalytics.getActiveTracks": {
|
|
29720
31004
|
capName: "pipeline-analytics",
|
|
29721
31005
|
capScope: "device",
|
|
@@ -29776,6 +31060,18 @@ Object.freeze({
|
|
|
29776
31060
|
addonId: null,
|
|
29777
31061
|
access: "view"
|
|
29778
31062
|
},
|
|
31063
|
+
"pipelineAnalytics.getRetrainExportUrl": {
|
|
31064
|
+
capName: "pipeline-analytics",
|
|
31065
|
+
capScope: "device",
|
|
31066
|
+
addonId: null,
|
|
31067
|
+
access: "view"
|
|
31068
|
+
},
|
|
31069
|
+
"pipelineAnalytics.getRetrainFrameImage": {
|
|
31070
|
+
capName: "pipeline-analytics",
|
|
31071
|
+
capScope: "device",
|
|
31072
|
+
addonId: null,
|
|
31073
|
+
access: "view"
|
|
31074
|
+
},
|
|
29779
31075
|
"pipelineAnalytics.getSensorEvents": {
|
|
29780
31076
|
capName: "pipeline-analytics",
|
|
29781
31077
|
capScope: "device",
|
|
@@ -29794,6 +31090,18 @@ Object.freeze({
|
|
|
29794
31090
|
addonId: null,
|
|
29795
31091
|
access: "view"
|
|
29796
31092
|
},
|
|
31093
|
+
"pipelineAnalytics.getTrainingExportSummary": {
|
|
31094
|
+
capName: "pipeline-analytics",
|
|
31095
|
+
capScope: "device",
|
|
31096
|
+
addonId: null,
|
|
31097
|
+
access: "view"
|
|
31098
|
+
},
|
|
31099
|
+
"pipelineAnalytics.getTrainingExportUrl": {
|
|
31100
|
+
capName: "pipeline-analytics",
|
|
31101
|
+
capScope: "device",
|
|
31102
|
+
addonId: null,
|
|
31103
|
+
access: "view"
|
|
31104
|
+
},
|
|
29797
31105
|
"pipelineAnalytics.listEventKinds": {
|
|
29798
31106
|
capName: "pipeline-analytics",
|
|
29799
31107
|
capScope: "device",
|
|
@@ -29818,6 +31126,24 @@ Object.freeze({
|
|
|
29818
31126
|
addonId: null,
|
|
29819
31127
|
access: "view"
|
|
29820
31128
|
},
|
|
31129
|
+
"pipelineAnalytics.listRetrainAnnotations": {
|
|
31130
|
+
capName: "pipeline-analytics",
|
|
31131
|
+
capScope: "device",
|
|
31132
|
+
addonId: null,
|
|
31133
|
+
access: "view"
|
|
31134
|
+
},
|
|
31135
|
+
"pipelineAnalytics.listRetrainFrames": {
|
|
31136
|
+
capName: "pipeline-analytics",
|
|
31137
|
+
capScope: "device",
|
|
31138
|
+
addonId: null,
|
|
31139
|
+
access: "view"
|
|
31140
|
+
},
|
|
31141
|
+
"pipelineAnalytics.listRetrainStaging": {
|
|
31142
|
+
capName: "pipeline-analytics",
|
|
31143
|
+
capScope: "device",
|
|
31144
|
+
addonId: null,
|
|
31145
|
+
access: "view"
|
|
31146
|
+
},
|
|
29821
31147
|
"pipelineAnalytics.listTrackMedia": {
|
|
29822
31148
|
capName: "pipeline-analytics",
|
|
29823
31149
|
capScope: "device",
|
|
@@ -29830,6 +31156,12 @@ Object.freeze({
|
|
|
29830
31156
|
addonId: null,
|
|
29831
31157
|
access: "view"
|
|
29832
31158
|
},
|
|
31159
|
+
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
31160
|
+
capName: "pipeline-analytics",
|
|
31161
|
+
capScope: "device",
|
|
31162
|
+
addonId: null,
|
|
31163
|
+
access: "create"
|
|
31164
|
+
},
|
|
29833
31165
|
"pipelineAnalytics.pruneEvents": {
|
|
29834
31166
|
capName: "pipeline-analytics",
|
|
29835
31167
|
capScope: "device",
|
|
@@ -29860,12 +31192,30 @@ Object.freeze({
|
|
|
29860
31192
|
addonId: null,
|
|
29861
31193
|
access: "create"
|
|
29862
31194
|
},
|
|
31195
|
+
"pipelineAnalytics.restageRetrainTrack": {
|
|
31196
|
+
capName: "pipeline-analytics",
|
|
31197
|
+
capScope: "device",
|
|
31198
|
+
addonId: null,
|
|
31199
|
+
access: "create"
|
|
31200
|
+
},
|
|
31201
|
+
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
31202
|
+
capName: "pipeline-analytics",
|
|
31203
|
+
capScope: "device",
|
|
31204
|
+
addonId: null,
|
|
31205
|
+
access: "create"
|
|
31206
|
+
},
|
|
29863
31207
|
"pipelineAnalytics.searchObjectEvents": {
|
|
29864
31208
|
capName: "pipeline-analytics",
|
|
29865
31209
|
capScope: "device",
|
|
29866
31210
|
addonId: null,
|
|
29867
31211
|
access: "view"
|
|
29868
31212
|
},
|
|
31213
|
+
"pipelineAnalytics.selectRetrainFrames": {
|
|
31214
|
+
capName: "pipeline-analytics",
|
|
31215
|
+
capScope: "device",
|
|
31216
|
+
addonId: null,
|
|
31217
|
+
access: "create"
|
|
31218
|
+
},
|
|
29869
31219
|
"pipelineAnalytics.setTrackFlags": {
|
|
29870
31220
|
capName: "pipeline-analytics",
|
|
29871
31221
|
capScope: "device",
|
|
@@ -31270,6 +32620,12 @@ Object.freeze({
|
|
|
31270
32620
|
addonId: null,
|
|
31271
32621
|
access: "view"
|
|
31272
32622
|
},
|
|
32623
|
+
"streamBroker.getDeviceAudioMute": {
|
|
32624
|
+
capName: "stream-broker",
|
|
32625
|
+
capScope: "system",
|
|
32626
|
+
addonId: null,
|
|
32627
|
+
access: "view"
|
|
32628
|
+
},
|
|
31273
32629
|
"streamBroker.getPreBufferInfo": {
|
|
31274
32630
|
capName: "stream-broker",
|
|
31275
32631
|
capScope: "system",
|
|
@@ -31390,6 +32746,12 @@ Object.freeze({
|
|
|
31390
32746
|
addonId: null,
|
|
31391
32747
|
access: "create"
|
|
31392
32748
|
},
|
|
32749
|
+
"streamBroker.setDeviceAudioMute": {
|
|
32750
|
+
capName: "stream-broker",
|
|
32751
|
+
capScope: "system",
|
|
32752
|
+
addonId: null,
|
|
32753
|
+
access: "create"
|
|
32754
|
+
},
|
|
31393
32755
|
"streamBroker.setPreBufferDuration": {
|
|
31394
32756
|
capName: "stream-broker",
|
|
31395
32757
|
capScope: "system",
|
|
@@ -32037,6 +33399,66 @@ Object.freeze({
|
|
|
32037
33399
|
"smtp-provider": "email"
|
|
32038
33400
|
});
|
|
32039
33401
|
new Set(["devices", "classes"]);
|
|
33402
|
+
var WEEKDAY_TO_DAY = {
|
|
33403
|
+
Sun: 0,
|
|
33404
|
+
Mon: 1,
|
|
33405
|
+
Tue: 2,
|
|
33406
|
+
Wed: 3,
|
|
33407
|
+
Thu: 4,
|
|
33408
|
+
Fri: 5,
|
|
33409
|
+
Sat: 6
|
|
33410
|
+
};
|
|
33411
|
+
/** Resolve (weekday, minute-of-day) of `atMs` in the schedule's timezone.
|
|
33412
|
+
* An invalid/unknown IANA name falls back to the host timezone. */
|
|
33413
|
+
function localDayMinute(atMs, timezone) {
|
|
33414
|
+
const d = new Date(atMs);
|
|
33415
|
+
if (timezone !== void 0) try {
|
|
33416
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
33417
|
+
timeZone: timezone,
|
|
33418
|
+
weekday: "short",
|
|
33419
|
+
hour: "numeric",
|
|
33420
|
+
minute: "numeric",
|
|
33421
|
+
hourCycle: "h23"
|
|
33422
|
+
}).formatToParts(d);
|
|
33423
|
+
let weekday;
|
|
33424
|
+
let hour;
|
|
33425
|
+
let minute;
|
|
33426
|
+
for (const p of parts) if (p.type === "weekday") weekday = p.value;
|
|
33427
|
+
else if (p.type === "hour") hour = Number(p.value);
|
|
33428
|
+
else if (p.type === "minute") minute = Number(p.value);
|
|
33429
|
+
const day = weekday !== void 0 ? WEEKDAY_TO_DAY[weekday] : void 0;
|
|
33430
|
+
if (day !== void 0 && hour !== void 0 && minute !== void 0) return {
|
|
33431
|
+
day,
|
|
33432
|
+
minute: hour * 60 + minute
|
|
33433
|
+
};
|
|
33434
|
+
} catch {}
|
|
33435
|
+
return {
|
|
33436
|
+
day: d.getDay(),
|
|
33437
|
+
minute: d.getHours() * 60 + d.getMinutes()
|
|
33438
|
+
};
|
|
33439
|
+
}
|
|
33440
|
+
/**
|
|
33441
|
+
* Is the schedule active at `atMs`? No schedule = always active. Windows
|
|
33442
|
+
* are OR'd; a window with `startMinute > endMinute` crosses midnight (it
|
|
33443
|
+
* starts on a listed day and spills into the next). `invert` flips the
|
|
33444
|
+
* result (active OUTSIDE the windows).
|
|
33445
|
+
*/
|
|
33446
|
+
function isScheduleActive(schedule, atMs) {
|
|
33447
|
+
if (schedule === void 0) return true;
|
|
33448
|
+
const { day, minute } = localDayMinute(atMs, schedule.timezone);
|
|
33449
|
+
const prevDay = (day + 6) % 7;
|
|
33450
|
+
let inside = false;
|
|
33451
|
+
for (const w of schedule.windows) if (w.startMinute <= w.endMinute) {
|
|
33452
|
+
if (w.days.includes(day) && minute >= w.startMinute && minute < w.endMinute) {
|
|
33453
|
+
inside = true;
|
|
33454
|
+
break;
|
|
33455
|
+
}
|
|
33456
|
+
} else if (w.days.includes(day) && minute >= w.startMinute || w.days.includes(prevDay) && minute < w.endMinute) {
|
|
33457
|
+
inside = true;
|
|
33458
|
+
break;
|
|
33459
|
+
}
|
|
33460
|
+
return schedule.invert === true ? !inside : inside;
|
|
33461
|
+
}
|
|
32040
33462
|
/**
|
|
32041
33463
|
* TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
|
|
32042
33464
|
*
|
|
@@ -32321,6 +33743,18 @@ Object.defineProperty(exports, "EventCategory", {
|
|
|
32321
33743
|
return EventCategory;
|
|
32322
33744
|
}
|
|
32323
33745
|
});
|
|
33746
|
+
Object.defineProperty(exports, "Fmp4BoxSplitter", {
|
|
33747
|
+
enumerable: true,
|
|
33748
|
+
get: function() {
|
|
33749
|
+
return Fmp4BoxSplitter;
|
|
33750
|
+
}
|
|
33751
|
+
});
|
|
33752
|
+
Object.defineProperty(exports, "LabelAttributionSchema", {
|
|
33753
|
+
enumerable: true,
|
|
33754
|
+
get: function() {
|
|
33755
|
+
return LabelAttributionSchema;
|
|
33756
|
+
}
|
|
33757
|
+
});
|
|
32324
33758
|
Object.defineProperty(exports, "MACRO_LABELS", {
|
|
32325
33759
|
enumerable: true,
|
|
32326
33760
|
get: function() {
|
|
@@ -32393,6 +33827,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
|
|
|
32393
33827
|
return OpsLogEntrySchema;
|
|
32394
33828
|
}
|
|
32395
33829
|
});
|
|
33830
|
+
Object.defineProperty(exports, "RetrainStatusSchema", {
|
|
33831
|
+
enumerable: true,
|
|
33832
|
+
get: function() {
|
|
33833
|
+
return RetrainStatusSchema;
|
|
33834
|
+
}
|
|
33835
|
+
});
|
|
32396
33836
|
Object.defineProperty(exports, "TimelapseRuleInputSchema", {
|
|
32397
33837
|
enumerable: true,
|
|
32398
33838
|
get: function() {
|
|
@@ -32459,6 +33899,12 @@ Object.defineProperty(exports, "buildEventKindDescriptor", {
|
|
|
32459
33899
|
return buildEventKindDescriptor;
|
|
32460
33900
|
}
|
|
32461
33901
|
});
|
|
33902
|
+
Object.defineProperty(exports, "buildFfmpegArgs", {
|
|
33903
|
+
enumerable: true,
|
|
33904
|
+
get: function() {
|
|
33905
|
+
return buildFfmpegArgs;
|
|
33906
|
+
}
|
|
33907
|
+
});
|
|
32462
33908
|
Object.defineProperty(exports, "cosineSimilarity", {
|
|
32463
33909
|
enumerable: true,
|
|
32464
33910
|
get: function() {
|
|
@@ -32525,6 +33971,18 @@ Object.defineProperty(exports, "isDeviceScopedCap", {
|
|
|
32525
33971
|
return isDeviceScopedCap;
|
|
32526
33972
|
}
|
|
32527
33973
|
});
|
|
33974
|
+
Object.defineProperty(exports, "isScheduleActive", {
|
|
33975
|
+
enumerable: true,
|
|
33976
|
+
get: function() {
|
|
33977
|
+
return isScheduleActive;
|
|
33978
|
+
}
|
|
33979
|
+
});
|
|
33980
|
+
Object.defineProperty(exports, "isSoftwareDecode", {
|
|
33981
|
+
enumerable: true,
|
|
33982
|
+
get: function() {
|
|
33983
|
+
return isSoftwareDecode;
|
|
33984
|
+
}
|
|
33985
|
+
});
|
|
32528
33986
|
Object.defineProperty(exports, "kebabToCamel", {
|
|
32529
33987
|
enumerable: true,
|
|
32530
33988
|
get: function() {
|