@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BtIolp2d.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -4398,10 +4398,28 @@ var SCENE_LATCH_INDEXES = [{
|
|
|
4398
4398
|
function sceneKey(deviceId, monitorId) {
|
|
4399
4399
|
return `${deviceId}:${monitorId}`;
|
|
4400
4400
|
}
|
|
4401
|
+
/**
|
|
4402
|
+
* Reseed cap for BOTH scene ledgers.
|
|
4403
|
+
*
|
|
4404
|
+
* `DurableLedger` defaults to 100_000 and its own docblock says the owner "sets
|
|
4405
|
+
* it from the row set it expects". Neither scene ledger did, so the 30s
|
|
4406
|
+
* reconcile asked SQLite — through hub-main's event loop, which is the
|
|
4407
|
+
* cluster's only queue (D181) — for up to a hundred thousand rows twice a tick,
|
|
4408
|
+
* on a hub holding one scene and one latch (measured live 2026-08-25). A bound
|
|
4409
|
+
* nobody chose is not a bound.
|
|
4410
|
+
*
|
|
4411
|
+
* A scene is an operator-drawn monitor on ONE camera and a latch is at most one
|
|
4412
|
+
* row per scene, so the row set is `cameras x monitors-per-camera`. 5_000 is
|
|
4413
|
+
* ~5 monitors on each of a thousand cameras — far past any installation this
|
|
4414
|
+
* ships to, and still small enough that a runaway write loop is CAUGHT by the
|
|
4415
|
+
* bound instead of being paged in whole.
|
|
4416
|
+
*/
|
|
4417
|
+
var SCENE_LEDGER_LOAD_LIMIT = 5e3;
|
|
4401
4418
|
var SCENE_CONFIG_SPEC = {
|
|
4402
4419
|
collection: SCENE_COLLECTION,
|
|
4403
4420
|
columns: SCENE_COLUMNS,
|
|
4404
4421
|
indexes: SCENE_INDEXES,
|
|
4422
|
+
loadLimit: SCENE_LEDGER_LOAD_LIMIT,
|
|
4405
4423
|
writeMode: "write-behind",
|
|
4406
4424
|
keyOf: (row) => row.key,
|
|
4407
4425
|
toValue: (row) => ({
|
|
@@ -4417,6 +4435,7 @@ var SCENE_LATCH_SPEC = {
|
|
|
4417
4435
|
collection: SCENE_LATCH_COLLECTION,
|
|
4418
4436
|
columns: SCENE_LATCH_COLUMNS,
|
|
4419
4437
|
indexes: SCENE_LATCH_INDEXES,
|
|
4438
|
+
loadLimit: SCENE_LEDGER_LOAD_LIMIT,
|
|
4420
4439
|
writeMode: "write-through",
|
|
4421
4440
|
keyOf: (row) => row.key,
|
|
4422
4441
|
toValue: (row) => ({
|
|
@@ -4446,6 +4465,9 @@ var SceneStore = class {
|
|
|
4446
4465
|
config;
|
|
4447
4466
|
latch;
|
|
4448
4467
|
logger;
|
|
4468
|
+
/** `scenes/latches` as last announced. Null until the first load, so the
|
|
4469
|
+
* first one always speaks. */
|
|
4470
|
+
lastLoggedCounts = null;
|
|
4449
4471
|
constructor(deps) {
|
|
4450
4472
|
this.logger = deps.logger;
|
|
4451
4473
|
this.config = new DurableLedger({
|
|
@@ -4470,10 +4492,20 @@ var SceneStore = class {
|
|
|
4470
4492
|
* a container recreate, "loaded 0" is the one line that explains why every
|
|
4471
4493
|
* scene suddenly reads `unknown`, and its absence is what turns a data loss
|
|
4472
4494
|
* into a three-hour hunt.
|
|
4495
|
+
*
|
|
4496
|
+
* ON CHANGE ONLY, though — including the first load, where there is nothing
|
|
4497
|
+
* to compare against. This runs on the 30s `SCENE_RELOAD_INTERVAL_MS` tick on
|
|
4498
|
+
* every node, and it used to print unconditionally: measured live 2026-08-25,
|
|
4499
|
+
* `scene state loaded {scenes=1 latches=1}` every 30 seconds forever. A count
|
|
4500
|
+
* reprinted unchanged twice a minute is exactly how the "loaded 0" this line
|
|
4501
|
+
* exists for gets scrolled past.
|
|
4473
4502
|
*/
|
|
4474
4503
|
async load() {
|
|
4475
4504
|
const scenes = await this.config.load();
|
|
4476
4505
|
const latches = await this.latch.load();
|
|
4506
|
+
const counts = `${scenes.length}/${latches.length}`;
|
|
4507
|
+
if (counts === this.lastLoggedCounts) return;
|
|
4508
|
+
this.lastLoggedCounts = counts;
|
|
4477
4509
|
this.logger.info("scene state loaded", { meta: {
|
|
4478
4510
|
scenes: scenes.length,
|
|
4479
4511
|
latches: latches.length
|
|
@@ -8539,6 +8571,49 @@ var NC_LANGUAGES = ["en", "it"];
|
|
|
8539
8571
|
* surface's view all validate against this one, so a third language is added
|
|
8540
8572
|
* to `NC_LANGUAGES` and nowhere else. */
|
|
8541
8573
|
var NcLanguageSchema = require_dist._enum(NC_LANGUAGES);
|
|
8574
|
+
/**
|
|
8575
|
+
* Whether two snapshots say the same thing.
|
|
8576
|
+
*
|
|
8577
|
+
* ## Why this is a function and not `===`
|
|
8578
|
+
*
|
|
8579
|
+
* The reload guard in `reloadTexts()` used to be
|
|
8580
|
+
* `before.overrides === next.overrides`, and `next` comes from
|
|
8581
|
+
* `NcTextCatalogStore.load()`, which `parse()`s the row through zod on every
|
|
8582
|
+
* call. The right-hand side is therefore a NEW object on every tick by
|
|
8583
|
+
* construction: the comparison was `false` at byte-identical content, forever.
|
|
8584
|
+
* Measured on the live hub 2026-08-25: 58 "notification texts loaded" lines in
|
|
8585
|
+
* 30 minutes on a hub whose language had not changed, from a line whose own
|
|
8586
|
+
* docblock promises it fires on CHANGE only.
|
|
8587
|
+
*
|
|
8588
|
+
* The document is two levels deep and installation-small (at most one entry per
|
|
8589
|
+
* language, each a flat key→string map), so the comparison is written out
|
|
8590
|
+
* rather than reached for through a deep-equal dependency — and written out it
|
|
8591
|
+
* can state the direction that matters: a REMOVED key must not read as equal,
|
|
8592
|
+
* which a one-directional key walk would get wrong.
|
|
8593
|
+
*/
|
|
8594
|
+
function ncTextSnapshotsEqual(a, b) {
|
|
8595
|
+
if (a.language !== b.language) return false;
|
|
8596
|
+
return overrideMapsEqual(a.overrides, b.overrides);
|
|
8597
|
+
}
|
|
8598
|
+
function overrideMapsEqual(a, b) {
|
|
8599
|
+
const languages = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
8600
|
+
for (const language of languages) {
|
|
8601
|
+
const left = a[language];
|
|
8602
|
+
const right = b[language];
|
|
8603
|
+
if (left === void 0 || right === void 0) {
|
|
8604
|
+
if (Object.keys(left ?? right ?? {}).length > 0) return false;
|
|
8605
|
+
continue;
|
|
8606
|
+
}
|
|
8607
|
+
if (!flatMapsEqual(left, right)) return false;
|
|
8608
|
+
}
|
|
8609
|
+
return true;
|
|
8610
|
+
}
|
|
8611
|
+
function flatMapsEqual(a, b) {
|
|
8612
|
+
const keys = Object.keys(a);
|
|
8613
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
8614
|
+
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
8615
|
+
return true;
|
|
8616
|
+
}
|
|
8542
8617
|
/** The shipped files, keyed by language. Data, never mutated. */
|
|
8543
8618
|
var NC_TEXT_BUNDLES = {
|
|
8544
8619
|
en: en_default,
|
|
@@ -22484,7 +22559,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
22484
22559
|
const before = this.texts.snapshot;
|
|
22485
22560
|
const next = await this.textStore.load();
|
|
22486
22561
|
this.texts.apply(next);
|
|
22487
|
-
if (before
|
|
22562
|
+
if (ncTextSnapshotsEqual(before, next)) return;
|
|
22488
22563
|
this.logger.info("notification texts loaded", { meta: {
|
|
22489
22564
|
language: next.language,
|
|
22490
22565
|
overriddenKeys: Object.values(next.overrides).reduce((sum, keys) => sum + Object.keys(keys ?? {}).length, 0)
|
|
@@ -25924,7 +25999,7 @@ var CLASS_RANK_VEHICLE = .8;
|
|
|
25924
25999
|
var CLASS_RANK_ANIMAL = .5;
|
|
25925
26000
|
var CLASS_RANK_DEFAULT = .25;
|
|
25926
26001
|
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
25927
|
-
var VEHICLE_CLASSES = new Set([
|
|
26002
|
+
var VEHICLE_CLASSES$1 = new Set([
|
|
25928
26003
|
"vehicle",
|
|
25929
26004
|
"car",
|
|
25930
26005
|
"truck",
|
|
@@ -25955,7 +26030,7 @@ function clamp01$1(x) {
|
|
|
25955
26030
|
function classRank(className) {
|
|
25956
26031
|
const c = className.toLowerCase();
|
|
25957
26032
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
25958
|
-
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
26033
|
+
if (VEHICLE_CLASSES$1.has(c)) return CLASS_RANK_VEHICLE;
|
|
25959
26034
|
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
25960
26035
|
return CLASS_RANK_DEFAULT;
|
|
25961
26036
|
}
|
|
@@ -26558,7 +26633,7 @@ var FaceGalleryProvider = class {
|
|
|
26558
26633
|
const identities = await this.identityStore.listIdentities();
|
|
26559
26634
|
const nameMap = new Map(identities.map((i) => [i.id, i.name]));
|
|
26560
26635
|
const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
|
|
26561
|
-
const
|
|
26636
|
+
const cropUrl = this.cropUrlFor(face);
|
|
26562
26637
|
return {
|
|
26563
26638
|
faceId: face.id,
|
|
26564
26639
|
deviceId: face.deviceId,
|
|
@@ -26567,7 +26642,7 @@ var FaceGalleryProvider = class {
|
|
|
26567
26642
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
26568
26643
|
...identityName !== void 0 ? { identityName } : {},
|
|
26569
26644
|
assigned: face.assigned,
|
|
26570
|
-
...
|
|
26645
|
+
...cropUrl !== void 0 ? { cropUrl } : {},
|
|
26571
26646
|
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
26572
26647
|
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
|
|
26573
26648
|
...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
|
|
@@ -29687,9 +29762,15 @@ var TrackCloser = class {
|
|
|
29687
29762
|
await this.suppressZeroMedia(t.deviceId, t.trackId);
|
|
29688
29763
|
return;
|
|
29689
29764
|
}
|
|
29690
|
-
|
|
29765
|
+
let thumbnailLanded = ownedMedia.some((m) => m.kind === "thumbnail");
|
|
29766
|
+
if (outcome === "raster-fallback" && closure?.rasterFallback) {
|
|
29767
|
+
await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
|
|
29768
|
+
thumbnailLanded = true;
|
|
29769
|
+
}
|
|
29691
29770
|
await this.maybePromoteLastFrame(t, ownedMedia);
|
|
29692
|
-
|
|
29771
|
+
const hasRasterFallback = closure?.rasterFallback !== void 0;
|
|
29772
|
+
if (!thumbnailLanded) thumbnailLanded = await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback);
|
|
29773
|
+
if (!thumbnailLanded && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
|
|
29693
29774
|
this.deps.logger.info("track ended", {
|
|
29694
29775
|
tags: { deviceId: t.deviceId },
|
|
29695
29776
|
meta: {
|
|
@@ -29807,14 +29888,23 @@ var TrackCloser = class {
|
|
|
29807
29888
|
* and the rate could only ever be counted from the database, after the fact.
|
|
29808
29889
|
* A branch that drops work silently reads as "never happened".
|
|
29809
29890
|
*/
|
|
29810
|
-
async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia) {
|
|
29811
|
-
if (ownedMedia.some((m) => m.kind === "thumbnail")) return;
|
|
29891
|
+
async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback) {
|
|
29892
|
+
if (ownedMedia.some((m) => m.kind === "thumbnail")) return true;
|
|
29812
29893
|
const derive = this.deps.deriveThumbnailFromKeyFrame;
|
|
29813
|
-
if (!derive)
|
|
29894
|
+
if (!derive) {
|
|
29895
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-not-wired");
|
|
29896
|
+
return false;
|
|
29897
|
+
}
|
|
29814
29898
|
const keyFrame = ownedMedia.find((m) => m.kind === "keyFrame");
|
|
29815
|
-
if (!keyFrame)
|
|
29899
|
+
if (!keyFrame) {
|
|
29900
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-key-frame");
|
|
29901
|
+
return false;
|
|
29902
|
+
}
|
|
29816
29903
|
const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
|
|
29817
|
-
if (!bestSeen)
|
|
29904
|
+
if (!bestSeen) {
|
|
29905
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
|
|
29906
|
+
return false;
|
|
29907
|
+
}
|
|
29818
29908
|
try {
|
|
29819
29909
|
const derived = await derive({
|
|
29820
29910
|
mediaKey: keyFrame.key,
|
|
@@ -29826,17 +29916,23 @@ var TrackCloser = class {
|
|
|
29826
29916
|
},
|
|
29827
29917
|
frameWidth: bestSeen.frameWidth,
|
|
29828
29918
|
frameHeight: bestSeen.frameHeight,
|
|
29829
|
-
timestamp:
|
|
29919
|
+
timestamp: keyFrame.timestamp
|
|
29830
29920
|
});
|
|
29831
|
-
if (!derived)
|
|
29921
|
+
if (!derived) {
|
|
29922
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
|
|
29923
|
+
return false;
|
|
29924
|
+
}
|
|
29832
29925
|
const mediaStore = this.deps.mediaStore();
|
|
29833
|
-
if (!mediaStore)
|
|
29926
|
+
if (!mediaStore) {
|
|
29927
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-media-store");
|
|
29928
|
+
return false;
|
|
29929
|
+
}
|
|
29834
29930
|
await mediaStore.put({
|
|
29835
29931
|
deviceId: t.deviceId,
|
|
29836
29932
|
ownerKind: "track",
|
|
29837
29933
|
ownerId: t.trackId,
|
|
29838
29934
|
kind: "thumbnail",
|
|
29839
|
-
timestamp:
|
|
29935
|
+
timestamp: keyFrame.timestamp,
|
|
29840
29936
|
data: derived.thumbnail
|
|
29841
29937
|
});
|
|
29842
29938
|
await mediaStore.put({
|
|
@@ -29844,7 +29940,7 @@ var TrackCloser = class {
|
|
|
29844
29940
|
ownerKind: "track",
|
|
29845
29941
|
ownerId: t.trackId,
|
|
29846
29942
|
kind: "thumbnailSmall",
|
|
29847
|
-
timestamp:
|
|
29943
|
+
timestamp: keyFrame.timestamp,
|
|
29848
29944
|
data: derived.thumbnailSmall
|
|
29849
29945
|
});
|
|
29850
29946
|
this.deps.logger.info("thumbnail derived from keyFrame at close (no live best-shot landed)", {
|
|
@@ -29855,6 +29951,7 @@ var TrackCloser = class {
|
|
|
29855
29951
|
skewMs: derived.skewMs
|
|
29856
29952
|
}
|
|
29857
29953
|
});
|
|
29954
|
+
return true;
|
|
29858
29955
|
} catch (err) {
|
|
29859
29956
|
this.deps.logger.debug("close-time keyFrame thumbnail derive failed", {
|
|
29860
29957
|
tags: { deviceId: t.deviceId },
|
|
@@ -29863,10 +29960,11 @@ var TrackCloser = class {
|
|
|
29863
29960
|
error: String(err)
|
|
29864
29961
|
}
|
|
29865
29962
|
});
|
|
29866
|
-
this.logNoThumbnailAtClose(t, "derive-threw", {
|
|
29963
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-threw", {
|
|
29867
29964
|
keyFrameKey: keyFrame.key,
|
|
29868
29965
|
error: String(err)
|
|
29869
29966
|
});
|
|
29967
|
+
return false;
|
|
29870
29968
|
}
|
|
29871
29969
|
}
|
|
29872
29970
|
/**
|
|
@@ -30736,6 +30834,7 @@ var PlateGalleryProvider = class {
|
|
|
30736
30834
|
logger;
|
|
30737
30835
|
refreshGallery;
|
|
30738
30836
|
emitPlateGalleryChanged;
|
|
30837
|
+
eventMediaBaseUrl;
|
|
30739
30838
|
constructor(deps) {
|
|
30740
30839
|
this.plateStore = deps.plateStore;
|
|
30741
30840
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -30745,6 +30844,7 @@ var PlateGalleryProvider = class {
|
|
|
30745
30844
|
this.logger = deps.logger;
|
|
30746
30845
|
this.refreshGallery = deps.refreshGallery;
|
|
30747
30846
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
30847
|
+
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
30748
30848
|
}
|
|
30749
30849
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
30750
30850
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -30752,6 +30852,12 @@ var PlateGalleryProvider = class {
|
|
|
30752
30852
|
this.emitPlateGalleryChanged(payload);
|
|
30753
30853
|
} catch {}
|
|
30754
30854
|
}
|
|
30855
|
+
cropUrlFor(plate) {
|
|
30856
|
+
if (plate.mediaKey === void 0 || plate.mediaKey === "") return void 0;
|
|
30857
|
+
const base = this.eventMediaBaseUrl();
|
|
30858
|
+
if (base === null) return void 0;
|
|
30859
|
+
return `${base}/${encodeURIComponent(plate.mediaKey)}`;
|
|
30860
|
+
}
|
|
30755
30861
|
async plateCropBase64(plate) {
|
|
30756
30862
|
if (plate.mediaKey) {
|
|
30757
30863
|
const m = await this.mediaStore.getByKey(plate.mediaKey);
|
|
@@ -30760,8 +30866,9 @@ var PlateGalleryProvider = class {
|
|
|
30760
30866
|
const media = await this.mediaStore.listByOwner("plate", plate.id);
|
|
30761
30867
|
return media.length > 0 ? media[0].base64 : void 0;
|
|
30762
30868
|
}
|
|
30763
|
-
async toPlateInfo(plate, nameMap) {
|
|
30764
|
-
const base64 = await this.plateCropBase64(plate);
|
|
30869
|
+
async toPlateInfo(plate, nameMap, opts) {
|
|
30870
|
+
const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
|
|
30871
|
+
const cropUrl = this.cropUrlFor(plate);
|
|
30765
30872
|
const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
|
|
30766
30873
|
return {
|
|
30767
30874
|
plateId: plate.id,
|
|
@@ -30776,7 +30883,8 @@ var PlateGalleryProvider = class {
|
|
|
30776
30883
|
...vehicleName !== void 0 ? { vehicleName } : {},
|
|
30777
30884
|
...plate.plateBbox !== void 0 ? { plateBbox: plate.plateBbox } : {},
|
|
30778
30885
|
...plate.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: plate.keyFrameMediaKey } : {},
|
|
30779
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
30886
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
30887
|
+
...cropUrl !== void 0 ? { cropUrl } : {}
|
|
30780
30888
|
};
|
|
30781
30889
|
}
|
|
30782
30890
|
async vehicleNameMap() {
|
|
@@ -30795,7 +30903,7 @@ var PlateGalleryProvider = class {
|
|
|
30795
30903
|
const plate = await this.plateStore.get(`plate-${input.trackId}`);
|
|
30796
30904
|
if (!plate || plate.deviceId !== input.deviceId) return null;
|
|
30797
30905
|
const nameMap = await this.vehicleNameMap();
|
|
30798
|
-
return this.toPlateInfo(plate, nameMap);
|
|
30906
|
+
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
30799
30907
|
}
|
|
30800
30908
|
async getPlateMedia(input) {
|
|
30801
30909
|
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
|
|
@@ -35426,6 +35534,18 @@ var EmbeddingDispatcher = class {
|
|
|
35426
35534
|
logger;
|
|
35427
35535
|
getRemoteFrame;
|
|
35428
35536
|
lastEmbedTime = /* @__PURE__ */ new Map();
|
|
35537
|
+
/**
|
|
35538
|
+
* trackId -> the frame pinned for it. Each entry holds a WHOLE decoded RGB
|
|
35539
|
+
* frame (`PendingCrop.frameData`, ~0.7 MB), not a crop.
|
|
35540
|
+
*
|
|
35541
|
+
* Under the shipped `cropStrategy: 'first'` this doubles as the in-flight
|
|
35542
|
+
* DEDUPE FLAG — `'first'` gates on `!pendingCrops.has(trackId)` — and there
|
|
35543
|
+
* is no flush timer for that strategy, so nothing ever sweeps it. Until
|
|
35544
|
+
* 2026-08-25 the delete lived on `processOne`'s success path only: an
|
|
35545
|
+
* `extractCrop` throw or an `encoder.encode` rejection (a cross-process cap
|
|
35546
|
+
* over UDS, with no timeout) stranded the frame permanently AND made that
|
|
35547
|
+
* trackId un-embeddable forever. `processOne` now releases in a `finally`.
|
|
35548
|
+
*/
|
|
35429
35549
|
pendingCrops = /* @__PURE__ */ new Map();
|
|
35430
35550
|
flushTimer = null;
|
|
35431
35551
|
unsubscribe = null;
|
|
@@ -35583,7 +35703,6 @@ var EmbeddingDispatcher = class {
|
|
|
35583
35703
|
this._processedCount++;
|
|
35584
35704
|
this._totalInferenceMs += inferenceMs;
|
|
35585
35705
|
this.lastEmbedTime.set(pending.deviceId, Date.now());
|
|
35586
|
-
this.pendingCrops.delete(pending.trackId);
|
|
35587
35706
|
const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`;
|
|
35588
35707
|
const payload = {
|
|
35589
35708
|
deviceId: Number(pending.deviceId),
|
|
@@ -35615,8 +35734,21 @@ var EmbeddingDispatcher = class {
|
|
|
35615
35734
|
error: String(err)
|
|
35616
35735
|
}
|
|
35617
35736
|
});
|
|
35737
|
+
} finally {
|
|
35738
|
+
if (this.pendingCrops.get(pending.trackId) === pending) this.pendingCrops.delete(pending.trackId);
|
|
35618
35739
|
}
|
|
35619
35740
|
}
|
|
35741
|
+
/**
|
|
35742
|
+
* Decoded frames currently pinned in {@link pendingCrops}.
|
|
35743
|
+
*
|
|
35744
|
+
* ~0.7 MB each (a whole RGB detection frame, not a crop), so this number IS
|
|
35745
|
+
* the dispatcher's memory footprint. Exposed because the entries are
|
|
35746
|
+
* invisible in every functional read: a stranded one looks exactly like a
|
|
35747
|
+
* track that was never detected.
|
|
35748
|
+
*/
|
|
35749
|
+
pendingFrameCount() {
|
|
35750
|
+
return this.pendingCrops.size;
|
|
35751
|
+
}
|
|
35620
35752
|
};
|
|
35621
35753
|
/**
|
|
35622
35754
|
* Re-embed every listed track from its key frame.
|
|
@@ -35936,6 +36068,19 @@ function createEventMediaHandler(deps) {
|
|
|
35936
36068
|
};
|
|
35937
36069
|
}
|
|
35938
36070
|
//#endregion
|
|
36071
|
+
//#region src/pipeline-analytics/copy-face-crop-preview.ts
|
|
36072
|
+
var TRACK_PREVIEW_KINDS = new Set(["thumbnail", "thumbnailSmall"]);
|
|
36073
|
+
async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
|
|
36074
|
+
if ((await store.listTrackMedia(input.trackId)).some((m) => TRACK_PREVIEW_KINDS.has(m.kind))) return false;
|
|
36075
|
+
await store.putTrackThumbnail({
|
|
36076
|
+
deviceId: input.deviceId,
|
|
36077
|
+
trackId: input.trackId,
|
|
36078
|
+
timestamp: input.timestamp,
|
|
36079
|
+
data: input.crop
|
|
36080
|
+
});
|
|
36081
|
+
return true;
|
|
36082
|
+
}
|
|
36083
|
+
//#endregion
|
|
35939
36084
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
35940
36085
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
35941
36086
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
@@ -35944,29 +36089,47 @@ function createEventMediaHandler(deps) {
|
|
|
35944
36089
|
function pickEventOwnedMedia(files) {
|
|
35945
36090
|
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
35946
36091
|
}
|
|
35947
|
-
/** Pick a track's fallback media
|
|
35948
|
-
*
|
|
35949
|
-
*
|
|
35950
|
-
* Undefined when the
|
|
36092
|
+
/** Pick a track's fallback media when no subject crop exists: `thumbnailSmall`
|
|
36093
|
+
* → `thumbnail` → native `keyFrameSmall`/`keyFrame`/`fullFrame` → rolling
|
|
36094
|
+
* `lastFrame` → `firstFrame` → newest `snapshot` → any blob. A full frame
|
|
36095
|
+
* beats a class icon. Undefined when the owner has no media. */
|
|
35951
36096
|
function pickTrackFallbackMedia(files) {
|
|
35952
|
-
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];
|
|
36097
|
+
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];
|
|
35953
36098
|
}
|
|
35954
36099
|
/**
|
|
35955
|
-
* Crop-forced resolution (the `?kind=crop`
|
|
35956
|
-
*
|
|
35957
|
-
*
|
|
35958
|
-
*
|
|
35959
|
-
*
|
|
35960
|
-
* returning undefined. A boxed frame (with the target's box drawn) is a real
|
|
35961
|
-
* preview; an empty tile is not. Clean is always tried FIRST so the common case
|
|
35962
|
-
* still serves an unboxed tile. Undefined only when the owner has NO media.
|
|
36100
|
+
* Crop-forced resolution (the `?kind=crop` / `?kind=thumbnailSmall` reel path).
|
|
36101
|
+
*
|
|
36102
|
+
* Ladder: body crop (track/event thumbnail) → tight face/plate crop → any
|
|
36103
|
+
* remaining image (keyFrame / lastFrame / …). The class icon is only honest
|
|
36104
|
+
* when the owner has no stored bytes at all.
|
|
35963
36105
|
*
|
|
35964
36106
|
* `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
|
|
35965
36107
|
* in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
|
|
35966
36108
|
* composition pure + unit-testable here.
|
|
35967
36109
|
*/
|
|
35968
|
-
function resolveCropForcedMedia(files, pickClean) {
|
|
35969
|
-
return pickClean(files) ?? pickTrackFallbackMedia(files);
|
|
36110
|
+
function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
|
|
36111
|
+
return pickClean(files) ?? pickClean(detailFiles) ?? pickTrackFallbackMedia([...files, ...detailFiles]);
|
|
36112
|
+
}
|
|
36113
|
+
/**
|
|
36114
|
+
* Event + track media (the body/scene) and face/plate media (the detail
|
|
36115
|
+
* crops). Face rows live under `face-${trackId}`; a track can close with
|
|
36116
|
+
* `hasEmbeddedFace` and ZERO track files (0e343f86).
|
|
36117
|
+
*/
|
|
36118
|
+
async function collectCropForcedMedia(source, id) {
|
|
36119
|
+
const eventFiles = await source.listByOwner("event", id);
|
|
36120
|
+
const trackFiles = await source.listByOwner("track", id);
|
|
36121
|
+
const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
|
|
36122
|
+
const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
|
|
36123
|
+
const trackId = ownerTrackId ?? id;
|
|
36124
|
+
const [faceFiles, plateFiles] = await Promise.all([source.listByOwner("face", `face-${trackId}`), source.listByOwner("plate", `plate-${trackId}`)]);
|
|
36125
|
+
return {
|
|
36126
|
+
bodyFiles: [
|
|
36127
|
+
...eventFiles,
|
|
36128
|
+
...trackFiles,
|
|
36129
|
+
...owningTrackFiles
|
|
36130
|
+
],
|
|
36131
|
+
detailFiles: [...faceFiles, ...plateFiles]
|
|
36132
|
+
};
|
|
35970
36133
|
}
|
|
35971
36134
|
/**
|
|
35972
36135
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
@@ -35980,6 +36143,14 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
35980
36143
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
35981
36144
|
}
|
|
35982
36145
|
//#endregion
|
|
36146
|
+
//#region src/pipeline-analytics/ensure-track-hierarchy.ts
|
|
36147
|
+
async function ensureTrackHierarchyMedia(deps, input) {
|
|
36148
|
+
const tasks = [];
|
|
36149
|
+
if (!deps.isThumbnailLanded(input.trackId)) tasks.push(deps.captureBodyThumbnail(input));
|
|
36150
|
+
if (!deps.isKeyFrameLanded(input.trackId)) tasks.push(deps.captureKeyFrame(input));
|
|
36151
|
+
await Promise.all(tasks);
|
|
36152
|
+
}
|
|
36153
|
+
//#endregion
|
|
35983
36154
|
//#region src/pipeline-analytics/face-settings.ts
|
|
35984
36155
|
/**
|
|
35985
36156
|
* Per-device face-recognition settings. Cascade: a per-device override on top
|
|
@@ -36498,6 +36669,21 @@ var FaceRecognizer = class {
|
|
|
36498
36669
|
...held,
|
|
36499
36670
|
crop
|
|
36500
36671
|
});
|
|
36672
|
+
if (crop !== void 0 && input.frameHandle !== void 0) this.deps.onLiveHierarchy?.({
|
|
36673
|
+
deviceId: input.deviceId,
|
|
36674
|
+
trackId: c.trackId,
|
|
36675
|
+
timestamp: input.timestamp,
|
|
36676
|
+
bodyBbox: {
|
|
36677
|
+
x: c.bbox.x,
|
|
36678
|
+
y: c.bbox.y,
|
|
36679
|
+
w: c.bbox.w,
|
|
36680
|
+
h: c.bbox.h
|
|
36681
|
+
},
|
|
36682
|
+
frameWidth: input.frameWidth,
|
|
36683
|
+
frameHeight: input.frameHeight,
|
|
36684
|
+
cropPadding: input.cropPadding,
|
|
36685
|
+
frameHandle: input.frameHandle
|
|
36686
|
+
});
|
|
36501
36687
|
this.deps.logger.debug("face: best-face held", {
|
|
36502
36688
|
tags: {
|
|
36503
36689
|
deviceId: input.deviceId,
|
|
@@ -36647,6 +36833,22 @@ var FaceRecognizer = class {
|
|
|
36647
36833
|
});
|
|
36648
36834
|
return;
|
|
36649
36835
|
}
|
|
36836
|
+
try {
|
|
36837
|
+
await this.deps.copyCropToTrackPreview?.({
|
|
36838
|
+
deviceId,
|
|
36839
|
+
trackId,
|
|
36840
|
+
crop: held.crop,
|
|
36841
|
+
timestamp: held.timestamp
|
|
36842
|
+
});
|
|
36843
|
+
} catch (err) {
|
|
36844
|
+
this.deps.logger.debug("face: track preview copy failed", {
|
|
36845
|
+
tags: { deviceId },
|
|
36846
|
+
meta: {
|
|
36847
|
+
trackId,
|
|
36848
|
+
error: String(err)
|
|
36849
|
+
}
|
|
36850
|
+
});
|
|
36851
|
+
}
|
|
36650
36852
|
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
36651
36853
|
try {
|
|
36652
36854
|
await this.deps.faceStore.insert({
|
|
@@ -44227,6 +44429,12 @@ var PlateRecognizer = class {
|
|
|
44227
44429
|
text: c.plateText,
|
|
44228
44430
|
score: c.plateScore,
|
|
44229
44431
|
bbox: c.plateBbox,
|
|
44432
|
+
bodyBbox: {
|
|
44433
|
+
x: c.bbox.x,
|
|
44434
|
+
y: c.bbox.y,
|
|
44435
|
+
w: c.bbox.w,
|
|
44436
|
+
h: c.bbox.h
|
|
44437
|
+
},
|
|
44230
44438
|
timestamp: input.timestamp,
|
|
44231
44439
|
frameWidth: input.frameWidth,
|
|
44232
44440
|
frameHeight: input.frameHeight,
|
|
@@ -44300,6 +44508,16 @@ var PlateRecognizer = class {
|
|
|
44300
44508
|
}
|
|
44301
44509
|
});
|
|
44302
44510
|
}
|
|
44511
|
+
if (crop !== void 0 && input.frameHandle !== void 0 && input.bodyBbox !== void 0) this.deps.onLiveHierarchy?.({
|
|
44512
|
+
deviceId: input.deviceId,
|
|
44513
|
+
trackId: input.trackId,
|
|
44514
|
+
timestamp: input.timestamp,
|
|
44515
|
+
bodyBbox: input.bodyBbox,
|
|
44516
|
+
frameWidth: input.frameWidth,
|
|
44517
|
+
frameHeight: input.frameHeight,
|
|
44518
|
+
cropPadding: input.cropPadding,
|
|
44519
|
+
frameHandle: input.frameHandle
|
|
44520
|
+
});
|
|
44303
44521
|
this.bestPlate.set(input.trackId, {
|
|
44304
44522
|
text: input.text,
|
|
44305
44523
|
score: input.score,
|
|
@@ -48334,14 +48552,23 @@ function isAfterCursor(row, cursor) {
|
|
|
48334
48552
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
48335
48553
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
48336
48554
|
}
|
|
48555
|
+
var VEHICLE_CLASSES = new Set([
|
|
48556
|
+
"vehicle",
|
|
48557
|
+
"car",
|
|
48558
|
+
"truck",
|
|
48559
|
+
"bus"
|
|
48560
|
+
]);
|
|
48561
|
+
function motionlessMaxPxForClass(className) {
|
|
48562
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
48563
|
+
}
|
|
48337
48564
|
function netDisplacementPx(positions) {
|
|
48338
48565
|
const first = positions[0];
|
|
48339
48566
|
const last = positions[positions.length - 1];
|
|
48340
48567
|
if (!first || !last) return 0;
|
|
48341
48568
|
return Math.hypot(last.x - first.x, last.y - first.y);
|
|
48342
48569
|
}
|
|
48343
|
-
function isShortMotionlessTrack(lifeMs, movedPx) {
|
|
48344
|
-
return lifeMs <= 25e3 && movedPx <=
|
|
48570
|
+
function isShortMotionlessTrack(lifeMs, movedPx, className) {
|
|
48571
|
+
return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
|
|
48345
48572
|
}
|
|
48346
48573
|
//#endregion
|
|
48347
48574
|
//#region src/pipeline-analytics/store/track-store.ts
|
|
@@ -48925,7 +49152,7 @@ var TrackStore = class {
|
|
|
48925
49152
|
for (const [trackId, t] of this.active) {
|
|
48926
49153
|
if (nowMs - t.lastSeen < this.config.ttlMs) continue;
|
|
48927
49154
|
t.active = false;
|
|
48928
|
-
if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions))) t.closedReason = "stationary-promotion";
|
|
49155
|
+
if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions), t.className)) t.closedReason = "stationary-promotion";
|
|
48929
49156
|
const record = cloneTrack(t);
|
|
48930
49157
|
try {
|
|
48931
49158
|
await this.persistCompleted(record);
|
|
@@ -51316,9 +51543,14 @@ function deriveFrameDispatchKind(content) {
|
|
|
51316
51543
|
}
|
|
51317
51544
|
var DEFAULT_MAX_IN_FLIGHT_PER_DEVICE = 3;
|
|
51318
51545
|
var DEFAULT_LOG_WINDOW_MS = 6e4;
|
|
51546
|
+
/** One `capture queue full` line per device per this interval. The condition
|
|
51547
|
+
* holds for as long as the lane is saturated; one line per shed request would
|
|
51548
|
+
* be thousands. */
|
|
51549
|
+
var OVERFLOW_WARN_THROTTLE_MS = 6e4;
|
|
51319
51550
|
var CaptureScheduler = class {
|
|
51320
51551
|
logger;
|
|
51321
51552
|
budget;
|
|
51553
|
+
maxQueued;
|
|
51322
51554
|
windowMs;
|
|
51323
51555
|
now;
|
|
51324
51556
|
lanes = /* @__PURE__ */ new Map();
|
|
@@ -51327,9 +51559,12 @@ var CaptureScheduler = class {
|
|
|
51327
51559
|
/** holdKey → refcount of queued/running requests holding it. */
|
|
51328
51560
|
heldKeys = /* @__PURE__ */ new Map();
|
|
51329
51561
|
windows = /* @__PURE__ */ new Map();
|
|
51562
|
+
/** Per-device throttle for the `capture queue full` warn. */
|
|
51563
|
+
lastOverflowWarnAt = /* @__PURE__ */ new Map();
|
|
51330
51564
|
constructor(options) {
|
|
51331
51565
|
this.logger = options.logger;
|
|
51332
51566
|
this.budget = options.maxInFlightPerDevice ?? DEFAULT_MAX_IN_FLIGHT_PER_DEVICE;
|
|
51567
|
+
this.maxQueued = Math.max(1, options.maxQueuedPerDevice ?? 24);
|
|
51333
51568
|
this.windowMs = options.logWindowMs ?? DEFAULT_LOG_WINDOW_MS;
|
|
51334
51569
|
this.now = options.now ?? Date.now;
|
|
51335
51570
|
}
|
|
@@ -51399,6 +51634,7 @@ var CaptureScheduler = class {
|
|
|
51399
51634
|
failed: w.failed,
|
|
51400
51635
|
coalesced: w.coalesced,
|
|
51401
51636
|
droppedStale: w.droppedStale,
|
|
51637
|
+
droppedOverflow: w.droppedOverflow,
|
|
51402
51638
|
cancelled: w.cancelled,
|
|
51403
51639
|
maxQueueDepth: w.maxQueueDepth
|
|
51404
51640
|
};
|
|
@@ -51447,7 +51683,37 @@ var CaptureScheduler = class {
|
|
|
51447
51683
|
throw err;
|
|
51448
51684
|
});
|
|
51449
51685
|
}
|
|
51450
|
-
/**
|
|
51686
|
+
/**
|
|
51687
|
+
* Queue an over-budget request (priority-ordered, FIFO within priority),
|
|
51688
|
+
* under a BOUND enforced here rather than at dequeue.
|
|
51689
|
+
*
|
|
51690
|
+
* ## Why the queue needs its own bound
|
|
51691
|
+
*
|
|
51692
|
+
* `staleAfterMs` is evaluated in {@link pump}, and `pump` only touches the
|
|
51693
|
+
* queue while `inFlight < budget`. A lane that stays saturated therefore
|
|
51694
|
+
* never inspects its queue at all — nothing expires, nothing is shed, and the
|
|
51695
|
+
* queue grows for as long as the pressure lasts. The budget bounds concurrent
|
|
51696
|
+
* WORK; it has never bounded queued work.
|
|
51697
|
+
*
|
|
51698
|
+
* That gap is not theoretical: the per-frame batch dispatch PINS a ~0.7 MB
|
|
51699
|
+
* copy of the detection frame BEFORE queueing (so ring recycling cannot
|
|
51700
|
+
* invalidate it), and the queued closure holds that copy alive. Measured on
|
|
51701
|
+
* the live hub, `hub/pipeline-analytics` ran at rss=3911MB with heapUsed only
|
|
51702
|
+
* 153MB and arrayBuffers=2281MB — 2.28 GB of JS-reachable Buffers, roughly
|
|
51703
|
+
* 3 200 pinned frames, in a runner whose heap limit is 1216 MB. The call site
|
|
51704
|
+
* that pins them says the cost is "bounded by the lane"; it was not.
|
|
51705
|
+
*
|
|
51706
|
+
* Two things happen on the way in, in this order:
|
|
51707
|
+
*
|
|
51708
|
+
* 1. **expire what is already dead.** Entries past their `staleAfterMs` are
|
|
51709
|
+
* dropped now. They were doomed the moment they expired; `pump` simply
|
|
51710
|
+
* never got to look. This is the reclaim that costs nothing.
|
|
51711
|
+
* 2. **shed the least important.** Still over the cap, the TAIL goes — the
|
|
51712
|
+
* queue is ordered by priority, so the tail is by construction the work
|
|
51713
|
+
* this scheduler already ranked last. Whether that is the newcomer or an
|
|
51714
|
+
* older low-priority entry is decided by the same ordering that decides
|
|
51715
|
+
* execution, so a `firstFrame` never loses its place to an `embed`.
|
|
51716
|
+
*/
|
|
51451
51717
|
enqueue(lane, input) {
|
|
51452
51718
|
return new Promise((resolve, reject) => {
|
|
51453
51719
|
const entry = {
|
|
@@ -51460,11 +51726,53 @@ var CaptureScheduler = class {
|
|
|
51460
51726
|
},
|
|
51461
51727
|
drop: () => resolve(null)
|
|
51462
51728
|
};
|
|
51729
|
+
this.dropExpired(input.deviceId, lane);
|
|
51463
51730
|
this.insertByPriority(lane, entry);
|
|
51731
|
+
this.shedToCap(input.deviceId, lane);
|
|
51464
51732
|
const w = this.window(input.deviceId);
|
|
51465
51733
|
if (lane.queue.length > w.maxQueueDepth) w.maxQueueDepth = lane.queue.length;
|
|
51466
51734
|
});
|
|
51467
51735
|
}
|
|
51736
|
+
/** Drop every queued entry already past its `staleAfterMs`. Entries without
|
|
51737
|
+
* one (e.g. `confirm`, which gates track birth) are never expired here. */
|
|
51738
|
+
dropExpired(deviceId, lane) {
|
|
51739
|
+
const nowMs = this.now();
|
|
51740
|
+
for (let i = lane.queue.length - 1; i >= 0; i -= 1) {
|
|
51741
|
+
const entry = lane.queue[i];
|
|
51742
|
+
if (entry === void 0) continue;
|
|
51743
|
+
if (entry.staleAfterMs === void 0 || nowMs - entry.enqueuedAt <= entry.staleAfterMs) continue;
|
|
51744
|
+
lane.queue.splice(i, 1);
|
|
51745
|
+
this.bump(deviceId, "droppedStale");
|
|
51746
|
+
entry.drop();
|
|
51747
|
+
}
|
|
51748
|
+
}
|
|
51749
|
+
/** Shed from the tail (lowest priority, newest within it) until the lane is
|
|
51750
|
+
* at its cap. Loud: a capture that is dropped rather than merely late is a
|
|
51751
|
+
* missing thumbnail or first frame, and silence would read as "never asked". */
|
|
51752
|
+
shedToCap(deviceId, lane) {
|
|
51753
|
+
let shed = 0;
|
|
51754
|
+
while (lane.queue.length > this.maxQueued) {
|
|
51755
|
+
const victim = lane.queue.pop();
|
|
51756
|
+
if (victim === void 0) break;
|
|
51757
|
+
this.bump(deviceId, "droppedOverflow");
|
|
51758
|
+
victim.drop();
|
|
51759
|
+
shed += 1;
|
|
51760
|
+
}
|
|
51761
|
+
if (shed === 0) return;
|
|
51762
|
+
const nowMs = this.now();
|
|
51763
|
+
if (nowMs - (this.lastOverflowWarnAt.get(deviceId) ?? Number.NEGATIVE_INFINITY) < OVERFLOW_WARN_THROTTLE_MS) return;
|
|
51764
|
+
this.lastOverflowWarnAt.set(deviceId, nowMs);
|
|
51765
|
+
this.logger.warn("capture queue full — shedding the lowest-priority captures for this camera", {
|
|
51766
|
+
tags: { deviceId },
|
|
51767
|
+
meta: {
|
|
51768
|
+
shed,
|
|
51769
|
+
cap: this.maxQueued,
|
|
51770
|
+
queueDepth: lane.queue.length,
|
|
51771
|
+
inFlight: lane.inFlight,
|
|
51772
|
+
budget: this.budget
|
|
51773
|
+
}
|
|
51774
|
+
});
|
|
51775
|
+
}
|
|
51468
51776
|
insertByPriority(lane, entry) {
|
|
51469
51777
|
let index = lane.queue.length;
|
|
51470
51778
|
while (index > 0) {
|
|
@@ -51511,6 +51819,7 @@ var CaptureScheduler = class {
|
|
|
51511
51819
|
failed: 0,
|
|
51512
51820
|
coalesced: 0,
|
|
51513
51821
|
droppedStale: 0,
|
|
51822
|
+
droppedOverflow: 0,
|
|
51514
51823
|
cancelled: 0,
|
|
51515
51824
|
maxQueueDepth: 0
|
|
51516
51825
|
};
|
|
@@ -51535,6 +51844,7 @@ var CaptureScheduler = class {
|
|
|
51535
51844
|
failed: w.failed,
|
|
51536
51845
|
coalesced: w.coalesced,
|
|
51537
51846
|
droppedStale: w.droppedStale,
|
|
51847
|
+
droppedOverflow: w.droppedOverflow,
|
|
51538
51848
|
cancelled: w.cancelled,
|
|
51539
51849
|
maxQueueDepth: w.maxQueueDepth,
|
|
51540
51850
|
queueDepth: this.queueDepth(deviceId),
|
|
@@ -53105,8 +53415,14 @@ var EventMediaDispatcher = class {
|
|
|
53105
53415
|
* scheduler request against a 3-slot device lane and can start seconds late,
|
|
53106
53416
|
* by which time the handle's ring slot is usually recycled and the by-handle
|
|
53107
53417
|
* resolve in {@link captureForFrame} finds nothing (the 2026-07-23 boxed-tile
|
|
53108
|
-
* regression). ~0.7MB per pinned frame, freed with the batch
|
|
53109
|
-
*
|
|
53418
|
+
* regression). ~0.7MB per pinned frame, freed with the batch.
|
|
53419
|
+
*
|
|
53420
|
+
* THE COPY IS ONLY AS BOUNDED AS THE QUEUE THAT HOLDS IT. This said "bounded
|
|
53421
|
+
* by the lane" while the lane bounded in-flight execs (3) and nothing at all
|
|
53422
|
+
* bounded the queue behind them, so a saturated camera accumulated pinned
|
|
53423
|
+
* frames without limit — 2.28 GB of `arrayBuffers` measured on the live hub.
|
|
53424
|
+
* Every caller must therefore pass `staleAfterMs`, and `CaptureScheduler`
|
|
53425
|
+
* caps queue depth per device. Returns `null` on a genuine live-window miss.
|
|
53110
53426
|
*
|
|
53111
53427
|
* Both miss paths are WARN, throttled to one line per device per minute. They
|
|
53112
53428
|
* are the FIRST half of a pair whose second half (`capture batch lost`) has
|
|
@@ -56931,10 +57247,12 @@ var CONFIRMATION_CROP_MAX_WIDTH = 320;
|
|
|
56931
57247
|
* a late confirmation is still worth running; see `DEFAULT_CAPTURE_PRIORITY`). */
|
|
56932
57248
|
var ENRICHMENT_CAPTURE_STALE_MS = 3e3;
|
|
56933
57249
|
/** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
|
|
56934
|
-
* The `device.state-changed` push
|
|
56935
|
-
*
|
|
56936
|
-
*
|
|
56937
|
-
|
|
57250
|
+
* The `device.state-changed` push reaches a forked child through
|
|
57251
|
+
* `createEventBusSliceSource` (the DeviceProxy no longer waits on
|
|
57252
|
+
* `live.onEvent`). Events are still lossy telemetry (D8); this timer
|
|
57253
|
+
* is the RPC reconcile, not the mechanism. Five minutes matches the
|
|
57254
|
+
* viewer's `RECONCILE_MS`. */
|
|
57255
|
+
var ZONE_SLICE_RECONCILE_MS = 3e5;
|
|
56938
57256
|
/** How often the evaluating node re-reads the central scene set. */
|
|
56939
57257
|
var SCENE_RELOAD_INTERVAL_MS = 3e4;
|
|
56940
57258
|
/** How long the install coordinates are trusted between settings reads. */
|
|
@@ -56998,38 +57316,31 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
|
56998
57316
|
*
|
|
56999
57317
|
* `thumbnail` joined the clean set on 2026-07-17: the track best-thumbnail is
|
|
57000
57318
|
* now a subject-centered crop (event-media-dispatcher), and for DEGENERATE
|
|
57001
|
-
* tracks (no object event → no `crop`) it is the ONLY subject-centered media
|
|
57002
|
-
*
|
|
57003
|
-
*
|
|
57004
|
-
*
|
|
57005
|
-
*
|
|
57006
|
-
*
|
|
57007
|
-
*
|
|
57008
|
-
*
|
|
57009
|
-
*
|
|
57010
|
-
* over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
|
|
57011
|
-
* keep resolving.
|
|
57319
|
+
* tracks (no object event → no `crop`) it is the ONLY subject-centered media.
|
|
57320
|
+
*
|
|
57321
|
+
* Full-scene kinds (`fullFrame`, `keyFrame`, `keyFrameSmall`) stay out of
|
|
57322
|
+
* this list so a crop is preferred when one exists. `resolveCropForcedMedia`
|
|
57323
|
+
* then falls through to those full-scene kinds — a keyFrame beats an icon.
|
|
57324
|
+
* An *explicit* `?kind=keyFrame` / `?kind=snapshot` still matches exactly, so
|
|
57325
|
+
* videoclips and other kind-specific fetches keep working.
|
|
57326
|
+
*
|
|
57327
|
+
* The retired-but-still-stored kind (`crop`) stays listed so old rows resolve.
|
|
57012
57328
|
*/
|
|
57013
57329
|
var CLEAN_MEDIA_KINDS = [
|
|
57014
57330
|
"crop",
|
|
57015
57331
|
"thumbnail",
|
|
57016
|
-
"thumbnailSmall"
|
|
57017
|
-
"fullFrame",
|
|
57018
|
-
"keyFrame",
|
|
57019
|
-
"keyFrameSmall"
|
|
57332
|
+
"thumbnailSmall"
|
|
57020
57333
|
];
|
|
57021
57334
|
/**
|
|
57022
|
-
* Pick
|
|
57023
|
-
* `
|
|
57024
|
-
* {@link CLEAN_MEDIA_KINDS}
|
|
57025
|
-
*
|
|
57335
|
+
* Pick media for a crop-forced request: the exact `preferKind` when present
|
|
57336
|
+
* (so `?kind=snapshot` still serves a snapshot), else the first available
|
|
57337
|
+
* subject crop in {@link CLEAN_MEDIA_KINDS}. Returns undefined when no crop
|
|
57338
|
+
* is in the list — the caller then falls through to any remaining image
|
|
57339
|
+
* (`resolveCropForcedMedia`); the class icon is only for a total miss.
|
|
57026
57340
|
*/
|
|
57027
57341
|
function pickCleanMedia(files, preferKind) {
|
|
57028
|
-
const
|
|
57029
|
-
if (
|
|
57030
|
-
const exact = files.find((f) => f.kind === preferKind);
|
|
57031
|
-
if (exact) return exact;
|
|
57032
|
-
}
|
|
57342
|
+
const exact = files.find((f) => f.kind === preferKind);
|
|
57343
|
+
if (exact) return exact;
|
|
57033
57344
|
for (const kind of CLEAN_MEDIA_KINDS) {
|
|
57034
57345
|
const found = files.find((f) => f.kind === kind);
|
|
57035
57346
|
if (found) return found;
|
|
@@ -58493,6 +58804,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58493
58804
|
}, trackId);
|
|
58494
58805
|
},
|
|
58495
58806
|
markEmbeddedFace: (trackId) => stores.trackStore.markEmbeddedFace(trackId),
|
|
58807
|
+
copyCropToTrackPreview: async (input) => {
|
|
58808
|
+
await copyFaceCropIfTrackHasNoThumbnail({
|
|
58809
|
+
listTrackMedia: (trackId) => stores.mediaStore.listByOwner("track", trackId),
|
|
58810
|
+
putTrackThumbnail: (put) => stores.mediaStore.put({
|
|
58811
|
+
deviceId: put.deviceId,
|
|
58812
|
+
ownerKind: "track",
|
|
58813
|
+
ownerId: put.trackId,
|
|
58814
|
+
kind: "thumbnail",
|
|
58815
|
+
timestamp: put.timestamp,
|
|
58816
|
+
data: put.data
|
|
58817
|
+
})
|
|
58818
|
+
}, input);
|
|
58819
|
+
},
|
|
58496
58820
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
58497
58821
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
|
|
58498
58822
|
onSemanticMatch: (match) => this.promoteSemanticRecognition({
|
|
@@ -58507,6 +58831,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58507
58831
|
confidence: match.confidence,
|
|
58508
58832
|
...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
|
|
58509
58833
|
}),
|
|
58834
|
+
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
58510
58835
|
logger: logger.child("FaceRecognizer")
|
|
58511
58836
|
});
|
|
58512
58837
|
this.faceRecognizer.refreshGallery();
|
|
@@ -58539,6 +58864,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58539
58864
|
deriveCropFromKeyFrame,
|
|
58540
58865
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
58541
58866
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
58867
|
+
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
58542
58868
|
logger: logger.child("PlateRecognizer")
|
|
58543
58869
|
});
|
|
58544
58870
|
this.plateRecognizer.refreshGallery();
|
|
@@ -59908,6 +60234,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
59908
60234
|
refreshGallery: () => {
|
|
59909
60235
|
this.plateRecognizer?.refreshGallery();
|
|
59910
60236
|
},
|
|
60237
|
+
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
59911
60238
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
59912
60239
|
});
|
|
59913
60240
|
return [
|
|
@@ -60569,6 +60896,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60569
60896
|
hasSnapshot: snapshotTargets.some((t) => t.appendSnapshot)
|
|
60570
60897
|
}),
|
|
60571
60898
|
holdKeys: heldCaptureKeys,
|
|
60899
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
60572
60900
|
exec: () => pinnedFramePromise.then((pinnedFrame) => dispatcher.captureForFrame({
|
|
60573
60901
|
deviceId,
|
|
60574
60902
|
frameHandle,
|
|
@@ -60964,7 +61292,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60964
61292
|
if (!first) return;
|
|
60965
61293
|
const lifeMs = track.lastSeen - track.firstSeen;
|
|
60966
61294
|
const moved = netDisplacementPx(track.positions ?? []);
|
|
60967
|
-
if (!isShortMotionlessTrack(lifeMs, moved)) {
|
|
61295
|
+
if (!isShortMotionlessTrack(lifeMs, moved, track.className)) {
|
|
60968
61296
|
this.phantomCells?.noteMovingClose({
|
|
60969
61297
|
deviceId: track.deviceId,
|
|
60970
61298
|
className: track.className,
|
|
@@ -61082,6 +61410,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61082
61410
|
}, d.score, d.label, frame.timestamp);
|
|
61083
61411
|
if (this.plateRecognizer) {
|
|
61084
61412
|
const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
|
|
61413
|
+
const body = this.residents.bestSeenBbox(trackId);
|
|
61085
61414
|
await this.plateRecognizer.observePlateRead({
|
|
61086
61415
|
deviceId,
|
|
61087
61416
|
trackId,
|
|
@@ -61097,7 +61426,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61097
61426
|
frameWidth: frame.frameWidth,
|
|
61098
61427
|
frameHeight: frame.frameHeight,
|
|
61099
61428
|
cropPadding: mediaSettings.cropPadding,
|
|
61100
|
-
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
61429
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {},
|
|
61430
|
+
...body !== void 0 ? { bodyBbox: {
|
|
61431
|
+
x: body.x,
|
|
61432
|
+
y: body.y,
|
|
61433
|
+
w: body.w,
|
|
61434
|
+
h: body.h
|
|
61435
|
+
} } : {}
|
|
61101
61436
|
});
|
|
61102
61437
|
}
|
|
61103
61438
|
}
|
|
@@ -61398,6 +61733,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61398
61733
|
}
|
|
61399
61734
|
}).catch(() => {})));
|
|
61400
61735
|
}
|
|
61736
|
+
/**
|
|
61737
|
+
* A face or plate crop just landed from a still-live handle. Fill the
|
|
61738
|
+
* missing parent rungs — person/vehicle thumbnail (display crop, same
|
|
61739
|
+
* surface that produced the detail) and native keyFrame — so Events never
|
|
61740
|
+
* has a child crop without a body and a scene.
|
|
61741
|
+
*/
|
|
61742
|
+
requestTrackHierarchy(input) {
|
|
61743
|
+
ensureTrackHierarchyMedia({
|
|
61744
|
+
isThumbnailLanded: (trackId) => this.residents.isThumbnailLanded(trackId),
|
|
61745
|
+
isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
|
|
61746
|
+
captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
|
|
61747
|
+
captureKeyFrame: async (req) => {
|
|
61748
|
+
await this.persistKeyFrames(req.deviceId, req.timestamp, [{
|
|
61749
|
+
trackId: req.trackId,
|
|
61750
|
+
retry: false
|
|
61751
|
+
}], req.frameHandle);
|
|
61752
|
+
}
|
|
61753
|
+
}, input);
|
|
61754
|
+
}
|
|
61755
|
+
async captureHierarchyBodyThumbnail(input) {
|
|
61756
|
+
const displayCrop = this.captureDisplayCropFn;
|
|
61757
|
+
const mediaStore = this.mediaStore;
|
|
61758
|
+
if (!displayCrop || !mediaStore) return;
|
|
61759
|
+
await this.captureScheduler.request({
|
|
61760
|
+
deviceId: input.deviceId,
|
|
61761
|
+
trackId: input.trackId,
|
|
61762
|
+
kind: "thumbnail",
|
|
61763
|
+
coalesceKey: captureCoalesceKey(input.deviceId, "thumbnail", `${input.trackId}:hierarchy`),
|
|
61764
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
61765
|
+
exec: async () => {
|
|
61766
|
+
if (this.residents.isThumbnailLanded(input.trackId)) return;
|
|
61767
|
+
const jpeg = await displayCrop(input.frameHandle, input.bodyBbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId);
|
|
61768
|
+
if (!jpeg) return;
|
|
61769
|
+
await mediaStore.put({
|
|
61770
|
+
deviceId: input.deviceId,
|
|
61771
|
+
ownerKind: "track",
|
|
61772
|
+
ownerId: input.trackId,
|
|
61773
|
+
kind: "thumbnail",
|
|
61774
|
+
timestamp: input.timestamp,
|
|
61775
|
+
data: jpeg
|
|
61776
|
+
});
|
|
61777
|
+
this.residents.markThumbnailLanded(input.deviceId, input.trackId);
|
|
61778
|
+
}
|
|
61779
|
+
}).catch(() => {});
|
|
61780
|
+
}
|
|
61401
61781
|
/** Attribution-time SEMANTIC best-shot promotion (D-semantic-bestshot,
|
|
61402
61782
|
* 2026-08-21; see `pipeline/semantic-promotion.ts`): offer a frame from
|
|
61403
61783
|
* which a recognition SUCCEEDED to the best-frame ranking with the
|
|
@@ -61446,7 +61826,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61446
61826
|
this.captureScheduler.request({
|
|
61447
61827
|
deviceId,
|
|
61448
61828
|
kind: "thumbnail",
|
|
61829
|
+
trackId,
|
|
61449
61830
|
holdKeys: [captureCoalesceKey(deviceId, "thumbnail", trackId)],
|
|
61831
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
61450
61832
|
exec: async () => {
|
|
61451
61833
|
const [pinnedFrame, media] = await Promise.all([pinnedFramePromise, this.resolveDeviceMediaSettings(deviceId)]);
|
|
61452
61834
|
const res = await dispatcher.captureForFrame({
|
|
@@ -62903,15 +63285,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62903
63285
|
* applies to the very next frame even when frames stop briefly
|
|
62904
63286
|
* (e.g. during binding flips).
|
|
62905
63287
|
*
|
|
62906
|
-
* RECONCILE: the push channel behind `subscribe`
|
|
62907
|
-
* via `
|
|
62908
|
-
*
|
|
62909
|
-
*
|
|
62910
|
-
*
|
|
62911
|
-
* telemetry (D8); the durable channel is RPC + reconcile — so each
|
|
62912
|
-
* proxy also refreshes its two slices on a slow timer. `refresh()`
|
|
62913
|
-
* round-trips `deviceState.getCapSlice` and fans out through the SAME
|
|
62914
|
-
* subscribe callbacks above, so a zone edit lands within one interval.
|
|
63288
|
+
* RECONCILE: the push channel behind `subscribe` is `device.state-changed`
|
|
63289
|
+
* via the addon's `ctx.eventBus` (`createEventBusSliceSource`). That
|
|
63290
|
+
* declares UDS interest so the parent actually fans the slice here. Events
|
|
63291
|
+
* remain lossy (D8); `refresh()` round-trips `deviceState.getCapSlice` on
|
|
63292
|
+
* this timer as the backstop.
|
|
62915
63293
|
*/
|
|
62916
63294
|
async ensureProxy(deviceId) {
|
|
62917
63295
|
const cached = this.proxies.get(deviceId);
|
|
@@ -64842,14 +65220,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
64842
65220
|
async readEventThumbnail(id, preferKind) {
|
|
64843
65221
|
const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
|
|
64844
65222
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
64845
|
-
const
|
|
64846
|
-
|
|
64847
|
-
|
|
64848
|
-
|
|
64849
|
-
|
|
64850
|
-
...trackFiles,
|
|
64851
|
-
...owningTrackFiles
|
|
64852
|
-
], (files) => pickCleanMedia(files, preferKind));
|
|
65223
|
+
const layers = await collectCropForcedMedia({
|
|
65224
|
+
listByOwner: (ownerKind, ownerId) => this.mediaStore?.listByOwner(ownerKind, ownerId) ?? Promise.resolve([]),
|
|
65225
|
+
getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
|
|
65226
|
+
}, id);
|
|
65227
|
+
const chosen = resolveCropForcedMedia(layers.bodyFiles, (files) => pickCleanMedia(files, preferKind), layers.detailFiles);
|
|
64853
65228
|
if (!chosen) return null;
|
|
64854
65229
|
return {
|
|
64855
65230
|
bytes: Buffer.from(chosen.base64, "base64"),
|