@camstack/addon-post-analysis 1.2.209 → 1.2.211
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-BV15Z8DR.mjs → dist-D2rFaMex.mjs} +116 -1
- package/dist/{dist-BttKNVmP.js → dist-FHSaZkf_.js} +127 -0
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DCfZE21-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-4gDJk8ED.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BYQbvSbq.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B2Kde85q.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-WwnFkrnz.mjs → hostInit-Dgzccgln.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +845 -46
- package/dist/pipeline-analytics/index.mjs +845 -46
- 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-FHSaZkf_.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");
|
|
@@ -3452,7 +3452,7 @@ var SceneConfirmGate = class {
|
|
|
3452
3452
|
};
|
|
3453
3453
|
}
|
|
3454
3454
|
};
|
|
3455
|
-
function clamp$
|
|
3455
|
+
function clamp$2(v, lo, hi) {
|
|
3456
3456
|
return Math.max(lo, Math.min(v, hi));
|
|
3457
3457
|
}
|
|
3458
3458
|
/**
|
|
@@ -3467,10 +3467,10 @@ async function cropSnapshotRoi(encoded, roi) {
|
|
|
3467
3467
|
const frameW = meta.width ?? 0;
|
|
3468
3468
|
const frameH = meta.height ?? 0;
|
|
3469
3469
|
if (frameW <= 0 || frameH <= 0) throw new Error("snapshot has no decodable dimensions");
|
|
3470
|
-
const left = clamp$
|
|
3471
|
-
const top = clamp$
|
|
3472
|
-
const width = clamp$
|
|
3473
|
-
const height = clamp$
|
|
3470
|
+
const left = clamp$2(Math.round(roi.x * frameW), 0, frameW - 1);
|
|
3471
|
+
const top = clamp$2(Math.round(roi.y * frameH), 0, frameH - 1);
|
|
3472
|
+
const width = clamp$2(Math.round(roi.width * frameW), 1, frameW - left);
|
|
3473
|
+
const height = clamp$2(Math.round(roi.height * frameH), 1, frameH - top);
|
|
3474
3474
|
return {
|
|
3475
3475
|
crop: await (0, sharp.default)(encoded).extract({
|
|
3476
3476
|
left,
|
|
@@ -28633,6 +28633,37 @@ var MediaStore = class {
|
|
|
28633
28633
|
return newKey;
|
|
28634
28634
|
}
|
|
28635
28635
|
/**
|
|
28636
|
+
* Exchange the bytes+instant held in the `firstFrame` and `lastFrame` slots
|
|
28637
|
+
* (close-time ordering repair, see `pipeline/first-last-frame-order.ts`).
|
|
28638
|
+
*
|
|
28639
|
+
* Both are single-instance kinds, so each `putReplacing` overwrites the slot
|
|
28640
|
+
* it names — the two rows end up holding each other's blob and timestamp.
|
|
28641
|
+
* Nothing is deleted and nothing is re-encoded: both blobs are already the
|
|
28642
|
+
* track's own stored pixels and the only thing wrong with them is which slot
|
|
28643
|
+
* they sit in. The `lastFrame` write lands first so the "ultimo" tile is
|
|
28644
|
+
* never momentarily the older view a viewer just refuted.
|
|
28645
|
+
*/
|
|
28646
|
+
async swapFirstAndLastFrame(input) {
|
|
28647
|
+
await this.gated(async () => {
|
|
28648
|
+
await this.putReplacingUnchecked({
|
|
28649
|
+
deviceId: input.deviceId,
|
|
28650
|
+
ownerKind: "track",
|
|
28651
|
+
ownerId: input.trackId,
|
|
28652
|
+
kind: "lastFrame",
|
|
28653
|
+
timestamp: input.firstFrame.timestamp,
|
|
28654
|
+
data: Buffer.from(input.firstFrame.base64, "base64")
|
|
28655
|
+
});
|
|
28656
|
+
await this.putReplacingUnchecked({
|
|
28657
|
+
deviceId: input.deviceId,
|
|
28658
|
+
ownerKind: "track",
|
|
28659
|
+
ownerId: input.trackId,
|
|
28660
|
+
kind: "firstFrame",
|
|
28661
|
+
timestamp: input.lastFrame.timestamp,
|
|
28662
|
+
data: Buffer.from(input.lastFrame.base64, "base64")
|
|
28663
|
+
});
|
|
28664
|
+
});
|
|
28665
|
+
}
|
|
28666
|
+
/**
|
|
28636
28667
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
28637
28668
|
* found in the index or if the blob is missing from storage.
|
|
28638
28669
|
*/
|
|
@@ -30505,6 +30536,17 @@ function trackDurationMs(facts) {
|
|
|
30505
30536
|
function hasKind(media, kind) {
|
|
30506
30537
|
return media.some((m) => m.kind === kind);
|
|
30507
30538
|
}
|
|
30539
|
+
/** Newest instant stamped on a row of `kind`, or `null` when the track owns
|
|
30540
|
+
* none. Single-instance kinds hold at most one row; the max guards a legacy
|
|
30541
|
+
* duplicate, mirroring `decideFirstLastFrameOrder`. */
|
|
30542
|
+
function newestOfKind(media, kind) {
|
|
30543
|
+
let newest = null;
|
|
30544
|
+
for (const m of media) {
|
|
30545
|
+
if (m.kind !== kind) continue;
|
|
30546
|
+
if (newest === null || m.timestamp > newest) newest = m.timestamp;
|
|
30547
|
+
}
|
|
30548
|
+
return newest;
|
|
30549
|
+
}
|
|
30508
30550
|
function latestMediaAt(media) {
|
|
30509
30551
|
let latest = null;
|
|
30510
30552
|
for (const m of media) if (latest === null || m.timestamp > latest) latest = m.timestamp;
|
|
@@ -30552,6 +30594,12 @@ function analyseDebugTrack(inventory) {
|
|
|
30552
30594
|
const lag = facts.lastSeen - lastFrameAt;
|
|
30553
30595
|
if (lag > 2e3) out.push(finding("LASTFRAME_STALE", "warn", `lastFrame is ${String(lag)} ms older than the track's end.`));
|
|
30554
30596
|
}
|
|
30597
|
+
const firstFrameAt = newestOfKind(media, "firstFrame");
|
|
30598
|
+
if (firstFrameAt !== null) {
|
|
30599
|
+
const lastFrameAt = newestOfKind(media, "lastFrame");
|
|
30600
|
+
if (lastFrameAt !== null && firstFrameAt > lastFrameAt) out.push(finding("FIRSTFRAME_AFTER_LASTFRAME", "error", `firstFrame is ${String(firstFrameAt - lastFrameAt)} ms NEWER than lastFrame — the "primo" tile post-dates the "ultimo" one. The first view is captured at the CONFIRMED birth, which the confirmation gate can defer by up to its maxDeferralMs, while keyFrame/thumbnail/lastFrame are stamped at the earlier frame they were cut from.`));
|
|
30601
|
+
if (closed && firstFrameAt > facts.lastSeen) out.push(finding("FIRSTFRAME_AFTER_TRACK_END", "error", `firstFrame was captured ${String(firstFrameAt - facts.lastSeen)} ms AFTER the track's last observation — the tile shows a scene the subject had already left (a birth confirmed on a coasted frame).`));
|
|
30602
|
+
}
|
|
30555
30603
|
if (closed && durationMs >= 1e4 && media.length > 0) {
|
|
30556
30604
|
const latest = latestMediaAt(media);
|
|
30557
30605
|
if (latest !== null) {
|
|
@@ -34151,6 +34199,29 @@ function firstFrameRetryBbox(current, birth) {
|
|
|
34151
34199
|
return { ...current };
|
|
34152
34200
|
}
|
|
34153
34201
|
//#endregion
|
|
34202
|
+
//#region src/pipeline-analytics/pipeline/birth-first-frame-gate.ts
|
|
34203
|
+
/**
|
|
34204
|
+
* Decide whether the confirmed-birth frame may be dispatched as this track's
|
|
34205
|
+
* `firstFrame` capture. Pure; the caller logs the refusal and keeps the
|
|
34206
|
+
* `firstFramePending` mark so a later frame retries.
|
|
34207
|
+
*
|
|
34208
|
+
* `coasted-frame` is reported ahead of `no-frame-handle` when both hold: a
|
|
34209
|
+
* handle-less coasted frame would have been refused for BEING coasted whatever
|
|
34210
|
+
* the decode plane did, and naming the decode is what would send the next
|
|
34211
|
+
* investigation at the wrong subsystem.
|
|
34212
|
+
*/
|
|
34213
|
+
function decideBirthFirstFrameCapture(input) {
|
|
34214
|
+
if (input.matchedThisFrame === false) return {
|
|
34215
|
+
capture: false,
|
|
34216
|
+
refusedBecause: "coasted-frame"
|
|
34217
|
+
};
|
|
34218
|
+
if (!input.hasFrameHandle) return {
|
|
34219
|
+
capture: false,
|
|
34220
|
+
refusedBecause: "no-frame-handle"
|
|
34221
|
+
};
|
|
34222
|
+
return { capture: true };
|
|
34223
|
+
}
|
|
34224
|
+
//#endregion
|
|
34154
34225
|
//#region src/pipeline-analytics/pipeline/events/event-filter.ts
|
|
34155
34226
|
var DEFAULT_EVENT_EMITTER_CONFIG = {
|
|
34156
34227
|
minTrackAge: 3,
|
|
@@ -34498,6 +34569,37 @@ var TWO_WHEELERS = new Set([
|
|
|
34498
34569
|
function intersection(a, b) {
|
|
34499
34570
|
return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
34500
34571
|
}
|
|
34572
|
+
/**
|
|
34573
|
+
* The FOLDED subject's box: the union of the rider and the machine.
|
|
34574
|
+
*
|
|
34575
|
+
* The fold's whole claim is that the two halves are ONE subject. Until
|
|
34576
|
+
* 2026-09-08 the box that survived the fold was the MACHINE's alone, so every
|
|
34577
|
+
* consumer that cuts pixels from `TrackedDetectionOut.bbox` — the best-shot
|
|
34578
|
+
* thumbnail, `wideCentralSquareLayout`'s central square, the close-time
|
|
34579
|
+
* keyFrame→thumbnail derive — framed a motorcycle with its rider's head cut
|
|
34580
|
+
* off. Operator, device 617, tracks `7f50622b` and `9cbc4791`, three minutes
|
|
34581
|
+
* apart: "motocicli e veicoli a due ruote devono includere il conducente nel
|
|
34582
|
+
* keyframe" (D407).
|
|
34583
|
+
*
|
|
34584
|
+
* This is the ONE place the folded subject's rectangle is formed, and it is
|
|
34585
|
+
* formed BEFORE the tracker — so no crop path derives a second rectangle
|
|
34586
|
+
* (D52 stays intact: `deriveDetailCropRect` is still the only detail-crop
|
|
34587
|
+
* derivation, and it now receives a subject box that already holds the rider).
|
|
34588
|
+
* Pure geometry: a strict union, never a pad — the fold widens the subject, it
|
|
34589
|
+
* never moves it.
|
|
34590
|
+
*/
|
|
34591
|
+
function riderSubjectBox(person, vehicle) {
|
|
34592
|
+
const x = Math.min(person.x, vehicle.x);
|
|
34593
|
+
const y = Math.min(person.y, vehicle.y);
|
|
34594
|
+
const right = Math.max(person.x + person.w, vehicle.x + vehicle.w);
|
|
34595
|
+
const bottom = Math.max(person.y + person.h, vehicle.y + vehicle.h);
|
|
34596
|
+
return {
|
|
34597
|
+
x,
|
|
34598
|
+
y,
|
|
34599
|
+
w: right - x,
|
|
34600
|
+
h: bottom - y
|
|
34601
|
+
};
|
|
34602
|
+
}
|
|
34501
34603
|
/** True when `vehicle` is a two-wheeler that `person` is riding. */
|
|
34502
34604
|
function isRiderPair(person, vehicle) {
|
|
34503
34605
|
if (person.macroClass !== "person") return false;
|
|
@@ -34759,7 +34861,7 @@ var DEFAULT_TRACKER_CONFIG = {
|
|
|
34759
34861
|
var PERSON_CLASS = "person";
|
|
34760
34862
|
var ANIMAL_CLASS = "animal";
|
|
34761
34863
|
var MAX_PATH_LENGTH = 300;
|
|
34762
|
-
function clamp(value, min, max) {
|
|
34864
|
+
function clamp$1(value, min, max) {
|
|
34763
34865
|
return Math.max(min, Math.min(max, value));
|
|
34764
34866
|
}
|
|
34765
34867
|
function iou$3(a, b) {
|
|
@@ -35199,8 +35301,8 @@ var SortTracker = class SortTracker {
|
|
|
35199
35301
|
const perFrameX = t.velocity.dx * t.emitIntervalMs;
|
|
35200
35302
|
const perFrameY = t.velocity.dy * t.emitIntervalMs;
|
|
35201
35303
|
return {
|
|
35202
|
-
x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
|
|
35203
|
-
y: t.bbox.y + clamp(perFrameY * f, -t.bbox.h, t.bbox.h),
|
|
35304
|
+
x: t.bbox.x + clamp$1(perFrameX * f, -t.bbox.w, t.bbox.w),
|
|
35305
|
+
y: t.bbox.y + clamp$1(perFrameY * f, -t.bbox.h, t.bbox.h),
|
|
35204
35306
|
w: t.bbox.w,
|
|
35205
35307
|
h: t.bbox.h
|
|
35206
35308
|
};
|
|
@@ -35209,8 +35311,8 @@ var SortTracker = class SortTracker {
|
|
|
35209
35311
|
const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
|
|
35210
35312
|
const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
|
|
35211
35313
|
return {
|
|
35212
|
-
x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
|
|
35213
|
-
y: t.bbox.y + clamp(t.velocity.dy * elapsedMs, -allowY, allowY),
|
|
35314
|
+
x: t.bbox.x + clamp$1(t.velocity.dx * elapsedMs, -allowX, allowX),
|
|
35315
|
+
y: t.bbox.y + clamp$1(t.velocity.dy * elapsedMs, -allowY, allowY),
|
|
35214
35316
|
w: t.bbox.w,
|
|
35215
35317
|
h: t.bbox.h
|
|
35216
35318
|
};
|
|
@@ -36264,17 +36366,64 @@ var FrameProcessor = class {
|
|
|
36264
36366
|
const foldByVehicleBbox = /* @__PURE__ */ new Map();
|
|
36265
36367
|
if (riderPairs.length > 0) {
|
|
36266
36368
|
const riderIdx = new Set(riderPairs.map((p) => Number(p.personId)));
|
|
36369
|
+
/**
|
|
36370
|
+
* Move every bbox-OBJECT-keyed side table from the machine's box to the
|
|
36371
|
+
* folded subject's box (D407).
|
|
36372
|
+
*
|
|
36373
|
+
* This file carries frame-local facts across the tracker by bbox-object
|
|
36374
|
+
* REFERENCE (`labelsByBbox`, `embeddingByBbox`, `maskByBbox`, …); the
|
|
36375
|
+
* tracker then assigns `track.bbox = det.bbox`, and the emit block below
|
|
36376
|
+
* reads every one of those maps with `td.bbox`. Widening the subject
|
|
36377
|
+
* therefore means the widened box must inherit the machine's entries —
|
|
36378
|
+
* otherwise a folded rider silently loses its vehicle-classifier label
|
|
36379
|
+
* ("Motorcycle"), its mask, its plate read and its source id. One place,
|
|
36380
|
+
* exhaustive, next to the maps it moves.
|
|
36381
|
+
*/
|
|
36382
|
+
const adoptSubjectBox = (from, to) => {
|
|
36383
|
+
const labels = labelsByBbox.get(from);
|
|
36384
|
+
if (labels !== void 0) labelsByBbox.set(to, labels);
|
|
36385
|
+
const tier = originalClassTierByBbox.get(from);
|
|
36386
|
+
if (tier !== void 0) originalClassTierByBbox.set(to, tier);
|
|
36387
|
+
const embedding = embeddingByBbox.get(from);
|
|
36388
|
+
if (embedding !== void 0) embeddingByBbox.set(to, embedding);
|
|
36389
|
+
const mask = maskByBbox.get(from);
|
|
36390
|
+
if (mask !== void 0) maskByBbox.set(to, mask);
|
|
36391
|
+
const plate = plateByBbox.get(from);
|
|
36392
|
+
if (plate !== void 0) plateByBbox.set(to, plate);
|
|
36393
|
+
const faceBbox = faceBboxByBbox.get(from);
|
|
36394
|
+
if (faceBbox !== void 0) faceBboxByBbox.set(to, faceBbox);
|
|
36395
|
+
const faceSize = nativeFaceSizeByBbox.get(from);
|
|
36396
|
+
if (faceSize !== void 0) nativeFaceSizeByBbox.set(to, faceSize);
|
|
36397
|
+
const alignedCrop = faceAlignedCropByBbox.get(from);
|
|
36398
|
+
if (alignedCrop !== void 0) faceAlignedCropByBbox.set(to, alignedCrop);
|
|
36399
|
+
const sourceId = sourceIdByBbox.get(from);
|
|
36400
|
+
if (sourceId !== void 0) {
|
|
36401
|
+
sourceIdByBbox.set(to, sourceId);
|
|
36402
|
+
firstLevelBboxById.set(sourceId, to);
|
|
36403
|
+
}
|
|
36404
|
+
};
|
|
36405
|
+
/** Folded vehicles, by their index in `filteredDetections`. */
|
|
36406
|
+
const foldedVehicles = /* @__PURE__ */ new Map();
|
|
36267
36407
|
for (const p of riderPairs) {
|
|
36268
|
-
const
|
|
36269
|
-
|
|
36270
|
-
|
|
36408
|
+
const vehicleIdx = Number(p.vehicleId);
|
|
36409
|
+
const vehicle = filteredDetections[vehicleIdx];
|
|
36410
|
+
const person = filteredDetections[Number(p.personId)];
|
|
36411
|
+
if (vehicle === void 0 || person === void 0) continue;
|
|
36412
|
+
const subjectBox = riderSubjectBox(person.bbox, vehicle.bbox);
|
|
36413
|
+
adoptSubjectBox(vehicle.bbox, subjectBox);
|
|
36414
|
+
const folded = {
|
|
36415
|
+
...vehicle,
|
|
36416
|
+
bbox: subjectBox
|
|
36417
|
+
};
|
|
36418
|
+
foldedVehicles.set(vehicleIdx, folded);
|
|
36419
|
+
foldByVehicleBbox.set(subjectBox, {
|
|
36271
36420
|
overlap: p.overlap,
|
|
36272
|
-
personScore:
|
|
36421
|
+
personScore: person.score,
|
|
36273
36422
|
vehicleScore: vehicle.score,
|
|
36274
36423
|
vehicleClass: vehicle.originalClass ?? "two-wheeler"
|
|
36275
36424
|
});
|
|
36276
36425
|
}
|
|
36277
|
-
filteredDetections = filteredDetections.filter((_, i) => !riderIdx.has(i));
|
|
36426
|
+
filteredDetections = filteredDetections.map((d, i) => foldedVehicles.get(i) ?? d).filter((_, i) => !riderIdx.has(i));
|
|
36278
36427
|
}
|
|
36279
36428
|
const gate = this.stationaryGate ? this.stationaryGate.filter({
|
|
36280
36429
|
detections: filteredDetections,
|
|
@@ -37287,6 +37436,44 @@ function decideLastFramePromotion(media) {
|
|
|
37287
37436
|
};
|
|
37288
37437
|
}
|
|
37289
37438
|
//#endregion
|
|
37439
|
+
//#region src/pipeline-analytics/pipeline/first-last-frame-order.ts
|
|
37440
|
+
/**
|
|
37441
|
+
* Decide whether a closing track's `firstFrame` and `lastFrame` slots hold each
|
|
37442
|
+
* other's bytes. Pure — see the module header for the contract.
|
|
37443
|
+
*
|
|
37444
|
+
* Swap when BOTH slots are filled and the `firstFrame` is STRICTLY newer than
|
|
37445
|
+
* the `lastFrame`. Equal timestamps are not an inversion.
|
|
37446
|
+
*/
|
|
37447
|
+
function decideFirstLastFrameOrder(media) {
|
|
37448
|
+
let firstFrame;
|
|
37449
|
+
let lastFrame;
|
|
37450
|
+
for (const m of media) if (m.kind === "firstFrame") {
|
|
37451
|
+
if (firstFrame === void 0 || m.timestamp > firstFrame.timestamp) firstFrame = m;
|
|
37452
|
+
} else if (m.kind === "lastFrame") {
|
|
37453
|
+
if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
|
|
37454
|
+
}
|
|
37455
|
+
if (firstFrame === void 0) return {
|
|
37456
|
+
swap: false,
|
|
37457
|
+
skippedBecause: "no-first-frame"
|
|
37458
|
+
};
|
|
37459
|
+
if (lastFrame === void 0) return {
|
|
37460
|
+
swap: false,
|
|
37461
|
+
skippedBecause: "no-last-frame"
|
|
37462
|
+
};
|
|
37463
|
+
if (firstFrame.timestamp <= lastFrame.timestamp) return {
|
|
37464
|
+
swap: false,
|
|
37465
|
+
skippedBecause: "ordered"
|
|
37466
|
+
};
|
|
37467
|
+
return {
|
|
37468
|
+
swap: true,
|
|
37469
|
+
firstFrameKey: firstFrame.key,
|
|
37470
|
+
lastFrameKey: lastFrame.key,
|
|
37471
|
+
firstFrameTimestamp: firstFrame.timestamp,
|
|
37472
|
+
lastFrameTimestamp: lastFrame.timestamp,
|
|
37473
|
+
inversionMs: firstFrame.timestamp - lastFrame.timestamp
|
|
37474
|
+
};
|
|
37475
|
+
}
|
|
37476
|
+
//#endregion
|
|
37290
37477
|
//#region src/pipeline-analytics/pipeline/static-track-gate.ts
|
|
37291
37478
|
/**
|
|
37292
37479
|
* Net displacement + path span for a track's centroid path, normalized to
|
|
@@ -37429,6 +37616,7 @@ var TrackCloser = class {
|
|
|
37429
37616
|
tile = "raster";
|
|
37430
37617
|
}
|
|
37431
37618
|
await this.maybePromoteLastFrame(t, ownedMedia);
|
|
37619
|
+
await this.repairFirstLastFrameOrder(t);
|
|
37432
37620
|
const hasRasterFallback = closure?.rasterFallback !== void 0;
|
|
37433
37621
|
let deriveMiss;
|
|
37434
37622
|
if (!thumbnailLanded) {
|
|
@@ -37772,6 +37960,56 @@ var TrackCloser = class {
|
|
|
37772
37960
|
}
|
|
37773
37961
|
}
|
|
37774
37962
|
/**
|
|
37963
|
+
* A track's "primo" tile is never newer than its "ultimo" tile
|
|
37964
|
+
* (`first-last-frame-order.ts`). Filed by hand three times on device 3829,
|
|
37965
|
+
* 2026-09-08 — `d035f251`, `448678bb`, `7628b19d`.
|
|
37966
|
+
*
|
|
37967
|
+
* The media list is RE-READ rather than reusing the caller's `ownedMedia`:
|
|
37968
|
+
* `maybePromoteLastFrame` has just run and is precisely what may have put the
|
|
37969
|
+
* older bytes in the `lastFrame` slot, so the caller's copy predates the pair
|
|
37970
|
+
* being ordered here. Best-effort — a failure never blocks the close.
|
|
37971
|
+
*/
|
|
37972
|
+
async repairFirstLastFrameOrder(t) {
|
|
37973
|
+
const store = this.deps.mediaStore();
|
|
37974
|
+
if (!store) return;
|
|
37975
|
+
const media = await store.listByOwner("track", t.trackId);
|
|
37976
|
+
const decision = decideFirstLastFrameOrder(media);
|
|
37977
|
+
if (!decision.swap) return;
|
|
37978
|
+
const firstFrame = media.find((m) => m.key === decision.firstFrameKey);
|
|
37979
|
+
const lastFrame = media.find((m) => m.key === decision.lastFrameKey);
|
|
37980
|
+
if (!firstFrame || !lastFrame) return;
|
|
37981
|
+
try {
|
|
37982
|
+
await store.swapFirstAndLastFrame({
|
|
37983
|
+
deviceId: t.deviceId,
|
|
37984
|
+
trackId: t.trackId,
|
|
37985
|
+
firstFrame,
|
|
37986
|
+
lastFrame
|
|
37987
|
+
});
|
|
37988
|
+
this.deps.logger.warn("firstFrame/lastFrame were out of order — slots swapped", {
|
|
37989
|
+
tags: { deviceId: t.deviceId },
|
|
37990
|
+
meta: {
|
|
37991
|
+
trackId: t.trackId,
|
|
37992
|
+
className: t.className,
|
|
37993
|
+
inversionMs: decision.inversionMs,
|
|
37994
|
+
firstFrameAt: decision.firstFrameTimestamp,
|
|
37995
|
+
lastFrameAt: decision.lastFrameTimestamp,
|
|
37996
|
+
/** How far the late capture ran past the track itself. Positive means
|
|
37997
|
+
* it was cut from a frame the subject was no longer observed in. */
|
|
37998
|
+
pastTrackEndMs: decision.firstFrameTimestamp - t.lastSeen,
|
|
37999
|
+
durationMs: t.lastSeen - t.firstSeen
|
|
38000
|
+
}
|
|
38001
|
+
});
|
|
38002
|
+
} catch (err) {
|
|
38003
|
+
this.deps.logger.warn("firstFrame/lastFrame order repair failed", {
|
|
38004
|
+
tags: { deviceId: t.deviceId },
|
|
38005
|
+
meta: {
|
|
38006
|
+
trackId: t.trackId,
|
|
38007
|
+
error: String(err)
|
|
38008
|
+
}
|
|
38009
|
+
});
|
|
38010
|
+
}
|
|
38011
|
+
}
|
|
38012
|
+
/**
|
|
37775
38013
|
* Key-event importance: score the just-expired track and persist. Peak
|
|
37776
38014
|
* confidence comes from the shared best-detection tracker; peak bbox area
|
|
37777
38015
|
* + the winning object-event id from one indexed queryObject(trackId) —
|
|
@@ -41587,7 +41825,10 @@ var TrackStore = class {
|
|
|
41587
41825
|
retrainStatus: active.retrainStatus ?? "none",
|
|
41588
41826
|
debug: active.debug === true,
|
|
41589
41827
|
debugNote: active.debugNote ?? "",
|
|
41590
|
-
favourited: active.favourited === true
|
|
41828
|
+
favourited: active.favourited === true,
|
|
41829
|
+
startedAt: active.firstSeen,
|
|
41830
|
+
className: active.className,
|
|
41831
|
+
...displayLabel(active) !== void 0 ? { label: displayLabel(active) } : {}
|
|
41591
41832
|
};
|
|
41592
41833
|
const persisted = await this.getPersistedByTrackId(trackId);
|
|
41593
41834
|
if (!persisted) return null;
|
|
@@ -41595,7 +41836,10 @@ var TrackStore = class {
|
|
|
41595
41836
|
retrainStatus: persisted.retrainStatus ?? "none",
|
|
41596
41837
|
debug: persisted.debug === true,
|
|
41597
41838
|
debugNote: persisted.debugNote ?? "",
|
|
41598
|
-
favourited: persisted.favourited === true
|
|
41839
|
+
favourited: persisted.favourited === true,
|
|
41840
|
+
startedAt: persisted.firstSeen,
|
|
41841
|
+
className: persisted.className,
|
|
41842
|
+
...displayLabel(persisted) !== void 0 ? { label: displayLabel(persisted) } : {}
|
|
41599
41843
|
};
|
|
41600
41844
|
}
|
|
41601
41845
|
/**
|
|
@@ -48902,10 +49146,9 @@ var FaceRecognizer = class {
|
|
|
48902
49146
|
margin: settings.margin,
|
|
48903
49147
|
minIdentitySamples: settings.minIdentitySamples
|
|
48904
49148
|
}) : /* @__PURE__ */ new Map();
|
|
48905
|
-
if (this.deps.
|
|
49149
|
+
if (this.deps.onSemanticFace !== void 0) for (const c of matchCandidates) {
|
|
48906
49150
|
const match = matches.get(c.trackId);
|
|
48907
|
-
|
|
48908
|
-
this.deps.onSemanticMatch({
|
|
49151
|
+
this.deps.onSemanticFace({
|
|
48909
49152
|
deviceId: input.deviceId,
|
|
48910
49153
|
trackId: c.trackId,
|
|
48911
49154
|
timestamp: input.timestamp,
|
|
@@ -48913,8 +49156,10 @@ var FaceRecognizer = class {
|
|
|
48913
49156
|
frameHeight: input.frameHeight,
|
|
48914
49157
|
bbox: c.bbox,
|
|
48915
49158
|
confidence: c.confidence,
|
|
48916
|
-
|
|
48917
|
-
|
|
49159
|
+
...match !== void 0 ? {
|
|
49160
|
+
matchScore: match.score,
|
|
49161
|
+
identityId: match.identityId
|
|
49162
|
+
} : {},
|
|
48918
49163
|
...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
|
|
48919
49164
|
});
|
|
48920
49165
|
}
|
|
@@ -53261,6 +53506,19 @@ var DeferredBirthRegistry = class {
|
|
|
53261
53506
|
}
|
|
53262
53507
|
};
|
|
53263
53508
|
//#endregion
|
|
53509
|
+
//#region src/pipeline-analytics/pipeline/detail-semantic-gate.ts
|
|
53510
|
+
/** The detail `className` whose result IS the recognition. */
|
|
53511
|
+
var PLATE_CLASS = "plate";
|
|
53512
|
+
/**
|
|
53513
|
+
* The moment `className`'s detail result earns the semantic tier.
|
|
53514
|
+
*
|
|
53515
|
+
* `recognition` — promote as soon as the result exists (the plate read).
|
|
53516
|
+
* `label-write` — promote only once the track ACCEPTED the label.
|
|
53517
|
+
*/
|
|
53518
|
+
function detailSemanticTrigger(className) {
|
|
53519
|
+
return className === PLATE_CLASS ? "recognition" : "label-write";
|
|
53520
|
+
}
|
|
53521
|
+
//#endregion
|
|
53264
53522
|
//#region src/pipeline-analytics/pipeline/dropout-skip.ts
|
|
53265
53523
|
function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
|
|
53266
53524
|
if (!cfg.enabled) return false;
|
|
@@ -55023,7 +55281,7 @@ var PackageDropDetector = class {
|
|
|
55023
55281
|
async onStationaryChange(change) {
|
|
55024
55282
|
try {
|
|
55025
55283
|
if (change.phase === "appeared") await this.onAppeared(change.entry, change.timestamp);
|
|
55026
|
-
else await this.onDeparted(change
|
|
55284
|
+
else await this.onDeparted(change);
|
|
55027
55285
|
} catch (err) {
|
|
55028
55286
|
this.deps.onError?.("onStationaryChange", err);
|
|
55029
55287
|
this.deps.logger.warn("package-drop detector failed on change", {
|
|
@@ -55201,7 +55459,9 @@ var PackageDropDetector = class {
|
|
|
55201
55459
|
}
|
|
55202
55460
|
});
|
|
55203
55461
|
}
|
|
55204
|
-
async onDeparted(
|
|
55462
|
+
async onDeparted(change) {
|
|
55463
|
+
const entry = change.entry;
|
|
55464
|
+
const timestamp = change.timestamp;
|
|
55205
55465
|
if (!(await this.deps.resolveSettings(entry.deviceId)).packageDropPickupEnabled) {
|
|
55206
55466
|
this.deps.logger.debug("package pick-up skipped — packageDropPickupEnabled is false", {
|
|
55207
55467
|
tags: { deviceId: entry.deviceId },
|
|
@@ -55236,13 +55496,25 @@ var PackageDropDetector = class {
|
|
|
55236
55496
|
});
|
|
55237
55497
|
return;
|
|
55238
55498
|
}
|
|
55499
|
+
const collector = change.collector;
|
|
55500
|
+
const packageTrackId = entry.sourceTrackId ?? entry.id;
|
|
55501
|
+
const mediaTrackId = collector?.trackId ?? packageTrackId;
|
|
55502
|
+
if (collector === void 0) this.deps.logger.info("package pick-up has no collector track — the notification carries the parcel frame", {
|
|
55503
|
+
tags: { deviceId: entry.deviceId },
|
|
55504
|
+
meta: {
|
|
55505
|
+
entryId: entry.id,
|
|
55506
|
+
eventId: pickedUpId,
|
|
55507
|
+
packageTrackId,
|
|
55508
|
+
pickupAt: entry.lastConfirmedAt
|
|
55509
|
+
}
|
|
55510
|
+
});
|
|
55239
55511
|
const ev = {
|
|
55240
55512
|
id: pickedUpId,
|
|
55241
55513
|
kind: "object",
|
|
55242
55514
|
deviceId: entry.deviceId,
|
|
55243
55515
|
timestamp,
|
|
55244
55516
|
source: "pipeline",
|
|
55245
|
-
trackId:
|
|
55517
|
+
trackId: mediaTrackId,
|
|
55246
55518
|
className: PACKAGE_EVENT_CLASS,
|
|
55247
55519
|
...entry.label !== void 0 ? { label: entry.label } : {},
|
|
55248
55520
|
confidence: PACKAGE_IMPORTANCE,
|
|
@@ -55256,6 +55528,7 @@ var PackageDropDetector = class {
|
|
|
55256
55528
|
state: PICKED_UP_STATE,
|
|
55257
55529
|
frameWidth: entry.frameWidth,
|
|
55258
55530
|
frameHeight: entry.frameHeight,
|
|
55531
|
+
...collector?.keyFrameMediaKey !== void 0 ? { mediaKey: collector.keyFrameMediaKey } : {},
|
|
55259
55532
|
importance: PACKAGE_IMPORTANCE
|
|
55260
55533
|
};
|
|
55261
55534
|
await this.deps.events.insertObject(ev);
|
|
@@ -55265,6 +55538,7 @@ var PackageDropDetector = class {
|
|
|
55265
55538
|
entryId: entry.id,
|
|
55266
55539
|
deliveredEventId: deliveredId,
|
|
55267
55540
|
className: entry.className,
|
|
55541
|
+
...collector !== void 0 ? { collectorTrackId: collector.trackId } : {},
|
|
55268
55542
|
timestamp
|
|
55269
55543
|
});
|
|
55270
55544
|
this.deps.logger.info("package picked up", {
|
|
@@ -55272,11 +55546,139 @@ var PackageDropDetector = class {
|
|
|
55272
55546
|
meta: {
|
|
55273
55547
|
entryId: entry.id,
|
|
55274
55548
|
eventId: pickedUpId,
|
|
55275
|
-
deliveredEventId: deliveredId
|
|
55549
|
+
deliveredEventId: deliveredId,
|
|
55550
|
+
mediaTrackId,
|
|
55551
|
+
...collector !== void 0 ? {
|
|
55552
|
+
collectorTrackId: collector.trackId,
|
|
55553
|
+
collectorClassName: collector.className,
|
|
55554
|
+
collectorSkewMs: collector.skewMs
|
|
55555
|
+
} : { collectorTrackId: null }
|
|
55276
55556
|
}
|
|
55277
55557
|
});
|
|
55278
55558
|
}
|
|
55279
55559
|
};
|
|
55560
|
+
/**
|
|
55561
|
+
* How long a presence survives in the mirror, in FRAME time.
|
|
55562
|
+
*
|
|
55563
|
+
* It must outlive the whole detection latency: a pick-up at T is concluded no
|
|
55564
|
+
* earlier than T + `PACKAGE_STILLNESS_TTL_MS` (5 min of observed time), and the
|
|
55565
|
+
* pick is made at conclusion time. 10 minutes is double that, so a collector
|
|
55566
|
+
* noted at the true instant is still there to be named — and a mirror that
|
|
55567
|
+
* expires the answer before the question is asked would be the same silent
|
|
55568
|
+
* hole in a different place.
|
|
55569
|
+
*/
|
|
55570
|
+
var PACKAGE_COLLECTOR_RETENTION_MS = 10 * 6e4;
|
|
55571
|
+
/** The pick-up instant a stationary entry implies. One derivation, named. */
|
|
55572
|
+
function pickupInstantOf(entry) {
|
|
55573
|
+
return entry.lastConfirmedAt;
|
|
55574
|
+
}
|
|
55575
|
+
var PackageCollectorMirror = class {
|
|
55576
|
+
/** Per device, per track. Every write replaces the record (immutable). */
|
|
55577
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
55578
|
+
/**
|
|
55579
|
+
* Record one sighting. Extends the track's presence span and refreshes its
|
|
55580
|
+
* key-frame handle (a re-shot key frame is a better one, and the newest is
|
|
55581
|
+
* also the nearest to the pick-up — the same trade as above).
|
|
55582
|
+
*/
|
|
55583
|
+
note(obs) {
|
|
55584
|
+
const current = this.byDevice.get(obs.deviceId);
|
|
55585
|
+
const next = /* @__PURE__ */ new Map();
|
|
55586
|
+
const horizon = obs.at - PACKAGE_COLLECTOR_RETENTION_MS;
|
|
55587
|
+
if (current !== void 0) for (const [id, p] of current) {
|
|
55588
|
+
if (p.lastAt < horizon) continue;
|
|
55589
|
+
next.set(id, p);
|
|
55590
|
+
}
|
|
55591
|
+
const prev = next.get(obs.trackId);
|
|
55592
|
+
next.set(obs.trackId, {
|
|
55593
|
+
trackId: obs.trackId,
|
|
55594
|
+
className: obs.className,
|
|
55595
|
+
firstAt: prev === void 0 ? obs.at : Math.min(prev.firstAt, obs.at),
|
|
55596
|
+
lastAt: prev === void 0 ? obs.at : Math.max(prev.lastAt, obs.at),
|
|
55597
|
+
...obs.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: obs.keyFrameMediaKey } : prev?.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: prev.keyFrameMediaKey } : {}
|
|
55598
|
+
});
|
|
55599
|
+
this.byDevice.set(obs.deviceId, evictOldest(next));
|
|
55600
|
+
}
|
|
55601
|
+
/**
|
|
55602
|
+
* The track nearest the pick-up instant, or `null` when nobody qualifies.
|
|
55603
|
+
*
|
|
55604
|
+
* `null` is a real answer and not an error: a parcel can stop being seen
|
|
55605
|
+
* because the light changed or the detector lost it, and naming a person who
|
|
55606
|
+
* was not there would be worse than naming nobody. The caller degrades to the
|
|
55607
|
+
* parcel's own owner and logs it.
|
|
55608
|
+
*/
|
|
55609
|
+
pickCollector(input) {
|
|
55610
|
+
const presences = this.byDevice.get(input.deviceId);
|
|
55611
|
+
if (presences === void 0 || presences.size === 0) return null;
|
|
55612
|
+
const maxSkewMs = input.maxSkewMs ?? 3e4;
|
|
55613
|
+
let best = null;
|
|
55614
|
+
let bestLastAt = Number.NEGATIVE_INFINITY;
|
|
55615
|
+
for (const p of presences.values()) {
|
|
55616
|
+
const observedAt = clamp(input.pickupAt, p.firstAt, p.lastAt);
|
|
55617
|
+
const skewMs = Math.abs(observedAt - input.pickupAt);
|
|
55618
|
+
if (skewMs > maxSkewMs) continue;
|
|
55619
|
+
if (best !== null && (skewMs > best.skewMs || skewMs === best.skewMs && p.lastAt <= bestLastAt)) continue;
|
|
55620
|
+
best = {
|
|
55621
|
+
trackId: p.trackId,
|
|
55622
|
+
className: p.className,
|
|
55623
|
+
observedAt,
|
|
55624
|
+
skewMs,
|
|
55625
|
+
...p.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: p.keyFrameMediaKey } : {}
|
|
55626
|
+
};
|
|
55627
|
+
bestLastAt = p.lastAt;
|
|
55628
|
+
}
|
|
55629
|
+
return best;
|
|
55630
|
+
}
|
|
55631
|
+
/**
|
|
55632
|
+
* Fold one pipeline frame in. Returns how many presences it touched, so the
|
|
55633
|
+
* caller can say "nobody was ever in the drop-off zone" with a number.
|
|
55634
|
+
*
|
|
55635
|
+
* This is the ONLY place the frame-shaped input is interpreted, so the
|
|
55636
|
+
* production path and the test path read the same rules: a non-package class,
|
|
55637
|
+
* inside a configured drop-off zone, on this frame.
|
|
55638
|
+
*/
|
|
55639
|
+
noteFrame(input) {
|
|
55640
|
+
if (input.packageZoneIds.size === 0) return 0;
|
|
55641
|
+
let noted = 0;
|
|
55642
|
+
for (const t of input.tracked) {
|
|
55643
|
+
if (input.packageClasses.has(t.className)) continue;
|
|
55644
|
+
if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
|
|
55645
|
+
const keyFrameMediaKey = input.keyFrameMediaKeyFor(t.trackId);
|
|
55646
|
+
this.note({
|
|
55647
|
+
deviceId: input.deviceId,
|
|
55648
|
+
trackId: t.trackId,
|
|
55649
|
+
className: t.className,
|
|
55650
|
+
at: input.timestamp,
|
|
55651
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
55652
|
+
});
|
|
55653
|
+
noted += 1;
|
|
55654
|
+
}
|
|
55655
|
+
return noted;
|
|
55656
|
+
}
|
|
55657
|
+
/** Drop a camera's memory (unbind / step disabled / device removal). */
|
|
55658
|
+
forgetDevice(deviceId) {
|
|
55659
|
+
this.byDevice.delete(deviceId);
|
|
55660
|
+
}
|
|
55661
|
+
reset() {
|
|
55662
|
+
this.byDevice.clear();
|
|
55663
|
+
}
|
|
55664
|
+
/** Retained presences for a camera — diagnostics and tests only. */
|
|
55665
|
+
debugPresenceCount(deviceId) {
|
|
55666
|
+
return this.byDevice.get(deviceId)?.size ?? 0;
|
|
55667
|
+
}
|
|
55668
|
+
};
|
|
55669
|
+
function clamp(value, low, high) {
|
|
55670
|
+
if (value < low) return low;
|
|
55671
|
+
if (value > high) return high;
|
|
55672
|
+
return value;
|
|
55673
|
+
}
|
|
55674
|
+
/** Keep the newest {@link PACKAGE_COLLECTOR_MAX_PRESENCES} by `lastAt`. */
|
|
55675
|
+
function evictOldest(presences) {
|
|
55676
|
+
if (presences.size <= 32) return presences;
|
|
55677
|
+
const ordered = [...presences.values()].sort((a, b) => b.lastAt - a.lastAt);
|
|
55678
|
+
const kept = /* @__PURE__ */ new Map();
|
|
55679
|
+
for (const p of ordered.slice(0, 32)) kept.set(p.trackId, p);
|
|
55680
|
+
return kept;
|
|
55681
|
+
}
|
|
55280
55682
|
//#endregion
|
|
55281
55683
|
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
55282
55684
|
/**
|
|
@@ -56752,6 +57154,61 @@ function classifyTrackAppearance(input) {
|
|
|
56752
57154
|
if (input.inPrevActive) return "continuing";
|
|
56753
57155
|
return input.positionsCount > 1 ? "resurrection" : "birth";
|
|
56754
57156
|
}
|
|
57157
|
+
//#endregion
|
|
57158
|
+
//#region src/pipeline-analytics/pipeline/archive-debug-note.ts
|
|
57159
|
+
/**
|
|
57160
|
+
* Archive the note a clear is about to destroy. Never throws.
|
|
57161
|
+
*
|
|
57162
|
+
* **Which way this fails, stated once.** The operator pressed a flag button;
|
|
57163
|
+
* the archive is this system's own bookkeeping. So a failing archive does NOT
|
|
57164
|
+
* block the flag write — the button must do what it says even when SQLite is
|
|
57165
|
+
* busy — and it does NOT retry, because the caller is a synchronous cap method
|
|
57166
|
+
* an operator is waiting on.
|
|
57167
|
+
*
|
|
57168
|
+
* What it must never do is lose the sentence quietly. The failure line carries
|
|
57169
|
+
* the NOTE TEXT verbatim, plus `tags: { deviceId }` and the trackId, so the
|
|
57170
|
+
* words are recoverable from the log by hand and the loss is attributable to
|
|
57171
|
+
* one camera. A branch that drops work silently reads as "never happened", and
|
|
57172
|
+
* the 3-hour media blackout produced not one line.
|
|
57173
|
+
*
|
|
57174
|
+
* An EMPTY note archives nothing: there is nothing to keep, and a row of `''`
|
|
57175
|
+
* would be a permanent entry in a corpus meant to be mined for sentences.
|
|
57176
|
+
*/
|
|
57177
|
+
async function archiveNoteBeforeClear(input) {
|
|
57178
|
+
const note = input.current.debugNote;
|
|
57179
|
+
if (note.trim() === "") return;
|
|
57180
|
+
if (input.archive === void 0) {
|
|
57181
|
+
input.logger.warn("debug note dropped — no archive is wired on this runner", {
|
|
57182
|
+
tags: { deviceId: input.deviceId },
|
|
57183
|
+
meta: {
|
|
57184
|
+
deviceId: input.deviceId,
|
|
57185
|
+
trackId: input.trackId,
|
|
57186
|
+
note
|
|
57187
|
+
}
|
|
57188
|
+
});
|
|
57189
|
+
return;
|
|
57190
|
+
}
|
|
57191
|
+
try {
|
|
57192
|
+
await input.archive.append({
|
|
57193
|
+
deviceId: input.deviceId,
|
|
57194
|
+
sourceTrackId: input.trackId,
|
|
57195
|
+
note,
|
|
57196
|
+
...input.current.startedAt !== void 0 ? { trackStartedAt: input.current.startedAt } : {},
|
|
57197
|
+
...input.current.className !== void 0 ? { trackClass: input.current.className } : {},
|
|
57198
|
+
...input.current.label !== void 0 ? { trackLabel: input.current.label } : {}
|
|
57199
|
+
});
|
|
57200
|
+
} catch (err) {
|
|
57201
|
+
input.logger.error("failed to archive a debug note — the text is in this line", {
|
|
57202
|
+
tags: { deviceId: input.deviceId },
|
|
57203
|
+
meta: {
|
|
57204
|
+
deviceId: input.deviceId,
|
|
57205
|
+
trackId: input.trackId,
|
|
57206
|
+
note,
|
|
57207
|
+
error: err instanceof Error ? err.message : String(err)
|
|
57208
|
+
}
|
|
57209
|
+
});
|
|
57210
|
+
}
|
|
57211
|
+
}
|
|
56755
57212
|
/** Thrown when the device is already holding its full staging budget. Distinct
|
|
56756
57213
|
* type so a surface can tell "you are out of room" from "that track is gone". */
|
|
56757
57214
|
var StagingBudgetExceededError = class extends Error {
|
|
@@ -56792,7 +57249,10 @@ var TrackAlreadyTrainedError = class extends Error {
|
|
|
56792
57249
|
* caller sent with it. A note surviving the flag is a leftover that reads
|
|
56793
57250
|
* as a live request, and the invariant belongs here rather than in the
|
|
56794
57251
|
* three surfaces that write this patch — one of which is a browser grid
|
|
56795
|
-
* that has no note UI at all and would never think to clear it.
|
|
57252
|
+
* that has no note UI at all and would never think to clear it. Since
|
|
57253
|
+
* D405 the clear is not a DELETE: `applyTrackFlags` ARCHIVES the note
|
|
57254
|
+
* first, into a table that outlives the track. The invariant above is
|
|
57255
|
+
* unchanged — only where the words go.
|
|
56796
57256
|
* 2. **No note in the patch → nothing is written.** This is what makes a
|
|
56797
57257
|
* cancelled prompt free: the app turns debug on, the operator declines to
|
|
56798
57258
|
* type, and the flag still lands. Refusing to write a note must never cost
|
|
@@ -56886,6 +57346,13 @@ async function applyTrackFlags(deps, input) {
|
|
|
56886
57346
|
...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
|
|
56887
57347
|
...resolveNotePatch(deps, input, current)
|
|
56888
57348
|
};
|
|
57349
|
+
if (patch.debugNote === "" && current.debugNote !== "") await archiveNoteBeforeClear({
|
|
57350
|
+
deviceId: input.deviceId,
|
|
57351
|
+
trackId: input.trackId,
|
|
57352
|
+
logger,
|
|
57353
|
+
archive: deps.noteArchive,
|
|
57354
|
+
current
|
|
57355
|
+
});
|
|
56889
57356
|
if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
|
|
56890
57357
|
const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
|
|
56891
57358
|
return {
|
|
@@ -59567,6 +60034,261 @@ var IdentityStore = class {
|
|
|
59567
60034
|
}
|
|
59568
60035
|
};
|
|
59569
60036
|
//#endregion
|
|
60037
|
+
//#region src/pipeline-analytics/store/debug-note-archive-store.ts
|
|
60038
|
+
/**
|
|
60039
|
+
* DebugNoteArchiveStore — the durable corpus of what operators asked to be
|
|
60040
|
+
* checked (D405).
|
|
60041
|
+
*
|
|
60042
|
+
* A declared SQL-backed collection, like the events ops-log next door, and for
|
|
60043
|
+
* the same reason: pipeline-analytics already owns SQLite collections, and an
|
|
60044
|
+
* undeclared collection crash-loops the runner. MUST be declared in
|
|
60045
|
+
* `onInitialize` before the first write.
|
|
60046
|
+
*
|
|
60047
|
+
* Collection name: pipeline-analytics:debug-note-archive
|
|
60048
|
+
*
|
|
60049
|
+
* Two properties separate it from every other table in this addon:
|
|
60050
|
+
*
|
|
60051
|
+
* - **It is not track-owned.** It names a track in `sourceTrackId` and that is
|
|
60052
|
+
* PROVENANCE, never ownership — the whole point is that the row outlives the
|
|
60053
|
+
* track, which `debug` deliberately never pinned (D353). It is classified
|
|
60054
|
+
* `never-orphaned` by hand in `collection-classification.ts` so the
|
|
60055
|
+
* ownership derivation cannot see the column and cascade it away.
|
|
60056
|
+
* - **It is bounded by COUNT, not by age.** An age sweep would delete the
|
|
60057
|
+
* corpus for being old, which is the failure this table exists to fix: a
|
|
60058
|
+
* six-month-old note is the most valuable row in it, because the pattern it
|
|
60059
|
+
* describes has had time to repeat. See {@link MAX_ARCHIVED_DEBUG_NOTES}.
|
|
60060
|
+
*/
|
|
60061
|
+
/**
|
|
60062
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
60063
|
+
* write="one row per debug note RETIRED by a review — appended by
|
|
60064
|
+
* `applyTrackFlags` immediately before `debug: false` clears the note off
|
|
60065
|
+
* the track row (D405). Idempotent per (sourceTrackId, note): the row id is
|
|
60066
|
+
* derived from the pair, so reviewing the same track twice cannot double
|
|
60067
|
+
* the corpus. An empty note writes nothing."
|
|
60068
|
+
* retention="NOT track retention and NOT an age clock — deleting a note for
|
|
60069
|
+
* being old is the failure this table fixes. Bounded by ROW COUNT
|
|
60070
|
+
* (MAX_ARCHIVED_DEBUG_NOTES, 5000, fleet-wide), oldest `archivedAt` first,
|
|
60071
|
+
* trimmed on append. ~3 MB of text at the ceiling."
|
|
60072
|
+
*/
|
|
60073
|
+
var DEBUG_NOTE_ARCHIVE_COLLECTION = "pipeline-analytics:debug-note-archive";
|
|
60074
|
+
var DEBUG_NOTE_ARCHIVE_COLUMNS = [
|
|
60075
|
+
{
|
|
60076
|
+
name: "id",
|
|
60077
|
+
type: "TEXT",
|
|
60078
|
+
primaryKey: true,
|
|
60079
|
+
notNull: true
|
|
60080
|
+
},
|
|
60081
|
+
{
|
|
60082
|
+
name: "archivedAt",
|
|
60083
|
+
type: "INTEGER",
|
|
60084
|
+
notNull: true
|
|
60085
|
+
},
|
|
60086
|
+
{
|
|
60087
|
+
name: "deviceId",
|
|
60088
|
+
type: "INTEGER",
|
|
60089
|
+
notNull: true
|
|
60090
|
+
},
|
|
60091
|
+
{
|
|
60092
|
+
name: "sourceTrackId",
|
|
60093
|
+
type: "TEXT",
|
|
60094
|
+
notNull: true
|
|
60095
|
+
},
|
|
60096
|
+
{
|
|
60097
|
+
name: "note",
|
|
60098
|
+
type: "TEXT",
|
|
60099
|
+
notNull: true
|
|
60100
|
+
},
|
|
60101
|
+
{
|
|
60102
|
+
name: "trackStartedAt",
|
|
60103
|
+
type: "INTEGER"
|
|
60104
|
+
},
|
|
60105
|
+
{
|
|
60106
|
+
name: "trackClass",
|
|
60107
|
+
type: "TEXT"
|
|
60108
|
+
},
|
|
60109
|
+
{
|
|
60110
|
+
name: "trackLabel",
|
|
60111
|
+
type: "TEXT"
|
|
60112
|
+
}
|
|
60113
|
+
];
|
|
60114
|
+
var DEBUG_NOTE_ARCHIVE_INDEXES = [{
|
|
60115
|
+
name: "idx_debugnote_archived_at",
|
|
60116
|
+
columns: ["archivedAt"]
|
|
60117
|
+
}, {
|
|
60118
|
+
name: "idx_debugnote_device_at",
|
|
60119
|
+
columns: ["deviceId", "archivedAt"]
|
|
60120
|
+
}];
|
|
60121
|
+
/**
|
|
60122
|
+
* The row id for one `(track, note)` pair.
|
|
60123
|
+
*
|
|
60124
|
+
* Deterministic on purpose: the idempotency the operator needs is "reviewing
|
|
60125
|
+
* twice does not double the row", and a derived primary key gets it from the
|
|
60126
|
+
* database rather than from a read-modify-write that two concurrent reviews
|
|
60127
|
+
* could interleave through. Truncated to 32 hex characters — 128 bits over a
|
|
60128
|
+
* table capped at 5 000 rows.
|
|
60129
|
+
*/
|
|
60130
|
+
function archiveRowId(sourceTrackId, note) {
|
|
60131
|
+
return (0, node_crypto.createHash)("sha256").update(`${sourceTrackId}${note}`).digest("hex").slice(0, 32);
|
|
60132
|
+
}
|
|
60133
|
+
var DebugNoteArchiveStore = class {
|
|
60134
|
+
store;
|
|
60135
|
+
logger;
|
|
60136
|
+
now;
|
|
60137
|
+
maxRows;
|
|
60138
|
+
constructor(deps) {
|
|
60139
|
+
this.store = deps.store;
|
|
60140
|
+
this.logger = deps.logger;
|
|
60141
|
+
this.now = deps.now ?? (() => Date.now());
|
|
60142
|
+
this.maxRows = deps.maxRows ?? 5e3;
|
|
60143
|
+
}
|
|
60144
|
+
/** The ceiling a store built without an override enforces. */
|
|
60145
|
+
static defaultMaxRows() {
|
|
60146
|
+
return require_dist.MAX_ARCHIVED_DEBUG_NOTES;
|
|
60147
|
+
}
|
|
60148
|
+
static async declare(store) {
|
|
60149
|
+
await store.declareCollection.mutate({
|
|
60150
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60151
|
+
columns: [...DEBUG_NOTE_ARCHIVE_COLUMNS],
|
|
60152
|
+
indexes: [...DEBUG_NOTE_ARCHIVE_INDEXES]
|
|
60153
|
+
});
|
|
60154
|
+
}
|
|
60155
|
+
/**
|
|
60156
|
+
* Append one retired note. THROWS if the row cannot be written.
|
|
60157
|
+
*
|
|
60158
|
+
* Deliberately not best-effort, unlike the ops-log's `append`: this is the
|
|
60159
|
+
* only copy of something a person typed, and the caller
|
|
60160
|
+
* (`archiveNoteBeforeClear`) has both the text and the camera and is the
|
|
60161
|
+
* right place to decide what a failure costs — it logs the note verbatim and
|
|
60162
|
+
* lets the operator's flag write proceed. Swallowing here would hide the
|
|
60163
|
+
* failure from the one caller able to preserve the words.
|
|
60164
|
+
*
|
|
60165
|
+
* The TRIM is the exception and is best-effort: the row has already landed,
|
|
60166
|
+
* and failing the append because housekeeping could not run would make the
|
|
60167
|
+
* caller report a note lost that is sitting in the table.
|
|
60168
|
+
*/
|
|
60169
|
+
async append(input) {
|
|
60170
|
+
const note = input.note.trim() === "" ? "" : input.note;
|
|
60171
|
+
if (note === "") return {
|
|
60172
|
+
archived: false,
|
|
60173
|
+
id: null
|
|
60174
|
+
};
|
|
60175
|
+
const id = archiveRowId(input.sourceTrackId, note);
|
|
60176
|
+
if ((await this.store.query.query({
|
|
60177
|
+
collection: "pipeline-analytics:debug-note-archive",
|
|
60178
|
+
filter: {
|
|
60179
|
+
where: { id },
|
|
60180
|
+
limit: 1
|
|
60181
|
+
}
|
|
60182
|
+
})).length > 0) return {
|
|
60183
|
+
archived: false,
|
|
60184
|
+
id
|
|
60185
|
+
};
|
|
60186
|
+
const row = require_dist.ArchivedDebugNoteSchema.parse({
|
|
60187
|
+
id,
|
|
60188
|
+
note,
|
|
60189
|
+
deviceId: input.deviceId,
|
|
60190
|
+
sourceTrackId: input.sourceTrackId,
|
|
60191
|
+
archivedAt: this.now(),
|
|
60192
|
+
trackStartedAt: input.trackStartedAt ?? null,
|
|
60193
|
+
trackClass: input.trackClass ?? null,
|
|
60194
|
+
trackLabel: input.trackLabel ?? null
|
|
60195
|
+
});
|
|
60196
|
+
await this.store.insert.mutate({
|
|
60197
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60198
|
+
record: {
|
|
60199
|
+
id: row.id,
|
|
60200
|
+
data: {
|
|
60201
|
+
note: row.note,
|
|
60202
|
+
deviceId: row.deviceId,
|
|
60203
|
+
sourceTrackId: row.sourceTrackId,
|
|
60204
|
+
archivedAt: row.archivedAt,
|
|
60205
|
+
trackStartedAt: row.trackStartedAt,
|
|
60206
|
+
trackClass: row.trackClass,
|
|
60207
|
+
trackLabel: row.trackLabel
|
|
60208
|
+
}
|
|
60209
|
+
}
|
|
60210
|
+
});
|
|
60211
|
+
await this.trim();
|
|
60212
|
+
return {
|
|
60213
|
+
archived: true,
|
|
60214
|
+
id: row.id
|
|
60215
|
+
};
|
|
60216
|
+
}
|
|
60217
|
+
/**
|
|
60218
|
+
* Hold the table at its ceiling, oldest first. Best-effort by design — see
|
|
60219
|
+
* {@link append}.
|
|
60220
|
+
*
|
|
60221
|
+
* NOT an age-keyed sweep and must never become one: the boundary below is
|
|
60222
|
+
* computed from the POSITION of the excess rows, never from a clock, so a
|
|
60223
|
+
* quiet fleet keeps its whole corpus for ever and a busy one drops its oldest
|
|
60224
|
+
* questions. Rows sharing the boundary timestamp go together; at a
|
|
60225
|
+
* one-per-review write rate that is a rounding error, and the alternative —
|
|
60226
|
+
* deleting by id, one round trip per row — is the N+1 drain this store layer
|
|
60227
|
+
* spent a release removing.
|
|
60228
|
+
*/
|
|
60229
|
+
async trim() {
|
|
60230
|
+
try {
|
|
60231
|
+
const held = await this.store.count.query({ collection: DEBUG_NOTE_ARCHIVE_COLLECTION });
|
|
60232
|
+
const excess = held - this.maxRows;
|
|
60233
|
+
if (excess <= 0) return;
|
|
60234
|
+
const boundary = (await this.store.query.query({
|
|
60235
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60236
|
+
filter: {
|
|
60237
|
+
orderBy: {
|
|
60238
|
+
field: "archivedAt",
|
|
60239
|
+
direction: "asc"
|
|
60240
|
+
},
|
|
60241
|
+
limit: excess
|
|
60242
|
+
},
|
|
60243
|
+
columns: ["archivedAt"]
|
|
60244
|
+
})).reduce((acc, r) => {
|
|
60245
|
+
const at = r.data["archivedAt"];
|
|
60246
|
+
return typeof at === "number" && at > acc ? at : acc;
|
|
60247
|
+
}, 0);
|
|
60248
|
+
if (boundary === 0) return;
|
|
60249
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
60250
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60251
|
+
filter: { whereBetween: { archivedAt: [0, boundary] } }
|
|
60252
|
+
});
|
|
60253
|
+
this.logger.info("debug-note archive trimmed to its ceiling", { meta: {
|
|
60254
|
+
held,
|
|
60255
|
+
ceiling: this.maxRows,
|
|
60256
|
+
deleted
|
|
60257
|
+
} });
|
|
60258
|
+
} catch (err) {
|
|
60259
|
+
this.logger.warn("debug-note archive trim failed — the archive will keep growing", { meta: {
|
|
60260
|
+
ceiling: this.maxRows,
|
|
60261
|
+
error: err instanceof Error ? err.message : String(err)
|
|
60262
|
+
} });
|
|
60263
|
+
}
|
|
60264
|
+
}
|
|
60265
|
+
/** Archived notes, newest first, optionally scoped to one camera. */
|
|
60266
|
+
async list(query) {
|
|
60267
|
+
const filter = {
|
|
60268
|
+
orderBy: {
|
|
60269
|
+
field: "archivedAt",
|
|
60270
|
+
direction: "desc"
|
|
60271
|
+
},
|
|
60272
|
+
limit: query.limit ?? 200
|
|
60273
|
+
};
|
|
60274
|
+
if (query.deviceId !== void 0) filter["where"] = { deviceId: query.deviceId };
|
|
60275
|
+
const rows = await this.store.query.query({
|
|
60276
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
60277
|
+
filter
|
|
60278
|
+
});
|
|
60279
|
+
const out = [];
|
|
60280
|
+
for (const r of rows) {
|
|
60281
|
+
const parsed = require_dist.ArchivedDebugNoteSchema.safeParse({
|
|
60282
|
+
id: r.id,
|
|
60283
|
+
...r.data
|
|
60284
|
+
});
|
|
60285
|
+
if (parsed.success) out.push(parsed.data);
|
|
60286
|
+
else this.logger.debug("debug-note archive: skipped a malformed row", { meta: { id: r.id } });
|
|
60287
|
+
}
|
|
60288
|
+
return out;
|
|
60289
|
+
}
|
|
60290
|
+
};
|
|
60291
|
+
//#endregion
|
|
59570
60292
|
//#region src/pipeline-analytics/store/ops-log-store.ts
|
|
59571
60293
|
/**
|
|
59572
60294
|
* OpsLogStore — the EVENTS-domain operations audit for pipeline-analytics.
|
|
@@ -60930,6 +61652,13 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
60930
61652
|
why: "the drained predecessor of the row above, declared only so its rows can be read once and moved (`viewer-settings-legacy-adoption.ts`). Retention must never touch it: its rows go one way only, to the successor, gated on a re-read that confirms they landed. Delete this entry together with that module."
|
|
60931
61653
|
}
|
|
60932
61654
|
},
|
|
61655
|
+
{
|
|
61656
|
+
collection: DEBUG_NOTE_ARCHIVE_COLLECTION,
|
|
61657
|
+
classification: {
|
|
61658
|
+
kind: "never-orphaned",
|
|
61659
|
+
why: "the archived operator notes (D405) — the ONE table here whose whole purpose is to outlive the track it names. `debug` never pinned a track against retention and D405 does not change that, so the track a note was written on IS expected to be evicted; deleting the note with it is the failure the archive was built to fix (25–27 notes from one sweep unreadable the same afternoon). Its column is called `sourceTrackId` and not `trackId` precisely so the ownership derivation cannot reach it, and its bound is a fleet-wide ROW COUNT trimmed on append — never an age sweep, which would delete the corpus for the one property that makes it valuable."
|
|
61660
|
+
}
|
|
61661
|
+
},
|
|
60933
61662
|
{
|
|
60934
61663
|
collection: STATIONARY_COLLECTION,
|
|
60935
61664
|
classification: {
|
|
@@ -68700,6 +69429,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68700
69429
|
eventStore = null;
|
|
68701
69430
|
/** Events-domain ops-log (footprint/prune/manual-delete audit). Null until onInitialize. */
|
|
68702
69431
|
eventsOpsLog = null;
|
|
69432
|
+
/**
|
|
69433
|
+
* The archived operator debug notes (D405). Null until onInitialize — and
|
|
69434
|
+
* `applyTrackFlags` treats null as "no archive wired": the flag write the
|
|
69435
|
+
* operator asked for never depends on this addon's own bookkeeping.
|
|
69436
|
+
*/
|
|
69437
|
+
debugNoteArchive = null;
|
|
68703
69438
|
/** Event-media relocation engine (entity-routing Phase 4). */
|
|
68704
69439
|
mediaRelocate = null;
|
|
68705
69440
|
mediaLocationStorage = null;
|
|
@@ -69257,6 +69992,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69257
69992
|
* which would let the streetlight wedge vouch for its own delivery.
|
|
69258
69993
|
*/
|
|
69259
69994
|
lastNonPackageActivityAt = /* @__PURE__ */ new Map();
|
|
69995
|
+
/**
|
|
69996
|
+
* WHO was in the drop-off zone, and when (D404). The pick-up is CONCLUDED
|
|
69997
|
+
* minutes after the fact, so the collector must be remembered while it is
|
|
69998
|
+
* still true — handles only, never pixels.
|
|
69999
|
+
*/
|
|
70000
|
+
packageCollectors = new PackageCollectorMirror();
|
|
69260
70001
|
/** Last time the oversize-sighting drop was logged per device — one line
|
|
69261
70002
|
* per 5 minutes, not one per governor round (the wedge re-sights all
|
|
69262
70003
|
* night; ~300 identical lines would bury the log it exists to serve). */
|
|
@@ -69682,6 +70423,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69682
70423
|
logger: logger.child("ops-log"),
|
|
69683
70424
|
nodeId: ownNodeId
|
|
69684
70425
|
});
|
|
70426
|
+
this.debugNoteArchive = new DebugNoteArchiveStore({
|
|
70427
|
+
store: api.settingsStore,
|
|
70428
|
+
logger: logger.child("debug-note-archive")
|
|
70429
|
+
});
|
|
69685
70430
|
{
|
|
69686
70431
|
const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
|
|
69687
70432
|
this.isPostProcessingNode = ownNodeId === designated;
|
|
@@ -70001,6 +70746,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70001
70746
|
RetrainFrameStore.declare(api.settingsStore),
|
|
70002
70747
|
RetrainAnnotationStore.declare(api.settingsStore),
|
|
70003
70748
|
OpsLogStore.declare(api.settingsStore),
|
|
70749
|
+
DebugNoteArchiveStore.declare(api.settingsStore),
|
|
70004
70750
|
AnalyticsLts.declare(api.settingsStore),
|
|
70005
70751
|
SceneStore.declare(api.settingsStore),
|
|
70006
70752
|
NotificationCenter.declare(api.settingsStore),
|
|
@@ -70472,18 +71218,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70472
71218
|
},
|
|
70473
71219
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
70474
71220
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
|
|
70475
|
-
|
|
70476
|
-
deviceId:
|
|
70477
|
-
trackId:
|
|
70478
|
-
timestamp:
|
|
70479
|
-
bbox:
|
|
70480
|
-
frameWidth:
|
|
70481
|
-
frameHeight:
|
|
70482
|
-
kind: "face",
|
|
70483
|
-
score:
|
|
70484
|
-
confidence:
|
|
70485
|
-
...
|
|
70486
|
-
}),
|
|
71221
|
+
onSemanticFace: (face) => this.promoteSemanticRecognition({
|
|
71222
|
+
deviceId: face.deviceId,
|
|
71223
|
+
trackId: face.trackId,
|
|
71224
|
+
timestamp: face.timestamp,
|
|
71225
|
+
bbox: face.bbox,
|
|
71226
|
+
frameWidth: face.frameWidth,
|
|
71227
|
+
frameHeight: face.frameHeight,
|
|
71228
|
+
kind: face.identityId !== void 0 ? "face" : "face-visible",
|
|
71229
|
+
score: face.matchScore ?? face.confidence,
|
|
71230
|
+
confidence: face.confidence,
|
|
71231
|
+
...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
|
|
71232
|
+
}, face.identityId !== void 0 ? "identified" : "evidence"),
|
|
70487
71233
|
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
70488
71234
|
logger: logger.child("FaceRecognizer")
|
|
70489
71235
|
});
|
|
@@ -71639,6 +72385,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71639
72385
|
this.packageStillness.forgetDevice(data.deviceId);
|
|
71640
72386
|
this.packageDropDetector?.forgetDevice(data.deviceId);
|
|
71641
72387
|
this.lastNonPackageActivityAt.delete(data.deviceId);
|
|
72388
|
+
this.packageCollectors.forgetDevice(data.deviceId);
|
|
71642
72389
|
this.packageOversizeLogAt.delete(data.deviceId);
|
|
71643
72390
|
this.overlayState.clearDevice(data.deviceId);
|
|
71644
72391
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
@@ -71658,6 +72405,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
71658
72405
|
this.packageStillness.forgetDevice(deviceId);
|
|
71659
72406
|
this.packageDropDetector?.forgetDevice(deviceId);
|
|
71660
72407
|
this.lastNonPackageActivityAt.delete(deviceId);
|
|
72408
|
+
this.packageCollectors.forgetDevice(deviceId);
|
|
71661
72409
|
this.packageOversizeLogAt.delete(deviceId);
|
|
71662
72410
|
this.overlayState.clearDevice(deviceId);
|
|
71663
72411
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
@@ -72541,12 +73289,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
72541
73289
|
this.residents.markFirstFramePending(deviceId, id);
|
|
72542
73290
|
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
72543
73291
|
this.residents.setLastFrameAt(deviceId, id, result.timestamp);
|
|
72544
|
-
|
|
73292
|
+
const birthCapture = decideBirthFirstFrameCapture({
|
|
73293
|
+
hasFrameHandle: frameHandle !== void 0,
|
|
73294
|
+
matchedThisFrame: t.matchedThisFrame
|
|
73295
|
+
});
|
|
73296
|
+
if (birthCapture.capture) firstFrameTargets.push({
|
|
72545
73297
|
trackId: id,
|
|
72546
73298
|
timestamp: result.timestamp,
|
|
72547
73299
|
bbox: { ...t.bbox },
|
|
72548
73300
|
...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {}
|
|
72549
73301
|
});
|
|
73302
|
+
else log.info("birth firstFrame not captured — retry armed", {
|
|
73303
|
+
tags: { deviceId },
|
|
73304
|
+
meta: {
|
|
73305
|
+
trackId: id,
|
|
73306
|
+
className: t.className,
|
|
73307
|
+
source,
|
|
73308
|
+
refusedBecause: birthCapture.refusedBecause,
|
|
73309
|
+
deferredMs: result.timestamp - (this.trackStore?.peekActive(id)?.firstSeen ?? result.timestamp)
|
|
73310
|
+
}
|
|
73311
|
+
});
|
|
72550
73312
|
}
|
|
72551
73313
|
this.ctx.eventBus.emit({
|
|
72552
73314
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
@@ -73483,10 +74245,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73483
74245
|
} } : {}
|
|
73484
74246
|
});
|
|
73485
74247
|
}
|
|
74248
|
+
if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
|
|
73486
74249
|
}
|
|
73487
74250
|
const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
|
|
73488
74251
|
if (resolved !== null && resolved.text !== void 0) {
|
|
73489
|
-
|
|
74252
|
+
const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
|
|
74253
|
+
if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
|
|
73490
74254
|
}
|
|
73491
74255
|
} else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
|
|
73492
74256
|
} catch (err) {
|
|
@@ -73927,8 +74691,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
73927
74691
|
* when it wins and the frame's pixels are still reachable. Callers: the
|
|
73928
74692
|
* FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
|
|
73929
74693
|
* fine-classification attribution. */
|
|
73930
|
-
promoteSemanticRecognition(input) {
|
|
73931
|
-
this.recognizedTrackIds.add(input.trackId);
|
|
74694
|
+
promoteSemanticRecognition(input, origin) {
|
|
74695
|
+
if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
|
|
73932
74696
|
const ctx = this.ctxIfReady;
|
|
73933
74697
|
if (ctx === null) return;
|
|
73934
74698
|
promoteSemanticBestFrame({
|
|
@@ -75256,6 +76020,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75256
76020
|
this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
|
|
75257
76021
|
break;
|
|
75258
76022
|
}
|
|
76023
|
+
this.packageCollectors.noteFrame({
|
|
76024
|
+
deviceId: input.deviceId,
|
|
76025
|
+
tracked: input.tracked,
|
|
76026
|
+
packageClasses: input.packageClasses,
|
|
76027
|
+
packageZoneIds: input.packageZoneIds,
|
|
76028
|
+
timestamp: input.timestamp,
|
|
76029
|
+
keyFrameMediaKeyFor: (trackId) => this.residents.keyFrameKey(trackId)
|
|
76030
|
+
});
|
|
75259
76031
|
const sightings = [];
|
|
75260
76032
|
for (const t of input.tracked) {
|
|
75261
76033
|
if (!input.packageClasses.has(t.className)) continue;
|
|
@@ -75367,10 +76139,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75367
76139
|
meta: { entryId: entry.id }
|
|
75368
76140
|
});
|
|
75369
76141
|
}).catch(() => void 0);
|
|
76142
|
+
const collector = this.packageCollectors.pickCollector({
|
|
76143
|
+
deviceId: input.deviceId,
|
|
76144
|
+
pickupAt: pickupInstantOf(entry)
|
|
76145
|
+
});
|
|
76146
|
+
if (collector === null) this.ctx.logger.info("package pick-up has no collector in the drop-off zone", {
|
|
76147
|
+
tags,
|
|
76148
|
+
meta: {
|
|
76149
|
+
entryId: entry.id,
|
|
76150
|
+
pickupAt: entry.lastConfirmedAt,
|
|
76151
|
+
departedAt: input.timestamp,
|
|
76152
|
+
presencesKnown: this.packageCollectors.debugPresenceCount(input.deviceId)
|
|
76153
|
+
}
|
|
76154
|
+
});
|
|
75370
76155
|
this.packageDropDetector.onStationaryChange({
|
|
75371
76156
|
phase: "departed",
|
|
75372
76157
|
entry,
|
|
75373
|
-
timestamp: input.timestamp
|
|
76158
|
+
timestamp: input.timestamp,
|
|
76159
|
+
...collector !== null ? { collector } : {}
|
|
75374
76160
|
});
|
|
75375
76161
|
}
|
|
75376
76162
|
for (const entry of outcome.departedWithoutActivity) {
|
|
@@ -77495,7 +78281,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77495
78281
|
setFlags: (trackId, patch) => trackStore.setFlags(trackId, patch),
|
|
77496
78282
|
countStaging: (query) => trackStore.countStaging(query)
|
|
77497
78283
|
},
|
|
77498
|
-
logger: this.ctx.logger
|
|
78284
|
+
logger: this.ctx.logger,
|
|
78285
|
+
...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
|
|
77499
78286
|
}, input);
|
|
77500
78287
|
this.ctx.logger.info("track operator flags set", {
|
|
77501
78288
|
tags: { deviceId: input.deviceId },
|
|
@@ -77626,6 +78413,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
77626
78413
|
return this.queryFacade.listOpsLog(input);
|
|
77627
78414
|
}
|
|
77628
78415
|
/**
|
|
78416
|
+
* The archived debug notes, newest first (D405).
|
|
78417
|
+
*
|
|
78418
|
+
* Answers from the archive alone — never from the track rows. The point of
|
|
78419
|
+
* the table is that the tracks these notes were written on are gone, so a
|
|
78420
|
+
* read that joined them would return exactly the rows the archive exists to
|
|
78421
|
+
* replace. An archive that has not been built yet answers EMPTY, which is
|
|
78422
|
+
* true: nothing has been archived on this runner.
|
|
78423
|
+
*/
|
|
78424
|
+
async listArchivedDebugNotes(input) {
|
|
78425
|
+
return await this.debugNoteArchive?.list(input) ?? [];
|
|
78426
|
+
}
|
|
78427
|
+
/**
|
|
77629
78428
|
* Track-centric time-based retention (design §5.1). Drains every persisted
|
|
77630
78429
|
* track for the device whose `lastSeen < cutoffMs`, page by page, through the
|
|
77631
78430
|
* widened cascade — enrolled faces/plates + identity media are exempt (design
|