@camstack/addon-post-analysis 1.2.5 → 1.2.6
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-Dlb4YrCt.js → dist-BvEJmXbR.js} +37 -2
- package/dist/{dist-Db0CsDGK.mjs → dist-DGkWhfba.mjs} +37 -2
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DWX3J7jh.js → node-CgyUW8_U.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ACzsl3tP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BNg3APbG.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BdlfdPP1.mjs → hostInit-CZvb3wMv.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +629 -106
- package/dist/pipeline-analytics/index.mjs +628 -105
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-DGkWhfba.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -2086,6 +2086,7 @@ var FrameProcessor = class {
|
|
|
2086
2086
|
const firstLevelBboxById = /* @__PURE__ */ new Map();
|
|
2087
2087
|
const sourceIdByBbox = /* @__PURE__ */ new Map();
|
|
2088
2088
|
const faceBboxByBbox = /* @__PURE__ */ new Map();
|
|
2089
|
+
const nativeFaceSizeByBbox = /* @__PURE__ */ new Map();
|
|
2089
2090
|
const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
|
|
2090
2091
|
const plateByBbox = /* @__PURE__ */ new Map();
|
|
2091
2092
|
const maskByBbox = /* @__PURE__ */ new Map();
|
|
@@ -2131,6 +2132,7 @@ var FrameProcessor = class {
|
|
|
2131
2132
|
h: det.bbox.height
|
|
2132
2133
|
});
|
|
2133
2134
|
if (det.faceAlignedCrop !== void 0) faceAlignedCropByBbox.set(parentBbox, det.faceAlignedCrop);
|
|
2135
|
+
if (det.nativeFaceShortSidePx !== void 0) nativeFaceSizeByBbox.set(parentBbox, det.nativeFaceShortSidePx);
|
|
2134
2136
|
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
2135
2137
|
embedding: det.embedding,
|
|
2136
2138
|
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
@@ -2186,6 +2188,7 @@ var FrameProcessor = class {
|
|
|
2186
2188
|
});
|
|
2187
2189
|
const emb = embeddingByBbox.get(td.bbox);
|
|
2188
2190
|
const faceBbox = faceBboxByBbox.get(td.bbox);
|
|
2191
|
+
const nativeFaceShortSidePx = nativeFaceSizeByBbox.get(td.bbox);
|
|
2189
2192
|
const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
|
|
2190
2193
|
const plate = plateByBbox.get(td.bbox);
|
|
2191
2194
|
const sourceDetectionId = sourceIdByBbox.get(td.bbox);
|
|
@@ -2204,6 +2207,7 @@ var FrameProcessor = class {
|
|
|
2204
2207
|
...emb.embeddingModelId !== void 0 ? { embeddingModelId: emb.embeddingModelId } : {}
|
|
2205
2208
|
} : {},
|
|
2206
2209
|
...faceBbox !== void 0 ? { faceBbox } : {},
|
|
2210
|
+
...nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx } : {},
|
|
2207
2211
|
...faceAlignedCrop !== void 0 ? { faceAlignedCrop } : {},
|
|
2208
2212
|
...plate !== void 0 ? {
|
|
2209
2213
|
plateText: plate.text,
|
|
@@ -2340,6 +2344,10 @@ function buildTrackLifecyclePayload(input) {
|
|
|
2340
2344
|
...hasMedia ? { media } : {}
|
|
2341
2345
|
};
|
|
2342
2346
|
}
|
|
2347
|
+
//#endregion
|
|
2348
|
+
//#region src/pipeline-analytics/pipeline/edge-clear.ts
|
|
2349
|
+
/** Default border tolerance — 1% of each dimension. */
|
|
2350
|
+
var DEFAULT_EDGE_TOLERANCE = .01;
|
|
2343
2351
|
/**
|
|
2344
2352
|
* True when `bbox` sits fully inside the frame — no side within the tolerance
|
|
2345
2353
|
* band of any border. Degenerate/unknown frame dims (≤ 0) return true so the
|
|
@@ -2385,6 +2393,104 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2385
2393
|
const dist = Math.hypot(dx, dy) / Math.SQRT2;
|
|
2386
2394
|
return Math.max(0, Math.min(1, 1 - dist));
|
|
2387
2395
|
}
|
|
2396
|
+
/** Maximum fraction of a candidate's score a SINGLE fully-spanning clipped side
|
|
2397
|
+
* can subtract (the edge-containment penalty). A clipped side that spans the
|
|
2398
|
+
* whole of its border removes up to this much; a side that only grazes the
|
|
2399
|
+
* border (small perpendicular span) removes proportionally less. 0.5 = a
|
|
2400
|
+
* subject flush against — and filling — one border loses half its effective
|
|
2401
|
+
* score, so a fully-contained near-equal detection outranks it, while a
|
|
2402
|
+
* DECISIVELY higher-confidence clipped detection still wins. */
|
|
2403
|
+
var CONTAINMENT_MAX_SIDE_PENALTY = .5;
|
|
2404
|
+
/** Floor on the containment factor — a subject clipped on several borders never
|
|
2405
|
+
* drops to zero (which would annihilate its score and could leave a track with
|
|
2406
|
+
* NO best frame at all). */
|
|
2407
|
+
var CONTAINMENT_MIN = .1;
|
|
2408
|
+
/**
|
|
2409
|
+
* Edge-CONTAINMENT factor for a (clamped) bbox: an estimate in
|
|
2410
|
+
* `[CONTAINMENT_MIN, 1]` of the fraction of the subject that is actually in
|
|
2411
|
+
* frame. 1 = fully contained (no border clipping); lower = the subject is
|
|
2412
|
+
* flush against one or more borders and is probably truncated (half of a person
|
|
2413
|
+
* walking out of view).
|
|
2414
|
+
*
|
|
2415
|
+
* Because detection bboxes are already CLAMPED to the frame, the true off-screen
|
|
2416
|
+
* extent is unknown, so this is a purely-geometric proxy: for each border the
|
|
2417
|
+
* bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band), the penalty is
|
|
2418
|
+
* scaled by how much of that border the bbox spans on the perpendicular axis —
|
|
2419
|
+
* i.e. the subject's aspect against that edge. A wide bbox flush to the
|
|
2420
|
+
* top/bottom (a wide subject cut off top/bottom) or a tall bbox flush to the
|
|
2421
|
+
* left/right (a tall subject cut off at the side) is heavily penalised, whereas
|
|
2422
|
+
* a narrow subject whose feet merely graze the bottom border keeps most of its
|
|
2423
|
+
* score. Degenerate/unknown dims (≤ 0) return 1 (neutral — matches
|
|
2424
|
+
* {@link isEdgeClear}).
|
|
2425
|
+
*/
|
|
2426
|
+
function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE_TOLERANCE) {
|
|
2427
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2428
|
+
const tolX = tolerance * frameWidth;
|
|
2429
|
+
const tolY = tolerance * frameHeight;
|
|
2430
|
+
const left = bbox.x;
|
|
2431
|
+
const top = bbox.y;
|
|
2432
|
+
const right = bbox.x + bbox.w;
|
|
2433
|
+
const bottom = bbox.y + bbox.h;
|
|
2434
|
+
const spanX = Math.max(0, Math.min(1, bbox.w / frameWidth));
|
|
2435
|
+
const spanY = Math.max(0, Math.min(1, bbox.h / frameHeight));
|
|
2436
|
+
let containment = 1;
|
|
2437
|
+
const penalize = (span) => {
|
|
2438
|
+
containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * span;
|
|
2439
|
+
};
|
|
2440
|
+
if (left <= tolX) penalize(spanY);
|
|
2441
|
+
if (right >= frameWidth - tolX) penalize(spanY);
|
|
2442
|
+
if (top <= tolY) penalize(spanX);
|
|
2443
|
+
if (bottom >= frameHeight - tolY) penalize(spanX);
|
|
2444
|
+
return Math.max(CONTAINMENT_MIN, Math.min(1, containment));
|
|
2445
|
+
}
|
|
2446
|
+
/** Fraction of the frame area at which a subject's size score saturates to 1.
|
|
2447
|
+
* 10% of the frame is already a large, close subject; anything bigger gains
|
|
2448
|
+
* no further preference (and the plausibility gate rejects exploded boxes). */
|
|
2449
|
+
var SIZE_SCORE_SATURATION_AREA_FRAC = .1;
|
|
2450
|
+
/** A candidate whose effective score beats the held peak by at least this
|
|
2451
|
+
* RATIO is a DECISIVE improvement: it bypasses the capture rate limit
|
|
2452
|
+
* (`minGapMs`) the way a tier upgrade does. Drive-through subjects hit their
|
|
2453
|
+
* best framing 1–3s after birth — exactly inside the rate-limit window — and
|
|
2454
|
+
* the "better shot" otherwise never gets captured (2026-07-22 audit:
|
|
2455
|
+
* BEST_SHOT_STALE on 9/63 tracks with 1.5–3.8× larger in-frame views). */
|
|
2456
|
+
var DECISIVE_IMPROVEMENT_RATIO = 1.25;
|
|
2457
|
+
/**
|
|
2458
|
+
* Subject-size score for the best-frame ranking: sqrt of the bbox's frame-area
|
|
2459
|
+
* fraction, saturating at {@link SIZE_SCORE_SATURATION_AREA_FRAC}. A bigger,
|
|
2460
|
+
* closer subject is a better SHOT even at equal detector confidence — raw
|
|
2461
|
+
* confidence does not correlate with human-perceived crop quality (a distant
|
|
2462
|
+
* 20px person can out-score a full-frame close-up). sqrt softens the term so
|
|
2463
|
+
* confidence still matters between similar sizes. Degenerate dims → 1
|
|
2464
|
+
* (neutral), matching {@link isEdgeClear}.
|
|
2465
|
+
*/
|
|
2466
|
+
function bboxSizeScore(bbox, frameWidth, frameHeight) {
|
|
2467
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2468
|
+
const areaFrac = bbox.w * bbox.h / (frameWidth * frameHeight);
|
|
2469
|
+
return Math.sqrt(Math.max(0, Math.min(1, areaFrac / SIZE_SCORE_SATURATION_AREA_FRAC)));
|
|
2470
|
+
}
|
|
2471
|
+
/** Floor of the size factor's influence: the raw sizeScore (0..1) is mapped to
|
|
2472
|
+
* `[SIZE_FACTOR_FLOOR, 1]` before scaling the confidence, so a tiny subject
|
|
2473
|
+
* halves its effective score at most. Keeps the effective scale comparable to
|
|
2474
|
+
* raw confidence — the ABSOLUTE hysteresis margin stays meaningful — while a
|
|
2475
|
+
* bigger view still earns up to a 2× relative preference. */
|
|
2476
|
+
var SIZE_FACTOR_FLOOR = .5;
|
|
2477
|
+
/** The effective within-tier score: detector confidence scaled by the
|
|
2478
|
+
* edge-containment and (floored) subject-size factors — each neutral at 1
|
|
2479
|
+
* when absent. */
|
|
2480
|
+
function effectiveScore(c) {
|
|
2481
|
+
const sizeFactor = c.sizeScore === void 0 ? 1 : SIZE_FACTOR_FLOOR + (1 - SIZE_FACTOR_FLOOR) * c.sizeScore;
|
|
2482
|
+
return c.confidence * (c.containment ?? 1) * sizeFactor;
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* True when `candidate` beats `current` DECISIVELY — same-or-better edge tier
|
|
2486
|
+
* AND an effective score at least {@link DECISIVE_IMPROVEMENT_RATIO}× the held
|
|
2487
|
+
* peak's. Used by `BestDetectionTracker` to bypass the capture rate limit
|
|
2488
|
+
* (`minGapMs`) for genuinely better framings that land inside the rate window.
|
|
2489
|
+
*/
|
|
2490
|
+
function isDecisiveImprovement(current, candidate) {
|
|
2491
|
+
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2492
|
+
return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
|
|
2493
|
+
}
|
|
2388
2494
|
/**
|
|
2389
2495
|
* Edge-aware "is `candidate` a new best over `current`?" comparator.
|
|
2390
2496
|
*
|
|
@@ -2393,8 +2499,18 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2393
2499
|
* confidence past the `hysteresis` margin wins. The tier upgrade
|
|
2394
2500
|
* (touching → clear) bypasses hysteresis — the first clear frame is always taken.
|
|
2395
2501
|
*
|
|
2502
|
+
* EDGE-CONTAINMENT (#edge-clip): within the same tier the comparison is on the
|
|
2503
|
+
* EFFECTIVE score `confidence × containment` (see {@link bboxContainment}), not
|
|
2504
|
+
* raw confidence — so a fully-contained detection outranks a larger/higher-
|
|
2505
|
+
* confidence but border-CLIPPED one unless the confidence margin is decisive.
|
|
2506
|
+
* The binary edge-clear tier still gates first (a whole subject beats a clipped
|
|
2507
|
+
* one regardless), and within the CLEAR tier both frames have containment 1, so
|
|
2508
|
+
* this is a no-op there; it discriminates WITHIN the touching tier (feet merely
|
|
2509
|
+
* grazing the bottom vs half the body out the side). `containment` defaults to
|
|
2510
|
+
* 1 when omitted (legacy face / object-embedding callers) → identical behaviour.
|
|
2511
|
+
*
|
|
2396
2512
|
* CENTERING TIE-BREAK (#27-D): within the same tier, when neither frame clearly
|
|
2397
|
-
* wins on
|
|
2513
|
+
* wins on the effective score (the two are within the `hysteresis` band) but the
|
|
2398
2514
|
* candidate is meaningfully better CENTRED ({@link CENTER_TIE_BREAK_MARGIN}), the
|
|
2399
2515
|
* candidate wins. This only engages when both sides carry a `centerScore` (the
|
|
2400
2516
|
* best-frame path), so low-importance short tracks stop keeping an edge-of-frame
|
|
@@ -2407,9 +2523,11 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2407
2523
|
function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
2408
2524
|
if (candidate.edgeClear && !current.edgeClear) return true;
|
|
2409
2525
|
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2410
|
-
|
|
2526
|
+
const curEffective = effectiveScore(current);
|
|
2527
|
+
const candEffective = effectiveScore(candidate);
|
|
2528
|
+
if (candEffective > curEffective + hysteresis) return true;
|
|
2411
2529
|
if (candidate.centerScore !== void 0 && current.centerScore !== void 0) {
|
|
2412
|
-
if (Math.abs(
|
|
2530
|
+
if (Math.abs(candEffective - curEffective) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
|
|
2413
2531
|
}
|
|
2414
2532
|
return false;
|
|
2415
2533
|
}
|
|
@@ -2451,6 +2569,14 @@ var BestDetectionTracker = class {
|
|
|
2451
2569
|
* does not supply centering (face / object-embedding paths) → the centering
|
|
2452
2570
|
* tie-break is disabled and the legacy confidence policy applies. */
|
|
2453
2571
|
centerScore = /* @__PURE__ */ new Map();
|
|
2572
|
+
/** Held peak's edge-containment factor (0..1), PARALLEL to `best`. Absent =
|
|
2573
|
+
* the caller does not supply containment (face / object-embedding paths) →
|
|
2574
|
+
* the within-tier comparison stays on raw confidence (containment treated as
|
|
2575
|
+
* 1). */
|
|
2576
|
+
containment = /* @__PURE__ */ new Map();
|
|
2577
|
+
/** Held peak's subject-size score (0..1), PARALLEL to `best`. Absent = the
|
|
2578
|
+
* caller does not supply size (face / object-embedding paths) → neutral. */
|
|
2579
|
+
sizeScore = /* @__PURE__ */ new Map();
|
|
2454
2580
|
constructor(options = {}) {
|
|
2455
2581
|
this.hysteresis = options.hysteresis ?? 0;
|
|
2456
2582
|
this.minGapMs = options.minGapMs ?? 0;
|
|
@@ -2467,7 +2593,7 @@ var BestDetectionTracker = class {
|
|
|
2467
2593
|
* the classic policy holds: a confidence past the `hysteresis` margin that also
|
|
2468
2594
|
* respects `minGapMs` wins. On acceptance the held peak advances.
|
|
2469
2595
|
*/
|
|
2470
|
-
observe(trackId, confidence, timestamp, edgeClear, centerScore) {
|
|
2596
|
+
observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore) {
|
|
2471
2597
|
const cur = this.best.get(trackId);
|
|
2472
2598
|
if (cur === void 0) {
|
|
2473
2599
|
this.best.set(trackId, {
|
|
@@ -2476,20 +2602,32 @@ var BestDetectionTracker = class {
|
|
|
2476
2602
|
});
|
|
2477
2603
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2478
2604
|
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2605
|
+
if (containment !== void 0) this.containment.set(trackId, containment);
|
|
2606
|
+
if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
|
|
2479
2607
|
return true;
|
|
2480
2608
|
}
|
|
2481
2609
|
const curClear = this.edgeClear.get(trackId) ?? true;
|
|
2482
2610
|
const candClear = edgeClear ?? true;
|
|
2483
2611
|
const curCenter = this.centerScore.get(trackId);
|
|
2484
|
-
const
|
|
2612
|
+
const curContainment = this.containment.get(trackId);
|
|
2613
|
+
const curSize = this.sizeScore.get(trackId);
|
|
2614
|
+
const held = {
|
|
2485
2615
|
confidence: cur.confidence,
|
|
2486
2616
|
edgeClear: curClear,
|
|
2487
|
-
...curCenter !== void 0 ? { centerScore: curCenter } : {}
|
|
2488
|
-
|
|
2617
|
+
...curCenter !== void 0 ? { centerScore: curCenter } : {},
|
|
2618
|
+
...curContainment !== void 0 ? { containment: curContainment } : {},
|
|
2619
|
+
...curSize !== void 0 ? { sizeScore: curSize } : {}
|
|
2620
|
+
};
|
|
2621
|
+
const cand = {
|
|
2489
2622
|
confidence,
|
|
2490
2623
|
edgeClear: candClear,
|
|
2491
|
-
...centerScore !== void 0 ? { centerScore } : {}
|
|
2492
|
-
|
|
2624
|
+
...centerScore !== void 0 ? { centerScore } : {},
|
|
2625
|
+
...containment !== void 0 ? { containment } : {},
|
|
2626
|
+
...sizeScore !== void 0 ? { sizeScore } : {}
|
|
2627
|
+
};
|
|
2628
|
+
const tierUpgrade = candClear && !curClear;
|
|
2629
|
+
const gapOk = timestamp - cur.atMs >= this.minGapMs || sizeScore !== void 0 && isDecisiveImprovement(held, cand);
|
|
2630
|
+
const isNewBest = tierUpgrade ? true : isEdgeAwareNewBest(held, cand, this.hysteresis) && gapOk;
|
|
2493
2631
|
if (isNewBest) {
|
|
2494
2632
|
this.best.set(trackId, {
|
|
2495
2633
|
confidence,
|
|
@@ -2497,6 +2635,8 @@ var BestDetectionTracker = class {
|
|
|
2497
2635
|
});
|
|
2498
2636
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2499
2637
|
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2638
|
+
if (containment !== void 0) this.containment.set(trackId, containment);
|
|
2639
|
+
if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
|
|
2500
2640
|
}
|
|
2501
2641
|
return isNewBest;
|
|
2502
2642
|
}
|
|
@@ -2509,14 +2649,44 @@ var BestDetectionTracker = class {
|
|
|
2509
2649
|
this.best.delete(trackId);
|
|
2510
2650
|
this.edgeClear.delete(trackId);
|
|
2511
2651
|
this.centerScore.delete(trackId);
|
|
2652
|
+
this.containment.delete(trackId);
|
|
2653
|
+
this.sizeScore.delete(trackId);
|
|
2512
2654
|
}
|
|
2513
2655
|
clear() {
|
|
2514
2656
|
this.best.clear();
|
|
2515
2657
|
this.edgeClear.clear();
|
|
2516
2658
|
this.centerScore.clear();
|
|
2659
|
+
this.containment.clear();
|
|
2660
|
+
this.sizeScore.clear();
|
|
2517
2661
|
}
|
|
2518
2662
|
};
|
|
2519
2663
|
//#endregion
|
|
2664
|
+
//#region src/pipeline-analytics/pipeline/last-frame-promotion.ts
|
|
2665
|
+
/**
|
|
2666
|
+
* Decide whether a closing track's newest `snapshot` should be promoted to be
|
|
2667
|
+
* its `lastFrame`. Pure — see the module header for the contract.
|
|
2668
|
+
*
|
|
2669
|
+
* Promote when there is at least one `snapshot` AND either there is no
|
|
2670
|
+
* `lastFrame` yet, or the newest snapshot is strictly newer than the held
|
|
2671
|
+
* `lastFrame`. Otherwise keep the current behaviour (no promotion).
|
|
2672
|
+
*/
|
|
2673
|
+
function decideLastFramePromotion(media) {
|
|
2674
|
+
let newestSnapshot;
|
|
2675
|
+
let lastFrame;
|
|
2676
|
+
for (const m of media) if (m.kind === "snapshot") {
|
|
2677
|
+
if (newestSnapshot === void 0 || m.timestamp > newestSnapshot.timestamp) newestSnapshot = m;
|
|
2678
|
+
} else if (m.kind === "lastFrame") {
|
|
2679
|
+
if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
|
|
2680
|
+
}
|
|
2681
|
+
if (newestSnapshot === void 0) return { promote: false };
|
|
2682
|
+
if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return { promote: false };
|
|
2683
|
+
return {
|
|
2684
|
+
promote: true,
|
|
2685
|
+
snapshotKey: newestSnapshot.key,
|
|
2686
|
+
snapshotTimestamp: newestSnapshot.timestamp
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
//#endregion
|
|
2520
2690
|
//#region src/pipeline-analytics/pipeline/track-best-detection.ts
|
|
2521
2691
|
/**
|
|
2522
2692
|
* `TrackBestSelector` — the ONE unified "best detection per track" primitive.
|
|
@@ -4681,6 +4851,31 @@ var MediaStore = class {
|
|
|
4681
4851
|
return newKey;
|
|
4682
4852
|
}
|
|
4683
4853
|
/**
|
|
4854
|
+
* Promote an existing `snapshot` blob to be the track's single `lastFrame`
|
|
4855
|
+
* (operator "genuinely-last view" policy, see `last-frame-promotion.ts`).
|
|
4856
|
+
*
|
|
4857
|
+
* Both kinds share the SAME 960-boxed rendering, so promotion re-writes the
|
|
4858
|
+
* snapshot's bytes into the single-instance `lastFrame` slot (`putReplacing`,
|
|
4859
|
+
* which overwrites any held `lastFrame` blob + row) and then removes the
|
|
4860
|
+
* promoted `snapshot` row + blob — so there is exactly ONE genuinely-last view
|
|
4861
|
+
* and no duplicate snapshot/lastFrame pair. The `lastFrame` write lands BEFORE
|
|
4862
|
+
* the snapshot delete, so the view is never momentarily absent. Returns the new
|
|
4863
|
+
* `lastFrame` key.
|
|
4864
|
+
*/
|
|
4865
|
+
async promoteToLastFrame(input) {
|
|
4866
|
+
const data = Buffer.from(input.snapshot.base64, "base64");
|
|
4867
|
+
const newKey = await this.putReplacing({
|
|
4868
|
+
deviceId: input.deviceId,
|
|
4869
|
+
ownerKind: "track",
|
|
4870
|
+
ownerId: input.trackId,
|
|
4871
|
+
kind: "lastFrame",
|
|
4872
|
+
timestamp: input.snapshot.timestamp,
|
|
4873
|
+
data
|
|
4874
|
+
});
|
|
4875
|
+
await this.deleteByKey(input.snapshot.key);
|
|
4876
|
+
return newKey;
|
|
4877
|
+
}
|
|
4878
|
+
/**
|
|
4684
4879
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
4685
4880
|
* found in the index or if the blob is missing from storage.
|
|
4686
4881
|
*/
|
|
@@ -6558,37 +6753,121 @@ function squareSubjectCropRegion(bbox, frame) {
|
|
|
6558
6753
|
};
|
|
6559
6754
|
}
|
|
6560
6755
|
/**
|
|
6561
|
-
*
|
|
6562
|
-
*
|
|
6563
|
-
*
|
|
6564
|
-
*
|
|
6565
|
-
*
|
|
6566
|
-
*
|
|
6756
|
+
* Compute the 16:9 central-square window layout for a subject bbox.
|
|
6757
|
+
*
|
|
6758
|
+
* Algorithm:
|
|
6759
|
+
* 1. central square `C = squareSubjectCropRegion(bbox, frame)` — the
|
|
6760
|
+
* subject-containing, frame-clamped square of side `c` (its contract also
|
|
6761
|
+
* handles the "subject bigger than the short edge / scale the window up"
|
|
6762
|
+
* cases). `C` becomes the middle square of the output.
|
|
6763
|
+
* 2. canvas: `canvasW = round(c × 16/9)`, `canvasH = c`.
|
|
6764
|
+
* 3. anchor: place the canvas so `C` is its horizontal middle →
|
|
6765
|
+
* `frameOriginX = C.x − (canvasW − c)/2` (frame-x mapping to canvas-x 0).
|
|
6766
|
+
* 4. in-frame slab: intersect `[frameOriginX, frameOriginX + canvasW]` with
|
|
6767
|
+
* `[0, W]`; the slab is that intersection at full height `c` (C ⊆ frame ⇒
|
|
6768
|
+
* the vertical extent is always in-frame). Its canvas offset is
|
|
6769
|
+
* `slabOffsetX = fxa − frameOriginX ≥ 0`; anything outside is padding.
|
|
6567
6770
|
*/
|
|
6568
|
-
function
|
|
6569
|
-
const
|
|
6771
|
+
function wideCentralSquareLayout(bbox, frame) {
|
|
6772
|
+
const central = squareSubjectCropRegion(bbox, frame);
|
|
6773
|
+
const c = central.w;
|
|
6774
|
+
const canvasW = Math.round(c * 16 / 9);
|
|
6775
|
+
const centralX0 = Math.round((canvasW - c) / 2);
|
|
6776
|
+
let frameOriginX = central.x - (canvasW - c) / 2;
|
|
6777
|
+
if (canvasW <= frame.W) {
|
|
6778
|
+
const slideRightMax = Math.max(0, bbox.x - central.x);
|
|
6779
|
+
const slideLeftMax = Math.max(0, central.x + c - (bbox.x + bbox.w));
|
|
6780
|
+
if (frameOriginX < 0) frameOriginX += Math.min(-frameOriginX, slideRightMax);
|
|
6781
|
+
const overRight = frameOriginX + canvasW - frame.W;
|
|
6782
|
+
if (overRight > 0) frameOriginX -= Math.min(overRight, slideLeftMax);
|
|
6783
|
+
} else frameOriginX = (frame.W - canvasW) / 2;
|
|
6784
|
+
const fxa = Math.max(0, frameOriginX);
|
|
6785
|
+
const slabW = Math.min(frame.W, frameOriginX + canvasW) - fxa;
|
|
6786
|
+
const slabOffsetX = fxa - frameOriginX;
|
|
6570
6787
|
return {
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6788
|
+
fetch: {
|
|
6789
|
+
x: fxa / frame.W,
|
|
6790
|
+
y: central.y / frame.H,
|
|
6791
|
+
w: slabW / frame.W,
|
|
6792
|
+
h: c / frame.H
|
|
6793
|
+
},
|
|
6794
|
+
canvasW,
|
|
6795
|
+
canvasH: c,
|
|
6796
|
+
slabOffsetX,
|
|
6797
|
+
slabW,
|
|
6798
|
+
slabH: c,
|
|
6799
|
+
centralX0,
|
|
6800
|
+
centralSide: c,
|
|
6801
|
+
frameOriginX
|
|
6575
6802
|
};
|
|
6576
6803
|
}
|
|
6804
|
+
//#endregion
|
|
6805
|
+
//#region src/shared/frame/subject-crop-variants.ts
|
|
6577
6806
|
/**
|
|
6578
|
-
*
|
|
6579
|
-
*
|
|
6580
|
-
*
|
|
6581
|
-
*
|
|
6582
|
-
*
|
|
6807
|
+
* Best-shot subject-crop variants (best-crop fast-load, 2026-07-21; 16:9
|
|
6808
|
+
* central-square reframe, 2026-07-22).
|
|
6809
|
+
*
|
|
6810
|
+
* ONE native fetch (the 16:9 central-square WINDOW — see
|
|
6811
|
+
* {@link wide-central-square-crop}) backs both persisted variants:
|
|
6812
|
+
* - `thumbnail` — the native uncapped 16:9 window JPEG (subject in the
|
|
6813
|
+
* central square, lateral scene context, never upscaled).
|
|
6814
|
+
* - `thumbnailSmall` — the SAME window downscaled to {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH}
|
|
6815
|
+
* long side (854×480), the reel / lists / grid fast-load
|
|
6816
|
+
* representative.
|
|
6817
|
+
*
|
|
6818
|
+
* The small variant is DERIVED from the already-composed native window — never a
|
|
6819
|
+
* second native round-trip and NEVER upscaled: if the window is already ≤ 854 on
|
|
6820
|
+
* its long side it is stored AS-IS (byte-identical). The window is composed from
|
|
6821
|
+
* the in-frame slab plus lateral letterbox padding for the (rare) part of the
|
|
6822
|
+
* 16:9 window that falls outside the frame at a horizontal edge.
|
|
6583
6823
|
*/
|
|
6584
|
-
|
|
6585
|
-
|
|
6824
|
+
/** Gaussian sigma for the blurred-scene letterbox fill. */
|
|
6825
|
+
var LETTERBOX_BLUR_SIGMA = 25;
|
|
6826
|
+
/** Brightness factor for the blurred fill — dimmed so the real slab reads as
|
|
6827
|
+
* the subject surface and the fill as ambience, never as sharp scene. */
|
|
6828
|
+
var LETTERBOX_BLUR_BRIGHTNESS = .55;
|
|
6829
|
+
/**
|
|
6830
|
+
* Compose the native-resolution 16:9 central-square WINDOW (`thumbnail`) from the
|
|
6831
|
+
* in-frame slab fetched for `layout`. The slab is placed at its computed offset
|
|
6832
|
+
* on a 16:9 canvas; any lateral part of the window outside the frame (a subject
|
|
6833
|
+
* flush against a frame edge — the geometry already slides the window in-frame
|
|
6834
|
+
* wherever the subject has slack) is filled with a BLURRED, dimmed stretch of
|
|
6835
|
+
* the slab itself instead of dead black bars (operator triage 2026-07-22).
|
|
6836
|
+
* When the whole window is in-frame (`slab` already spans the full canvas) the
|
|
6837
|
+
* slab is returned VERBATIM (no re-encode). NEVER upscales the real image —
|
|
6838
|
+
* the canvas is at the slab's native scale; only the out-of-frame ambience fill
|
|
6839
|
+
* is synthesized.
|
|
6840
|
+
*
|
|
6841
|
+
* `slab` must be the JPEG of `layout.fetch`; its native pixel dimensions set the
|
|
6842
|
+
* canvas scale (native px per source px = `slabNativeHeight / layout.slabH`).
|
|
6843
|
+
*/
|
|
6844
|
+
async function composeWideCentralSquareThumbnail(slab, layout) {
|
|
6845
|
+
const meta = await sharp(slab).metadata();
|
|
6846
|
+
const nativeW = meta.width ?? 0;
|
|
6847
|
+
const nativeH = meta.height ?? 0;
|
|
6848
|
+
if (nativeW <= 0 || nativeH <= 0 || layout.slabH <= 0) return slab;
|
|
6849
|
+
const scale = nativeH / layout.slabH;
|
|
6850
|
+
const canvasNativeW = Math.max(Math.round(layout.canvasW * scale), nativeW);
|
|
6851
|
+
const leftPad = Math.max(0, Math.round(layout.slabOffsetX * scale));
|
|
6852
|
+
const rightPad = Math.max(0, canvasNativeW - nativeW - leftPad);
|
|
6853
|
+
if (leftPad === 0 && rightPad === 0) return slab;
|
|
6854
|
+
return sharp(await sharp(slab).resize(canvasNativeW, nativeH, { fit: "fill" }).blur(LETTERBOX_BLUR_SIGMA).modulate({ brightness: LETTERBOX_BLUR_BRIGHTNESS }).toBuffer()).composite([{
|
|
6855
|
+
input: slab,
|
|
6856
|
+
left: leftPad,
|
|
6857
|
+
top: 0
|
|
6858
|
+
}]).jpeg({ quality: 88 }).toBuffer();
|
|
6859
|
+
}
|
|
6860
|
+
/**
|
|
6861
|
+
* Derive the `thumbnailSmall` JPEG from the composed native 16:9 window.
|
|
6862
|
+
* Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (854) on the long
|
|
6863
|
+
* side (aspect preserved → ~854×480). If the window's long side is already ≤ the
|
|
6864
|
+
* cap, the ORIGINAL buffer is returned unchanged (store as-is, never upscale).
|
|
6865
|
+
*/
|
|
6866
|
+
async function deriveThumbnailSmall(nativeWideJpeg) {
|
|
6867
|
+
const meta = await sharp(nativeWideJpeg).metadata();
|
|
6586
6868
|
const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
6587
|
-
if (longSide > 0 && longSide <=
|
|
6588
|
-
return sharp(
|
|
6589
|
-
fit: "inside",
|
|
6590
|
-
withoutEnlargement: true
|
|
6591
|
-
}).jpeg({ quality: 88 }).toBuffer();
|
|
6869
|
+
if (longSide > 0 && longSide <= 854) return nativeWideJpeg;
|
|
6870
|
+
return sharp(nativeWideJpeg).resize(854, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
|
|
6592
6871
|
}
|
|
6593
6872
|
//#endregion
|
|
6594
6873
|
//#region src/shared/frame/box-drawer.ts
|
|
@@ -6671,6 +6950,9 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
|
|
|
6671
6950
|
* upscale).
|
|
6672
6951
|
*/
|
|
6673
6952
|
var SNAPSHOT_MAX_WIDTH = 960;
|
|
6953
|
+
/** Bound the once-per-owner display-fallback memo so a long-lived process never
|
|
6954
|
+
* leaks it. */
|
|
6955
|
+
var MAX_LOGGED_DISPLAY_FALLBACKS = 5e3;
|
|
6674
6956
|
/**
|
|
6675
6957
|
* Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
|
|
6676
6958
|
* ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
|
|
@@ -6750,6 +7032,9 @@ var EventMediaDispatcher = class {
|
|
|
6750
7032
|
* {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
|
|
6751
7033
|
*/
|
|
6752
7034
|
sharedNativeFullFrame;
|
|
7035
|
+
/** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
|
|
7036
|
+
* info log — see {@link logDisplayFallbackOnce}. */
|
|
7037
|
+
loggedDisplayFallbacks = /* @__PURE__ */ new Set();
|
|
6753
7038
|
constructor(deps) {
|
|
6754
7039
|
this.deps = deps;
|
|
6755
7040
|
this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
|
|
@@ -6816,7 +7101,7 @@ var EventMediaDispatcher = class {
|
|
|
6816
7101
|
return empty;
|
|
6817
7102
|
}
|
|
6818
7103
|
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
|
|
6819
|
-
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
7104
|
+
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
|
|
6820
7105
|
const firstFrameTrackIds = [];
|
|
6821
7106
|
for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
|
|
6822
7107
|
const storedSnapshots = [];
|
|
@@ -6947,56 +7232,89 @@ var EventMediaDispatcher = class {
|
|
|
6947
7232
|
};
|
|
6948
7233
|
}
|
|
6949
7234
|
/**
|
|
6950
|
-
* Clean subject-centered crop of `bbox`
|
|
6951
|
-
*
|
|
6952
|
-
*
|
|
7235
|
+
* Clean subject-centered crop of `bbox` for the DISPLAY child media
|
|
7236
|
+
* (`faceCrop`/`plateCrop`): the square-safe 16:9 region around the bbox with NO
|
|
7237
|
+
* box drawn.
|
|
6953
7238
|
*
|
|
6954
|
-
*
|
|
6955
|
-
*
|
|
6956
|
-
*
|
|
6957
|
-
*
|
|
6958
|
-
*
|
|
6959
|
-
*
|
|
6960
|
-
*
|
|
7239
|
+
* These are operator gallery images, NOT model inputs (the spec's native-source
|
|
7240
|
+
* hard rule governs model inputs, which stay native-strict). So a native-ROI
|
|
7241
|
+
* miss degrades gracefully — always honest, NEVER upscaled:
|
|
7242
|
+
* 1. native ROI crop (the runner's retained native surface) — best quality;
|
|
7243
|
+
* 2. the retained full frame (keyframe-native or the runner's ≤640 RAM tier)
|
|
7244
|
+
* cropped to the SAME region;
|
|
7245
|
+
* 3. the already-resolved ≤640 detection raster (`frameData`, in hand) cropped
|
|
7246
|
+
* to the region — the last-resort honest sub-native preview.
|
|
7247
|
+
* Only a total miss (all three unavailable) returns `null`. A fallback is
|
|
7248
|
+
* logged ONCE per owner (`${eventId}:${kind}`) at info level. Runs inside the
|
|
6961
7249
|
* live-handle window opened by `captureForFrame`.
|
|
6962
7250
|
*/
|
|
6963
|
-
async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
|
|
6964
|
-
|
|
6965
|
-
|
|
7251
|
+
async cropSubjectRegion(frameHandle, frameData, fw, fh, bbox, cropPadding, ownerId) {
|
|
7252
|
+
const norm = squareSafeCropRegionNormalized(bbox, {
|
|
7253
|
+
W: fw,
|
|
7254
|
+
H: fh
|
|
7255
|
+
}, cropPadding);
|
|
7256
|
+
if (this.deps.getNativeCropJpeg) try {
|
|
7257
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
7258
|
+
if (native) return native;
|
|
7259
|
+
} catch (err) {
|
|
7260
|
+
this.deps.logger.debug("display child crop: native ROI fetch threw", { meta: {
|
|
6966
7261
|
shmId: frameHandle.shmId,
|
|
6967
|
-
|
|
7262
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6968
7263
|
} });
|
|
6969
|
-
return null;
|
|
6970
7264
|
}
|
|
6971
7265
|
try {
|
|
6972
|
-
const
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
7266
|
+
const full = await this.sharedNativeFullFrame(frameHandle);
|
|
7267
|
+
if (full && full.format === "rgb" && full.width > 0 && full.height > 0) {
|
|
7268
|
+
const { crop } = await extractCrop(full.data, full.width, full.height, norm);
|
|
7269
|
+
this.logDisplayFallbackOnce(ownerId, "native-full-frame");
|
|
7270
|
+
return crop;
|
|
7271
|
+
}
|
|
7272
|
+
} catch (err) {
|
|
7273
|
+
this.deps.logger.debug("display child crop: full-frame fallback threw", { meta: {
|
|
6979
7274
|
shmId: frameHandle.shmId,
|
|
6980
|
-
|
|
7275
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6981
7276
|
} });
|
|
6982
|
-
|
|
7277
|
+
}
|
|
7278
|
+
try {
|
|
7279
|
+
const { crop } = await extractCrop(frameData, fw, fh, norm);
|
|
7280
|
+
this.logDisplayFallbackOnce(ownerId, "detection-raster");
|
|
7281
|
+
return crop;
|
|
6983
7282
|
} catch (err) {
|
|
6984
|
-
this.deps.logger.warn("
|
|
7283
|
+
this.deps.logger.warn("display child crop: all sources missed", { meta: {
|
|
6985
7284
|
shmId: frameHandle.shmId,
|
|
6986
7285
|
error: err instanceof Error ? err.message : String(err)
|
|
6987
7286
|
} });
|
|
6988
7287
|
return null;
|
|
6989
7288
|
}
|
|
6990
7289
|
}
|
|
7290
|
+
/** Info-log a DISPLAY-crop fallback ONCE per owner (`${eventId}:${kind}`) so a
|
|
7291
|
+
* short native window is visible without flooding the log every frame. */
|
|
7292
|
+
logDisplayFallbackOnce(ownerId, reason) {
|
|
7293
|
+
if (this.loggedDisplayFallbacks.has(ownerId)) return;
|
|
7294
|
+
if (this.loggedDisplayFallbacks.size >= MAX_LOGGED_DISPLAY_FALLBACKS) this.loggedDisplayFallbacks.clear();
|
|
7295
|
+
this.loggedDisplayFallbacks.add(ownerId);
|
|
7296
|
+
this.deps.logger.info("display child crop fallback used (honest sub-native, not upscaled)", { meta: {
|
|
7297
|
+
ownerId,
|
|
7298
|
+
reason
|
|
7299
|
+
} });
|
|
7300
|
+
}
|
|
6991
7301
|
/**
|
|
6992
7302
|
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6993
|
-
* fast-load, 2026-07-21
|
|
6994
|
-
*
|
|
6995
|
-
*
|
|
6996
|
-
*
|
|
6997
|
-
*
|
|
6998
|
-
*
|
|
6999
|
-
*
|
|
7303
|
+
* fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22). ONE native
|
|
7304
|
+
* fetch: the in-frame slab of the 16:9 central-square WINDOW
|
|
7305
|
+
* ({@link wideCentralSquareLayout} — a `side × 16/9` window whose middle square
|
|
7306
|
+
* of side `max(w,h)×1.2` fully contains the subject, clamped/anchored so the
|
|
7307
|
+
* subject stays in that central square at every frame edge) is requested from
|
|
7308
|
+
* the runner's retained native surface with NO `maxWidth` (uncapped TRUE
|
|
7309
|
+
* native). It is composed into the full 16:9 window (lateral letterbox only for
|
|
7310
|
+
* the part outside the frame) → the `thumbnail`. The `thumbnailSmall` is
|
|
7311
|
+
* DERIVED by downscaling that SAME window to the 854 long-side cap (854×480) —
|
|
7312
|
+
* never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
|
|
7313
|
+
* returns the window as-is when it is already ≤ 854).
|
|
7314
|
+
*
|
|
7315
|
+
* Both variants share the SAME framing + central-square containment guarantee:
|
|
7316
|
+
* wide consumers use the 16:9 image as-is (subject centered, no cut); square
|
|
7317
|
+
* consumers center-crop the middle square (subject always inside it).
|
|
7000
7318
|
*
|
|
7001
7319
|
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
7002
7320
|
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
@@ -7012,21 +7330,22 @@ var EventMediaDispatcher = class {
|
|
|
7012
7330
|
return null;
|
|
7013
7331
|
}
|
|
7014
7332
|
try {
|
|
7015
|
-
const
|
|
7333
|
+
const layout = wideCentralSquareLayout(bbox, {
|
|
7016
7334
|
W: fw,
|
|
7017
7335
|
H: fh
|
|
7018
7336
|
});
|
|
7019
|
-
const
|
|
7020
|
-
if (!
|
|
7337
|
+
const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch);
|
|
7338
|
+
if (!slab) {
|
|
7021
7339
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
7022
7340
|
shmId: frameHandle.shmId,
|
|
7023
7341
|
reason: "native-miss"
|
|
7024
7342
|
} });
|
|
7025
7343
|
return null;
|
|
7026
7344
|
}
|
|
7345
|
+
const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
|
|
7027
7346
|
return {
|
|
7028
|
-
thumbnail
|
|
7029
|
-
thumbnailSmall: await deriveThumbnailSmall(
|
|
7347
|
+
thumbnail,
|
|
7348
|
+
thumbnailSmall: await deriveThumbnailSmall(thumbnail)
|
|
7030
7349
|
};
|
|
7031
7350
|
} catch (err) {
|
|
7032
7351
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
@@ -7115,9 +7434,9 @@ var EventMediaDispatcher = class {
|
|
|
7115
7434
|
return false;
|
|
7116
7435
|
}
|
|
7117
7436
|
}
|
|
7118
|
-
async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
|
|
7437
|
+
async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
|
|
7119
7438
|
if (ev.childCrops) for (const child of ev.childCrops) try {
|
|
7120
|
-
const childCropData = await this.cropSubjectRegion(frameHandle, fw, fh, child.bbox, cropPadding);
|
|
7439
|
+
const childCropData = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, child.bbox, cropPadding, `${ev.eventId}:${child.kind}`);
|
|
7121
7440
|
if (!childCropData) continue;
|
|
7122
7441
|
await this.deps.mediaStore.put({
|
|
7123
7442
|
deviceId,
|
|
@@ -8945,7 +9264,8 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
8945
9264
|
*/
|
|
8946
9265
|
function planPeriodicMedia(input) {
|
|
8947
9266
|
const appendSnapshot = input.dueSnapshot;
|
|
8948
|
-
const
|
|
9267
|
+
const lastFrameInterval = input.lastFrameIntervalMs ?? input.intervalMs;
|
|
9268
|
+
const rollingLastFrame = input.saveThumbnails && input.now - input.lastFrameAt >= lastFrameInterval && !appendSnapshot;
|
|
8949
9269
|
const thumbnailLanded = input.thumbnailLanded ?? true;
|
|
8950
9270
|
return {
|
|
8951
9271
|
appendSnapshot,
|
|
@@ -8953,7 +9273,10 @@ function planPeriodicMedia(input) {
|
|
|
8953
9273
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8954
9274
|
};
|
|
8955
9275
|
}
|
|
8956
|
-
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9276
|
+
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9277
|
+
suppressMaxDurationMs: 1e3,
|
|
9278
|
+
nothingToShowMaxDurationMs: 1500
|
|
9279
|
+
};
|
|
8957
9280
|
/**
|
|
8958
9281
|
* Classify a closing track's persistence outcome. Pure — see the module header
|
|
8959
9282
|
* for the full contract.
|
|
@@ -8961,6 +9284,7 @@ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
|
|
|
8961
9284
|
function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
|
|
8962
9285
|
if (input.hasMedia) return "persist";
|
|
8963
9286
|
if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
|
|
9287
|
+
if (!input.hasRasterFallback && input.durationMs < config.nothingToShowMaxDurationMs) return "suppress";
|
|
8964
9288
|
return input.hasRasterFallback ? "raster-fallback" : "persist";
|
|
8965
9289
|
}
|
|
8966
9290
|
//#endregion
|
|
@@ -10136,6 +10460,7 @@ var FaceRecognizer = class {
|
|
|
10136
10460
|
embedding: input.embedding,
|
|
10137
10461
|
embeddingModelId: modelId,
|
|
10138
10462
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
10463
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {},
|
|
10139
10464
|
...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
|
|
10140
10465
|
};
|
|
10141
10466
|
await this.processFrame({
|
|
@@ -10151,7 +10476,7 @@ var FaceRecognizer = class {
|
|
|
10151
10476
|
}
|
|
10152
10477
|
async processFrame(input) {
|
|
10153
10478
|
const { settings } = input;
|
|
10154
|
-
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.
|
|
10479
|
+
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
|
|
10155
10480
|
if (candidates.length === 0) return;
|
|
10156
10481
|
this.deps.logger.debug("face: frame candidates", {
|
|
10157
10482
|
tags: { deviceId: input.deviceId },
|
|
@@ -10189,7 +10514,7 @@ var FaceRecognizer = class {
|
|
|
10189
10514
|
let crop;
|
|
10190
10515
|
if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
|
|
10191
10516
|
else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
10192
|
-
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
10517
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding, c.trackId) ?? void 0;
|
|
10193
10518
|
} catch (err) {
|
|
10194
10519
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
10195
10520
|
tags: { deviceId: input.deviceId },
|
|
@@ -11720,7 +12045,7 @@ var PlateRecognizer = class {
|
|
|
11720
12045
|
if (held !== void 0 && input.score <= held.score) return;
|
|
11721
12046
|
let crop;
|
|
11722
12047
|
if (input.frameHandle !== void 0) try {
|
|
11723
|
-
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
12048
|
+
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId) ?? void 0;
|
|
11724
12049
|
} catch (err) {
|
|
11725
12050
|
this.deps.logger.debug("PlateRecognizer crop capture failed", {
|
|
11726
12051
|
tags: { deviceId: input.deviceId },
|
|
@@ -11735,6 +12060,9 @@ var PlateRecognizer = class {
|
|
|
11735
12060
|
score: input.score,
|
|
11736
12061
|
bbox: input.bbox,
|
|
11737
12062
|
timestamp: input.timestamp,
|
|
12063
|
+
frameWidth: input.frameWidth,
|
|
12064
|
+
frameHeight: input.frameHeight,
|
|
12065
|
+
cropPadding: input.cropPadding,
|
|
11738
12066
|
...crop !== void 0 ? { crop } : {}
|
|
11739
12067
|
});
|
|
11740
12068
|
}
|
|
@@ -11745,15 +12073,48 @@ var PlateRecognizer = class {
|
|
|
11745
12073
|
this.bestPlate.delete(trackId);
|
|
11746
12074
|
if (held === void 0) return;
|
|
11747
12075
|
const plateId = `plate-${trackId}`;
|
|
12076
|
+
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
12077
|
+
let cropData = held.crop;
|
|
12078
|
+
if (cropData === void 0 && keyFrameMediaKey !== void 0 && this.deps.deriveCropFromKeyFrame) try {
|
|
12079
|
+
const derived = await this.deps.deriveCropFromKeyFrame({
|
|
12080
|
+
mediaKey: keyFrameMediaKey,
|
|
12081
|
+
bbox: held.bbox,
|
|
12082
|
+
frameWidth: held.frameWidth,
|
|
12083
|
+
frameHeight: held.frameHeight,
|
|
12084
|
+
padding: held.cropPadding,
|
|
12085
|
+
timestamp: held.timestamp
|
|
12086
|
+
});
|
|
12087
|
+
if (derived) {
|
|
12088
|
+
cropData = derived.jpeg;
|
|
12089
|
+
this.deps.logger.info("plate crop recovered from keyFrame (live capture missed)", {
|
|
12090
|
+
tags: {
|
|
12091
|
+
deviceId,
|
|
12092
|
+
trackId
|
|
12093
|
+
},
|
|
12094
|
+
meta: {
|
|
12095
|
+
plateId,
|
|
12096
|
+
skewMs: derived.skewMs
|
|
12097
|
+
}
|
|
12098
|
+
});
|
|
12099
|
+
}
|
|
12100
|
+
} catch (err) {
|
|
12101
|
+
this.deps.logger.debug("PlateRecognizer keyFrame crop derive failed", {
|
|
12102
|
+
tags: { deviceId },
|
|
12103
|
+
meta: {
|
|
12104
|
+
plateId,
|
|
12105
|
+
error: String(err)
|
|
12106
|
+
}
|
|
12107
|
+
});
|
|
12108
|
+
}
|
|
11748
12109
|
let mediaKey;
|
|
11749
|
-
if (
|
|
12110
|
+
if (cropData !== void 0) try {
|
|
11750
12111
|
mediaKey = await this.deps.mediaStore.put({
|
|
11751
12112
|
deviceId,
|
|
11752
12113
|
ownerKind: "plate",
|
|
11753
12114
|
ownerId: plateId,
|
|
11754
12115
|
kind: "crop",
|
|
11755
12116
|
timestamp: held.timestamp,
|
|
11756
|
-
data:
|
|
12117
|
+
data: cropData
|
|
11757
12118
|
});
|
|
11758
12119
|
} catch (err) {
|
|
11759
12120
|
this.deps.logger.warn("PlateRecognizer plate crop put failed", {
|
|
@@ -11765,7 +12126,6 @@ var PlateRecognizer = class {
|
|
|
11765
12126
|
});
|
|
11766
12127
|
}
|
|
11767
12128
|
const match = this.matchVehicle(held.text, held.score);
|
|
11768
|
-
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
11769
12129
|
try {
|
|
11770
12130
|
await this.deps.plateStore.insert({
|
|
11771
12131
|
id: plateId,
|
|
@@ -12021,6 +12381,86 @@ function createCaptureCrop(deps) {
|
|
|
12021
12381
|
};
|
|
12022
12382
|
}
|
|
12023
12383
|
//#endregion
|
|
12384
|
+
//#region src/pipeline-analytics/pipeline/display-crop.ts
|
|
12385
|
+
/** Bound the once-per-owner memo so a long-lived process never leaks it. */
|
|
12386
|
+
var MAX_LOGGED_OWNERS = 5e3;
|
|
12387
|
+
function createDisplayCrop(deps) {
|
|
12388
|
+
const loggedOwners = /* @__PURE__ */ new Set();
|
|
12389
|
+
const logFallbackOnce = (ownerId, reason) => {
|
|
12390
|
+
if (loggedOwners.has(ownerId)) return;
|
|
12391
|
+
if (loggedOwners.size >= MAX_LOGGED_OWNERS) loggedOwners.clear();
|
|
12392
|
+
loggedOwners.add(ownerId);
|
|
12393
|
+
deps.logger.info("display crop fallback used (honest sub-native, not upscaled)", { meta: {
|
|
12394
|
+
ownerId,
|
|
12395
|
+
reason
|
|
12396
|
+
} });
|
|
12397
|
+
};
|
|
12398
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, ownerId, maxWidth) => {
|
|
12399
|
+
const norm = padBbox({
|
|
12400
|
+
x: bbox.x / frameWidth,
|
|
12401
|
+
y: bbox.y / frameHeight,
|
|
12402
|
+
w: bbox.w / frameWidth,
|
|
12403
|
+
h: bbox.h / frameHeight
|
|
12404
|
+
}, padding);
|
|
12405
|
+
const native = await deps.fetchNativeRoiRgb(frameHandle, norm, maxWidth);
|
|
12406
|
+
if (native) {
|
|
12407
|
+
const jpeg = await deps.encodeRgb(native.bytes, native.width, native.height);
|
|
12408
|
+
if (jpeg) return jpeg;
|
|
12409
|
+
}
|
|
12410
|
+
const fb = await deps.fetchFallbackFrame(frameHandle);
|
|
12411
|
+
if (fb) try {
|
|
12412
|
+
const jpeg = await deps.cropRegionToJpeg(fb.frame.bytes, fb.frame.width, fb.frame.height, norm);
|
|
12413
|
+
logFallbackOnce(ownerId, fb.reason);
|
|
12414
|
+
return jpeg;
|
|
12415
|
+
} catch (err) {
|
|
12416
|
+
deps.logger.debug("display crop fallback crop failed", { meta: {
|
|
12417
|
+
ownerId,
|
|
12418
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12419
|
+
} });
|
|
12420
|
+
}
|
|
12421
|
+
logFallbackOnce(ownerId, "unavailable");
|
|
12422
|
+
return null;
|
|
12423
|
+
};
|
|
12424
|
+
}
|
|
12425
|
+
//#endregion
|
|
12426
|
+
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
12427
|
+
function createKeyFrameCrop(deps) {
|
|
12428
|
+
return async (input) => {
|
|
12429
|
+
if (input.frameWidth <= 0 || input.frameHeight <= 0) return null;
|
|
12430
|
+
const media = await deps.getMedia(input.mediaKey);
|
|
12431
|
+
if (!media) return null;
|
|
12432
|
+
const skewMs = Math.abs(media.timestamp - input.timestamp);
|
|
12433
|
+
if (skewMs > deps.maxSkewMs) {
|
|
12434
|
+
deps.logger.debug("keyframe crop rejected — temporal skew too large", { meta: {
|
|
12435
|
+
mediaKey: input.mediaKey,
|
|
12436
|
+
skewMs,
|
|
12437
|
+
maxSkewMs: deps.maxSkewMs
|
|
12438
|
+
} });
|
|
12439
|
+
return null;
|
|
12440
|
+
}
|
|
12441
|
+
try {
|
|
12442
|
+
const rgb = await deps.decodeJpegToRgb(media.base64);
|
|
12443
|
+
if (rgb.width <= 0 || rgb.height <= 0) return null;
|
|
12444
|
+
const norm = padBbox({
|
|
12445
|
+
x: input.bbox.x / input.frameWidth,
|
|
12446
|
+
y: input.bbox.y / input.frameHeight,
|
|
12447
|
+
w: input.bbox.w / input.frameWidth,
|
|
12448
|
+
h: input.bbox.h / input.frameHeight
|
|
12449
|
+
}, input.padding);
|
|
12450
|
+
return {
|
|
12451
|
+
jpeg: await deps.cropRegionToJpeg(Buffer.from(rgb.bytes), rgb.width, rgb.height, norm),
|
|
12452
|
+
skewMs
|
|
12453
|
+
};
|
|
12454
|
+
} catch (err) {
|
|
12455
|
+
deps.logger.debug("keyframe crop derive failed", { meta: {
|
|
12456
|
+
mediaKey: input.mediaKey,
|
|
12457
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12458
|
+
} });
|
|
12459
|
+
return null;
|
|
12460
|
+
}
|
|
12461
|
+
};
|
|
12462
|
+
}
|
|
12463
|
+
//#endregion
|
|
12024
12464
|
//#region src/pipeline-analytics/pipeline/dropout-skip.ts
|
|
12025
12465
|
function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
|
|
12026
12466
|
if (!cfg.enabled) return false;
|
|
@@ -12267,6 +12707,16 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
|
12267
12707
|
/** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
|
|
12268
12708
|
* scheduled detail call's frameHandle lease is already gone. */
|
|
12269
12709
|
var DETAIL_FALLBACK_CROP_PADDING = .15;
|
|
12710
|
+
/** Long-side cap for the DISPLAY-crop full-frame fallback (plate/face gallery
|
|
12711
|
+
* tiles) when the native ROI missed. A generous native cap so a keyframe-native
|
|
12712
|
+
* tier still yields a legible plate/face crop; the runner's ≤640 RAM tier is
|
|
12713
|
+
* returned as-is (honest sub-native). Never triggers an upscale. */
|
|
12714
|
+
var DISPLAY_FALLBACK_MAX_WIDTH = 1920;
|
|
12715
|
+
/** Max |keyFrame − plate-read| skew (ms) accepted for the DURABLE keyFrame-
|
|
12716
|
+
* derived plate-crop last resort. A readable plate implies a slow/stopping
|
|
12717
|
+
* vehicle, so within this window the subject barely moves off its bbox;
|
|
12718
|
+
* beyond it the crop would show empty scene, so it is rejected (no image). */
|
|
12719
|
+
var KEYFRAME_CROP_MAX_SKEW_MS = 1500;
|
|
12270
12720
|
/** How long the active CLIP model id (from the embedding-encoder) is cached
|
|
12271
12721
|
* before re-reading. */
|
|
12272
12722
|
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
@@ -12288,6 +12738,13 @@ var ZONE_SLICE_RECONCILE_MS = 3e4;
|
|
|
12288
12738
|
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
12289
12739
|
* detection confidence beats the held best by at least this margin (hysteresis
|
|
12290
12740
|
* so jitter around a plateau doesn't churn the write). */
|
|
12741
|
+
/** Rolling `lastFrame` cadence — deliberately DENSER than the 5s snapshot
|
|
12742
|
+
* interval: most real tracks live 3–9s, so on the snapshot cadence they
|
|
12743
|
+
* closed with NO `lastFrame` at all (29/68 in the 2026-07-22 4h audit) and
|
|
12744
|
+
* the close-time promotion had nothing to absorb. 1.5s bounds the "Ultimo"
|
|
12745
|
+
* tile's staleness at ~1.5s + the coast window, at one 960-boxed re-encode
|
|
12746
|
+
* per 1.5s per active track (putReplacing keeps a single row). */
|
|
12747
|
+
var LAST_FRAME_INTERVAL_MS = 1500;
|
|
12291
12748
|
var BEST_FRAME_HYSTERESIS = .05;
|
|
12292
12749
|
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
12293
12750
|
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
@@ -12542,6 +12999,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12542
12999
|
packageDropDetector = null;
|
|
12543
13000
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
12544
13001
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
13002
|
+
/** Reentrancy latch for `sweepExpiredTracks` — one sweep at a time (a stalled
|
|
13003
|
+
* sweep + queued interval ticks used to process the same close ~20×). */
|
|
13004
|
+
sweepInFlight = false;
|
|
12545
13005
|
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
12546
13006
|
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
12547
13007
|
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
@@ -12964,13 +13424,31 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12964
13424
|
logger: logger.child("CaptureCrop")
|
|
12965
13425
|
});
|
|
12966
13426
|
this.captureCrop = captureCrop;
|
|
13427
|
+
const captureDisplayCrop = createDisplayCrop({
|
|
13428
|
+
fetchNativeRoiRgb: (handle, norm, maxWidth) => fetchNativeCropRgb(handle, norm, maxWidth),
|
|
13429
|
+
fetchFallbackFrame: async (handle) => {
|
|
13430
|
+
const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH);
|
|
13431
|
+
if (!tiered || tiered.frame.format !== "rgb") return null;
|
|
13432
|
+
return {
|
|
13433
|
+
frame: {
|
|
13434
|
+
bytes: Buffer.from(tiered.frame.data),
|
|
13435
|
+
width: tiered.frame.width,
|
|
13436
|
+
height: tiered.frame.height
|
|
13437
|
+
},
|
|
13438
|
+
reason: tiered.tier === "ram-fullframe" ? "detection-raster" : "keyframe-native"
|
|
13439
|
+
};
|
|
13440
|
+
},
|
|
13441
|
+
encodeRgb: (bytes, w, h) => encodeRgbCropToJpeg(bytes, w, h),
|
|
13442
|
+
cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
|
|
13443
|
+
logger: logger.child("DisplayCrop")
|
|
13444
|
+
});
|
|
12967
13445
|
this.faceRecognizer = new FaceRecognizer({
|
|
12968
13446
|
identityStore: this.identityStore,
|
|
12969
13447
|
faceStore: this.faceStore,
|
|
12970
13448
|
mediaStore: this.mediaStore,
|
|
12971
13449
|
trackStore: this.trackStore,
|
|
12972
13450
|
eventStore: this.eventStore,
|
|
12973
|
-
captureCrop,
|
|
13451
|
+
captureCrop: captureDisplayCrop,
|
|
12974
13452
|
recomputeImportance: (trackId) => {
|
|
12975
13453
|
const trackStore = this.trackStore;
|
|
12976
13454
|
const eventStore = this.eventStore;
|
|
@@ -12985,11 +13463,26 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12985
13463
|
logger: logger.child("FaceRecognizer")
|
|
12986
13464
|
});
|
|
12987
13465
|
this.faceRecognizer.refreshGallery();
|
|
13466
|
+
const plateMediaStore = this.mediaStore;
|
|
13467
|
+
const deriveCropFromKeyFrame = createKeyFrameCrop({
|
|
13468
|
+
getMedia: async (mediaKey) => {
|
|
13469
|
+
const m = await plateMediaStore.getByKey(mediaKey);
|
|
13470
|
+
return m ? {
|
|
13471
|
+
base64: m.base64,
|
|
13472
|
+
timestamp: m.timestamp
|
|
13473
|
+
} : null;
|
|
13474
|
+
},
|
|
13475
|
+
decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
|
|
13476
|
+
cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
|
|
13477
|
+
maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
|
|
13478
|
+
logger: logger.child("KeyFrameCrop")
|
|
13479
|
+
});
|
|
12988
13480
|
this.plateRecognizer = new PlateRecognizer({
|
|
12989
13481
|
plateStore: this.plateStore,
|
|
12990
13482
|
vehicleStore: this.vehicleStore,
|
|
12991
13483
|
mediaStore: this.mediaStore,
|
|
12992
|
-
captureCrop,
|
|
13484
|
+
captureCrop: captureDisplayCrop,
|
|
13485
|
+
deriveCropFromKeyFrame,
|
|
12993
13486
|
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
12994
13487
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
12995
13488
|
logger: logger.child("PlateRecognizer")
|
|
@@ -13639,6 +14132,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13639
14132
|
source,
|
|
13640
14133
|
resurrected: true
|
|
13641
14134
|
} });
|
|
14135
|
+
if (this.eventMediaDispatcher) this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13642
14136
|
continue;
|
|
13643
14137
|
}
|
|
13644
14138
|
bornCandidates.push({
|
|
@@ -13825,7 +14319,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13825
14319
|
for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
|
|
13826
14320
|
const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
|
|
13827
14321
|
if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
|
|
13828
|
-
|
|
14322
|
+
const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
|
|
14323
|
+
const rasterFallbackCandidates = [];
|
|
14324
|
+
const widenedRasterWantedIds = /* @__PURE__ */ new Set();
|
|
14325
|
+
for (const t of result.tracked) {
|
|
14326
|
+
if (t.matchedThisFrame === false) continue;
|
|
14327
|
+
const closure = this.trackClosureState.get(t.trackId);
|
|
14328
|
+
if (!closure?.confirmed) continue;
|
|
14329
|
+
if (closure.rasterFallback) continue;
|
|
14330
|
+
if (targetedThisFrame.has(t.trackId)) continue;
|
|
14331
|
+
widenedRasterWantedIds.add(t.trackId);
|
|
14332
|
+
rasterFallbackCandidates.push({
|
|
14333
|
+
trackId: t.trackId,
|
|
14334
|
+
timestamp: result.timestamp,
|
|
14335
|
+
bbox: { ...t.bbox }
|
|
14336
|
+
});
|
|
14337
|
+
}
|
|
14338
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0 || widenedRasterWantedIds.size > 0) {
|
|
13829
14339
|
const captureCounts = {
|
|
13830
14340
|
events: eventTargets.length,
|
|
13831
14341
|
trackFrames: firstFrameTargets.length,
|
|
@@ -13851,24 +14361,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13851
14361
|
const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
|
|
13852
14362
|
for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
|
|
13853
14363
|
const dispatchTimestamp = result.timestamp;
|
|
13854
|
-
const rasterFallbackWantedTrackIds =
|
|
14364
|
+
const rasterFallbackWantedTrackIds = new Set(widenedRasterWantedIds);
|
|
13855
14365
|
for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13856
14366
|
for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
|
|
13857
|
-
const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
|
|
13858
|
-
const rasterFallbackCandidates = [];
|
|
13859
|
-
for (const t of result.tracked) {
|
|
13860
|
-
if (t.matchedThisFrame === false) continue;
|
|
13861
|
-
const closure = this.trackClosureState.get(t.trackId);
|
|
13862
|
-
if (!closure?.confirmed) continue;
|
|
13863
|
-
if (closure.rasterFallback) continue;
|
|
13864
|
-
if (targetedThisFrame.has(t.trackId)) continue;
|
|
13865
|
-
rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13866
|
-
rasterFallbackCandidates.push({
|
|
13867
|
-
trackId: t.trackId,
|
|
13868
|
-
timestamp: result.timestamp,
|
|
13869
|
-
bbox: { ...t.bbox }
|
|
13870
|
-
});
|
|
13871
|
-
}
|
|
13872
14367
|
this.eventMediaDispatcher.captureForFrame({
|
|
13873
14368
|
deviceId,
|
|
13874
14369
|
frameHandle,
|
|
@@ -14310,7 +14805,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14310
14805
|
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
14311
14806
|
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
14312
14807
|
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
14313
|
-
|
|
14808
|
+
const faceShortSidePx = detail.nativeFaceShortSidePx ?? (detail.bbox !== void 0 ? Math.min(detail.bbox.w, detail.bbox.h) : void 0);
|
|
14809
|
+
if (faceShortSidePx !== void 0 && faceShortSidePx < settings.minFacePx) return;
|
|
14314
14810
|
await this.faceRecognizer.ingestFaceDetail({
|
|
14315
14811
|
deviceId,
|
|
14316
14812
|
trackId,
|
|
@@ -14656,7 +15152,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14656
15152
|
frameHeight
|
|
14657
15153
|
});
|
|
14658
15154
|
const centerScore = bboxCenterScore(t.bbox, frameWidth, frameHeight);
|
|
14659
|
-
const
|
|
15155
|
+
const containment = bboxContainment(t.bbox, frameWidth, frameHeight);
|
|
15156
|
+
const sizeScore = bboxSizeScore(t.bbox, frameWidth, frameHeight);
|
|
15157
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore);
|
|
14660
15158
|
this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
|
|
14661
15159
|
const plan = planPeriodicMedia({
|
|
14662
15160
|
saveThumbnails: media.saveThumbnails,
|
|
@@ -14665,7 +15163,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14665
15163
|
thumbnailLanded: this.thumbnailLandedTracks.has(t.trackId),
|
|
14666
15164
|
lastFrameAt: this.lastFrameAtByTrack.get(t.trackId) ?? 0,
|
|
14667
15165
|
now: timestamp,
|
|
14668
|
-
intervalMs: media.snapshotIntervalMs
|
|
15166
|
+
intervalMs: media.snapshotIntervalMs,
|
|
15167
|
+
lastFrameIntervalMs: Math.min(LAST_FRAME_INTERVAL_MS, media.snapshotIntervalMs)
|
|
14669
15168
|
});
|
|
14670
15169
|
const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
|
|
14671
15170
|
if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
|
|
@@ -14926,14 +15425,17 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14926
15425
|
}
|
|
14927
15426
|
async sweepExpiredTracks() {
|
|
14928
15427
|
if (this.shuttingDown || !this.trackStore) return;
|
|
15428
|
+
if (this.sweepInFlight) return;
|
|
15429
|
+
this.sweepInFlight = true;
|
|
14929
15430
|
try {
|
|
14930
15431
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14931
15432
|
for (const t of expired) {
|
|
14932
15433
|
const duration = t.lastSeen - t.firstSeen;
|
|
14933
15434
|
const closure = this.trackClosureState.get(t.trackId);
|
|
15435
|
+
const ownedMedia = await this.mediaStore?.listByOwner("track", t.trackId) ?? [];
|
|
14934
15436
|
const outcome = decideZeroMediaPolicy({
|
|
14935
15437
|
durationMs: duration,
|
|
14936
|
-
hasMedia:
|
|
15438
|
+
hasMedia: ownedMedia.length > 0,
|
|
14937
15439
|
confirmed: closure?.confirmed ?? false,
|
|
14938
15440
|
hasRasterFallback: closure?.rasterFallback !== void 0
|
|
14939
15441
|
});
|
|
@@ -14968,6 +15470,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14968
15470
|
}
|
|
14969
15471
|
});
|
|
14970
15472
|
}
|
|
15473
|
+
const promotion = decideLastFramePromotion(ownedMedia);
|
|
15474
|
+
if (promotion.promote) {
|
|
15475
|
+
const snapshot = ownedMedia.find((m) => m.key === promotion.snapshotKey);
|
|
15476
|
+
if (snapshot) try {
|
|
15477
|
+
await this.mediaStore?.promoteToLastFrame({
|
|
15478
|
+
deviceId: t.deviceId,
|
|
15479
|
+
trackId: t.trackId,
|
|
15480
|
+
snapshot
|
|
15481
|
+
});
|
|
15482
|
+
} catch (err) {
|
|
15483
|
+
this.ctx.logger.debug("lastFrame promotion failed", {
|
|
15484
|
+
tags: { deviceId: t.deviceId },
|
|
15485
|
+
meta: {
|
|
15486
|
+
trackId: t.trackId,
|
|
15487
|
+
error: String(err)
|
|
15488
|
+
}
|
|
15489
|
+
});
|
|
15490
|
+
}
|
|
15491
|
+
}
|
|
14971
15492
|
this.ctx.logger.info("track ended", {
|
|
14972
15493
|
tags: { deviceId: t.deviceId },
|
|
14973
15494
|
meta: {
|
|
@@ -15086,6 +15607,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15086
15607
|
} catch (err) {
|
|
15087
15608
|
if (this.shuttingDown) return;
|
|
15088
15609
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
15610
|
+
} finally {
|
|
15611
|
+
this.sweepInFlight = false;
|
|
15089
15612
|
}
|
|
15090
15613
|
}
|
|
15091
15614
|
/**
|