@camstack/addon-post-analysis 1.2.4 → 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-Ck79MtSp.js → dist-BvEJmXbR.js} +65 -4
- package/dist/{dist-gQ5DHTYd.mjs → dist-DGkWhfba.mjs} +65 -4
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-C4bKtLou.js → node-CgyUW8_U.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ChHurWQz.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BNg3APbG.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CDgDsFf_.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DQbbiJYs.mjs → hostInit-CZvb3wMv.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +770 -124
- package/dist/pipeline-analytics/index.mjs +769 -123
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CUKChUeW.mjs +0 -26
|
@@ -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
|
|
6811
|
+
/**
|
|
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.
|
|
6828
|
+
*/
|
|
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
|
+
}
|
|
6582
6865
|
/**
|
|
6583
|
-
* Derive the `thumbnailSmall` JPEG from
|
|
6584
|
-
*
|
|
6585
|
-
* (aspect preserved). If the
|
|
6586
|
-
* ORIGINAL buffer is returned unchanged (store as-is, never upscale
|
|
6587
|
-
* re-encode).
|
|
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).
|
|
6588
6870
|
*/
|
|
6589
|
-
async function deriveThumbnailSmall(
|
|
6590
|
-
const meta = await (0, sharp.default)(
|
|
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));
|
|
@@ -6765,9 +7050,12 @@ var EventMediaDispatcher = class {
|
|
|
6765
7050
|
const empty = {
|
|
6766
7051
|
storedSnapshots: [],
|
|
6767
7052
|
thumbnailTrackIds: [],
|
|
7053
|
+
firstFrameTrackIds: [],
|
|
7054
|
+
lastFrameTrackIds: [],
|
|
6768
7055
|
rasterFallbacks: []
|
|
6769
7056
|
};
|
|
6770
|
-
|
|
7057
|
+
const extraCandidateCount = input.rasterFallbackCandidates?.length ?? 0;
|
|
7058
|
+
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0 && extraCandidateCount === 0) return empty;
|
|
6771
7059
|
let decoded;
|
|
6772
7060
|
try {
|
|
6773
7061
|
decoded = await resolveFrame(frameHandle, { getRemoteFrame: this.deps.getRemoteFrame });
|
|
@@ -6817,44 +7105,59 @@ var EventMediaDispatcher = class {
|
|
|
6817
7105
|
});
|
|
6818
7106
|
return empty;
|
|
6819
7107
|
}
|
|
6820
|
-
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
|
|
6821
|
-
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
6822
|
-
|
|
7108
|
+
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
|
|
7109
|
+
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
|
|
7110
|
+
const firstFrameTrackIds = [];
|
|
7111
|
+
for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
|
|
6823
7112
|
const storedSnapshots = [];
|
|
6824
7113
|
const thumbnailTrackIds = [];
|
|
7114
|
+
const lastFrameTrackIds = [];
|
|
6825
7115
|
for (const sn of snapshots) {
|
|
6826
7116
|
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
|
|
6827
7117
|
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6828
7118
|
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
7119
|
+
if (res.lastFrameWritten) lastFrameTrackIds.push(sn.trackId);
|
|
6829
7120
|
}
|
|
6830
7121
|
return {
|
|
6831
7122
|
storedSnapshots,
|
|
6832
7123
|
thumbnailTrackIds,
|
|
7124
|
+
firstFrameTrackIds,
|
|
7125
|
+
lastFrameTrackIds,
|
|
6833
7126
|
rasterFallbacks
|
|
6834
7127
|
};
|
|
6835
7128
|
}
|
|
6836
7129
|
/**
|
|
6837
|
-
* Cut ONE clean detection-raster subject crop per WANTED track
|
|
6838
|
-
*
|
|
6839
|
-
*
|
|
6840
|
-
*
|
|
6841
|
-
*
|
|
6842
|
-
*
|
|
7130
|
+
* Cut ONE clean detection-raster subject crop per WANTED track observed this
|
|
7131
|
+
* frame — the "first available detection-raster frame" of the zero-media
|
|
7132
|
+
* fallback. Candidate bboxes come from this frame's firstFrame/snapshot targets
|
|
7133
|
+
* AND (widened) the explicit `extraCandidates` (confirmed tracks with no such
|
|
7134
|
+
* target). Cropped from the already-resolved `frameData` at its REAL resolution
|
|
7135
|
+
* via {@link extractCrop} (extract-only — NEVER upscaled). Deduped per trackId
|
|
7136
|
+
* (first candidate wins; target-derived candidates precede the extras). A
|
|
7137
|
+
* per-track encode failure is skipped (logged) — a missing fallback simply
|
|
6843
7138
|
* leaves the track with no last-resort preview, never an error.
|
|
6844
7139
|
*/
|
|
6845
|
-
async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
|
|
7140
|
+
async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted, extraCandidates) {
|
|
6846
7141
|
if (!wanted || wanted.size === 0) return [];
|
|
6847
7142
|
const seen = /* @__PURE__ */ new Set();
|
|
6848
7143
|
const out = [];
|
|
6849
|
-
const candidates = [
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
7144
|
+
const candidates = [
|
|
7145
|
+
...trackFrames.map((t) => ({
|
|
7146
|
+
trackId: t.trackId,
|
|
7147
|
+
timestamp: t.timestamp,
|
|
7148
|
+
bbox: t.bbox
|
|
7149
|
+
})),
|
|
7150
|
+
...snapshots.map((s) => ({
|
|
7151
|
+
trackId: s.trackId,
|
|
7152
|
+
timestamp: s.timestamp,
|
|
7153
|
+
bbox: s.bbox
|
|
7154
|
+
})),
|
|
7155
|
+
...(extraCandidates ?? []).map((c) => ({
|
|
7156
|
+
trackId: c.trackId,
|
|
7157
|
+
timestamp: c.timestamp,
|
|
7158
|
+
bbox: c.bbox
|
|
7159
|
+
}))
|
|
7160
|
+
];
|
|
6858
7161
|
for (const c of candidates) {
|
|
6859
7162
|
if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
|
|
6860
7163
|
seen.add(c.trackId);
|
|
@@ -6888,11 +7191,15 @@ var EventMediaDispatcher = class {
|
|
|
6888
7191
|
* TrackStore wiring (null when `appendSnapshot` is false or the encode
|
|
6889
7192
|
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6890
7193
|
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
7194
|
+
* `lastFrameWritten` reports whether the rolling `lastFrame` actually landed
|
|
7195
|
+
* (DEFECT B) so the caller advances its `lastFrameAt` clock ONLY on a real
|
|
7196
|
+
* write — a dropped roll leaves the clock put and retries next frame.
|
|
6891
7197
|
*/
|
|
6892
7198
|
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
|
|
6893
7199
|
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6894
7200
|
storedSnapshot: null,
|
|
6895
|
-
thumbnailWritten: false
|
|
7201
|
+
thumbnailWritten: false,
|
|
7202
|
+
lastFrameWritten: false
|
|
6896
7203
|
};
|
|
6897
7204
|
let boxed = null;
|
|
6898
7205
|
if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
|
|
@@ -6913,7 +7220,8 @@ var EventMediaDispatcher = class {
|
|
|
6913
7220
|
bbox: sn.bbox
|
|
6914
7221
|
};
|
|
6915
7222
|
} catch {}
|
|
6916
|
-
|
|
7223
|
+
let lastFrameWritten = false;
|
|
7224
|
+
if (sn.rollingLastFrame && boxed) lastFrameWritten = await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6917
7225
|
let thumbnailWritten = false;
|
|
6918
7226
|
if (sn.bestThumbnail) {
|
|
6919
7227
|
const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
|
|
@@ -6924,60 +7232,94 @@ var EventMediaDispatcher = class {
|
|
|
6924
7232
|
}
|
|
6925
7233
|
return {
|
|
6926
7234
|
storedSnapshot: stored,
|
|
6927
|
-
thumbnailWritten
|
|
7235
|
+
thumbnailWritten,
|
|
7236
|
+
lastFrameWritten
|
|
6928
7237
|
};
|
|
6929
7238
|
}
|
|
6930
7239
|
/**
|
|
6931
|
-
* Clean subject-centered crop of `bbox`
|
|
6932
|
-
*
|
|
6933
|
-
*
|
|
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.
|
|
6934
7243
|
*
|
|
6935
|
-
*
|
|
6936
|
-
*
|
|
6937
|
-
*
|
|
6938
|
-
*
|
|
6939
|
-
*
|
|
6940
|
-
*
|
|
6941
|
-
*
|
|
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
|
|
6942
7254
|
* live-handle window opened by `captureForFrame`.
|
|
6943
7255
|
*/
|
|
6944
|
-
async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
|
|
6945
|
-
|
|
6946
|
-
|
|
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: {
|
|
6947
7266
|
shmId: frameHandle.shmId,
|
|
6948
|
-
|
|
7267
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6949
7268
|
} });
|
|
6950
|
-
return null;
|
|
6951
7269
|
}
|
|
6952
7270
|
try {
|
|
6953
|
-
const
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
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: {
|
|
6960
7279
|
shmId: frameHandle.shmId,
|
|
6961
|
-
|
|
7280
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6962
7281
|
} });
|
|
6963
|
-
|
|
7282
|
+
}
|
|
7283
|
+
try {
|
|
7284
|
+
const { crop } = await extractCrop(frameData, fw, fh, norm);
|
|
7285
|
+
this.logDisplayFallbackOnce(ownerId, "detection-raster");
|
|
7286
|
+
return crop;
|
|
6964
7287
|
} catch (err) {
|
|
6965
|
-
this.deps.logger.warn("
|
|
7288
|
+
this.deps.logger.warn("display child crop: all sources missed", { meta: {
|
|
6966
7289
|
shmId: frameHandle.shmId,
|
|
6967
7290
|
error: err instanceof Error ? err.message : String(err)
|
|
6968
7291
|
} });
|
|
6969
7292
|
return null;
|
|
6970
7293
|
}
|
|
6971
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
|
+
}
|
|
6972
7306
|
/**
|
|
6973
7307
|
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6974
|
-
* fast-load, 2026-07-21
|
|
6975
|
-
*
|
|
6976
|
-
*
|
|
6977
|
-
*
|
|
6978
|
-
*
|
|
6979
|
-
*
|
|
6980
|
-
*
|
|
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).
|
|
6981
7323
|
*
|
|
6982
7324
|
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
6983
7325
|
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
@@ -6993,21 +7335,22 @@ var EventMediaDispatcher = class {
|
|
|
6993
7335
|
return null;
|
|
6994
7336
|
}
|
|
6995
7337
|
try {
|
|
6996
|
-
const
|
|
7338
|
+
const layout = wideCentralSquareLayout(bbox, {
|
|
6997
7339
|
W: fw,
|
|
6998
7340
|
H: fh
|
|
6999
7341
|
});
|
|
7000
|
-
const
|
|
7001
|
-
if (!
|
|
7342
|
+
const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch);
|
|
7343
|
+
if (!slab) {
|
|
7002
7344
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
7003
7345
|
shmId: frameHandle.shmId,
|
|
7004
7346
|
reason: "native-miss"
|
|
7005
7347
|
} });
|
|
7006
7348
|
return null;
|
|
7007
7349
|
}
|
|
7350
|
+
const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
|
|
7008
7351
|
return {
|
|
7009
|
-
thumbnail
|
|
7010
|
-
thumbnailSmall: await deriveThumbnailSmall(
|
|
7352
|
+
thumbnail,
|
|
7353
|
+
thumbnailSmall: await deriveThumbnailSmall(thumbnail)
|
|
7011
7354
|
};
|
|
7012
7355
|
} catch (err) {
|
|
7013
7356
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
@@ -7096,9 +7439,9 @@ var EventMediaDispatcher = class {
|
|
|
7096
7439
|
return false;
|
|
7097
7440
|
}
|
|
7098
7441
|
}
|
|
7099
|
-
async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
|
|
7442
|
+
async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
|
|
7100
7443
|
if (ev.childCrops) for (const child of ev.childCrops) try {
|
|
7101
|
-
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}`);
|
|
7102
7445
|
if (!childCropData) continue;
|
|
7103
7446
|
await this.deps.mediaStore.put({
|
|
7104
7447
|
deviceId,
|
|
@@ -7121,7 +7464,7 @@ var EventMediaDispatcher = class {
|
|
|
7121
7464
|
}
|
|
7122
7465
|
async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
|
|
7123
7466
|
const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
|
|
7124
|
-
if (!boxed) return;
|
|
7467
|
+
if (!boxed) return false;
|
|
7125
7468
|
try {
|
|
7126
7469
|
await this.deps.mediaStore.put({
|
|
7127
7470
|
deviceId,
|
|
@@ -7131,6 +7474,7 @@ var EventMediaDispatcher = class {
|
|
|
7131
7474
|
timestamp: tf.timestamp,
|
|
7132
7475
|
data: boxed
|
|
7133
7476
|
});
|
|
7477
|
+
return true;
|
|
7134
7478
|
} catch (err) {
|
|
7135
7479
|
this.deps.logger.warn("event media: track frame failed", {
|
|
7136
7480
|
tags: { deviceId },
|
|
@@ -7140,6 +7484,7 @@ var EventMediaDispatcher = class {
|
|
|
7140
7484
|
error: err instanceof Error ? err.message : String(err)
|
|
7141
7485
|
}
|
|
7142
7486
|
});
|
|
7487
|
+
return false;
|
|
7143
7488
|
}
|
|
7144
7489
|
}
|
|
7145
7490
|
};
|
|
@@ -8924,7 +9269,8 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
8924
9269
|
*/
|
|
8925
9270
|
function planPeriodicMedia(input) {
|
|
8926
9271
|
const appendSnapshot = input.dueSnapshot;
|
|
8927
|
-
const
|
|
9272
|
+
const lastFrameInterval = input.lastFrameIntervalMs ?? input.intervalMs;
|
|
9273
|
+
const rollingLastFrame = input.saveThumbnails && input.now - input.lastFrameAt >= lastFrameInterval && !appendSnapshot;
|
|
8928
9274
|
const thumbnailLanded = input.thumbnailLanded ?? true;
|
|
8929
9275
|
return {
|
|
8930
9276
|
appendSnapshot,
|
|
@@ -8932,7 +9278,10 @@ function planPeriodicMedia(input) {
|
|
|
8932
9278
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8933
9279
|
};
|
|
8934
9280
|
}
|
|
8935
|
-
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9281
|
+
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9282
|
+
suppressMaxDurationMs: 1e3,
|
|
9283
|
+
nothingToShowMaxDurationMs: 1500
|
|
9284
|
+
};
|
|
8936
9285
|
/**
|
|
8937
9286
|
* Classify a closing track's persistence outcome. Pure — see the module header
|
|
8938
9287
|
* for the full contract.
|
|
@@ -8940,6 +9289,7 @@ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
|
|
|
8940
9289
|
function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
|
|
8941
9290
|
if (input.hasMedia) return "persist";
|
|
8942
9291
|
if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
|
|
9292
|
+
if (!input.hasRasterFallback && input.durationMs < config.nothingToShowMaxDurationMs) return "suppress";
|
|
8943
9293
|
return input.hasRasterFallback ? "raster-fallback" : "persist";
|
|
8944
9294
|
}
|
|
8945
9295
|
//#endregion
|
|
@@ -10115,6 +10465,7 @@ var FaceRecognizer = class {
|
|
|
10115
10465
|
embedding: input.embedding,
|
|
10116
10466
|
embeddingModelId: modelId,
|
|
10117
10467
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
10468
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {},
|
|
10118
10469
|
...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
|
|
10119
10470
|
};
|
|
10120
10471
|
await this.processFrame({
|
|
@@ -10130,7 +10481,7 @@ var FaceRecognizer = class {
|
|
|
10130
10481
|
}
|
|
10131
10482
|
async processFrame(input) {
|
|
10132
10483
|
const { settings } = input;
|
|
10133
|
-
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);
|
|
10134
10485
|
if (candidates.length === 0) return;
|
|
10135
10486
|
this.deps.logger.debug("face: frame candidates", {
|
|
10136
10487
|
tags: { deviceId: input.deviceId },
|
|
@@ -10168,7 +10519,7 @@ var FaceRecognizer = class {
|
|
|
10168
10519
|
let crop;
|
|
10169
10520
|
if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
|
|
10170
10521
|
else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
10171
|
-
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;
|
|
10172
10523
|
} catch (err) {
|
|
10173
10524
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
10174
10525
|
tags: { deviceId: input.deviceId },
|
|
@@ -11699,7 +12050,7 @@ var PlateRecognizer = class {
|
|
|
11699
12050
|
if (held !== void 0 && input.score <= held.score) return;
|
|
11700
12051
|
let crop;
|
|
11701
12052
|
if (input.frameHandle !== void 0) try {
|
|
11702
|
-
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;
|
|
11703
12054
|
} catch (err) {
|
|
11704
12055
|
this.deps.logger.debug("PlateRecognizer crop capture failed", {
|
|
11705
12056
|
tags: { deviceId: input.deviceId },
|
|
@@ -11714,6 +12065,9 @@ var PlateRecognizer = class {
|
|
|
11714
12065
|
score: input.score,
|
|
11715
12066
|
bbox: input.bbox,
|
|
11716
12067
|
timestamp: input.timestamp,
|
|
12068
|
+
frameWidth: input.frameWidth,
|
|
12069
|
+
frameHeight: input.frameHeight,
|
|
12070
|
+
cropPadding: input.cropPadding,
|
|
11717
12071
|
...crop !== void 0 ? { crop } : {}
|
|
11718
12072
|
});
|
|
11719
12073
|
}
|
|
@@ -11724,15 +12078,48 @@ var PlateRecognizer = class {
|
|
|
11724
12078
|
this.bestPlate.delete(trackId);
|
|
11725
12079
|
if (held === void 0) return;
|
|
11726
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
|
+
}
|
|
11727
12114
|
let mediaKey;
|
|
11728
|
-
if (
|
|
12115
|
+
if (cropData !== void 0) try {
|
|
11729
12116
|
mediaKey = await this.deps.mediaStore.put({
|
|
11730
12117
|
deviceId,
|
|
11731
12118
|
ownerKind: "plate",
|
|
11732
12119
|
ownerId: plateId,
|
|
11733
12120
|
kind: "crop",
|
|
11734
12121
|
timestamp: held.timestamp,
|
|
11735
|
-
data:
|
|
12122
|
+
data: cropData
|
|
11736
12123
|
});
|
|
11737
12124
|
} catch (err) {
|
|
11738
12125
|
this.deps.logger.warn("PlateRecognizer plate crop put failed", {
|
|
@@ -11744,7 +12131,6 @@ var PlateRecognizer = class {
|
|
|
11744
12131
|
});
|
|
11745
12132
|
}
|
|
11746
12133
|
const match = this.matchVehicle(held.text, held.score);
|
|
11747
|
-
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
11748
12134
|
try {
|
|
11749
12135
|
await this.deps.plateStore.insert({
|
|
11750
12136
|
id: plateId,
|
|
@@ -12000,6 +12386,86 @@ function createCaptureCrop(deps) {
|
|
|
12000
12386
|
};
|
|
12001
12387
|
}
|
|
12002
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
|
|
12003
12469
|
//#region src/pipeline-analytics/pipeline/dropout-skip.ts
|
|
12004
12470
|
function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
|
|
12005
12471
|
if (!cfg.enabled) return false;
|
|
@@ -12246,6 +12712,16 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
|
12246
12712
|
/** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
|
|
12247
12713
|
* scheduled detail call's frameHandle lease is already gone. */
|
|
12248
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;
|
|
12249
12725
|
/** How long the active CLIP model id (from the embedding-encoder) is cached
|
|
12250
12726
|
* before re-reading. */
|
|
12251
12727
|
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
@@ -12267,6 +12743,13 @@ var ZONE_SLICE_RECONCILE_MS = 3e4;
|
|
|
12267
12743
|
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
12268
12744
|
* detection confidence beats the held best by at least this margin (hysteresis
|
|
12269
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;
|
|
12270
12753
|
var BEST_FRAME_HYSTERESIS = .05;
|
|
12271
12754
|
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
12272
12755
|
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
@@ -12521,6 +13004,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12521
13004
|
packageDropDetector = null;
|
|
12522
13005
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
12523
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;
|
|
12524
13010
|
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
12525
13011
|
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
12526
13012
|
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
@@ -12549,6 +13035,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12549
13035
|
* where no `snapshot` is appended, so it is never byte-identical to a stored
|
|
12550
13036
|
* `snapshot` (kills the end-of-track duplicate). Cleared on track end. */
|
|
12551
13037
|
lastFrameAtByTrack = /* @__PURE__ */ new Map();
|
|
13038
|
+
/** Tracks with a rolling-`lastFrame` capture CURRENTLY in flight (RC-1,
|
|
13039
|
+
* DEFECT B). The `lastFrameAt` clock now advances ONLY when the write lands
|
|
13040
|
+
* (dispatcher completion), not synchronously at plan time — so a dropped roll
|
|
13041
|
+
* (recycled/blank frame) leaves the clock put and re-rolls next frame. Without
|
|
13042
|
+
* an in-flight guard that re-roll would fire EVERY frame while the first
|
|
13043
|
+
* capture is still resolving (0.1–3s under the native path), stacking N
|
|
13044
|
+
* overlapping captures. While a track sits here `buildSnapshotTargets`
|
|
13045
|
+
* suppresses a new rolling-`lastFrame` request; the pending dispatch clears it
|
|
13046
|
+
* (and, if it landed, advances `lastFrameAtByTrack`). Cleared on track end +
|
|
13047
|
+
* reset. */
|
|
13048
|
+
lastFrameInFlight = /* @__PURE__ */ new Set();
|
|
13049
|
+
/** Confirmed-birth tracks whose `firstFrame` has NOT yet actually persisted
|
|
13050
|
+
* (DEFECT A). Seeded on a confirmed birth (alongside the birth firstFrame
|
|
13051
|
+
* target) and removed the moment the write lands. While a track sits here and
|
|
13052
|
+
* is matched this frame, `collectFirstFrameRetries` re-schedules a firstFrame
|
|
13053
|
+
* target so a birth capture dropped by a recycled/blank live frame is retried
|
|
13054
|
+
* on a later frame (earliest available view still beats none). Scoped to
|
|
13055
|
+
* confirmed births ONLY — a suppressed false-positive birth or resurrection
|
|
13056
|
+
* never enters, so it never gets a retro firstFrame. Cleared on track end +
|
|
13057
|
+
* reset. */
|
|
13058
|
+
firstFramePendingTracks = /* @__PURE__ */ new Set();
|
|
13059
|
+
/** Tracks with a `firstFrame` capture CURRENTLY in flight (RC-1, DEFECT A).
|
|
13060
|
+
* Mirrors `thumbnailInFlight`: while a firstFrame capture is resolving the
|
|
13061
|
+
* per-frame retry is suppressed so at most one capture is outstanding per
|
|
13062
|
+
* track. Cleared on dispatch settle (+ track end / reset). */
|
|
13063
|
+
firstFrameInFlight = /* @__PURE__ */ new Set();
|
|
12552
13064
|
/** Track ids whose best `thumbnail` has ACTUALLY been persisted (#27-A). A
|
|
12553
13065
|
* track absent here keeps forcing a best-thumbnail capture every frame until
|
|
12554
13066
|
* one lands, so a short / high-churn track whose first capture was dropped
|
|
@@ -12639,7 +13151,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12639
13151
|
let storage = this.ctx.kernel.storage;
|
|
12640
13152
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
12641
13153
|
if (mediaRoot) {
|
|
12642
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
13154
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-CgyUW8_U.js"));
|
|
12643
13155
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
12644
13156
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
12645
13157
|
}
|
|
@@ -12917,13 +13429,31 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12917
13429
|
logger: logger.child("CaptureCrop")
|
|
12918
13430
|
});
|
|
12919
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
|
+
});
|
|
12920
13450
|
this.faceRecognizer = new FaceRecognizer({
|
|
12921
13451
|
identityStore: this.identityStore,
|
|
12922
13452
|
faceStore: this.faceStore,
|
|
12923
13453
|
mediaStore: this.mediaStore,
|
|
12924
13454
|
trackStore: this.trackStore,
|
|
12925
13455
|
eventStore: this.eventStore,
|
|
12926
|
-
captureCrop,
|
|
13456
|
+
captureCrop: captureDisplayCrop,
|
|
12927
13457
|
recomputeImportance: (trackId) => {
|
|
12928
13458
|
const trackStore = this.trackStore;
|
|
12929
13459
|
const eventStore = this.eventStore;
|
|
@@ -12938,11 +13468,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
12938
13468
|
logger: logger.child("FaceRecognizer")
|
|
12939
13469
|
});
|
|
12940
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
|
+
});
|
|
12941
13485
|
this.plateRecognizer = new PlateRecognizer({
|
|
12942
13486
|
plateStore: this.plateStore,
|
|
12943
13487
|
vehicleStore: this.vehicleStore,
|
|
12944
13488
|
mediaStore: this.mediaStore,
|
|
12945
|
-
captureCrop,
|
|
13489
|
+
captureCrop: captureDisplayCrop,
|
|
13490
|
+
deriveCropFromKeyFrame,
|
|
12946
13491
|
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
12947
13492
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
12948
13493
|
logger: logger.child("PlateRecognizer")
|
|
@@ -13443,6 +13988,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13443
13988
|
this.dropoutSkipsByKey.clear();
|
|
13444
13989
|
this.bestFrameTracker.clear();
|
|
13445
13990
|
this.lastFrameAtByTrack.clear();
|
|
13991
|
+
this.lastFrameInFlight.clear();
|
|
13992
|
+
this.firstFramePendingTracks.clear();
|
|
13993
|
+
this.firstFrameInFlight.clear();
|
|
13446
13994
|
this.thumbnailLandedTracks.clear();
|
|
13447
13995
|
this.thumbnailInFlight.clear();
|
|
13448
13996
|
this.keyFrameInFlight.clear();
|
|
@@ -13589,6 +14137,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13589
14137
|
source,
|
|
13590
14138
|
resurrected: true
|
|
13591
14139
|
} });
|
|
14140
|
+
if (this.eventMediaDispatcher) this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13592
14141
|
continue;
|
|
13593
14142
|
}
|
|
13594
14143
|
bornCandidates.push({
|
|
@@ -13614,15 +14163,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13614
14163
|
className: t.className,
|
|
13615
14164
|
source
|
|
13616
14165
|
} });
|
|
13617
|
-
if (this.eventMediaDispatcher
|
|
13618
|
-
|
|
14166
|
+
if (this.eventMediaDispatcher) {
|
|
14167
|
+
this.firstFramePendingTracks.add(id);
|
|
14168
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
14169
|
+
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
14170
|
+
if (frameHandle) firstFrameTargets.push({
|
|
13619
14171
|
trackId: id,
|
|
13620
14172
|
timestamp: result.timestamp,
|
|
13621
14173
|
bbox: { ...t.bbox },
|
|
13622
14174
|
...t.label ? { label: t.label } : {}
|
|
13623
14175
|
});
|
|
13624
|
-
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
13625
|
-
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13626
14176
|
}
|
|
13627
14177
|
this.ctx.eventBus.emit({
|
|
13628
14178
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
@@ -13771,9 +14321,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13771
14321
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
13772
14322
|
else plateCrops += 1;
|
|
13773
14323
|
const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
|
|
14324
|
+
for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
|
|
13774
14325
|
const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
|
|
13775
14326
|
if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
|
|
13776
|
-
|
|
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) {
|
|
13777
14344
|
const captureCounts = {
|
|
13778
14345
|
events: eventTargets.length,
|
|
13779
14346
|
trackFrames: firstFrameTargets.length,
|
|
@@ -13794,7 +14361,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13794
14361
|
} });
|
|
13795
14362
|
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13796
14363
|
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
|
|
13797
|
-
const
|
|
14364
|
+
const firstFrameInFlightTrackIds = firstFrameTargets.map((t) => t.trackId);
|
|
14365
|
+
for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.add(trackId);
|
|
14366
|
+
const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
|
|
14367
|
+
for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
|
|
14368
|
+
const dispatchTimestamp = result.timestamp;
|
|
14369
|
+
const rasterFallbackWantedTrackIds = new Set(widenedRasterWantedIds);
|
|
13798
14370
|
for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13799
14371
|
for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
|
|
13800
14372
|
this.eventMediaDispatcher.captureForFrame({
|
|
@@ -13804,7 +14376,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13804
14376
|
trackFrames: firstFrameTargets,
|
|
13805
14377
|
snapshots: snapshotTargets,
|
|
13806
14378
|
cropPadding: mediaSettings.cropPadding,
|
|
13807
|
-
rasterFallbackWantedTrackIds
|
|
14379
|
+
rasterFallbackWantedTrackIds,
|
|
14380
|
+
rasterFallbackCandidates
|
|
13808
14381
|
}).then((res) => {
|
|
13809
14382
|
for (const rf of res.rasterFallbacks) {
|
|
13810
14383
|
const st = this.ensureTrackClosureState(deviceId, rf.trackId);
|
|
@@ -13824,8 +14397,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13824
14397
|
mediaKey: s.mediaKey
|
|
13825
14398
|
});
|
|
13826
14399
|
for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
|
|
14400
|
+
for (const trackId of res.firstFrameTrackIds) this.firstFramePendingTracks.delete(trackId);
|
|
14401
|
+
for (const trackId of res.lastFrameTrackIds) this.lastFrameAtByTrack.set(trackId, dispatchTimestamp);
|
|
13827
14402
|
}).catch(() => {}).finally(() => {
|
|
13828
14403
|
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.delete(trackId);
|
|
14404
|
+
for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.delete(trackId);
|
|
14405
|
+
for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.delete(trackId);
|
|
13829
14406
|
});
|
|
13830
14407
|
}
|
|
13831
14408
|
}
|
|
@@ -13889,6 +14466,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13889
14466
|
}
|
|
13890
14467
|
overlayDetections = frame.detections;
|
|
13891
14468
|
}
|
|
14469
|
+
const hasMovingTrack = result.tracked.some((t) => t.state === "moving" || t.state === "entered" || t.state === "left");
|
|
13892
14470
|
this.ctx.eventBus.emit({
|
|
13893
14471
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
13894
14472
|
timestamp: new Date(result.timestamp),
|
|
@@ -13903,7 +14481,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
13903
14481
|
timestamp: result.timestamp,
|
|
13904
14482
|
frameWidth: result.frameWidth,
|
|
13905
14483
|
frameHeight: result.frameHeight,
|
|
13906
|
-
detections: overlayDetections
|
|
14484
|
+
detections: overlayDetections,
|
|
14485
|
+
hasMovingTrack
|
|
13907
14486
|
}
|
|
13908
14487
|
});
|
|
13909
14488
|
}
|
|
@@ -14231,7 +14810,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14231
14810
|
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
14232
14811
|
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
14233
14812
|
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
14234
|
-
|
|
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;
|
|
14235
14815
|
await this.faceRecognizer.ingestFaceDetail({
|
|
14236
14816
|
deviceId,
|
|
14237
14817
|
trackId,
|
|
@@ -14527,6 +15107,34 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14527
15107
|
});
|
|
14528
15108
|
this.emitTrackLifecycle(payload, timestamp);
|
|
14529
15109
|
}
|
|
15110
|
+
/**
|
|
15111
|
+
* DEFECT A: build `firstFrame` RETRY targets for confirmed-birth tracks whose
|
|
15112
|
+
* birth capture never landed. A track qualifies when it is still pending
|
|
15113
|
+
* (`firstFramePendingTracks`), is OBSERVED this frame (`matchedThisFrame` — a
|
|
15114
|
+
* coasted/stale box would crop the empty scene), is NOT already targeted this
|
|
15115
|
+
* frame (its birth target), and has no capture in flight (RC-1). Each target is
|
|
15116
|
+
* stamped with the CURRENT frame timestamp so the media carries the real
|
|
15117
|
+
* capture instant, not the birth ts — the earliest AVAILABLE view still beats
|
|
15118
|
+
* no firstFrame at all.
|
|
15119
|
+
*/
|
|
15120
|
+
collectFirstFrameRetries(tracked, timestamp, alreadyTargeted) {
|
|
15121
|
+
if (this.firstFramePendingTracks.size === 0) return [];
|
|
15122
|
+
const bornThisFrame = new Set(alreadyTargeted.map((t) => t.trackId));
|
|
15123
|
+
const retries = [];
|
|
15124
|
+
for (const t of tracked) {
|
|
15125
|
+
if (t.matchedThisFrame === false) continue;
|
|
15126
|
+
if (!this.firstFramePendingTracks.has(t.trackId)) continue;
|
|
15127
|
+
if (bornThisFrame.has(t.trackId)) continue;
|
|
15128
|
+
if (this.firstFrameInFlight.has(t.trackId)) continue;
|
|
15129
|
+
retries.push({
|
|
15130
|
+
trackId: t.trackId,
|
|
15131
|
+
timestamp,
|
|
15132
|
+
bbox: { ...t.bbox },
|
|
15133
|
+
...t.label ? { label: t.label } : {}
|
|
15134
|
+
});
|
|
15135
|
+
}
|
|
15136
|
+
return retries;
|
|
15137
|
+
}
|
|
14530
15138
|
buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
|
|
14531
15139
|
const targets = [];
|
|
14532
15140
|
for (const t of tracked) {
|
|
@@ -14549,7 +15157,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14549
15157
|
frameHeight
|
|
14550
15158
|
});
|
|
14551
15159
|
const centerScore = bboxCenterScore(t.bbox, frameWidth, frameHeight);
|
|
14552
|
-
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);
|
|
14553
15163
|
this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
|
|
14554
15164
|
const plan = planPeriodicMedia({
|
|
14555
15165
|
saveThumbnails: media.saveThumbnails,
|
|
@@ -14558,21 +15168,22 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14558
15168
|
thumbnailLanded: this.thumbnailLandedTracks.has(t.trackId),
|
|
14559
15169
|
lastFrameAt: this.lastFrameAtByTrack.get(t.trackId) ?? 0,
|
|
14560
15170
|
now: timestamp,
|
|
14561
|
-
intervalMs: media.snapshotIntervalMs
|
|
15171
|
+
intervalMs: media.snapshotIntervalMs,
|
|
15172
|
+
lastFrameIntervalMs: Math.min(LAST_FRAME_INTERVAL_MS, media.snapshotIntervalMs)
|
|
14562
15173
|
});
|
|
14563
|
-
|
|
15174
|
+
const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
|
|
14564
15175
|
if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
|
|
14565
15176
|
const plausibleBox = isPlausibleThumbnailBox(t.bbox, frameWidth, frameHeight);
|
|
14566
15177
|
const bestThumbnail = plan.bestThumbnail && plausibleBox && !this.thumbnailInFlight.has(t.trackId);
|
|
14567
15178
|
const keyFrame = isNewBest && plausibleBox;
|
|
14568
|
-
if (!plan.appendSnapshot && !
|
|
15179
|
+
if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && !keyFrame) continue;
|
|
14569
15180
|
targets.push({
|
|
14570
15181
|
trackId: t.trackId,
|
|
14571
15182
|
timestamp,
|
|
14572
15183
|
bbox: { ...t.bbox },
|
|
14573
15184
|
...t.label ? { label: t.label } : {},
|
|
14574
15185
|
appendSnapshot: plan.appendSnapshot,
|
|
14575
|
-
rollingLastFrame
|
|
15186
|
+
rollingLastFrame,
|
|
14576
15187
|
bestThumbnail,
|
|
14577
15188
|
keyFrame
|
|
14578
15189
|
});
|
|
@@ -14819,14 +15430,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14819
15430
|
}
|
|
14820
15431
|
async sweepExpiredTracks() {
|
|
14821
15432
|
if (this.shuttingDown || !this.trackStore) return;
|
|
15433
|
+
if (this.sweepInFlight) return;
|
|
15434
|
+
this.sweepInFlight = true;
|
|
14822
15435
|
try {
|
|
14823
15436
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14824
15437
|
for (const t of expired) {
|
|
14825
15438
|
const duration = t.lastSeen - t.firstSeen;
|
|
14826
15439
|
const closure = this.trackClosureState.get(t.trackId);
|
|
15440
|
+
const ownedMedia = await this.mediaStore?.listByOwner("track", t.trackId) ?? [];
|
|
14827
15441
|
const outcome = decideZeroMediaPolicy({
|
|
14828
15442
|
durationMs: duration,
|
|
14829
|
-
hasMedia:
|
|
15443
|
+
hasMedia: ownedMedia.length > 0,
|
|
14830
15444
|
confirmed: closure?.confirmed ?? false,
|
|
14831
15445
|
hasRasterFallback: closure?.rasterFallback !== void 0
|
|
14832
15446
|
});
|
|
@@ -14861,6 +15475,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14861
15475
|
}
|
|
14862
15476
|
});
|
|
14863
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
|
+
}
|
|
14864
15497
|
this.ctx.logger.info("track ended", {
|
|
14865
15498
|
tags: { deviceId: t.deviceId },
|
|
14866
15499
|
meta: {
|
|
@@ -14918,6 +15551,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14918
15551
|
this.bestFrameTracker.delete(t.trackId);
|
|
14919
15552
|
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
14920
15553
|
this.lastFrameAtByTrack.delete(t.trackId);
|
|
15554
|
+
this.lastFrameInFlight.delete(t.trackId);
|
|
15555
|
+
this.firstFramePendingTracks.delete(t.trackId);
|
|
15556
|
+
this.firstFrameInFlight.delete(t.trackId);
|
|
14921
15557
|
this.thumbnailLandedTracks.delete(t.trackId);
|
|
14922
15558
|
this.thumbnailInFlight.delete(t.trackId);
|
|
14923
15559
|
this.keyFrameInFlight.delete(t.trackId);
|
|
@@ -14976,6 +15612,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14976
15612
|
} catch (err) {
|
|
14977
15613
|
if (this.shuttingDown) return;
|
|
14978
15614
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
15615
|
+
} finally {
|
|
15616
|
+
this.sweepInFlight = false;
|
|
14979
15617
|
}
|
|
14980
15618
|
}
|
|
14981
15619
|
/**
|
|
@@ -14995,6 +15633,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
14995
15633
|
this.bestFrameTracker.delete(trackId);
|
|
14996
15634
|
this.objectEmbeddingBestSelector.delete(trackId);
|
|
14997
15635
|
this.lastFrameAtByTrack.delete(trackId);
|
|
15636
|
+
this.lastFrameInFlight.delete(trackId);
|
|
15637
|
+
this.firstFramePendingTracks.delete(trackId);
|
|
15638
|
+
this.firstFrameInFlight.delete(trackId);
|
|
14998
15639
|
this.thumbnailLandedTracks.delete(trackId);
|
|
14999
15640
|
this.thumbnailInFlight.delete(trackId);
|
|
15000
15641
|
this.keyFrameInFlight.delete(trackId);
|
|
@@ -15223,6 +15864,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15223
15864
|
this.bestFrameTracker.delete(track.trackId);
|
|
15224
15865
|
this.objectEmbeddingBestSelector.delete(track.trackId);
|
|
15225
15866
|
this.lastFrameAtByTrack.delete(track.trackId);
|
|
15867
|
+
this.lastFrameInFlight.delete(track.trackId);
|
|
15868
|
+
this.firstFramePendingTracks.delete(track.trackId);
|
|
15869
|
+
this.firstFrameInFlight.delete(track.trackId);
|
|
15226
15870
|
this.thumbnailLandedTracks.delete(track.trackId);
|
|
15227
15871
|
this.thumbnailInFlight.delete(track.trackId);
|
|
15228
15872
|
this.keyFrameInFlight.delete(track.trackId);
|
|
@@ -15582,7 +16226,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
15582
16226
|
return input.kind ? all.filter((m) => m.kind === input.kind) : all;
|
|
15583
16227
|
}
|
|
15584
16228
|
async getTrackMedia(input) {
|
|
15585
|
-
|
|
16229
|
+
const all = await (this.mediaStore?.listByOwner("track", input.trackId) ?? Promise.resolve([]));
|
|
16230
|
+
const kinds = input.kinds;
|
|
16231
|
+
return kinds && kinds.length > 0 ? all.filter((m) => kinds.includes(m.kind)) : all;
|
|
15586
16232
|
}
|
|
15587
16233
|
/**
|
|
15588
16234
|
* Search object events by text using CLIP cosine similarity.
|