@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
|
@@ -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-BvEJmXbR.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let sharp = require("sharp");
|
|
8
8
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -2091,6 +2091,7 @@ var FrameProcessor = class {
|
|
|
2091
2091
|
const firstLevelBboxById = /* @__PURE__ */ new Map();
|
|
2092
2092
|
const sourceIdByBbox = /* @__PURE__ */ new Map();
|
|
2093
2093
|
const faceBboxByBbox = /* @__PURE__ */ new Map();
|
|
2094
|
+
const nativeFaceSizeByBbox = /* @__PURE__ */ new Map();
|
|
2094
2095
|
const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
|
|
2095
2096
|
const plateByBbox = /* @__PURE__ */ new Map();
|
|
2096
2097
|
const maskByBbox = /* @__PURE__ */ new Map();
|
|
@@ -2136,6 +2137,7 @@ var FrameProcessor = class {
|
|
|
2136
2137
|
h: det.bbox.height
|
|
2137
2138
|
});
|
|
2138
2139
|
if (det.faceAlignedCrop !== void 0) faceAlignedCropByBbox.set(parentBbox, det.faceAlignedCrop);
|
|
2140
|
+
if (det.nativeFaceShortSidePx !== void 0) nativeFaceSizeByBbox.set(parentBbox, det.nativeFaceShortSidePx);
|
|
2139
2141
|
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
2140
2142
|
embedding: det.embedding,
|
|
2141
2143
|
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
@@ -2191,6 +2193,7 @@ var FrameProcessor = class {
|
|
|
2191
2193
|
});
|
|
2192
2194
|
const emb = embeddingByBbox.get(td.bbox);
|
|
2193
2195
|
const faceBbox = faceBboxByBbox.get(td.bbox);
|
|
2196
|
+
const nativeFaceShortSidePx = nativeFaceSizeByBbox.get(td.bbox);
|
|
2194
2197
|
const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
|
|
2195
2198
|
const plate = plateByBbox.get(td.bbox);
|
|
2196
2199
|
const sourceDetectionId = sourceIdByBbox.get(td.bbox);
|
|
@@ -2209,6 +2212,7 @@ var FrameProcessor = class {
|
|
|
2209
2212
|
...emb.embeddingModelId !== void 0 ? { embeddingModelId: emb.embeddingModelId } : {}
|
|
2210
2213
|
} : {},
|
|
2211
2214
|
...faceBbox !== void 0 ? { faceBbox } : {},
|
|
2215
|
+
...nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx } : {},
|
|
2212
2216
|
...faceAlignedCrop !== void 0 ? { faceAlignedCrop } : {},
|
|
2213
2217
|
...plate !== void 0 ? {
|
|
2214
2218
|
plateText: plate.text,
|
|
@@ -2345,6 +2349,10 @@ function buildTrackLifecyclePayload(input) {
|
|
|
2345
2349
|
...hasMedia ? { media } : {}
|
|
2346
2350
|
};
|
|
2347
2351
|
}
|
|
2352
|
+
//#endregion
|
|
2353
|
+
//#region src/pipeline-analytics/pipeline/edge-clear.ts
|
|
2354
|
+
/** Default border tolerance — 1% of each dimension. */
|
|
2355
|
+
var DEFAULT_EDGE_TOLERANCE = .01;
|
|
2348
2356
|
/**
|
|
2349
2357
|
* True when `bbox` sits fully inside the frame — no side within the tolerance
|
|
2350
2358
|
* band of any border. Degenerate/unknown frame dims (≤ 0) return true so the
|
|
@@ -2390,6 +2398,104 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2390
2398
|
const dist = Math.hypot(dx, dy) / Math.SQRT2;
|
|
2391
2399
|
return Math.max(0, Math.min(1, 1 - dist));
|
|
2392
2400
|
}
|
|
2401
|
+
/** Maximum fraction of a candidate's score a SINGLE fully-spanning clipped side
|
|
2402
|
+
* can subtract (the edge-containment penalty). A clipped side that spans the
|
|
2403
|
+
* whole of its border removes up to this much; a side that only grazes the
|
|
2404
|
+
* border (small perpendicular span) removes proportionally less. 0.5 = a
|
|
2405
|
+
* subject flush against — and filling — one border loses half its effective
|
|
2406
|
+
* score, so a fully-contained near-equal detection outranks it, while a
|
|
2407
|
+
* DECISIVELY higher-confidence clipped detection still wins. */
|
|
2408
|
+
var CONTAINMENT_MAX_SIDE_PENALTY = .5;
|
|
2409
|
+
/** Floor on the containment factor — a subject clipped on several borders never
|
|
2410
|
+
* drops to zero (which would annihilate its score and could leave a track with
|
|
2411
|
+
* NO best frame at all). */
|
|
2412
|
+
var CONTAINMENT_MIN = .1;
|
|
2413
|
+
/**
|
|
2414
|
+
* Edge-CONTAINMENT factor for a (clamped) bbox: an estimate in
|
|
2415
|
+
* `[CONTAINMENT_MIN, 1]` of the fraction of the subject that is actually in
|
|
2416
|
+
* frame. 1 = fully contained (no border clipping); lower = the subject is
|
|
2417
|
+
* flush against one or more borders and is probably truncated (half of a person
|
|
2418
|
+
* walking out of view).
|
|
2419
|
+
*
|
|
2420
|
+
* Because detection bboxes are already CLAMPED to the frame, the true off-screen
|
|
2421
|
+
* extent is unknown, so this is a purely-geometric proxy: for each border the
|
|
2422
|
+
* bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band), the penalty is
|
|
2423
|
+
* scaled by how much of that border the bbox spans on the perpendicular axis —
|
|
2424
|
+
* i.e. the subject's aspect against that edge. A wide bbox flush to the
|
|
2425
|
+
* top/bottom (a wide subject cut off top/bottom) or a tall bbox flush to the
|
|
2426
|
+
* left/right (a tall subject cut off at the side) is heavily penalised, whereas
|
|
2427
|
+
* a narrow subject whose feet merely graze the bottom border keeps most of its
|
|
2428
|
+
* score. Degenerate/unknown dims (≤ 0) return 1 (neutral — matches
|
|
2429
|
+
* {@link isEdgeClear}).
|
|
2430
|
+
*/
|
|
2431
|
+
function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE_TOLERANCE) {
|
|
2432
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2433
|
+
const tolX = tolerance * frameWidth;
|
|
2434
|
+
const tolY = tolerance * frameHeight;
|
|
2435
|
+
const left = bbox.x;
|
|
2436
|
+
const top = bbox.y;
|
|
2437
|
+
const right = bbox.x + bbox.w;
|
|
2438
|
+
const bottom = bbox.y + bbox.h;
|
|
2439
|
+
const spanX = Math.max(0, Math.min(1, bbox.w / frameWidth));
|
|
2440
|
+
const spanY = Math.max(0, Math.min(1, bbox.h / frameHeight));
|
|
2441
|
+
let containment = 1;
|
|
2442
|
+
const penalize = (span) => {
|
|
2443
|
+
containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * span;
|
|
2444
|
+
};
|
|
2445
|
+
if (left <= tolX) penalize(spanY);
|
|
2446
|
+
if (right >= frameWidth - tolX) penalize(spanY);
|
|
2447
|
+
if (top <= tolY) penalize(spanX);
|
|
2448
|
+
if (bottom >= frameHeight - tolY) penalize(spanX);
|
|
2449
|
+
return Math.max(CONTAINMENT_MIN, Math.min(1, containment));
|
|
2450
|
+
}
|
|
2451
|
+
/** Fraction of the frame area at which a subject's size score saturates to 1.
|
|
2452
|
+
* 10% of the frame is already a large, close subject; anything bigger gains
|
|
2453
|
+
* no further preference (and the plausibility gate rejects exploded boxes). */
|
|
2454
|
+
var SIZE_SCORE_SATURATION_AREA_FRAC = .1;
|
|
2455
|
+
/** A candidate whose effective score beats the held peak by at least this
|
|
2456
|
+
* RATIO is a DECISIVE improvement: it bypasses the capture rate limit
|
|
2457
|
+
* (`minGapMs`) the way a tier upgrade does. Drive-through subjects hit their
|
|
2458
|
+
* best framing 1–3s after birth — exactly inside the rate-limit window — and
|
|
2459
|
+
* the "better shot" otherwise never gets captured (2026-07-22 audit:
|
|
2460
|
+
* BEST_SHOT_STALE on 9/63 tracks with 1.5–3.8× larger in-frame views). */
|
|
2461
|
+
var DECISIVE_IMPROVEMENT_RATIO = 1.25;
|
|
2462
|
+
/**
|
|
2463
|
+
* Subject-size score for the best-frame ranking: sqrt of the bbox's frame-area
|
|
2464
|
+
* fraction, saturating at {@link SIZE_SCORE_SATURATION_AREA_FRAC}. A bigger,
|
|
2465
|
+
* closer subject is a better SHOT even at equal detector confidence — raw
|
|
2466
|
+
* confidence does not correlate with human-perceived crop quality (a distant
|
|
2467
|
+
* 20px person can out-score a full-frame close-up). sqrt softens the term so
|
|
2468
|
+
* confidence still matters between similar sizes. Degenerate dims → 1
|
|
2469
|
+
* (neutral), matching {@link isEdgeClear}.
|
|
2470
|
+
*/
|
|
2471
|
+
function bboxSizeScore(bbox, frameWidth, frameHeight) {
|
|
2472
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2473
|
+
const areaFrac = bbox.w * bbox.h / (frameWidth * frameHeight);
|
|
2474
|
+
return Math.sqrt(Math.max(0, Math.min(1, areaFrac / SIZE_SCORE_SATURATION_AREA_FRAC)));
|
|
2475
|
+
}
|
|
2476
|
+
/** Floor of the size factor's influence: the raw sizeScore (0..1) is mapped to
|
|
2477
|
+
* `[SIZE_FACTOR_FLOOR, 1]` before scaling the confidence, so a tiny subject
|
|
2478
|
+
* halves its effective score at most. Keeps the effective scale comparable to
|
|
2479
|
+
* raw confidence — the ABSOLUTE hysteresis margin stays meaningful — while a
|
|
2480
|
+
* bigger view still earns up to a 2× relative preference. */
|
|
2481
|
+
var SIZE_FACTOR_FLOOR = .5;
|
|
2482
|
+
/** The effective within-tier score: detector confidence scaled by the
|
|
2483
|
+
* edge-containment and (floored) subject-size factors — each neutral at 1
|
|
2484
|
+
* when absent. */
|
|
2485
|
+
function effectiveScore(c) {
|
|
2486
|
+
const sizeFactor = c.sizeScore === void 0 ? 1 : SIZE_FACTOR_FLOOR + (1 - SIZE_FACTOR_FLOOR) * c.sizeScore;
|
|
2487
|
+
return c.confidence * (c.containment ?? 1) * sizeFactor;
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* True when `candidate` beats `current` DECISIVELY — same-or-better edge tier
|
|
2491
|
+
* AND an effective score at least {@link DECISIVE_IMPROVEMENT_RATIO}× the held
|
|
2492
|
+
* peak's. Used by `BestDetectionTracker` to bypass the capture rate limit
|
|
2493
|
+
* (`minGapMs`) for genuinely better framings that land inside the rate window.
|
|
2494
|
+
*/
|
|
2495
|
+
function isDecisiveImprovement(current, candidate) {
|
|
2496
|
+
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2497
|
+
return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
|
|
2498
|
+
}
|
|
2393
2499
|
/**
|
|
2394
2500
|
* Edge-aware "is `candidate` a new best over `current`?" comparator.
|
|
2395
2501
|
*
|
|
@@ -2398,8 +2504,18 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2398
2504
|
* confidence past the `hysteresis` margin wins. The tier upgrade
|
|
2399
2505
|
* (touching → clear) bypasses hysteresis — the first clear frame is always taken.
|
|
2400
2506
|
*
|
|
2507
|
+
* EDGE-CONTAINMENT (#edge-clip): within the same tier the comparison is on the
|
|
2508
|
+
* EFFECTIVE score `confidence × containment` (see {@link bboxContainment}), not
|
|
2509
|
+
* raw confidence — so a fully-contained detection outranks a larger/higher-
|
|
2510
|
+
* confidence but border-CLIPPED one unless the confidence margin is decisive.
|
|
2511
|
+
* The binary edge-clear tier still gates first (a whole subject beats a clipped
|
|
2512
|
+
* one regardless), and within the CLEAR tier both frames have containment 1, so
|
|
2513
|
+
* this is a no-op there; it discriminates WITHIN the touching tier (feet merely
|
|
2514
|
+
* grazing the bottom vs half the body out the side). `containment` defaults to
|
|
2515
|
+
* 1 when omitted (legacy face / object-embedding callers) → identical behaviour.
|
|
2516
|
+
*
|
|
2401
2517
|
* CENTERING TIE-BREAK (#27-D): within the same tier, when neither frame clearly
|
|
2402
|
-
* wins on
|
|
2518
|
+
* wins on the effective score (the two are within the `hysteresis` band) but the
|
|
2403
2519
|
* candidate is meaningfully better CENTRED ({@link CENTER_TIE_BREAK_MARGIN}), the
|
|
2404
2520
|
* candidate wins. This only engages when both sides carry a `centerScore` (the
|
|
2405
2521
|
* best-frame path), so low-importance short tracks stop keeping an edge-of-frame
|
|
@@ -2412,9 +2528,11 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2412
2528
|
function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
2413
2529
|
if (candidate.edgeClear && !current.edgeClear) return true;
|
|
2414
2530
|
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2415
|
-
|
|
2531
|
+
const curEffective = effectiveScore(current);
|
|
2532
|
+
const candEffective = effectiveScore(candidate);
|
|
2533
|
+
if (candEffective > curEffective + hysteresis) return true;
|
|
2416
2534
|
if (candidate.centerScore !== void 0 && current.centerScore !== void 0) {
|
|
2417
|
-
if (Math.abs(
|
|
2535
|
+
if (Math.abs(candEffective - curEffective) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
|
|
2418
2536
|
}
|
|
2419
2537
|
return false;
|
|
2420
2538
|
}
|
|
@@ -2456,6 +2574,14 @@ var BestDetectionTracker = class {
|
|
|
2456
2574
|
* does not supply centering (face / object-embedding paths) → the centering
|
|
2457
2575
|
* tie-break is disabled and the legacy confidence policy applies. */
|
|
2458
2576
|
centerScore = /* @__PURE__ */ new Map();
|
|
2577
|
+
/** Held peak's edge-containment factor (0..1), PARALLEL to `best`. Absent =
|
|
2578
|
+
* the caller does not supply containment (face / object-embedding paths) →
|
|
2579
|
+
* the within-tier comparison stays on raw confidence (containment treated as
|
|
2580
|
+
* 1). */
|
|
2581
|
+
containment = /* @__PURE__ */ new Map();
|
|
2582
|
+
/** Held peak's subject-size score (0..1), PARALLEL to `best`. Absent = the
|
|
2583
|
+
* caller does not supply size (face / object-embedding paths) → neutral. */
|
|
2584
|
+
sizeScore = /* @__PURE__ */ new Map();
|
|
2459
2585
|
constructor(options = {}) {
|
|
2460
2586
|
this.hysteresis = options.hysteresis ?? 0;
|
|
2461
2587
|
this.minGapMs = options.minGapMs ?? 0;
|
|
@@ -2472,7 +2598,7 @@ var BestDetectionTracker = class {
|
|
|
2472
2598
|
* the classic policy holds: a confidence past the `hysteresis` margin that also
|
|
2473
2599
|
* respects `minGapMs` wins. On acceptance the held peak advances.
|
|
2474
2600
|
*/
|
|
2475
|
-
observe(trackId, confidence, timestamp, edgeClear, centerScore) {
|
|
2601
|
+
observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore) {
|
|
2476
2602
|
const cur = this.best.get(trackId);
|
|
2477
2603
|
if (cur === void 0) {
|
|
2478
2604
|
this.best.set(trackId, {
|
|
@@ -2481,20 +2607,32 @@ var BestDetectionTracker = class {
|
|
|
2481
2607
|
});
|
|
2482
2608
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2483
2609
|
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2610
|
+
if (containment !== void 0) this.containment.set(trackId, containment);
|
|
2611
|
+
if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
|
|
2484
2612
|
return true;
|
|
2485
2613
|
}
|
|
2486
2614
|
const curClear = this.edgeClear.get(trackId) ?? true;
|
|
2487
2615
|
const candClear = edgeClear ?? true;
|
|
2488
2616
|
const curCenter = this.centerScore.get(trackId);
|
|
2489
|
-
const
|
|
2617
|
+
const curContainment = this.containment.get(trackId);
|
|
2618
|
+
const curSize = this.sizeScore.get(trackId);
|
|
2619
|
+
const held = {
|
|
2490
2620
|
confidence: cur.confidence,
|
|
2491
2621
|
edgeClear: curClear,
|
|
2492
|
-
...curCenter !== void 0 ? { centerScore: curCenter } : {}
|
|
2493
|
-
|
|
2622
|
+
...curCenter !== void 0 ? { centerScore: curCenter } : {},
|
|
2623
|
+
...curContainment !== void 0 ? { containment: curContainment } : {},
|
|
2624
|
+
...curSize !== void 0 ? { sizeScore: curSize } : {}
|
|
2625
|
+
};
|
|
2626
|
+
const cand = {
|
|
2494
2627
|
confidence,
|
|
2495
2628
|
edgeClear: candClear,
|
|
2496
|
-
...centerScore !== void 0 ? { centerScore } : {}
|
|
2497
|
-
|
|
2629
|
+
...centerScore !== void 0 ? { centerScore } : {},
|
|
2630
|
+
...containment !== void 0 ? { containment } : {},
|
|
2631
|
+
...sizeScore !== void 0 ? { sizeScore } : {}
|
|
2632
|
+
};
|
|
2633
|
+
const tierUpgrade = candClear && !curClear;
|
|
2634
|
+
const gapOk = timestamp - cur.atMs >= this.minGapMs || sizeScore !== void 0 && isDecisiveImprovement(held, cand);
|
|
2635
|
+
const isNewBest = tierUpgrade ? true : isEdgeAwareNewBest(held, cand, this.hysteresis) && gapOk;
|
|
2498
2636
|
if (isNewBest) {
|
|
2499
2637
|
this.best.set(trackId, {
|
|
2500
2638
|
confidence,
|
|
@@ -2502,6 +2640,8 @@ var BestDetectionTracker = class {
|
|
|
2502
2640
|
});
|
|
2503
2641
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2504
2642
|
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2643
|
+
if (containment !== void 0) this.containment.set(trackId, containment);
|
|
2644
|
+
if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
|
|
2505
2645
|
}
|
|
2506
2646
|
return isNewBest;
|
|
2507
2647
|
}
|
|
@@ -2514,14 +2654,44 @@ var BestDetectionTracker = class {
|
|
|
2514
2654
|
this.best.delete(trackId);
|
|
2515
2655
|
this.edgeClear.delete(trackId);
|
|
2516
2656
|
this.centerScore.delete(trackId);
|
|
2657
|
+
this.containment.delete(trackId);
|
|
2658
|
+
this.sizeScore.delete(trackId);
|
|
2517
2659
|
}
|
|
2518
2660
|
clear() {
|
|
2519
2661
|
this.best.clear();
|
|
2520
2662
|
this.edgeClear.clear();
|
|
2521
2663
|
this.centerScore.clear();
|
|
2664
|
+
this.containment.clear();
|
|
2665
|
+
this.sizeScore.clear();
|
|
2522
2666
|
}
|
|
2523
2667
|
};
|
|
2524
2668
|
//#endregion
|
|
2669
|
+
//#region src/pipeline-analytics/pipeline/last-frame-promotion.ts
|
|
2670
|
+
/**
|
|
2671
|
+
* Decide whether a closing track's newest `snapshot` should be promoted to be
|
|
2672
|
+
* its `lastFrame`. Pure — see the module header for the contract.
|
|
2673
|
+
*
|
|
2674
|
+
* Promote when there is at least one `snapshot` AND either there is no
|
|
2675
|
+
* `lastFrame` yet, or the newest snapshot is strictly newer than the held
|
|
2676
|
+
* `lastFrame`. Otherwise keep the current behaviour (no promotion).
|
|
2677
|
+
*/
|
|
2678
|
+
function decideLastFramePromotion(media) {
|
|
2679
|
+
let newestSnapshot;
|
|
2680
|
+
let lastFrame;
|
|
2681
|
+
for (const m of media) if (m.kind === "snapshot") {
|
|
2682
|
+
if (newestSnapshot === void 0 || m.timestamp > newestSnapshot.timestamp) newestSnapshot = m;
|
|
2683
|
+
} else if (m.kind === "lastFrame") {
|
|
2684
|
+
if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
|
|
2685
|
+
}
|
|
2686
|
+
if (newestSnapshot === void 0) return { promote: false };
|
|
2687
|
+
if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return { promote: false };
|
|
2688
|
+
return {
|
|
2689
|
+
promote: true,
|
|
2690
|
+
snapshotKey: newestSnapshot.key,
|
|
2691
|
+
snapshotTimestamp: newestSnapshot.timestamp
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
//#endregion
|
|
2525
2695
|
//#region src/pipeline-analytics/pipeline/track-best-detection.ts
|
|
2526
2696
|
/**
|
|
2527
2697
|
* `TrackBestSelector` — the ONE unified "best detection per track" primitive.
|
|
@@ -4686,6 +4856,31 @@ var MediaStore = class {
|
|
|
4686
4856
|
return newKey;
|
|
4687
4857
|
}
|
|
4688
4858
|
/**
|
|
4859
|
+
* Promote an existing `snapshot` blob to be the track's single `lastFrame`
|
|
4860
|
+
* (operator "genuinely-last view" policy, see `last-frame-promotion.ts`).
|
|
4861
|
+
*
|
|
4862
|
+
* Both kinds share the SAME 960-boxed rendering, so promotion re-writes the
|
|
4863
|
+
* snapshot's bytes into the single-instance `lastFrame` slot (`putReplacing`,
|
|
4864
|
+
* which overwrites any held `lastFrame` blob + row) and then removes the
|
|
4865
|
+
* promoted `snapshot` row + blob — so there is exactly ONE genuinely-last view
|
|
4866
|
+
* and no duplicate snapshot/lastFrame pair. The `lastFrame` write lands BEFORE
|
|
4867
|
+
* the snapshot delete, so the view is never momentarily absent. Returns the new
|
|
4868
|
+
* `lastFrame` key.
|
|
4869
|
+
*/
|
|
4870
|
+
async promoteToLastFrame(input) {
|
|
4871
|
+
const data = Buffer.from(input.snapshot.base64, "base64");
|
|
4872
|
+
const newKey = await this.putReplacing({
|
|
4873
|
+
deviceId: input.deviceId,
|
|
4874
|
+
ownerKind: "track",
|
|
4875
|
+
ownerId: input.trackId,
|
|
4876
|
+
kind: "lastFrame",
|
|
4877
|
+
timestamp: input.snapshot.timestamp,
|
|
4878
|
+
data
|
|
4879
|
+
});
|
|
4880
|
+
await this.deleteByKey(input.snapshot.key);
|
|
4881
|
+
return newKey;
|
|
4882
|
+
}
|
|
4883
|
+
/**
|
|
4689
4884
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
4690
4885
|
* found in the index or if the blob is missing from storage.
|
|
4691
4886
|
*/
|
|
@@ -6563,37 +6758,121 @@ function squareSubjectCropRegion(bbox, frame) {
|
|
|
6563
6758
|
};
|
|
6564
6759
|
}
|
|
6565
6760
|
/**
|
|
6566
|
-
*
|
|
6567
|
-
*
|
|
6568
|
-
*
|
|
6569
|
-
*
|
|
6570
|
-
*
|
|
6571
|
-
*
|
|
6761
|
+
* Compute the 16:9 central-square window layout for a subject bbox.
|
|
6762
|
+
*
|
|
6763
|
+
* Algorithm:
|
|
6764
|
+
* 1. central square `C = squareSubjectCropRegion(bbox, frame)` — the
|
|
6765
|
+
* subject-containing, frame-clamped square of side `c` (its contract also
|
|
6766
|
+
* handles the "subject bigger than the short edge / scale the window up"
|
|
6767
|
+
* cases). `C` becomes the middle square of the output.
|
|
6768
|
+
* 2. canvas: `canvasW = round(c × 16/9)`, `canvasH = c`.
|
|
6769
|
+
* 3. anchor: place the canvas so `C` is its horizontal middle →
|
|
6770
|
+
* `frameOriginX = C.x − (canvasW − c)/2` (frame-x mapping to canvas-x 0).
|
|
6771
|
+
* 4. in-frame slab: intersect `[frameOriginX, frameOriginX + canvasW]` with
|
|
6772
|
+
* `[0, W]`; the slab is that intersection at full height `c` (C ⊆ frame ⇒
|
|
6773
|
+
* the vertical extent is always in-frame). Its canvas offset is
|
|
6774
|
+
* `slabOffsetX = fxa − frameOriginX ≥ 0`; anything outside is padding.
|
|
6572
6775
|
*/
|
|
6573
|
-
function
|
|
6574
|
-
const
|
|
6776
|
+
function wideCentralSquareLayout(bbox, frame) {
|
|
6777
|
+
const central = squareSubjectCropRegion(bbox, frame);
|
|
6778
|
+
const c = central.w;
|
|
6779
|
+
const canvasW = Math.round(c * 16 / 9);
|
|
6780
|
+
const centralX0 = Math.round((canvasW - c) / 2);
|
|
6781
|
+
let frameOriginX = central.x - (canvasW - c) / 2;
|
|
6782
|
+
if (canvasW <= frame.W) {
|
|
6783
|
+
const slideRightMax = Math.max(0, bbox.x - central.x);
|
|
6784
|
+
const slideLeftMax = Math.max(0, central.x + c - (bbox.x + bbox.w));
|
|
6785
|
+
if (frameOriginX < 0) frameOriginX += Math.min(-frameOriginX, slideRightMax);
|
|
6786
|
+
const overRight = frameOriginX + canvasW - frame.W;
|
|
6787
|
+
if (overRight > 0) frameOriginX -= Math.min(overRight, slideLeftMax);
|
|
6788
|
+
} else frameOriginX = (frame.W - canvasW) / 2;
|
|
6789
|
+
const fxa = Math.max(0, frameOriginX);
|
|
6790
|
+
const slabW = Math.min(frame.W, frameOriginX + canvasW) - fxa;
|
|
6791
|
+
const slabOffsetX = fxa - frameOriginX;
|
|
6575
6792
|
return {
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6793
|
+
fetch: {
|
|
6794
|
+
x: fxa / frame.W,
|
|
6795
|
+
y: central.y / frame.H,
|
|
6796
|
+
w: slabW / frame.W,
|
|
6797
|
+
h: c / frame.H
|
|
6798
|
+
},
|
|
6799
|
+
canvasW,
|
|
6800
|
+
canvasH: c,
|
|
6801
|
+
slabOffsetX,
|
|
6802
|
+
slabW,
|
|
6803
|
+
slabH: c,
|
|
6804
|
+
centralX0,
|
|
6805
|
+
centralSide: c,
|
|
6806
|
+
frameOriginX
|
|
6580
6807
|
};
|
|
6581
6808
|
}
|
|
6809
|
+
//#endregion
|
|
6810
|
+
//#region src/shared/frame/subject-crop-variants.ts
|
|
6582
6811
|
/**
|
|
6583
|
-
*
|
|
6584
|
-
*
|
|
6585
|
-
*
|
|
6586
|
-
*
|
|
6587
|
-
*
|
|
6812
|
+
* Best-shot subject-crop variants (best-crop fast-load, 2026-07-21; 16:9
|
|
6813
|
+
* central-square reframe, 2026-07-22).
|
|
6814
|
+
*
|
|
6815
|
+
* ONE native fetch (the 16:9 central-square WINDOW — see
|
|
6816
|
+
* {@link wide-central-square-crop}) backs both persisted variants:
|
|
6817
|
+
* - `thumbnail` — the native uncapped 16:9 window JPEG (subject in the
|
|
6818
|
+
* central square, lateral scene context, never upscaled).
|
|
6819
|
+
* - `thumbnailSmall` — the SAME window downscaled to {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH}
|
|
6820
|
+
* long side (854×480), the reel / lists / grid fast-load
|
|
6821
|
+
* representative.
|
|
6822
|
+
*
|
|
6823
|
+
* The small variant is DERIVED from the already-composed native window — never a
|
|
6824
|
+
* second native round-trip and NEVER upscaled: if the window is already ≤ 854 on
|
|
6825
|
+
* its long side it is stored AS-IS (byte-identical). The window is composed from
|
|
6826
|
+
* the in-frame slab plus lateral letterbox padding for the (rare) part of the
|
|
6827
|
+
* 16:9 window that falls outside the frame at a horizontal edge.
|
|
6588
6828
|
*/
|
|
6589
|
-
|
|
6590
|
-
|
|
6829
|
+
/** Gaussian sigma for the blurred-scene letterbox fill. */
|
|
6830
|
+
var LETTERBOX_BLUR_SIGMA = 25;
|
|
6831
|
+
/** Brightness factor for the blurred fill — dimmed so the real slab reads as
|
|
6832
|
+
* the subject surface and the fill as ambience, never as sharp scene. */
|
|
6833
|
+
var LETTERBOX_BLUR_BRIGHTNESS = .55;
|
|
6834
|
+
/**
|
|
6835
|
+
* Compose the native-resolution 16:9 central-square WINDOW (`thumbnail`) from the
|
|
6836
|
+
* in-frame slab fetched for `layout`. The slab is placed at its computed offset
|
|
6837
|
+
* on a 16:9 canvas; any lateral part of the window outside the frame (a subject
|
|
6838
|
+
* flush against a frame edge — the geometry already slides the window in-frame
|
|
6839
|
+
* wherever the subject has slack) is filled with a BLURRED, dimmed stretch of
|
|
6840
|
+
* the slab itself instead of dead black bars (operator triage 2026-07-22).
|
|
6841
|
+
* When the whole window is in-frame (`slab` already spans the full canvas) the
|
|
6842
|
+
* slab is returned VERBATIM (no re-encode). NEVER upscales the real image —
|
|
6843
|
+
* the canvas is at the slab's native scale; only the out-of-frame ambience fill
|
|
6844
|
+
* is synthesized.
|
|
6845
|
+
*
|
|
6846
|
+
* `slab` must be the JPEG of `layout.fetch`; its native pixel dimensions set the
|
|
6847
|
+
* canvas scale (native px per source px = `slabNativeHeight / layout.slabH`).
|
|
6848
|
+
*/
|
|
6849
|
+
async function composeWideCentralSquareThumbnail(slab, layout) {
|
|
6850
|
+
const meta = await (0, sharp.default)(slab).metadata();
|
|
6851
|
+
const nativeW = meta.width ?? 0;
|
|
6852
|
+
const nativeH = meta.height ?? 0;
|
|
6853
|
+
if (nativeW <= 0 || nativeH <= 0 || layout.slabH <= 0) return slab;
|
|
6854
|
+
const scale = nativeH / layout.slabH;
|
|
6855
|
+
const canvasNativeW = Math.max(Math.round(layout.canvasW * scale), nativeW);
|
|
6856
|
+
const leftPad = Math.max(0, Math.round(layout.slabOffsetX * scale));
|
|
6857
|
+
const rightPad = Math.max(0, canvasNativeW - nativeW - leftPad);
|
|
6858
|
+
if (leftPad === 0 && rightPad === 0) return slab;
|
|
6859
|
+
return (0, sharp.default)(await (0, sharp.default)(slab).resize(canvasNativeW, nativeH, { fit: "fill" }).blur(LETTERBOX_BLUR_SIGMA).modulate({ brightness: LETTERBOX_BLUR_BRIGHTNESS }).toBuffer()).composite([{
|
|
6860
|
+
input: slab,
|
|
6861
|
+
left: leftPad,
|
|
6862
|
+
top: 0
|
|
6863
|
+
}]).jpeg({ quality: 88 }).toBuffer();
|
|
6864
|
+
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Derive the `thumbnailSmall` JPEG from the composed native 16:9 window.
|
|
6867
|
+
* Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (854) on the long
|
|
6868
|
+
* side (aspect preserved → ~854×480). If the window's long side is already ≤ the
|
|
6869
|
+
* cap, the ORIGINAL buffer is returned unchanged (store as-is, never upscale).
|
|
6870
|
+
*/
|
|
6871
|
+
async function deriveThumbnailSmall(nativeWideJpeg) {
|
|
6872
|
+
const meta = await (0, sharp.default)(nativeWideJpeg).metadata();
|
|
6591
6873
|
const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
6592
|
-
if (longSide > 0 && longSide <=
|
|
6593
|
-
return (0, sharp.default)(
|
|
6594
|
-
fit: "inside",
|
|
6595
|
-
withoutEnlargement: true
|
|
6596
|
-
}).jpeg({ quality: 88 }).toBuffer();
|
|
6874
|
+
if (longSide > 0 && longSide <= 854) return nativeWideJpeg;
|
|
6875
|
+
return (0, sharp.default)(nativeWideJpeg).resize(854, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
|
|
6597
6876
|
}
|
|
6598
6877
|
//#endregion
|
|
6599
6878
|
//#region src/shared/frame/box-drawer.ts
|
|
@@ -6676,6 +6955,9 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
|
|
|
6676
6955
|
* upscale).
|
|
6677
6956
|
*/
|
|
6678
6957
|
var SNAPSHOT_MAX_WIDTH = 960;
|
|
6958
|
+
/** Bound the once-per-owner display-fallback memo so a long-lived process never
|
|
6959
|
+
* leaks it. */
|
|
6960
|
+
var MAX_LOGGED_DISPLAY_FALLBACKS = 5e3;
|
|
6679
6961
|
/**
|
|
6680
6962
|
* Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
|
|
6681
6963
|
* ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
|
|
@@ -6755,6 +7037,9 @@ var EventMediaDispatcher = class {
|
|
|
6755
7037
|
* {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
|
|
6756
7038
|
*/
|
|
6757
7039
|
sharedNativeFullFrame;
|
|
7040
|
+
/** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
|
|
7041
|
+
* info log — see {@link logDisplayFallbackOnce}. */
|
|
7042
|
+
loggedDisplayFallbacks = /* @__PURE__ */ new Set();
|
|
6758
7043
|
constructor(deps) {
|
|
6759
7044
|
this.deps = deps;
|
|
6760
7045
|
this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
|
|
@@ -6821,7 +7106,7 @@ var EventMediaDispatcher = class {
|
|
|
6821
7106
|
return empty;
|
|
6822
7107
|
}
|
|
6823
7108
|
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
|
|
6824
|
-
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
7109
|
+
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
|
|
6825
7110
|
const firstFrameTrackIds = [];
|
|
6826
7111
|
for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
|
|
6827
7112
|
const storedSnapshots = [];
|
|
@@ -6952,56 +7237,89 @@ var EventMediaDispatcher = class {
|
|
|
6952
7237
|
};
|
|
6953
7238
|
}
|
|
6954
7239
|
/**
|
|
6955
|
-
* Clean subject-centered crop of `bbox`
|
|
6956
|
-
*
|
|
6957
|
-
*
|
|
7240
|
+
* Clean subject-centered crop of `bbox` for the DISPLAY child media
|
|
7241
|
+
* (`faceCrop`/`plateCrop`): the square-safe 16:9 region around the bbox with NO
|
|
7242
|
+
* box drawn.
|
|
6958
7243
|
*
|
|
6959
|
-
*
|
|
6960
|
-
*
|
|
6961
|
-
*
|
|
6962
|
-
*
|
|
6963
|
-
*
|
|
6964
|
-
*
|
|
6965
|
-
*
|
|
7244
|
+
* These are operator gallery images, NOT model inputs (the spec's native-source
|
|
7245
|
+
* hard rule governs model inputs, which stay native-strict). So a native-ROI
|
|
7246
|
+
* miss degrades gracefully — always honest, NEVER upscaled:
|
|
7247
|
+
* 1. native ROI crop (the runner's retained native surface) — best quality;
|
|
7248
|
+
* 2. the retained full frame (keyframe-native or the runner's ≤640 RAM tier)
|
|
7249
|
+
* cropped to the SAME region;
|
|
7250
|
+
* 3. the already-resolved ≤640 detection raster (`frameData`, in hand) cropped
|
|
7251
|
+
* to the region — the last-resort honest sub-native preview.
|
|
7252
|
+
* Only a total miss (all three unavailable) returns `null`. A fallback is
|
|
7253
|
+
* logged ONCE per owner (`${eventId}:${kind}`) at info level. Runs inside the
|
|
6966
7254
|
* live-handle window opened by `captureForFrame`.
|
|
6967
7255
|
*/
|
|
6968
|
-
async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
|
|
6969
|
-
|
|
6970
|
-
|
|
7256
|
+
async cropSubjectRegion(frameHandle, frameData, fw, fh, bbox, cropPadding, ownerId) {
|
|
7257
|
+
const norm = squareSafeCropRegionNormalized(bbox, {
|
|
7258
|
+
W: fw,
|
|
7259
|
+
H: fh
|
|
7260
|
+
}, cropPadding);
|
|
7261
|
+
if (this.deps.getNativeCropJpeg) try {
|
|
7262
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
7263
|
+
if (native) return native;
|
|
7264
|
+
} catch (err) {
|
|
7265
|
+
this.deps.logger.debug("display child crop: native ROI fetch threw", { meta: {
|
|
6971
7266
|
shmId: frameHandle.shmId,
|
|
6972
|
-
|
|
7267
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6973
7268
|
} });
|
|
6974
|
-
return null;
|
|
6975
7269
|
}
|
|
6976
7270
|
try {
|
|
6977
|
-
const
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
7271
|
+
const full = await this.sharedNativeFullFrame(frameHandle);
|
|
7272
|
+
if (full && full.format === "rgb" && full.width > 0 && full.height > 0) {
|
|
7273
|
+
const { crop } = await extractCrop(full.data, full.width, full.height, norm);
|
|
7274
|
+
this.logDisplayFallbackOnce(ownerId, "native-full-frame");
|
|
7275
|
+
return crop;
|
|
7276
|
+
}
|
|
7277
|
+
} catch (err) {
|
|
7278
|
+
this.deps.logger.debug("display child crop: full-frame fallback threw", { meta: {
|
|
6984
7279
|
shmId: frameHandle.shmId,
|
|
6985
|
-
|
|
7280
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6986
7281
|
} });
|
|
6987
|
-
|
|
7282
|
+
}
|
|
7283
|
+
try {
|
|
7284
|
+
const { crop } = await extractCrop(frameData, fw, fh, norm);
|
|
7285
|
+
this.logDisplayFallbackOnce(ownerId, "detection-raster");
|
|
7286
|
+
return crop;
|
|
6988
7287
|
} catch (err) {
|
|
6989
|
-
this.deps.logger.warn("
|
|
7288
|
+
this.deps.logger.warn("display child crop: all sources missed", { meta: {
|
|
6990
7289
|
shmId: frameHandle.shmId,
|
|
6991
7290
|
error: err instanceof Error ? err.message : String(err)
|
|
6992
7291
|
} });
|
|
6993
7292
|
return null;
|
|
6994
7293
|
}
|
|
6995
7294
|
}
|
|
7295
|
+
/** Info-log a DISPLAY-crop fallback ONCE per owner (`${eventId}:${kind}`) so a
|
|
7296
|
+
* short native window is visible without flooding the log every frame. */
|
|
7297
|
+
logDisplayFallbackOnce(ownerId, reason) {
|
|
7298
|
+
if (this.loggedDisplayFallbacks.has(ownerId)) return;
|
|
7299
|
+
if (this.loggedDisplayFallbacks.size >= MAX_LOGGED_DISPLAY_FALLBACKS) this.loggedDisplayFallbacks.clear();
|
|
7300
|
+
this.loggedDisplayFallbacks.add(ownerId);
|
|
7301
|
+
this.deps.logger.info("display child crop fallback used (honest sub-native, not upscaled)", { meta: {
|
|
7302
|
+
ownerId,
|
|
7303
|
+
reason
|
|
7304
|
+
} });
|
|
7305
|
+
}
|
|
6996
7306
|
/**
|
|
6997
7307
|
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6998
|
-
* fast-load, 2026-07-21
|
|
6999
|
-
*
|
|
7000
|
-
*
|
|
7001
|
-
*
|
|
7002
|
-
*
|
|
7003
|
-
*
|
|
7004
|
-
*
|
|
7308
|
+
* fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22). ONE native
|
|
7309
|
+
* fetch: the in-frame slab of the 16:9 central-square WINDOW
|
|
7310
|
+
* ({@link wideCentralSquareLayout} — a `side × 16/9` window whose middle square
|
|
7311
|
+
* of side `max(w,h)×1.2` fully contains the subject, clamped/anchored so the
|
|
7312
|
+
* subject stays in that central square at every frame edge) is requested from
|
|
7313
|
+
* the runner's retained native surface with NO `maxWidth` (uncapped TRUE
|
|
7314
|
+
* native). It is composed into the full 16:9 window (lateral letterbox only for
|
|
7315
|
+
* the part outside the frame) → the `thumbnail`. The `thumbnailSmall` is
|
|
7316
|
+
* DERIVED by downscaling that SAME window to the 854 long-side cap (854×480) —
|
|
7317
|
+
* never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
|
|
7318
|
+
* returns the window as-is when it is already ≤ 854).
|
|
7319
|
+
*
|
|
7320
|
+
* Both variants share the SAME framing + central-square containment guarantee:
|
|
7321
|
+
* wide consumers use the 16:9 image as-is (subject centered, no cut); square
|
|
7322
|
+
* consumers center-crop the middle square (subject always inside it).
|
|
7005
7323
|
*
|
|
7006
7324
|
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
7007
7325
|
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
@@ -7017,21 +7335,22 @@ var EventMediaDispatcher = class {
|
|
|
7017
7335
|
return null;
|
|
7018
7336
|
}
|
|
7019
7337
|
try {
|
|
7020
|
-
const
|
|
7338
|
+
const layout = wideCentralSquareLayout(bbox, {
|
|
7021
7339
|
W: fw,
|
|
7022
7340
|
H: fh
|
|
7023
7341
|
});
|
|
7024
|
-
const
|
|
7025
|
-
if (!
|
|
7342
|
+
const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch);
|
|
7343
|
+
if (!slab) {
|
|
7026
7344
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
7027
7345
|
shmId: frameHandle.shmId,
|
|
7028
7346
|
reason: "native-miss"
|
|
7029
7347
|
} });
|
|
7030
7348
|
return null;
|
|
7031
7349
|
}
|
|
7350
|
+
const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
|
|
7032
7351
|
return {
|
|
7033
|
-
thumbnail
|
|
7034
|
-
thumbnailSmall: await deriveThumbnailSmall(
|
|
7352
|
+
thumbnail,
|
|
7353
|
+
thumbnailSmall: await deriveThumbnailSmall(thumbnail)
|
|
7035
7354
|
};
|
|
7036
7355
|
} catch (err) {
|
|
7037
7356
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
@@ -7120,9 +7439,9 @@ var EventMediaDispatcher = class {
|
|
|
7120
7439
|
return false;
|
|
7121
7440
|
}
|
|
7122
7441
|
}
|
|
7123
|
-
async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
|
|
7442
|
+
async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
|
|
7124
7443
|
if (ev.childCrops) for (const child of ev.childCrops) try {
|
|
7125
|
-
const childCropData = await this.cropSubjectRegion(frameHandle, fw, fh, child.bbox, cropPadding);
|
|
7444
|
+
const childCropData = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, child.bbox, cropPadding, `${ev.eventId}:${child.kind}`);
|
|
7126
7445
|
if (!childCropData) continue;
|
|
7127
7446
|
await this.deps.mediaStore.put({
|
|
7128
7447
|
deviceId,
|
|
@@ -8950,7 +9269,8 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
8950
9269
|
*/
|
|
8951
9270
|
function planPeriodicMedia(input) {
|
|
8952
9271
|
const appendSnapshot = input.dueSnapshot;
|
|
8953
|
-
const
|
|
9272
|
+
const lastFrameInterval = input.lastFrameIntervalMs ?? input.intervalMs;
|
|
9273
|
+
const rollingLastFrame = input.saveThumbnails && input.now - input.lastFrameAt >= lastFrameInterval && !appendSnapshot;
|
|
8954
9274
|
const thumbnailLanded = input.thumbnailLanded ?? true;
|
|
8955
9275
|
return {
|
|
8956
9276
|
appendSnapshot,
|
|
@@ -8958,7 +9278,10 @@ function planPeriodicMedia(input) {
|
|
|
8958
9278
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8959
9279
|
};
|
|
8960
9280
|
}
|
|
8961
|
-
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9281
|
+
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9282
|
+
suppressMaxDurationMs: 1e3,
|
|
9283
|
+
nothingToShowMaxDurationMs: 1500
|
|
9284
|
+
};
|
|
8962
9285
|
/**
|
|
8963
9286
|
* Classify a closing track's persistence outcome. Pure — see the module header
|
|
8964
9287
|
* for the full contract.
|
|
@@ -8966,6 +9289,7 @@ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
|
|
|
8966
9289
|
function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
|
|
8967
9290
|
if (input.hasMedia) return "persist";
|
|
8968
9291
|
if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
|
|
9292
|
+
if (!input.hasRasterFallback && input.durationMs < config.nothingToShowMaxDurationMs) return "suppress";
|
|
8969
9293
|
return input.hasRasterFallback ? "raster-fallback" : "persist";
|
|
8970
9294
|
}
|
|
8971
9295
|
//#endregion
|
|
@@ -10141,6 +10465,7 @@ var FaceRecognizer = class {
|
|
|
10141
10465
|
embedding: input.embedding,
|
|
10142
10466
|
embeddingModelId: modelId,
|
|
10143
10467
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
10468
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {},
|
|
10144
10469
|
...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
|
|
10145
10470
|
};
|
|
10146
10471
|
await this.processFrame({
|
|
@@ -10156,7 +10481,7 @@ var FaceRecognizer = class {
|
|
|
10156
10481
|
}
|
|
10157
10482
|
async processFrame(input) {
|
|
10158
10483
|
const { settings } = input;
|
|
10159
|
-
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.
|
|
10484
|
+
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);
|
|
10160
10485
|
if (candidates.length === 0) return;
|
|
10161
10486
|
this.deps.logger.debug("face: frame candidates", {
|
|
10162
10487
|
tags: { deviceId: input.deviceId },
|
|
@@ -10194,7 +10519,7 @@ var FaceRecognizer = class {
|
|
|
10194
10519
|
let crop;
|
|
10195
10520
|
if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
|
|
10196
10521
|
else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
10197
|
-
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
10522
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding, c.trackId) ?? void 0;
|
|
10198
10523
|
} catch (err) {
|
|
10199
10524
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
10200
10525
|
tags: { deviceId: input.deviceId },
|
|
@@ -11725,7 +12050,7 @@ var PlateRecognizer = class {
|
|
|
11725
12050
|
if (held !== void 0 && input.score <= held.score) return;
|
|
11726
12051
|
let crop;
|
|
11727
12052
|
if (input.frameHandle !== void 0) try {
|
|
11728
|
-
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
12053
|
+
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId) ?? void 0;
|
|
11729
12054
|
} catch (err) {
|
|
11730
12055
|
this.deps.logger.debug("PlateRecognizer crop capture failed", {
|
|
11731
12056
|
tags: { deviceId: input.deviceId },
|
|
@@ -11740,6 +12065,9 @@ var PlateRecognizer = class {
|
|
|
11740
12065
|
score: input.score,
|
|
11741
12066
|
bbox: input.bbox,
|
|
11742
12067
|
timestamp: input.timestamp,
|
|
12068
|
+
frameWidth: input.frameWidth,
|
|
12069
|
+
frameHeight: input.frameHeight,
|
|
12070
|
+
cropPadding: input.cropPadding,
|
|
11743
12071
|
...crop !== void 0 ? { crop } : {}
|
|
11744
12072
|
});
|
|
11745
12073
|
}
|
|
@@ -11750,15 +12078,48 @@ var PlateRecognizer = class {
|
|
|
11750
12078
|
this.bestPlate.delete(trackId);
|
|
11751
12079
|
if (held === void 0) return;
|
|
11752
12080
|
const plateId = `plate-${trackId}`;
|
|
12081
|
+
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
12082
|
+
let cropData = held.crop;
|
|
12083
|
+
if (cropData === void 0 && keyFrameMediaKey !== void 0 && this.deps.deriveCropFromKeyFrame) try {
|
|
12084
|
+
const derived = await this.deps.deriveCropFromKeyFrame({
|
|
12085
|
+
mediaKey: keyFrameMediaKey,
|
|
12086
|
+
bbox: held.bbox,
|
|
12087
|
+
frameWidth: held.frameWidth,
|
|
12088
|
+
frameHeight: held.frameHeight,
|
|
12089
|
+
padding: held.cropPadding,
|
|
12090
|
+
timestamp: held.timestamp
|
|
12091
|
+
});
|
|
12092
|
+
if (derived) {
|
|
12093
|
+
cropData = derived.jpeg;
|
|
12094
|
+
this.deps.logger.info("plate crop recovered from keyFrame (live capture missed)", {
|
|
12095
|
+
tags: {
|
|
12096
|
+
deviceId,
|
|
12097
|
+
trackId
|
|
12098
|
+
},
|
|
12099
|
+
meta: {
|
|
12100
|
+
plateId,
|
|
12101
|
+
skewMs: derived.skewMs
|
|
12102
|
+
}
|
|
12103
|
+
});
|
|
12104
|
+
}
|
|
12105
|
+
} catch (err) {
|
|
12106
|
+
this.deps.logger.debug("PlateRecognizer keyFrame crop derive failed", {
|
|
12107
|
+
tags: { deviceId },
|
|
12108
|
+
meta: {
|
|
12109
|
+
plateId,
|
|
12110
|
+
error: String(err)
|
|
12111
|
+
}
|
|
12112
|
+
});
|
|
12113
|
+
}
|
|
11753
12114
|
let mediaKey;
|
|
11754
|
-
if (
|
|
12115
|
+
if (cropData !== void 0) try {
|
|
11755
12116
|
mediaKey = await this.deps.mediaStore.put({
|
|
11756
12117
|
deviceId,
|
|
11757
12118
|
ownerKind: "plate",
|
|
11758
12119
|
ownerId: plateId,
|
|
11759
12120
|
kind: "crop",
|
|
11760
12121
|
timestamp: held.timestamp,
|
|
11761
|
-
data:
|
|
12122
|
+
data: cropData
|
|
11762
12123
|
});
|
|
11763
12124
|
} catch (err) {
|
|
11764
12125
|
this.deps.logger.warn("PlateRecognizer plate crop put failed", {
|
|
@@ -11770,7 +12131,6 @@ var PlateRecognizer = class {
|
|
|
11770
12131
|
});
|
|
11771
12132
|
}
|
|
11772
12133
|
const match = this.matchVehicle(held.text, held.score);
|
|
11773
|
-
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
11774
12134
|
try {
|
|
11775
12135
|
await this.deps.plateStore.insert({
|
|
11776
12136
|
id: plateId,
|
|
@@ -12026,6 +12386,86 @@ function createCaptureCrop(deps) {
|
|
|
12026
12386
|
};
|
|
12027
12387
|
}
|
|
12028
12388
|
//#endregion
|
|
12389
|
+
//#region src/pipeline-analytics/pipeline/display-crop.ts
|
|
12390
|
+
/** Bound the once-per-owner memo so a long-lived process never leaks it. */
|
|
12391
|
+
var MAX_LOGGED_OWNERS = 5e3;
|
|
12392
|
+
function createDisplayCrop(deps) {
|
|
12393
|
+
const loggedOwners = /* @__PURE__ */ new Set();
|
|
12394
|
+
const logFallbackOnce = (ownerId, reason) => {
|
|
12395
|
+
if (loggedOwners.has(ownerId)) return;
|
|
12396
|
+
if (loggedOwners.size >= MAX_LOGGED_OWNERS) loggedOwners.clear();
|
|
12397
|
+
loggedOwners.add(ownerId);
|
|
12398
|
+
deps.logger.info("display crop fallback used (honest sub-native, not upscaled)", { meta: {
|
|
12399
|
+
ownerId,
|
|
12400
|
+
reason
|
|
12401
|
+
} });
|
|
12402
|
+
};
|
|
12403
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, ownerId, maxWidth) => {
|
|
12404
|
+
const norm = padBbox({
|
|
12405
|
+
x: bbox.x / frameWidth,
|
|
12406
|
+
y: bbox.y / frameHeight,
|
|
12407
|
+
w: bbox.w / frameWidth,
|
|
12408
|
+
h: bbox.h / frameHeight
|
|
12409
|
+
}, padding);
|
|
12410
|
+
const native = await deps.fetchNativeRoiRgb(frameHandle, norm, maxWidth);
|
|
12411
|
+
if (native) {
|
|
12412
|
+
const jpeg = await deps.encodeRgb(native.bytes, native.width, native.height);
|
|
12413
|
+
if (jpeg) return jpeg;
|
|
12414
|
+
}
|
|
12415
|
+
const fb = await deps.fetchFallbackFrame(frameHandle);
|
|
12416
|
+
if (fb) try {
|
|
12417
|
+
const jpeg = await deps.cropRegionToJpeg(fb.frame.bytes, fb.frame.width, fb.frame.height, norm);
|
|
12418
|
+
logFallbackOnce(ownerId, fb.reason);
|
|
12419
|
+
return jpeg;
|
|
12420
|
+
} catch (err) {
|
|
12421
|
+
deps.logger.debug("display crop fallback crop failed", { meta: {
|
|
12422
|
+
ownerId,
|
|
12423
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12424
|
+
} });
|
|
12425
|
+
}
|
|
12426
|
+
logFallbackOnce(ownerId, "unavailable");
|
|
12427
|
+
return null;
|
|
12428
|
+
};
|
|
12429
|
+
}
|
|
12430
|
+
//#endregion
|
|
12431
|
+
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
12432
|
+
function createKeyFrameCrop(deps) {
|
|
12433
|
+
return async (input) => {
|
|
12434
|
+
if (input.frameWidth <= 0 || input.frameHeight <= 0) return null;
|
|
12435
|
+
const media = await deps.getMedia(input.mediaKey);
|
|
12436
|
+
if (!media) return null;
|
|
12437
|
+
const skewMs = Math.abs(media.timestamp - input.timestamp);
|
|
12438
|
+
if (skewMs > deps.maxSkewMs) {
|
|
12439
|
+
deps.logger.debug("keyframe crop rejected — temporal skew too large", { meta: {
|
|
12440
|
+
mediaKey: input.mediaKey,
|
|
12441
|
+
skewMs,
|
|
12442
|
+
maxSkewMs: deps.maxSkewMs
|
|
12443
|
+
} });
|
|
12444
|
+
return null;
|
|
12445
|
+
}
|
|
12446
|
+
try {
|
|
12447
|
+
const rgb = await deps.decodeJpegToRgb(media.base64);
|
|
12448
|
+
if (rgb.width <= 0 || rgb.height <= 0) return null;
|
|
12449
|
+
const norm = padBbox({
|
|
12450
|
+
x: input.bbox.x / input.frameWidth,
|
|
12451
|
+
y: input.bbox.y / input.frameHeight,
|
|
12452
|
+
w: input.bbox.w / input.frameWidth,
|
|
12453
|
+
h: input.bbox.h / input.frameHeight
|
|
12454
|
+
}, input.padding);
|
|
12455
|
+
return {
|
|
12456
|
+
jpeg: await deps.cropRegionToJpeg(Buffer.from(rgb.bytes), rgb.width, rgb.height, norm),
|
|
12457
|
+
skewMs
|
|
12458
|
+
};
|
|
12459
|
+
} catch (err) {
|
|
12460
|
+
deps.logger.debug("keyframe crop derive failed", { meta: {
|
|
12461
|
+
mediaKey: input.mediaKey,
|
|
12462
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12463
|
+
} });
|
|
12464
|
+
return null;
|
|
12465
|
+
}
|
|
12466
|
+
};
|
|
12467
|
+
}
|
|
12468
|
+
//#endregion
|
|
12029
12469
|
//#region src/pipeline-analytics/pipeline/dropout-skip.ts
|
|
12030
12470
|
function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
|
|
12031
12471
|
if (!cfg.enabled) return false;
|
|
@@ -12272,6 +12712,16 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
|
12272
12712
|
/** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
|
|
12273
12713
|
* scheduled detail call's frameHandle lease is already gone. */
|
|
12274
12714
|
var DETAIL_FALLBACK_CROP_PADDING = .15;
|
|
12715
|
+
/** Long-side cap for the DISPLAY-crop full-frame fallback (plate/face gallery
|
|
12716
|
+
* tiles) when the native ROI missed. A generous native cap so a keyframe-native
|
|
12717
|
+
* tier still yields a legible plate/face crop; the runner's ≤640 RAM tier is
|
|
12718
|
+
* returned as-is (honest sub-native). Never triggers an upscale. */
|
|
12719
|
+
var DISPLAY_FALLBACK_MAX_WIDTH = 1920;
|
|
12720
|
+
/** Max |keyFrame − plate-read| skew (ms) accepted for the DURABLE keyFrame-
|
|
12721
|
+
* derived plate-crop last resort. A readable plate implies a slow/stopping
|
|
12722
|
+
* vehicle, so within this window the subject barely moves off its bbox;
|
|
12723
|
+
* beyond it the crop would show empty scene, so it is rejected (no image). */
|
|
12724
|
+
var KEYFRAME_CROP_MAX_SKEW_MS = 1500;
|
|
12275
12725
|
/** How long the active CLIP model id (from the embedding-encoder) is cached
|
|
12276
12726
|
* before re-reading. */
|
|
12277
12727
|
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
@@ -12293,6 +12743,13 @@ var ZONE_SLICE_RECONCILE_MS = 3e4;
|
|
|
12293
12743
|
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
12294
12744
|
* detection confidence beats the held best by at least this margin (hysteresis
|
|
12295
12745
|
* so jitter around a plateau doesn't churn the write). */
|
|
12746
|
+
/** Rolling `lastFrame` cadence — deliberately DENSER than the 5s snapshot
|
|
12747
|
+
* interval: most real tracks live 3–9s, so on the snapshot cadence they
|
|
12748
|
+
* closed with NO `lastFrame` at all (29/68 in the 2026-07-22 4h audit) and
|
|
12749
|
+
* the close-time promotion had nothing to absorb. 1.5s bounds the "Ultimo"
|
|
12750
|
+
* tile's staleness at ~1.5s + the coast window, at one 960-boxed re-encode
|
|
12751
|
+
* per 1.5s per active track (putReplacing keeps a single row). */
|
|
12752
|
+
var LAST_FRAME_INTERVAL_MS = 1500;
|
|
12296
12753
|
var BEST_FRAME_HYSTERESIS = .05;
|
|
12297
12754
|
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
12298
12755
|
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
@@ -12547,6 +13004,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12547
13004
|
packageDropDetector = null;
|
|
12548
13005
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
12549
13006
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
13007
|
+
/** Reentrancy latch for `sweepExpiredTracks` — one sweep at a time (a stalled
|
|
13008
|
+
* sweep + queued interval ticks used to process the same close ~20×). */
|
|
13009
|
+
sweepInFlight = false;
|
|
12550
13010
|
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
12551
13011
|
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
12552
13012
|
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
@@ -12691,7 +13151,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12691
13151
|
let storage = this.ctx.kernel.storage;
|
|
12692
13152
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
12693
13153
|
if (mediaRoot) {
|
|
12694
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
13154
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-CgyUW8_U.js"));
|
|
12695
13155
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
12696
13156
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
12697
13157
|
}
|
|
@@ -12969,13 +13429,31 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12969
13429
|
logger: logger.child("CaptureCrop")
|
|
12970
13430
|
});
|
|
12971
13431
|
this.captureCrop = captureCrop;
|
|
13432
|
+
const captureDisplayCrop = createDisplayCrop({
|
|
13433
|
+
fetchNativeRoiRgb: (handle, norm, maxWidth) => fetchNativeCropRgb(handle, norm, maxWidth),
|
|
13434
|
+
fetchFallbackFrame: async (handle) => {
|
|
13435
|
+
const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH);
|
|
13436
|
+
if (!tiered || tiered.frame.format !== "rgb") return null;
|
|
13437
|
+
return {
|
|
13438
|
+
frame: {
|
|
13439
|
+
bytes: Buffer.from(tiered.frame.data),
|
|
13440
|
+
width: tiered.frame.width,
|
|
13441
|
+
height: tiered.frame.height
|
|
13442
|
+
},
|
|
13443
|
+
reason: tiered.tier === "ram-fullframe" ? "detection-raster" : "keyframe-native"
|
|
13444
|
+
};
|
|
13445
|
+
},
|
|
13446
|
+
encodeRgb: (bytes, w, h) => encodeRgbCropToJpeg(bytes, w, h),
|
|
13447
|
+
cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
|
|
13448
|
+
logger: logger.child("DisplayCrop")
|
|
13449
|
+
});
|
|
12972
13450
|
this.faceRecognizer = new FaceRecognizer({
|
|
12973
13451
|
identityStore: this.identityStore,
|
|
12974
13452
|
faceStore: this.faceStore,
|
|
12975
13453
|
mediaStore: this.mediaStore,
|
|
12976
13454
|
trackStore: this.trackStore,
|
|
12977
13455
|
eventStore: this.eventStore,
|
|
12978
|
-
captureCrop,
|
|
13456
|
+
captureCrop: captureDisplayCrop,
|
|
12979
13457
|
recomputeImportance: (trackId) => {
|
|
12980
13458
|
const trackStore = this.trackStore;
|
|
12981
13459
|
const eventStore = this.eventStore;
|
|
@@ -12990,11 +13468,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12990
13468
|
logger: logger.child("FaceRecognizer")
|
|
12991
13469
|
});
|
|
12992
13470
|
this.faceRecognizer.refreshGallery();
|
|
13471
|
+
const plateMediaStore = this.mediaStore;
|
|
13472
|
+
const deriveCropFromKeyFrame = createKeyFrameCrop({
|
|
13473
|
+
getMedia: async (mediaKey) => {
|
|
13474
|
+
const m = await plateMediaStore.getByKey(mediaKey);
|
|
13475
|
+
return m ? {
|
|
13476
|
+
base64: m.base64,
|
|
13477
|
+
timestamp: m.timestamp
|
|
13478
|
+
} : null;
|
|
13479
|
+
},
|
|
13480
|
+
decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
|
|
13481
|
+
cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
|
|
13482
|
+
maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
|
|
13483
|
+
logger: logger.child("KeyFrameCrop")
|
|
13484
|
+
});
|
|
12993
13485
|
this.plateRecognizer = new PlateRecognizer({
|
|
12994
13486
|
plateStore: this.plateStore,
|
|
12995
13487
|
vehicleStore: this.vehicleStore,
|
|
12996
13488
|
mediaStore: this.mediaStore,
|
|
12997
|
-
captureCrop,
|
|
13489
|
+
captureCrop: captureDisplayCrop,
|
|
13490
|
+
deriveCropFromKeyFrame,
|
|
12998
13491
|
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
12999
13492
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
13000
13493
|
logger: logger.child("PlateRecognizer")
|
|
@@ -13644,6 +14137,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13644
14137
|
source,
|
|
13645
14138
|
resurrected: true
|
|
13646
14139
|
} });
|
|
14140
|
+
if (this.eventMediaDispatcher) this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13647
14141
|
continue;
|
|
13648
14142
|
}
|
|
13649
14143
|
bornCandidates.push({
|
|
@@ -13830,7 +14324,23 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13830
14324
|
for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
|
|
13831
14325
|
const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
|
|
13832
14326
|
if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
|
|
13833
|
-
|
|
14327
|
+
const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
|
|
14328
|
+
const rasterFallbackCandidates = [];
|
|
14329
|
+
const widenedRasterWantedIds = /* @__PURE__ */ new Set();
|
|
14330
|
+
for (const t of result.tracked) {
|
|
14331
|
+
if (t.matchedThisFrame === false) continue;
|
|
14332
|
+
const closure = this.trackClosureState.get(t.trackId);
|
|
14333
|
+
if (!closure?.confirmed) continue;
|
|
14334
|
+
if (closure.rasterFallback) continue;
|
|
14335
|
+
if (targetedThisFrame.has(t.trackId)) continue;
|
|
14336
|
+
widenedRasterWantedIds.add(t.trackId);
|
|
14337
|
+
rasterFallbackCandidates.push({
|
|
14338
|
+
trackId: t.trackId,
|
|
14339
|
+
timestamp: result.timestamp,
|
|
14340
|
+
bbox: { ...t.bbox }
|
|
14341
|
+
});
|
|
14342
|
+
}
|
|
14343
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0 || widenedRasterWantedIds.size > 0) {
|
|
13834
14344
|
const captureCounts = {
|
|
13835
14345
|
events: eventTargets.length,
|
|
13836
14346
|
trackFrames: firstFrameTargets.length,
|
|
@@ -13856,24 +14366,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13856
14366
|
const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
|
|
13857
14367
|
for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
|
|
13858
14368
|
const dispatchTimestamp = result.timestamp;
|
|
13859
|
-
const rasterFallbackWantedTrackIds =
|
|
14369
|
+
const rasterFallbackWantedTrackIds = new Set(widenedRasterWantedIds);
|
|
13860
14370
|
for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13861
14371
|
for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
|
|
13862
|
-
const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
|
|
13863
|
-
const rasterFallbackCandidates = [];
|
|
13864
|
-
for (const t of result.tracked) {
|
|
13865
|
-
if (t.matchedThisFrame === false) continue;
|
|
13866
|
-
const closure = this.trackClosureState.get(t.trackId);
|
|
13867
|
-
if (!closure?.confirmed) continue;
|
|
13868
|
-
if (closure.rasterFallback) continue;
|
|
13869
|
-
if (targetedThisFrame.has(t.trackId)) continue;
|
|
13870
|
-
rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13871
|
-
rasterFallbackCandidates.push({
|
|
13872
|
-
trackId: t.trackId,
|
|
13873
|
-
timestamp: result.timestamp,
|
|
13874
|
-
bbox: { ...t.bbox }
|
|
13875
|
-
});
|
|
13876
|
-
}
|
|
13877
14372
|
this.eventMediaDispatcher.captureForFrame({
|
|
13878
14373
|
deviceId,
|
|
13879
14374
|
frameHandle,
|
|
@@ -14315,7 +14810,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14315
14810
|
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
14316
14811
|
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
14317
14812
|
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
14318
|
-
|
|
14813
|
+
const faceShortSidePx = detail.nativeFaceShortSidePx ?? (detail.bbox !== void 0 ? Math.min(detail.bbox.w, detail.bbox.h) : void 0);
|
|
14814
|
+
if (faceShortSidePx !== void 0 && faceShortSidePx < settings.minFacePx) return;
|
|
14319
14815
|
await this.faceRecognizer.ingestFaceDetail({
|
|
14320
14816
|
deviceId,
|
|
14321
14817
|
trackId,
|
|
@@ -14661,7 +15157,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14661
15157
|
frameHeight
|
|
14662
15158
|
});
|
|
14663
15159
|
const centerScore = bboxCenterScore(t.bbox, frameWidth, frameHeight);
|
|
14664
|
-
const
|
|
15160
|
+
const containment = bboxContainment(t.bbox, frameWidth, frameHeight);
|
|
15161
|
+
const sizeScore = bboxSizeScore(t.bbox, frameWidth, frameHeight);
|
|
15162
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore);
|
|
14665
15163
|
this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
|
|
14666
15164
|
const plan = planPeriodicMedia({
|
|
14667
15165
|
saveThumbnails: media.saveThumbnails,
|
|
@@ -14670,7 +15168,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14670
15168
|
thumbnailLanded: this.thumbnailLandedTracks.has(t.trackId),
|
|
14671
15169
|
lastFrameAt: this.lastFrameAtByTrack.get(t.trackId) ?? 0,
|
|
14672
15170
|
now: timestamp,
|
|
14673
|
-
intervalMs: media.snapshotIntervalMs
|
|
15171
|
+
intervalMs: media.snapshotIntervalMs,
|
|
15172
|
+
lastFrameIntervalMs: Math.min(LAST_FRAME_INTERVAL_MS, media.snapshotIntervalMs)
|
|
14674
15173
|
});
|
|
14675
15174
|
const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
|
|
14676
15175
|
if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
|
|
@@ -14931,14 +15430,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14931
15430
|
}
|
|
14932
15431
|
async sweepExpiredTracks() {
|
|
14933
15432
|
if (this.shuttingDown || !this.trackStore) return;
|
|
15433
|
+
if (this.sweepInFlight) return;
|
|
15434
|
+
this.sweepInFlight = true;
|
|
14934
15435
|
try {
|
|
14935
15436
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14936
15437
|
for (const t of expired) {
|
|
14937
15438
|
const duration = t.lastSeen - t.firstSeen;
|
|
14938
15439
|
const closure = this.trackClosureState.get(t.trackId);
|
|
15440
|
+
const ownedMedia = await this.mediaStore?.listByOwner("track", t.trackId) ?? [];
|
|
14939
15441
|
const outcome = decideZeroMediaPolicy({
|
|
14940
15442
|
durationMs: duration,
|
|
14941
|
-
hasMedia:
|
|
15443
|
+
hasMedia: ownedMedia.length > 0,
|
|
14942
15444
|
confirmed: closure?.confirmed ?? false,
|
|
14943
15445
|
hasRasterFallback: closure?.rasterFallback !== void 0
|
|
14944
15446
|
});
|
|
@@ -14973,6 +15475,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14973
15475
|
}
|
|
14974
15476
|
});
|
|
14975
15477
|
}
|
|
15478
|
+
const promotion = decideLastFramePromotion(ownedMedia);
|
|
15479
|
+
if (promotion.promote) {
|
|
15480
|
+
const snapshot = ownedMedia.find((m) => m.key === promotion.snapshotKey);
|
|
15481
|
+
if (snapshot) try {
|
|
15482
|
+
await this.mediaStore?.promoteToLastFrame({
|
|
15483
|
+
deviceId: t.deviceId,
|
|
15484
|
+
trackId: t.trackId,
|
|
15485
|
+
snapshot
|
|
15486
|
+
});
|
|
15487
|
+
} catch (err) {
|
|
15488
|
+
this.ctx.logger.debug("lastFrame promotion failed", {
|
|
15489
|
+
tags: { deviceId: t.deviceId },
|
|
15490
|
+
meta: {
|
|
15491
|
+
trackId: t.trackId,
|
|
15492
|
+
error: String(err)
|
|
15493
|
+
}
|
|
15494
|
+
});
|
|
15495
|
+
}
|
|
15496
|
+
}
|
|
14976
15497
|
this.ctx.logger.info("track ended", {
|
|
14977
15498
|
tags: { deviceId: t.deviceId },
|
|
14978
15499
|
meta: {
|
|
@@ -15091,6 +15612,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15091
15612
|
} catch (err) {
|
|
15092
15613
|
if (this.shuttingDown) return;
|
|
15093
15614
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
15615
|
+
} finally {
|
|
15616
|
+
this.sweepInFlight = false;
|
|
15094
15617
|
}
|
|
15095
15618
|
}
|
|
15096
15619
|
/**
|