@camstack/addon-post-analysis 1.2.117 → 1.2.119
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-Dk-BdVHw.js → dist-BtIolp2d.js} +17 -2
- package/dist/{dist-PMOtjzh2.mjs → dist-R0Wd863b.mjs} +17 -2
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BmR_bLi_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CEBcz1NX.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-Ddl2vUu6.mjs → hostInit-CP5WsAw5.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +467 -92
- package/dist/pipeline-analytics/index.mjs +467 -92
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-
|
|
1
|
+
import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-R0Wd863b.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -4391,10 +4391,28 @@ var SCENE_LATCH_INDEXES = [{
|
|
|
4391
4391
|
function sceneKey(deviceId, monitorId) {
|
|
4392
4392
|
return `${deviceId}:${monitorId}`;
|
|
4393
4393
|
}
|
|
4394
|
+
/**
|
|
4395
|
+
* Reseed cap for BOTH scene ledgers.
|
|
4396
|
+
*
|
|
4397
|
+
* `DurableLedger` defaults to 100_000 and its own docblock says the owner "sets
|
|
4398
|
+
* it from the row set it expects". Neither scene ledger did, so the 30s
|
|
4399
|
+
* reconcile asked SQLite — through hub-main's event loop, which is the
|
|
4400
|
+
* cluster's only queue (D181) — for up to a hundred thousand rows twice a tick,
|
|
4401
|
+
* on a hub holding one scene and one latch (measured live 2026-08-25). A bound
|
|
4402
|
+
* nobody chose is not a bound.
|
|
4403
|
+
*
|
|
4404
|
+
* A scene is an operator-drawn monitor on ONE camera and a latch is at most one
|
|
4405
|
+
* row per scene, so the row set is `cameras x monitors-per-camera`. 5_000 is
|
|
4406
|
+
* ~5 monitors on each of a thousand cameras — far past any installation this
|
|
4407
|
+
* ships to, and still small enough that a runaway write loop is CAUGHT by the
|
|
4408
|
+
* bound instead of being paged in whole.
|
|
4409
|
+
*/
|
|
4410
|
+
var SCENE_LEDGER_LOAD_LIMIT = 5e3;
|
|
4394
4411
|
var SCENE_CONFIG_SPEC = {
|
|
4395
4412
|
collection: SCENE_COLLECTION,
|
|
4396
4413
|
columns: SCENE_COLUMNS,
|
|
4397
4414
|
indexes: SCENE_INDEXES,
|
|
4415
|
+
loadLimit: SCENE_LEDGER_LOAD_LIMIT,
|
|
4398
4416
|
writeMode: "write-behind",
|
|
4399
4417
|
keyOf: (row) => row.key,
|
|
4400
4418
|
toValue: (row) => ({
|
|
@@ -4410,6 +4428,7 @@ var SCENE_LATCH_SPEC = {
|
|
|
4410
4428
|
collection: SCENE_LATCH_COLLECTION,
|
|
4411
4429
|
columns: SCENE_LATCH_COLUMNS,
|
|
4412
4430
|
indexes: SCENE_LATCH_INDEXES,
|
|
4431
|
+
loadLimit: SCENE_LEDGER_LOAD_LIMIT,
|
|
4413
4432
|
writeMode: "write-through",
|
|
4414
4433
|
keyOf: (row) => row.key,
|
|
4415
4434
|
toValue: (row) => ({
|
|
@@ -4439,6 +4458,9 @@ var SceneStore = class {
|
|
|
4439
4458
|
config;
|
|
4440
4459
|
latch;
|
|
4441
4460
|
logger;
|
|
4461
|
+
/** `scenes/latches` as last announced. Null until the first load, so the
|
|
4462
|
+
* first one always speaks. */
|
|
4463
|
+
lastLoggedCounts = null;
|
|
4442
4464
|
constructor(deps) {
|
|
4443
4465
|
this.logger = deps.logger;
|
|
4444
4466
|
this.config = new DurableLedger({
|
|
@@ -4463,10 +4485,20 @@ var SceneStore = class {
|
|
|
4463
4485
|
* a container recreate, "loaded 0" is the one line that explains why every
|
|
4464
4486
|
* scene suddenly reads `unknown`, and its absence is what turns a data loss
|
|
4465
4487
|
* into a three-hour hunt.
|
|
4488
|
+
*
|
|
4489
|
+
* ON CHANGE ONLY, though — including the first load, where there is nothing
|
|
4490
|
+
* to compare against. This runs on the 30s `SCENE_RELOAD_INTERVAL_MS` tick on
|
|
4491
|
+
* every node, and it used to print unconditionally: measured live 2026-08-25,
|
|
4492
|
+
* `scene state loaded {scenes=1 latches=1}` every 30 seconds forever. A count
|
|
4493
|
+
* reprinted unchanged twice a minute is exactly how the "loaded 0" this line
|
|
4494
|
+
* exists for gets scrolled past.
|
|
4466
4495
|
*/
|
|
4467
4496
|
async load() {
|
|
4468
4497
|
const scenes = await this.config.load();
|
|
4469
4498
|
const latches = await this.latch.load();
|
|
4499
|
+
const counts = `${scenes.length}/${latches.length}`;
|
|
4500
|
+
if (counts === this.lastLoggedCounts) return;
|
|
4501
|
+
this.lastLoggedCounts = counts;
|
|
4470
4502
|
this.logger.info("scene state loaded", { meta: {
|
|
4471
4503
|
scenes: scenes.length,
|
|
4472
4504
|
latches: latches.length
|
|
@@ -8532,6 +8564,49 @@ var NC_LANGUAGES = ["en", "it"];
|
|
|
8532
8564
|
* surface's view all validate against this one, so a third language is added
|
|
8533
8565
|
* to `NC_LANGUAGES` and nowhere else. */
|
|
8534
8566
|
var NcLanguageSchema = _enum(NC_LANGUAGES);
|
|
8567
|
+
/**
|
|
8568
|
+
* Whether two snapshots say the same thing.
|
|
8569
|
+
*
|
|
8570
|
+
* ## Why this is a function and not `===`
|
|
8571
|
+
*
|
|
8572
|
+
* The reload guard in `reloadTexts()` used to be
|
|
8573
|
+
* `before.overrides === next.overrides`, and `next` comes from
|
|
8574
|
+
* `NcTextCatalogStore.load()`, which `parse()`s the row through zod on every
|
|
8575
|
+
* call. The right-hand side is therefore a NEW object on every tick by
|
|
8576
|
+
* construction: the comparison was `false` at byte-identical content, forever.
|
|
8577
|
+
* Measured on the live hub 2026-08-25: 58 "notification texts loaded" lines in
|
|
8578
|
+
* 30 minutes on a hub whose language had not changed, from a line whose own
|
|
8579
|
+
* docblock promises it fires on CHANGE only.
|
|
8580
|
+
*
|
|
8581
|
+
* The document is two levels deep and installation-small (at most one entry per
|
|
8582
|
+
* language, each a flat key→string map), so the comparison is written out
|
|
8583
|
+
* rather than reached for through a deep-equal dependency — and written out it
|
|
8584
|
+
* can state the direction that matters: a REMOVED key must not read as equal,
|
|
8585
|
+
* which a one-directional key walk would get wrong.
|
|
8586
|
+
*/
|
|
8587
|
+
function ncTextSnapshotsEqual(a, b) {
|
|
8588
|
+
if (a.language !== b.language) return false;
|
|
8589
|
+
return overrideMapsEqual(a.overrides, b.overrides);
|
|
8590
|
+
}
|
|
8591
|
+
function overrideMapsEqual(a, b) {
|
|
8592
|
+
const languages = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
8593
|
+
for (const language of languages) {
|
|
8594
|
+
const left = a[language];
|
|
8595
|
+
const right = b[language];
|
|
8596
|
+
if (left === void 0 || right === void 0) {
|
|
8597
|
+
if (Object.keys(left ?? right ?? {}).length > 0) return false;
|
|
8598
|
+
continue;
|
|
8599
|
+
}
|
|
8600
|
+
if (!flatMapsEqual(left, right)) return false;
|
|
8601
|
+
}
|
|
8602
|
+
return true;
|
|
8603
|
+
}
|
|
8604
|
+
function flatMapsEqual(a, b) {
|
|
8605
|
+
const keys = Object.keys(a);
|
|
8606
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
8607
|
+
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
8608
|
+
return true;
|
|
8609
|
+
}
|
|
8535
8610
|
/** The shipped files, keyed by language. Data, never mutated. */
|
|
8536
8611
|
var NC_TEXT_BUNDLES = {
|
|
8537
8612
|
en: en_default,
|
|
@@ -22462,7 +22537,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
22462
22537
|
const before = this.texts.snapshot;
|
|
22463
22538
|
const next = await this.textStore.load();
|
|
22464
22539
|
this.texts.apply(next);
|
|
22465
|
-
if (before
|
|
22540
|
+
if (ncTextSnapshotsEqual(before, next)) return;
|
|
22466
22541
|
this.logger.info("notification texts loaded", { meta: {
|
|
22467
22542
|
language: next.language,
|
|
22468
22543
|
overriddenKeys: Object.values(next.overrides).reduce((sum, keys) => sum + Object.keys(keys ?? {}).length, 0)
|
|
@@ -25902,7 +25977,7 @@ var CLASS_RANK_VEHICLE = .8;
|
|
|
25902
25977
|
var CLASS_RANK_ANIMAL = .5;
|
|
25903
25978
|
var CLASS_RANK_DEFAULT = .25;
|
|
25904
25979
|
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
25905
|
-
var VEHICLE_CLASSES = new Set([
|
|
25980
|
+
var VEHICLE_CLASSES$1 = new Set([
|
|
25906
25981
|
"vehicle",
|
|
25907
25982
|
"car",
|
|
25908
25983
|
"truck",
|
|
@@ -25933,7 +26008,7 @@ function clamp01$1(x) {
|
|
|
25933
26008
|
function classRank(className) {
|
|
25934
26009
|
const c = className.toLowerCase();
|
|
25935
26010
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
25936
|
-
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
26011
|
+
if (VEHICLE_CLASSES$1.has(c)) return CLASS_RANK_VEHICLE;
|
|
25937
26012
|
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
25938
26013
|
return CLASS_RANK_DEFAULT;
|
|
25939
26014
|
}
|
|
@@ -26536,7 +26611,7 @@ var FaceGalleryProvider = class {
|
|
|
26536
26611
|
const identities = await this.identityStore.listIdentities();
|
|
26537
26612
|
const nameMap = new Map(identities.map((i) => [i.id, i.name]));
|
|
26538
26613
|
const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
|
|
26539
|
-
const
|
|
26614
|
+
const cropUrl = this.cropUrlFor(face);
|
|
26540
26615
|
return {
|
|
26541
26616
|
faceId: face.id,
|
|
26542
26617
|
deviceId: face.deviceId,
|
|
@@ -26545,7 +26620,7 @@ var FaceGalleryProvider = class {
|
|
|
26545
26620
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
26546
26621
|
...identityName !== void 0 ? { identityName } : {},
|
|
26547
26622
|
assigned: face.assigned,
|
|
26548
|
-
...
|
|
26623
|
+
...cropUrl !== void 0 ? { cropUrl } : {},
|
|
26549
26624
|
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
26550
26625
|
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
|
|
26551
26626
|
...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
|
|
@@ -29663,9 +29738,15 @@ var TrackCloser = class {
|
|
|
29663
29738
|
await this.suppressZeroMedia(t.deviceId, t.trackId);
|
|
29664
29739
|
return;
|
|
29665
29740
|
}
|
|
29666
|
-
|
|
29741
|
+
let thumbnailLanded = ownedMedia.some((m) => m.kind === "thumbnail");
|
|
29742
|
+
if (outcome === "raster-fallback" && closure?.rasterFallback) {
|
|
29743
|
+
await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
|
|
29744
|
+
thumbnailLanded = true;
|
|
29745
|
+
}
|
|
29667
29746
|
await this.maybePromoteLastFrame(t, ownedMedia);
|
|
29668
|
-
|
|
29747
|
+
const hasRasterFallback = closure?.rasterFallback !== void 0;
|
|
29748
|
+
if (!thumbnailLanded) thumbnailLanded = await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback);
|
|
29749
|
+
if (!thumbnailLanded && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
|
|
29669
29750
|
this.deps.logger.info("track ended", {
|
|
29670
29751
|
tags: { deviceId: t.deviceId },
|
|
29671
29752
|
meta: {
|
|
@@ -29783,14 +29864,23 @@ var TrackCloser = class {
|
|
|
29783
29864
|
* and the rate could only ever be counted from the database, after the fact.
|
|
29784
29865
|
* A branch that drops work silently reads as "never happened".
|
|
29785
29866
|
*/
|
|
29786
|
-
async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia) {
|
|
29787
|
-
if (ownedMedia.some((m) => m.kind === "thumbnail")) return;
|
|
29867
|
+
async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback) {
|
|
29868
|
+
if (ownedMedia.some((m) => m.kind === "thumbnail")) return true;
|
|
29788
29869
|
const derive = this.deps.deriveThumbnailFromKeyFrame;
|
|
29789
|
-
if (!derive)
|
|
29870
|
+
if (!derive) {
|
|
29871
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-not-wired");
|
|
29872
|
+
return false;
|
|
29873
|
+
}
|
|
29790
29874
|
const keyFrame = ownedMedia.find((m) => m.kind === "keyFrame");
|
|
29791
|
-
if (!keyFrame)
|
|
29875
|
+
if (!keyFrame) {
|
|
29876
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-key-frame");
|
|
29877
|
+
return false;
|
|
29878
|
+
}
|
|
29792
29879
|
const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
|
|
29793
|
-
if (!bestSeen)
|
|
29880
|
+
if (!bestSeen) {
|
|
29881
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
|
|
29882
|
+
return false;
|
|
29883
|
+
}
|
|
29794
29884
|
try {
|
|
29795
29885
|
const derived = await derive({
|
|
29796
29886
|
mediaKey: keyFrame.key,
|
|
@@ -29802,17 +29892,23 @@ var TrackCloser = class {
|
|
|
29802
29892
|
},
|
|
29803
29893
|
frameWidth: bestSeen.frameWidth,
|
|
29804
29894
|
frameHeight: bestSeen.frameHeight,
|
|
29805
|
-
timestamp:
|
|
29895
|
+
timestamp: keyFrame.timestamp
|
|
29806
29896
|
});
|
|
29807
|
-
if (!derived)
|
|
29897
|
+
if (!derived) {
|
|
29898
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
|
|
29899
|
+
return false;
|
|
29900
|
+
}
|
|
29808
29901
|
const mediaStore = this.deps.mediaStore();
|
|
29809
|
-
if (!mediaStore)
|
|
29902
|
+
if (!mediaStore) {
|
|
29903
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-media-store");
|
|
29904
|
+
return false;
|
|
29905
|
+
}
|
|
29810
29906
|
await mediaStore.put({
|
|
29811
29907
|
deviceId: t.deviceId,
|
|
29812
29908
|
ownerKind: "track",
|
|
29813
29909
|
ownerId: t.trackId,
|
|
29814
29910
|
kind: "thumbnail",
|
|
29815
|
-
timestamp:
|
|
29911
|
+
timestamp: keyFrame.timestamp,
|
|
29816
29912
|
data: derived.thumbnail
|
|
29817
29913
|
});
|
|
29818
29914
|
await mediaStore.put({
|
|
@@ -29820,7 +29916,7 @@ var TrackCloser = class {
|
|
|
29820
29916
|
ownerKind: "track",
|
|
29821
29917
|
ownerId: t.trackId,
|
|
29822
29918
|
kind: "thumbnailSmall",
|
|
29823
|
-
timestamp:
|
|
29919
|
+
timestamp: keyFrame.timestamp,
|
|
29824
29920
|
data: derived.thumbnailSmall
|
|
29825
29921
|
});
|
|
29826
29922
|
this.deps.logger.info("thumbnail derived from keyFrame at close (no live best-shot landed)", {
|
|
@@ -29831,6 +29927,7 @@ var TrackCloser = class {
|
|
|
29831
29927
|
skewMs: derived.skewMs
|
|
29832
29928
|
}
|
|
29833
29929
|
});
|
|
29930
|
+
return true;
|
|
29834
29931
|
} catch (err) {
|
|
29835
29932
|
this.deps.logger.debug("close-time keyFrame thumbnail derive failed", {
|
|
29836
29933
|
tags: { deviceId: t.deviceId },
|
|
@@ -29839,10 +29936,11 @@ var TrackCloser = class {
|
|
|
29839
29936
|
error: String(err)
|
|
29840
29937
|
}
|
|
29841
29938
|
});
|
|
29842
|
-
this.logNoThumbnailAtClose(t, "derive-threw", {
|
|
29939
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-threw", {
|
|
29843
29940
|
keyFrameKey: keyFrame.key,
|
|
29844
29941
|
error: String(err)
|
|
29845
29942
|
});
|
|
29943
|
+
return false;
|
|
29846
29944
|
}
|
|
29847
29945
|
}
|
|
29848
29946
|
/**
|
|
@@ -30712,6 +30810,7 @@ var PlateGalleryProvider = class {
|
|
|
30712
30810
|
logger;
|
|
30713
30811
|
refreshGallery;
|
|
30714
30812
|
emitPlateGalleryChanged;
|
|
30813
|
+
eventMediaBaseUrl;
|
|
30715
30814
|
constructor(deps) {
|
|
30716
30815
|
this.plateStore = deps.plateStore;
|
|
30717
30816
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -30721,6 +30820,7 @@ var PlateGalleryProvider = class {
|
|
|
30721
30820
|
this.logger = deps.logger;
|
|
30722
30821
|
this.refreshGallery = deps.refreshGallery;
|
|
30723
30822
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
30823
|
+
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
30724
30824
|
}
|
|
30725
30825
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
30726
30826
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -30728,6 +30828,12 @@ var PlateGalleryProvider = class {
|
|
|
30728
30828
|
this.emitPlateGalleryChanged(payload);
|
|
30729
30829
|
} catch {}
|
|
30730
30830
|
}
|
|
30831
|
+
cropUrlFor(plate) {
|
|
30832
|
+
if (plate.mediaKey === void 0 || plate.mediaKey === "") return void 0;
|
|
30833
|
+
const base = this.eventMediaBaseUrl();
|
|
30834
|
+
if (base === null) return void 0;
|
|
30835
|
+
return `${base}/${encodeURIComponent(plate.mediaKey)}`;
|
|
30836
|
+
}
|
|
30731
30837
|
async plateCropBase64(plate) {
|
|
30732
30838
|
if (plate.mediaKey) {
|
|
30733
30839
|
const m = await this.mediaStore.getByKey(plate.mediaKey);
|
|
@@ -30736,8 +30842,9 @@ var PlateGalleryProvider = class {
|
|
|
30736
30842
|
const media = await this.mediaStore.listByOwner("plate", plate.id);
|
|
30737
30843
|
return media.length > 0 ? media[0].base64 : void 0;
|
|
30738
30844
|
}
|
|
30739
|
-
async toPlateInfo(plate, nameMap) {
|
|
30740
|
-
const base64 = await this.plateCropBase64(plate);
|
|
30845
|
+
async toPlateInfo(plate, nameMap, opts) {
|
|
30846
|
+
const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
|
|
30847
|
+
const cropUrl = this.cropUrlFor(plate);
|
|
30741
30848
|
const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
|
|
30742
30849
|
return {
|
|
30743
30850
|
plateId: plate.id,
|
|
@@ -30752,7 +30859,8 @@ var PlateGalleryProvider = class {
|
|
|
30752
30859
|
...vehicleName !== void 0 ? { vehicleName } : {},
|
|
30753
30860
|
...plate.plateBbox !== void 0 ? { plateBbox: plate.plateBbox } : {},
|
|
30754
30861
|
...plate.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: plate.keyFrameMediaKey } : {},
|
|
30755
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
30862
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
30863
|
+
...cropUrl !== void 0 ? { cropUrl } : {}
|
|
30756
30864
|
};
|
|
30757
30865
|
}
|
|
30758
30866
|
async vehicleNameMap() {
|
|
@@ -30771,7 +30879,7 @@ var PlateGalleryProvider = class {
|
|
|
30771
30879
|
const plate = await this.plateStore.get(`plate-${input.trackId}`);
|
|
30772
30880
|
if (!plate || plate.deviceId !== input.deviceId) return null;
|
|
30773
30881
|
const nameMap = await this.vehicleNameMap();
|
|
30774
|
-
return this.toPlateInfo(plate, nameMap);
|
|
30882
|
+
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
30775
30883
|
}
|
|
30776
30884
|
async getPlateMedia(input) {
|
|
30777
30885
|
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
|
|
@@ -35364,6 +35472,18 @@ var EmbeddingDispatcher = class {
|
|
|
35364
35472
|
logger;
|
|
35365
35473
|
getRemoteFrame;
|
|
35366
35474
|
lastEmbedTime = /* @__PURE__ */ new Map();
|
|
35475
|
+
/**
|
|
35476
|
+
* trackId -> the frame pinned for it. Each entry holds a WHOLE decoded RGB
|
|
35477
|
+
* frame (`PendingCrop.frameData`, ~0.7 MB), not a crop.
|
|
35478
|
+
*
|
|
35479
|
+
* Under the shipped `cropStrategy: 'first'` this doubles as the in-flight
|
|
35480
|
+
* DEDUPE FLAG — `'first'` gates on `!pendingCrops.has(trackId)` — and there
|
|
35481
|
+
* is no flush timer for that strategy, so nothing ever sweeps it. Until
|
|
35482
|
+
* 2026-08-25 the delete lived on `processOne`'s success path only: an
|
|
35483
|
+
* `extractCrop` throw or an `encoder.encode` rejection (a cross-process cap
|
|
35484
|
+
* over UDS, with no timeout) stranded the frame permanently AND made that
|
|
35485
|
+
* trackId un-embeddable forever. `processOne` now releases in a `finally`.
|
|
35486
|
+
*/
|
|
35367
35487
|
pendingCrops = /* @__PURE__ */ new Map();
|
|
35368
35488
|
flushTimer = null;
|
|
35369
35489
|
unsubscribe = null;
|
|
@@ -35521,7 +35641,6 @@ var EmbeddingDispatcher = class {
|
|
|
35521
35641
|
this._processedCount++;
|
|
35522
35642
|
this._totalInferenceMs += inferenceMs;
|
|
35523
35643
|
this.lastEmbedTime.set(pending.deviceId, Date.now());
|
|
35524
|
-
this.pendingCrops.delete(pending.trackId);
|
|
35525
35644
|
const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`;
|
|
35526
35645
|
const payload = {
|
|
35527
35646
|
deviceId: Number(pending.deviceId),
|
|
@@ -35553,8 +35672,21 @@ var EmbeddingDispatcher = class {
|
|
|
35553
35672
|
error: String(err)
|
|
35554
35673
|
}
|
|
35555
35674
|
});
|
|
35675
|
+
} finally {
|
|
35676
|
+
if (this.pendingCrops.get(pending.trackId) === pending) this.pendingCrops.delete(pending.trackId);
|
|
35556
35677
|
}
|
|
35557
35678
|
}
|
|
35679
|
+
/**
|
|
35680
|
+
* Decoded frames currently pinned in {@link pendingCrops}.
|
|
35681
|
+
*
|
|
35682
|
+
* ~0.7 MB each (a whole RGB detection frame, not a crop), so this number IS
|
|
35683
|
+
* the dispatcher's memory footprint. Exposed because the entries are
|
|
35684
|
+
* invisible in every functional read: a stranded one looks exactly like a
|
|
35685
|
+
* track that was never detected.
|
|
35686
|
+
*/
|
|
35687
|
+
pendingFrameCount() {
|
|
35688
|
+
return this.pendingCrops.size;
|
|
35689
|
+
}
|
|
35558
35690
|
};
|
|
35559
35691
|
/**
|
|
35560
35692
|
* Re-embed every listed track from its key frame.
|
|
@@ -35874,6 +36006,19 @@ function createEventMediaHandler(deps) {
|
|
|
35874
36006
|
};
|
|
35875
36007
|
}
|
|
35876
36008
|
//#endregion
|
|
36009
|
+
//#region src/pipeline-analytics/copy-face-crop-preview.ts
|
|
36010
|
+
var TRACK_PREVIEW_KINDS = new Set(["thumbnail", "thumbnailSmall"]);
|
|
36011
|
+
async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
|
|
36012
|
+
if ((await store.listTrackMedia(input.trackId)).some((m) => TRACK_PREVIEW_KINDS.has(m.kind))) return false;
|
|
36013
|
+
await store.putTrackThumbnail({
|
|
36014
|
+
deviceId: input.deviceId,
|
|
36015
|
+
trackId: input.trackId,
|
|
36016
|
+
timestamp: input.timestamp,
|
|
36017
|
+
data: input.crop
|
|
36018
|
+
});
|
|
36019
|
+
return true;
|
|
36020
|
+
}
|
|
36021
|
+
//#endregion
|
|
35877
36022
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
35878
36023
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
35879
36024
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
@@ -35882,29 +36027,47 @@ function createEventMediaHandler(deps) {
|
|
|
35882
36027
|
function pickEventOwnedMedia(files) {
|
|
35883
36028
|
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
35884
36029
|
}
|
|
35885
|
-
/** Pick a track's fallback media
|
|
35886
|
-
*
|
|
35887
|
-
*
|
|
35888
|
-
* Undefined when the
|
|
36030
|
+
/** Pick a track's fallback media when no subject crop exists: `thumbnailSmall`
|
|
36031
|
+
* → `thumbnail` → native `keyFrameSmall`/`keyFrame`/`fullFrame` → rolling
|
|
36032
|
+
* `lastFrame` → `firstFrame` → newest `snapshot` → any blob. A full frame
|
|
36033
|
+
* beats a class icon. Undefined when the owner has no media. */
|
|
35889
36034
|
function pickTrackFallbackMedia(files) {
|
|
35890
|
-
return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
36035
|
+
return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame") ?? files.find((f) => f.kind === "fullFrame") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
35891
36036
|
}
|
|
35892
36037
|
/**
|
|
35893
|
-
* Crop-forced resolution (the `?kind=crop`
|
|
35894
|
-
*
|
|
35895
|
-
*
|
|
35896
|
-
*
|
|
35897
|
-
*
|
|
35898
|
-
* returning undefined. A boxed frame (with the target's box drawn) is a real
|
|
35899
|
-
* preview; an empty tile is not. Clean is always tried FIRST so the common case
|
|
35900
|
-
* still serves an unboxed tile. Undefined only when the owner has NO media.
|
|
36038
|
+
* Crop-forced resolution (the `?kind=crop` / `?kind=thumbnailSmall` reel path).
|
|
36039
|
+
*
|
|
36040
|
+
* Ladder: body crop (track/event thumbnail) → tight face/plate crop → any
|
|
36041
|
+
* remaining image (keyFrame / lastFrame / …). The class icon is only honest
|
|
36042
|
+
* when the owner has no stored bytes at all.
|
|
35901
36043
|
*
|
|
35902
36044
|
* `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
|
|
35903
36045
|
* in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
|
|
35904
36046
|
* composition pure + unit-testable here.
|
|
35905
36047
|
*/
|
|
35906
|
-
function resolveCropForcedMedia(files, pickClean) {
|
|
35907
|
-
return pickClean(files) ?? pickTrackFallbackMedia(files);
|
|
36048
|
+
function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
|
|
36049
|
+
return pickClean(files) ?? pickClean(detailFiles) ?? pickTrackFallbackMedia([...files, ...detailFiles]);
|
|
36050
|
+
}
|
|
36051
|
+
/**
|
|
36052
|
+
* Event + track media (the body/scene) and face/plate media (the detail
|
|
36053
|
+
* crops). Face rows live under `face-${trackId}`; a track can close with
|
|
36054
|
+
* `hasEmbeddedFace` and ZERO track files (0e343f86).
|
|
36055
|
+
*/
|
|
36056
|
+
async function collectCropForcedMedia(source, id) {
|
|
36057
|
+
const eventFiles = await source.listByOwner("event", id);
|
|
36058
|
+
const trackFiles = await source.listByOwner("track", id);
|
|
36059
|
+
const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
|
|
36060
|
+
const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
|
|
36061
|
+
const trackId = ownerTrackId ?? id;
|
|
36062
|
+
const [faceFiles, plateFiles] = await Promise.all([source.listByOwner("face", `face-${trackId}`), source.listByOwner("plate", `plate-${trackId}`)]);
|
|
36063
|
+
return {
|
|
36064
|
+
bodyFiles: [
|
|
36065
|
+
...eventFiles,
|
|
36066
|
+
...trackFiles,
|
|
36067
|
+
...owningTrackFiles
|
|
36068
|
+
],
|
|
36069
|
+
detailFiles: [...faceFiles, ...plateFiles]
|
|
36070
|
+
};
|
|
35908
36071
|
}
|
|
35909
36072
|
/**
|
|
35910
36073
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
@@ -35918,6 +36081,14 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
35918
36081
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
35919
36082
|
}
|
|
35920
36083
|
//#endregion
|
|
36084
|
+
//#region src/pipeline-analytics/ensure-track-hierarchy.ts
|
|
36085
|
+
async function ensureTrackHierarchyMedia(deps, input) {
|
|
36086
|
+
const tasks = [];
|
|
36087
|
+
if (!deps.isThumbnailLanded(input.trackId)) tasks.push(deps.captureBodyThumbnail(input));
|
|
36088
|
+
if (!deps.isKeyFrameLanded(input.trackId)) tasks.push(deps.captureKeyFrame(input));
|
|
36089
|
+
await Promise.all(tasks);
|
|
36090
|
+
}
|
|
36091
|
+
//#endregion
|
|
35921
36092
|
//#region src/pipeline-analytics/face-settings.ts
|
|
35922
36093
|
/**
|
|
35923
36094
|
* Per-device face-recognition settings. Cascade: a per-device override on top
|
|
@@ -36436,6 +36607,21 @@ var FaceRecognizer = class {
|
|
|
36436
36607
|
...held,
|
|
36437
36608
|
crop
|
|
36438
36609
|
});
|
|
36610
|
+
if (crop !== void 0 && input.frameHandle !== void 0) this.deps.onLiveHierarchy?.({
|
|
36611
|
+
deviceId: input.deviceId,
|
|
36612
|
+
trackId: c.trackId,
|
|
36613
|
+
timestamp: input.timestamp,
|
|
36614
|
+
bodyBbox: {
|
|
36615
|
+
x: c.bbox.x,
|
|
36616
|
+
y: c.bbox.y,
|
|
36617
|
+
w: c.bbox.w,
|
|
36618
|
+
h: c.bbox.h
|
|
36619
|
+
},
|
|
36620
|
+
frameWidth: input.frameWidth,
|
|
36621
|
+
frameHeight: input.frameHeight,
|
|
36622
|
+
cropPadding: input.cropPadding,
|
|
36623
|
+
frameHandle: input.frameHandle
|
|
36624
|
+
});
|
|
36439
36625
|
this.deps.logger.debug("face: best-face held", {
|
|
36440
36626
|
tags: {
|
|
36441
36627
|
deviceId: input.deviceId,
|
|
@@ -36585,6 +36771,22 @@ var FaceRecognizer = class {
|
|
|
36585
36771
|
});
|
|
36586
36772
|
return;
|
|
36587
36773
|
}
|
|
36774
|
+
try {
|
|
36775
|
+
await this.deps.copyCropToTrackPreview?.({
|
|
36776
|
+
deviceId,
|
|
36777
|
+
trackId,
|
|
36778
|
+
crop: held.crop,
|
|
36779
|
+
timestamp: held.timestamp
|
|
36780
|
+
});
|
|
36781
|
+
} catch (err) {
|
|
36782
|
+
this.deps.logger.debug("face: track preview copy failed", {
|
|
36783
|
+
tags: { deviceId },
|
|
36784
|
+
meta: {
|
|
36785
|
+
trackId,
|
|
36786
|
+
error: String(err)
|
|
36787
|
+
}
|
|
36788
|
+
});
|
|
36789
|
+
}
|
|
36588
36790
|
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
36589
36791
|
try {
|
|
36590
36792
|
await this.deps.faceStore.insert({
|
|
@@ -44164,6 +44366,12 @@ var PlateRecognizer = class {
|
|
|
44164
44366
|
text: c.plateText,
|
|
44165
44367
|
score: c.plateScore,
|
|
44166
44368
|
bbox: c.plateBbox,
|
|
44369
|
+
bodyBbox: {
|
|
44370
|
+
x: c.bbox.x,
|
|
44371
|
+
y: c.bbox.y,
|
|
44372
|
+
w: c.bbox.w,
|
|
44373
|
+
h: c.bbox.h
|
|
44374
|
+
},
|
|
44167
44375
|
timestamp: input.timestamp,
|
|
44168
44376
|
frameWidth: input.frameWidth,
|
|
44169
44377
|
frameHeight: input.frameHeight,
|
|
@@ -44237,6 +44445,16 @@ var PlateRecognizer = class {
|
|
|
44237
44445
|
}
|
|
44238
44446
|
});
|
|
44239
44447
|
}
|
|
44448
|
+
if (crop !== void 0 && input.frameHandle !== void 0 && input.bodyBbox !== void 0) this.deps.onLiveHierarchy?.({
|
|
44449
|
+
deviceId: input.deviceId,
|
|
44450
|
+
trackId: input.trackId,
|
|
44451
|
+
timestamp: input.timestamp,
|
|
44452
|
+
bodyBbox: input.bodyBbox,
|
|
44453
|
+
frameWidth: input.frameWidth,
|
|
44454
|
+
frameHeight: input.frameHeight,
|
|
44455
|
+
cropPadding: input.cropPadding,
|
|
44456
|
+
frameHandle: input.frameHandle
|
|
44457
|
+
});
|
|
44240
44458
|
this.bestPlate.set(input.trackId, {
|
|
44241
44459
|
text: input.text,
|
|
44242
44460
|
score: input.score,
|
|
@@ -48271,14 +48489,23 @@ function isAfterCursor(row, cursor) {
|
|
|
48271
48489
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
48272
48490
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
48273
48491
|
}
|
|
48492
|
+
var VEHICLE_CLASSES = new Set([
|
|
48493
|
+
"vehicle",
|
|
48494
|
+
"car",
|
|
48495
|
+
"truck",
|
|
48496
|
+
"bus"
|
|
48497
|
+
]);
|
|
48498
|
+
function motionlessMaxPxForClass(className) {
|
|
48499
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
48500
|
+
}
|
|
48274
48501
|
function netDisplacementPx(positions) {
|
|
48275
48502
|
const first = positions[0];
|
|
48276
48503
|
const last = positions[positions.length - 1];
|
|
48277
48504
|
if (!first || !last) return 0;
|
|
48278
48505
|
return Math.hypot(last.x - first.x, last.y - first.y);
|
|
48279
48506
|
}
|
|
48280
|
-
function isShortMotionlessTrack(lifeMs, movedPx) {
|
|
48281
|
-
return lifeMs <= 25e3 && movedPx <=
|
|
48507
|
+
function isShortMotionlessTrack(lifeMs, movedPx, className) {
|
|
48508
|
+
return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
|
|
48282
48509
|
}
|
|
48283
48510
|
//#endregion
|
|
48284
48511
|
//#region src/pipeline-analytics/store/track-store.ts
|
|
@@ -48862,7 +49089,7 @@ var TrackStore = class {
|
|
|
48862
49089
|
for (const [trackId, t] of this.active) {
|
|
48863
49090
|
if (nowMs - t.lastSeen < this.config.ttlMs) continue;
|
|
48864
49091
|
t.active = false;
|
|
48865
|
-
if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions))) t.closedReason = "stationary-promotion";
|
|
49092
|
+
if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions), t.className)) t.closedReason = "stationary-promotion";
|
|
48866
49093
|
const record = cloneTrack(t);
|
|
48867
49094
|
try {
|
|
48868
49095
|
await this.persistCompleted(record);
|
|
@@ -51253,9 +51480,14 @@ function deriveFrameDispatchKind(content) {
|
|
|
51253
51480
|
}
|
|
51254
51481
|
var DEFAULT_MAX_IN_FLIGHT_PER_DEVICE = 3;
|
|
51255
51482
|
var DEFAULT_LOG_WINDOW_MS = 6e4;
|
|
51483
|
+
/** One `capture queue full` line per device per this interval. The condition
|
|
51484
|
+
* holds for as long as the lane is saturated; one line per shed request would
|
|
51485
|
+
* be thousands. */
|
|
51486
|
+
var OVERFLOW_WARN_THROTTLE_MS = 6e4;
|
|
51256
51487
|
var CaptureScheduler = class {
|
|
51257
51488
|
logger;
|
|
51258
51489
|
budget;
|
|
51490
|
+
maxQueued;
|
|
51259
51491
|
windowMs;
|
|
51260
51492
|
now;
|
|
51261
51493
|
lanes = /* @__PURE__ */ new Map();
|
|
@@ -51264,9 +51496,12 @@ var CaptureScheduler = class {
|
|
|
51264
51496
|
/** holdKey → refcount of queued/running requests holding it. */
|
|
51265
51497
|
heldKeys = /* @__PURE__ */ new Map();
|
|
51266
51498
|
windows = /* @__PURE__ */ new Map();
|
|
51499
|
+
/** Per-device throttle for the `capture queue full` warn. */
|
|
51500
|
+
lastOverflowWarnAt = /* @__PURE__ */ new Map();
|
|
51267
51501
|
constructor(options) {
|
|
51268
51502
|
this.logger = options.logger;
|
|
51269
51503
|
this.budget = options.maxInFlightPerDevice ?? DEFAULT_MAX_IN_FLIGHT_PER_DEVICE;
|
|
51504
|
+
this.maxQueued = Math.max(1, options.maxQueuedPerDevice ?? 24);
|
|
51270
51505
|
this.windowMs = options.logWindowMs ?? DEFAULT_LOG_WINDOW_MS;
|
|
51271
51506
|
this.now = options.now ?? Date.now;
|
|
51272
51507
|
}
|
|
@@ -51336,6 +51571,7 @@ var CaptureScheduler = class {
|
|
|
51336
51571
|
failed: w.failed,
|
|
51337
51572
|
coalesced: w.coalesced,
|
|
51338
51573
|
droppedStale: w.droppedStale,
|
|
51574
|
+
droppedOverflow: w.droppedOverflow,
|
|
51339
51575
|
cancelled: w.cancelled,
|
|
51340
51576
|
maxQueueDepth: w.maxQueueDepth
|
|
51341
51577
|
};
|
|
@@ -51384,7 +51620,37 @@ var CaptureScheduler = class {
|
|
|
51384
51620
|
throw err;
|
|
51385
51621
|
});
|
|
51386
51622
|
}
|
|
51387
|
-
/**
|
|
51623
|
+
/**
|
|
51624
|
+
* Queue an over-budget request (priority-ordered, FIFO within priority),
|
|
51625
|
+
* under a BOUND enforced here rather than at dequeue.
|
|
51626
|
+
*
|
|
51627
|
+
* ## Why the queue needs its own bound
|
|
51628
|
+
*
|
|
51629
|
+
* `staleAfterMs` is evaluated in {@link pump}, and `pump` only touches the
|
|
51630
|
+
* queue while `inFlight < budget`. A lane that stays saturated therefore
|
|
51631
|
+
* never inspects its queue at all — nothing expires, nothing is shed, and the
|
|
51632
|
+
* queue grows for as long as the pressure lasts. The budget bounds concurrent
|
|
51633
|
+
* WORK; it has never bounded queued work.
|
|
51634
|
+
*
|
|
51635
|
+
* That gap is not theoretical: the per-frame batch dispatch PINS a ~0.7 MB
|
|
51636
|
+
* copy of the detection frame BEFORE queueing (so ring recycling cannot
|
|
51637
|
+
* invalidate it), and the queued closure holds that copy alive. Measured on
|
|
51638
|
+
* the live hub, `hub/pipeline-analytics` ran at rss=3911MB with heapUsed only
|
|
51639
|
+
* 153MB and arrayBuffers=2281MB — 2.28 GB of JS-reachable Buffers, roughly
|
|
51640
|
+
* 3 200 pinned frames, in a runner whose heap limit is 1216 MB. The call site
|
|
51641
|
+
* that pins them says the cost is "bounded by the lane"; it was not.
|
|
51642
|
+
*
|
|
51643
|
+
* Two things happen on the way in, in this order:
|
|
51644
|
+
*
|
|
51645
|
+
* 1. **expire what is already dead.** Entries past their `staleAfterMs` are
|
|
51646
|
+
* dropped now. They were doomed the moment they expired; `pump` simply
|
|
51647
|
+
* never got to look. This is the reclaim that costs nothing.
|
|
51648
|
+
* 2. **shed the least important.** Still over the cap, the TAIL goes — the
|
|
51649
|
+
* queue is ordered by priority, so the tail is by construction the work
|
|
51650
|
+
* this scheduler already ranked last. Whether that is the newcomer or an
|
|
51651
|
+
* older low-priority entry is decided by the same ordering that decides
|
|
51652
|
+
* execution, so a `firstFrame` never loses its place to an `embed`.
|
|
51653
|
+
*/
|
|
51388
51654
|
enqueue(lane, input) {
|
|
51389
51655
|
return new Promise((resolve, reject) => {
|
|
51390
51656
|
const entry = {
|
|
@@ -51397,11 +51663,53 @@ var CaptureScheduler = class {
|
|
|
51397
51663
|
},
|
|
51398
51664
|
drop: () => resolve(null)
|
|
51399
51665
|
};
|
|
51666
|
+
this.dropExpired(input.deviceId, lane);
|
|
51400
51667
|
this.insertByPriority(lane, entry);
|
|
51668
|
+
this.shedToCap(input.deviceId, lane);
|
|
51401
51669
|
const w = this.window(input.deviceId);
|
|
51402
51670
|
if (lane.queue.length > w.maxQueueDepth) w.maxQueueDepth = lane.queue.length;
|
|
51403
51671
|
});
|
|
51404
51672
|
}
|
|
51673
|
+
/** Drop every queued entry already past its `staleAfterMs`. Entries without
|
|
51674
|
+
* one (e.g. `confirm`, which gates track birth) are never expired here. */
|
|
51675
|
+
dropExpired(deviceId, lane) {
|
|
51676
|
+
const nowMs = this.now();
|
|
51677
|
+
for (let i = lane.queue.length - 1; i >= 0; i -= 1) {
|
|
51678
|
+
const entry = lane.queue[i];
|
|
51679
|
+
if (entry === void 0) continue;
|
|
51680
|
+
if (entry.staleAfterMs === void 0 || nowMs - entry.enqueuedAt <= entry.staleAfterMs) continue;
|
|
51681
|
+
lane.queue.splice(i, 1);
|
|
51682
|
+
this.bump(deviceId, "droppedStale");
|
|
51683
|
+
entry.drop();
|
|
51684
|
+
}
|
|
51685
|
+
}
|
|
51686
|
+
/** Shed from the tail (lowest priority, newest within it) until the lane is
|
|
51687
|
+
* at its cap. Loud: a capture that is dropped rather than merely late is a
|
|
51688
|
+
* missing thumbnail or first frame, and silence would read as "never asked". */
|
|
51689
|
+
shedToCap(deviceId, lane) {
|
|
51690
|
+
let shed = 0;
|
|
51691
|
+
while (lane.queue.length > this.maxQueued) {
|
|
51692
|
+
const victim = lane.queue.pop();
|
|
51693
|
+
if (victim === void 0) break;
|
|
51694
|
+
this.bump(deviceId, "droppedOverflow");
|
|
51695
|
+
victim.drop();
|
|
51696
|
+
shed += 1;
|
|
51697
|
+
}
|
|
51698
|
+
if (shed === 0) return;
|
|
51699
|
+
const nowMs = this.now();
|
|
51700
|
+
if (nowMs - (this.lastOverflowWarnAt.get(deviceId) ?? Number.NEGATIVE_INFINITY) < OVERFLOW_WARN_THROTTLE_MS) return;
|
|
51701
|
+
this.lastOverflowWarnAt.set(deviceId, nowMs);
|
|
51702
|
+
this.logger.warn("capture queue full — shedding the lowest-priority captures for this camera", {
|
|
51703
|
+
tags: { deviceId },
|
|
51704
|
+
meta: {
|
|
51705
|
+
shed,
|
|
51706
|
+
cap: this.maxQueued,
|
|
51707
|
+
queueDepth: lane.queue.length,
|
|
51708
|
+
inFlight: lane.inFlight,
|
|
51709
|
+
budget: this.budget
|
|
51710
|
+
}
|
|
51711
|
+
});
|
|
51712
|
+
}
|
|
51405
51713
|
insertByPriority(lane, entry) {
|
|
51406
51714
|
let index = lane.queue.length;
|
|
51407
51715
|
while (index > 0) {
|
|
@@ -51448,6 +51756,7 @@ var CaptureScheduler = class {
|
|
|
51448
51756
|
failed: 0,
|
|
51449
51757
|
coalesced: 0,
|
|
51450
51758
|
droppedStale: 0,
|
|
51759
|
+
droppedOverflow: 0,
|
|
51451
51760
|
cancelled: 0,
|
|
51452
51761
|
maxQueueDepth: 0
|
|
51453
51762
|
};
|
|
@@ -51472,6 +51781,7 @@ var CaptureScheduler = class {
|
|
|
51472
51781
|
failed: w.failed,
|
|
51473
51782
|
coalesced: w.coalesced,
|
|
51474
51783
|
droppedStale: w.droppedStale,
|
|
51784
|
+
droppedOverflow: w.droppedOverflow,
|
|
51475
51785
|
cancelled: w.cancelled,
|
|
51476
51786
|
maxQueueDepth: w.maxQueueDepth,
|
|
51477
51787
|
queueDepth: this.queueDepth(deviceId),
|
|
@@ -53042,8 +53352,14 @@ var EventMediaDispatcher = class {
|
|
|
53042
53352
|
* scheduler request against a 3-slot device lane and can start seconds late,
|
|
53043
53353
|
* by which time the handle's ring slot is usually recycled and the by-handle
|
|
53044
53354
|
* resolve in {@link captureForFrame} finds nothing (the 2026-07-23 boxed-tile
|
|
53045
|
-
* regression). ~0.7MB per pinned frame, freed with the batch
|
|
53046
|
-
*
|
|
53355
|
+
* regression). ~0.7MB per pinned frame, freed with the batch.
|
|
53356
|
+
*
|
|
53357
|
+
* THE COPY IS ONLY AS BOUNDED AS THE QUEUE THAT HOLDS IT. This said "bounded
|
|
53358
|
+
* by the lane" while the lane bounded in-flight execs (3) and nothing at all
|
|
53359
|
+
* bounded the queue behind them, so a saturated camera accumulated pinned
|
|
53360
|
+
* frames without limit — 2.28 GB of `arrayBuffers` measured on the live hub.
|
|
53361
|
+
* Every caller must therefore pass `staleAfterMs`, and `CaptureScheduler`
|
|
53362
|
+
* caps queue depth per device. Returns `null` on a genuine live-window miss.
|
|
53047
53363
|
*
|
|
53048
53364
|
* Both miss paths are WARN, throttled to one line per device per minute. They
|
|
53049
53365
|
* are the FIRST half of a pair whose second half (`capture batch lost`) has
|
|
@@ -56868,10 +57184,12 @@ var CONFIRMATION_CROP_MAX_WIDTH = 320;
|
|
|
56868
57184
|
* a late confirmation is still worth running; see `DEFAULT_CAPTURE_PRIORITY`). */
|
|
56869
57185
|
var ENRICHMENT_CAPTURE_STALE_MS = 3e3;
|
|
56870
57186
|
/** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
|
|
56871
|
-
* The `device.state-changed` push
|
|
56872
|
-
*
|
|
56873
|
-
*
|
|
56874
|
-
|
|
57187
|
+
* The `device.state-changed` push reaches a forked child through
|
|
57188
|
+
* `createEventBusSliceSource` (the DeviceProxy no longer waits on
|
|
57189
|
+
* `live.onEvent`). Events are still lossy telemetry (D8); this timer
|
|
57190
|
+
* is the RPC reconcile, not the mechanism. Five minutes matches the
|
|
57191
|
+
* viewer's `RECONCILE_MS`. */
|
|
57192
|
+
var ZONE_SLICE_RECONCILE_MS = 3e5;
|
|
56875
57193
|
/** How often the evaluating node re-reads the central scene set. */
|
|
56876
57194
|
var SCENE_RELOAD_INTERVAL_MS = 3e4;
|
|
56877
57195
|
/** How long the install coordinates are trusted between settings reads. */
|
|
@@ -56935,38 +57253,31 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
|
56935
57253
|
*
|
|
56936
57254
|
* `thumbnail` joined the clean set on 2026-07-17: the track best-thumbnail is
|
|
56937
57255
|
* now a subject-centered crop (event-media-dispatcher), and for DEGENERATE
|
|
56938
|
-
* tracks (no object event → no `crop`) it is the ONLY subject-centered media
|
|
56939
|
-
*
|
|
56940
|
-
*
|
|
56941
|
-
*
|
|
56942
|
-
*
|
|
56943
|
-
*
|
|
56944
|
-
*
|
|
56945
|
-
*
|
|
56946
|
-
*
|
|
56947
|
-
* over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
|
|
56948
|
-
* keep resolving.
|
|
57256
|
+
* tracks (no object event → no `crop`) it is the ONLY subject-centered media.
|
|
57257
|
+
*
|
|
57258
|
+
* Full-scene kinds (`fullFrame`, `keyFrame`, `keyFrameSmall`) stay out of
|
|
57259
|
+
* this list so a crop is preferred when one exists. `resolveCropForcedMedia`
|
|
57260
|
+
* then falls through to those full-scene kinds — a keyFrame beats an icon.
|
|
57261
|
+
* An *explicit* `?kind=keyFrame` / `?kind=snapshot` still matches exactly, so
|
|
57262
|
+
* videoclips and other kind-specific fetches keep working.
|
|
57263
|
+
*
|
|
57264
|
+
* The retired-but-still-stored kind (`crop`) stays listed so old rows resolve.
|
|
56949
57265
|
*/
|
|
56950
57266
|
var CLEAN_MEDIA_KINDS = [
|
|
56951
57267
|
"crop",
|
|
56952
57268
|
"thumbnail",
|
|
56953
|
-
"thumbnailSmall"
|
|
56954
|
-
"fullFrame",
|
|
56955
|
-
"keyFrame",
|
|
56956
|
-
"keyFrameSmall"
|
|
57269
|
+
"thumbnailSmall"
|
|
56957
57270
|
];
|
|
56958
57271
|
/**
|
|
56959
|
-
* Pick
|
|
56960
|
-
* `
|
|
56961
|
-
* {@link CLEAN_MEDIA_KINDS}
|
|
56962
|
-
*
|
|
57272
|
+
* Pick media for a crop-forced request: the exact `preferKind` when present
|
|
57273
|
+
* (so `?kind=snapshot` still serves a snapshot), else the first available
|
|
57274
|
+
* subject crop in {@link CLEAN_MEDIA_KINDS}. Returns undefined when no crop
|
|
57275
|
+
* is in the list — the caller then falls through to any remaining image
|
|
57276
|
+
* (`resolveCropForcedMedia`); the class icon is only for a total miss.
|
|
56963
57277
|
*/
|
|
56964
57278
|
function pickCleanMedia(files, preferKind) {
|
|
56965
|
-
const
|
|
56966
|
-
if (
|
|
56967
|
-
const exact = files.find((f) => f.kind === preferKind);
|
|
56968
|
-
if (exact) return exact;
|
|
56969
|
-
}
|
|
57279
|
+
const exact = files.find((f) => f.kind === preferKind);
|
|
57280
|
+
if (exact) return exact;
|
|
56970
57281
|
for (const kind of CLEAN_MEDIA_KINDS) {
|
|
56971
57282
|
const found = files.find((f) => f.kind === kind);
|
|
56972
57283
|
if (found) return found;
|
|
@@ -58430,6 +58741,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58430
58741
|
}, trackId);
|
|
58431
58742
|
},
|
|
58432
58743
|
markEmbeddedFace: (trackId) => stores.trackStore.markEmbeddedFace(trackId),
|
|
58744
|
+
copyCropToTrackPreview: async (input) => {
|
|
58745
|
+
await copyFaceCropIfTrackHasNoThumbnail({
|
|
58746
|
+
listTrackMedia: (trackId) => stores.mediaStore.listByOwner("track", trackId),
|
|
58747
|
+
putTrackThumbnail: (put) => stores.mediaStore.put({
|
|
58748
|
+
deviceId: put.deviceId,
|
|
58749
|
+
ownerKind: "track",
|
|
58750
|
+
ownerId: put.trackId,
|
|
58751
|
+
kind: "thumbnail",
|
|
58752
|
+
timestamp: put.timestamp,
|
|
58753
|
+
data: put.data
|
|
58754
|
+
})
|
|
58755
|
+
}, input);
|
|
58756
|
+
},
|
|
58433
58757
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
58434
58758
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
|
|
58435
58759
|
onSemanticMatch: (match) => this.promoteSemanticRecognition({
|
|
@@ -58444,6 +58768,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58444
58768
|
confidence: match.confidence,
|
|
58445
58769
|
...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
|
|
58446
58770
|
}),
|
|
58771
|
+
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
58447
58772
|
logger: logger.child("FaceRecognizer")
|
|
58448
58773
|
});
|
|
58449
58774
|
this.faceRecognizer.refreshGallery();
|
|
@@ -58476,6 +58801,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58476
58801
|
deriveCropFromKeyFrame,
|
|
58477
58802
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
58478
58803
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
58804
|
+
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
58479
58805
|
logger: logger.child("PlateRecognizer")
|
|
58480
58806
|
});
|
|
58481
58807
|
this.plateRecognizer.refreshGallery();
|
|
@@ -59845,6 +60171,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
59845
60171
|
refreshGallery: () => {
|
|
59846
60172
|
this.plateRecognizer?.refreshGallery();
|
|
59847
60173
|
},
|
|
60174
|
+
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
59848
60175
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
59849
60176
|
});
|
|
59850
60177
|
return [
|
|
@@ -60506,6 +60833,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60506
60833
|
hasSnapshot: snapshotTargets.some((t) => t.appendSnapshot)
|
|
60507
60834
|
}),
|
|
60508
60835
|
holdKeys: heldCaptureKeys,
|
|
60836
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
60509
60837
|
exec: () => pinnedFramePromise.then((pinnedFrame) => dispatcher.captureForFrame({
|
|
60510
60838
|
deviceId,
|
|
60511
60839
|
frameHandle,
|
|
@@ -60901,7 +61229,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60901
61229
|
if (!first) return;
|
|
60902
61230
|
const lifeMs = track.lastSeen - track.firstSeen;
|
|
60903
61231
|
const moved = netDisplacementPx(track.positions ?? []);
|
|
60904
|
-
if (!isShortMotionlessTrack(lifeMs, moved)) {
|
|
61232
|
+
if (!isShortMotionlessTrack(lifeMs, moved, track.className)) {
|
|
60905
61233
|
this.phantomCells?.noteMovingClose({
|
|
60906
61234
|
deviceId: track.deviceId,
|
|
60907
61235
|
className: track.className,
|
|
@@ -61019,6 +61347,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61019
61347
|
}, d.score, d.label, frame.timestamp);
|
|
61020
61348
|
if (this.plateRecognizer) {
|
|
61021
61349
|
const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
|
|
61350
|
+
const body = this.residents.bestSeenBbox(trackId);
|
|
61022
61351
|
await this.plateRecognizer.observePlateRead({
|
|
61023
61352
|
deviceId,
|
|
61024
61353
|
trackId,
|
|
@@ -61034,7 +61363,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61034
61363
|
frameWidth: frame.frameWidth,
|
|
61035
61364
|
frameHeight: frame.frameHeight,
|
|
61036
61365
|
cropPadding: mediaSettings.cropPadding,
|
|
61037
|
-
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
61366
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {},
|
|
61367
|
+
...body !== void 0 ? { bodyBbox: {
|
|
61368
|
+
x: body.x,
|
|
61369
|
+
y: body.y,
|
|
61370
|
+
w: body.w,
|
|
61371
|
+
h: body.h
|
|
61372
|
+
} } : {}
|
|
61038
61373
|
});
|
|
61039
61374
|
}
|
|
61040
61375
|
}
|
|
@@ -61335,6 +61670,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61335
61670
|
}
|
|
61336
61671
|
}).catch(() => {})));
|
|
61337
61672
|
}
|
|
61673
|
+
/**
|
|
61674
|
+
* A face or plate crop just landed from a still-live handle. Fill the
|
|
61675
|
+
* missing parent rungs — person/vehicle thumbnail (display crop, same
|
|
61676
|
+
* surface that produced the detail) and native keyFrame — so Events never
|
|
61677
|
+
* has a child crop without a body and a scene.
|
|
61678
|
+
*/
|
|
61679
|
+
requestTrackHierarchy(input) {
|
|
61680
|
+
ensureTrackHierarchyMedia({
|
|
61681
|
+
isThumbnailLanded: (trackId) => this.residents.isThumbnailLanded(trackId),
|
|
61682
|
+
isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
|
|
61683
|
+
captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
|
|
61684
|
+
captureKeyFrame: async (req) => {
|
|
61685
|
+
await this.persistKeyFrames(req.deviceId, req.timestamp, [{
|
|
61686
|
+
trackId: req.trackId,
|
|
61687
|
+
retry: false
|
|
61688
|
+
}], req.frameHandle);
|
|
61689
|
+
}
|
|
61690
|
+
}, input);
|
|
61691
|
+
}
|
|
61692
|
+
async captureHierarchyBodyThumbnail(input) {
|
|
61693
|
+
const displayCrop = this.captureDisplayCropFn;
|
|
61694
|
+
const mediaStore = this.mediaStore;
|
|
61695
|
+
if (!displayCrop || !mediaStore) return;
|
|
61696
|
+
await this.captureScheduler.request({
|
|
61697
|
+
deviceId: input.deviceId,
|
|
61698
|
+
trackId: input.trackId,
|
|
61699
|
+
kind: "thumbnail",
|
|
61700
|
+
coalesceKey: captureCoalesceKey(input.deviceId, "thumbnail", `${input.trackId}:hierarchy`),
|
|
61701
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
61702
|
+
exec: async () => {
|
|
61703
|
+
if (this.residents.isThumbnailLanded(input.trackId)) return;
|
|
61704
|
+
const jpeg = await displayCrop(input.frameHandle, input.bodyBbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId);
|
|
61705
|
+
if (!jpeg) return;
|
|
61706
|
+
await mediaStore.put({
|
|
61707
|
+
deviceId: input.deviceId,
|
|
61708
|
+
ownerKind: "track",
|
|
61709
|
+
ownerId: input.trackId,
|
|
61710
|
+
kind: "thumbnail",
|
|
61711
|
+
timestamp: input.timestamp,
|
|
61712
|
+
data: jpeg
|
|
61713
|
+
});
|
|
61714
|
+
this.residents.markThumbnailLanded(input.deviceId, input.trackId);
|
|
61715
|
+
}
|
|
61716
|
+
}).catch(() => {});
|
|
61717
|
+
}
|
|
61338
61718
|
/** Attribution-time SEMANTIC best-shot promotion (D-semantic-bestshot,
|
|
61339
61719
|
* 2026-08-21; see `pipeline/semantic-promotion.ts`): offer a frame from
|
|
61340
61720
|
* which a recognition SUCCEEDED to the best-frame ranking with the
|
|
@@ -61383,7 +61763,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61383
61763
|
this.captureScheduler.request({
|
|
61384
61764
|
deviceId,
|
|
61385
61765
|
kind: "thumbnail",
|
|
61766
|
+
trackId,
|
|
61386
61767
|
holdKeys: [captureCoalesceKey(deviceId, "thumbnail", trackId)],
|
|
61768
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
61387
61769
|
exec: async () => {
|
|
61388
61770
|
const [pinnedFrame, media] = await Promise.all([pinnedFramePromise, this.resolveDeviceMediaSettings(deviceId)]);
|
|
61389
61771
|
const res = await dispatcher.captureForFrame({
|
|
@@ -62840,15 +63222,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62840
63222
|
* applies to the very next frame even when frames stop briefly
|
|
62841
63223
|
* (e.g. during binding flips).
|
|
62842
63224
|
*
|
|
62843
|
-
* RECONCILE: the push channel behind `subscribe`
|
|
62844
|
-
* via `
|
|
62845
|
-
*
|
|
62846
|
-
*
|
|
62847
|
-
*
|
|
62848
|
-
* telemetry (D8); the durable channel is RPC + reconcile — so each
|
|
62849
|
-
* proxy also refreshes its two slices on a slow timer. `refresh()`
|
|
62850
|
-
* round-trips `deviceState.getCapSlice` and fans out through the SAME
|
|
62851
|
-
* subscribe callbacks above, so a zone edit lands within one interval.
|
|
63225
|
+
* RECONCILE: the push channel behind `subscribe` is `device.state-changed`
|
|
63226
|
+
* via the addon's `ctx.eventBus` (`createEventBusSliceSource`). That
|
|
63227
|
+
* declares UDS interest so the parent actually fans the slice here. Events
|
|
63228
|
+
* remain lossy (D8); `refresh()` round-trips `deviceState.getCapSlice` on
|
|
63229
|
+
* this timer as the backstop.
|
|
62852
63230
|
*/
|
|
62853
63231
|
async ensureProxy(deviceId) {
|
|
62854
63232
|
const cached = this.proxies.get(deviceId);
|
|
@@ -64779,14 +65157,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
64779
65157
|
async readEventThumbnail(id, preferKind) {
|
|
64780
65158
|
const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
|
|
64781
65159
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
64782
|
-
const
|
|
64783
|
-
|
|
64784
|
-
|
|
64785
|
-
|
|
64786
|
-
|
|
64787
|
-
...trackFiles,
|
|
64788
|
-
...owningTrackFiles
|
|
64789
|
-
], (files) => pickCleanMedia(files, preferKind));
|
|
65160
|
+
const layers = await collectCropForcedMedia({
|
|
65161
|
+
listByOwner: (ownerKind, ownerId) => this.mediaStore?.listByOwner(ownerKind, ownerId) ?? Promise.resolve([]),
|
|
65162
|
+
getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
|
|
65163
|
+
}, id);
|
|
65164
|
+
const chosen = resolveCropForcedMedia(layers.bodyFiles, (files) => pickCleanMedia(files, preferKind), layers.detailFiles);
|
|
64790
65165
|
if (!chosen) return null;
|
|
64791
65166
|
return {
|
|
64792
65167
|
bytes: Buffer.from(chosen.base64, "base64"),
|