@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-
|
|
1
|
+
import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-DGkWhfba.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import sharp from "sharp";
|
|
4
4
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
@@ -2086,6 +2086,7 @@ var FrameProcessor = class {
|
|
|
2086
2086
|
const firstLevelBboxById = /* @__PURE__ */ new Map();
|
|
2087
2087
|
const sourceIdByBbox = /* @__PURE__ */ new Map();
|
|
2088
2088
|
const faceBboxByBbox = /* @__PURE__ */ new Map();
|
|
2089
|
+
const nativeFaceSizeByBbox = /* @__PURE__ */ new Map();
|
|
2089
2090
|
const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
|
|
2090
2091
|
const plateByBbox = /* @__PURE__ */ new Map();
|
|
2091
2092
|
const maskByBbox = /* @__PURE__ */ new Map();
|
|
@@ -2131,6 +2132,7 @@ var FrameProcessor = class {
|
|
|
2131
2132
|
h: det.bbox.height
|
|
2132
2133
|
});
|
|
2133
2134
|
if (det.faceAlignedCrop !== void 0) faceAlignedCropByBbox.set(parentBbox, det.faceAlignedCrop);
|
|
2135
|
+
if (det.nativeFaceShortSidePx !== void 0) nativeFaceSizeByBbox.set(parentBbox, det.nativeFaceShortSidePx);
|
|
2134
2136
|
if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
|
|
2135
2137
|
embedding: det.embedding,
|
|
2136
2138
|
...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
|
|
@@ -2186,6 +2188,7 @@ var FrameProcessor = class {
|
|
|
2186
2188
|
});
|
|
2187
2189
|
const emb = embeddingByBbox.get(td.bbox);
|
|
2188
2190
|
const faceBbox = faceBboxByBbox.get(td.bbox);
|
|
2191
|
+
const nativeFaceShortSidePx = nativeFaceSizeByBbox.get(td.bbox);
|
|
2189
2192
|
const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
|
|
2190
2193
|
const plate = plateByBbox.get(td.bbox);
|
|
2191
2194
|
const sourceDetectionId = sourceIdByBbox.get(td.bbox);
|
|
@@ -2204,6 +2207,7 @@ var FrameProcessor = class {
|
|
|
2204
2207
|
...emb.embeddingModelId !== void 0 ? { embeddingModelId: emb.embeddingModelId } : {}
|
|
2205
2208
|
} : {},
|
|
2206
2209
|
...faceBbox !== void 0 ? { faceBbox } : {},
|
|
2210
|
+
...nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx } : {},
|
|
2207
2211
|
...faceAlignedCrop !== void 0 ? { faceAlignedCrop } : {},
|
|
2208
2212
|
...plate !== void 0 ? {
|
|
2209
2213
|
plateText: plate.text,
|
|
@@ -2340,6 +2344,10 @@ function buildTrackLifecyclePayload(input) {
|
|
|
2340
2344
|
...hasMedia ? { media } : {}
|
|
2341
2345
|
};
|
|
2342
2346
|
}
|
|
2347
|
+
//#endregion
|
|
2348
|
+
//#region src/pipeline-analytics/pipeline/edge-clear.ts
|
|
2349
|
+
/** Default border tolerance — 1% of each dimension. */
|
|
2350
|
+
var DEFAULT_EDGE_TOLERANCE = .01;
|
|
2343
2351
|
/**
|
|
2344
2352
|
* True when `bbox` sits fully inside the frame — no side within the tolerance
|
|
2345
2353
|
* band of any border. Degenerate/unknown frame dims (≤ 0) return true so the
|
|
@@ -2385,6 +2393,104 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2385
2393
|
const dist = Math.hypot(dx, dy) / Math.SQRT2;
|
|
2386
2394
|
return Math.max(0, Math.min(1, 1 - dist));
|
|
2387
2395
|
}
|
|
2396
|
+
/** Maximum fraction of a candidate's score a SINGLE fully-spanning clipped side
|
|
2397
|
+
* can subtract (the edge-containment penalty). A clipped side that spans the
|
|
2398
|
+
* whole of its border removes up to this much; a side that only grazes the
|
|
2399
|
+
* border (small perpendicular span) removes proportionally less. 0.5 = a
|
|
2400
|
+
* subject flush against — and filling — one border loses half its effective
|
|
2401
|
+
* score, so a fully-contained near-equal detection outranks it, while a
|
|
2402
|
+
* DECISIVELY higher-confidence clipped detection still wins. */
|
|
2403
|
+
var CONTAINMENT_MAX_SIDE_PENALTY = .5;
|
|
2404
|
+
/** Floor on the containment factor — a subject clipped on several borders never
|
|
2405
|
+
* drops to zero (which would annihilate its score and could leave a track with
|
|
2406
|
+
* NO best frame at all). */
|
|
2407
|
+
var CONTAINMENT_MIN = .1;
|
|
2408
|
+
/**
|
|
2409
|
+
* Edge-CONTAINMENT factor for a (clamped) bbox: an estimate in
|
|
2410
|
+
* `[CONTAINMENT_MIN, 1]` of the fraction of the subject that is actually in
|
|
2411
|
+
* frame. 1 = fully contained (no border clipping); lower = the subject is
|
|
2412
|
+
* flush against one or more borders and is probably truncated (half of a person
|
|
2413
|
+
* walking out of view).
|
|
2414
|
+
*
|
|
2415
|
+
* Because detection bboxes are already CLAMPED to the frame, the true off-screen
|
|
2416
|
+
* extent is unknown, so this is a purely-geometric proxy: for each border the
|
|
2417
|
+
* bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band), the penalty is
|
|
2418
|
+
* scaled by how much of that border the bbox spans on the perpendicular axis —
|
|
2419
|
+
* i.e. the subject's aspect against that edge. A wide bbox flush to the
|
|
2420
|
+
* top/bottom (a wide subject cut off top/bottom) or a tall bbox flush to the
|
|
2421
|
+
* left/right (a tall subject cut off at the side) is heavily penalised, whereas
|
|
2422
|
+
* a narrow subject whose feet merely graze the bottom border keeps most of its
|
|
2423
|
+
* score. Degenerate/unknown dims (≤ 0) return 1 (neutral — matches
|
|
2424
|
+
* {@link isEdgeClear}).
|
|
2425
|
+
*/
|
|
2426
|
+
function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE_TOLERANCE) {
|
|
2427
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2428
|
+
const tolX = tolerance * frameWidth;
|
|
2429
|
+
const tolY = tolerance * frameHeight;
|
|
2430
|
+
const left = bbox.x;
|
|
2431
|
+
const top = bbox.y;
|
|
2432
|
+
const right = bbox.x + bbox.w;
|
|
2433
|
+
const bottom = bbox.y + bbox.h;
|
|
2434
|
+
const spanX = Math.max(0, Math.min(1, bbox.w / frameWidth));
|
|
2435
|
+
const spanY = Math.max(0, Math.min(1, bbox.h / frameHeight));
|
|
2436
|
+
let containment = 1;
|
|
2437
|
+
const penalize = (span) => {
|
|
2438
|
+
containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * span;
|
|
2439
|
+
};
|
|
2440
|
+
if (left <= tolX) penalize(spanY);
|
|
2441
|
+
if (right >= frameWidth - tolX) penalize(spanY);
|
|
2442
|
+
if (top <= tolY) penalize(spanX);
|
|
2443
|
+
if (bottom >= frameHeight - tolY) penalize(spanX);
|
|
2444
|
+
return Math.max(CONTAINMENT_MIN, Math.min(1, containment));
|
|
2445
|
+
}
|
|
2446
|
+
/** Fraction of the frame area at which a subject's size score saturates to 1.
|
|
2447
|
+
* 10% of the frame is already a large, close subject; anything bigger gains
|
|
2448
|
+
* no further preference (and the plausibility gate rejects exploded boxes). */
|
|
2449
|
+
var SIZE_SCORE_SATURATION_AREA_FRAC = .1;
|
|
2450
|
+
/** A candidate whose effective score beats the held peak by at least this
|
|
2451
|
+
* RATIO is a DECISIVE improvement: it bypasses the capture rate limit
|
|
2452
|
+
* (`minGapMs`) the way a tier upgrade does. Drive-through subjects hit their
|
|
2453
|
+
* best framing 1–3s after birth — exactly inside the rate-limit window — and
|
|
2454
|
+
* the "better shot" otherwise never gets captured (2026-07-22 audit:
|
|
2455
|
+
* BEST_SHOT_STALE on 9/63 tracks with 1.5–3.8× larger in-frame views). */
|
|
2456
|
+
var DECISIVE_IMPROVEMENT_RATIO = 1.25;
|
|
2457
|
+
/**
|
|
2458
|
+
* Subject-size score for the best-frame ranking: sqrt of the bbox's frame-area
|
|
2459
|
+
* fraction, saturating at {@link SIZE_SCORE_SATURATION_AREA_FRAC}. A bigger,
|
|
2460
|
+
* closer subject is a better SHOT even at equal detector confidence — raw
|
|
2461
|
+
* confidence does not correlate with human-perceived crop quality (a distant
|
|
2462
|
+
* 20px person can out-score a full-frame close-up). sqrt softens the term so
|
|
2463
|
+
* confidence still matters between similar sizes. Degenerate dims → 1
|
|
2464
|
+
* (neutral), matching {@link isEdgeClear}.
|
|
2465
|
+
*/
|
|
2466
|
+
function bboxSizeScore(bbox, frameWidth, frameHeight) {
|
|
2467
|
+
if (frameWidth <= 0 || frameHeight <= 0) return 1;
|
|
2468
|
+
const areaFrac = bbox.w * bbox.h / (frameWidth * frameHeight);
|
|
2469
|
+
return Math.sqrt(Math.max(0, Math.min(1, areaFrac / SIZE_SCORE_SATURATION_AREA_FRAC)));
|
|
2470
|
+
}
|
|
2471
|
+
/** Floor of the size factor's influence: the raw sizeScore (0..1) is mapped to
|
|
2472
|
+
* `[SIZE_FACTOR_FLOOR, 1]` before scaling the confidence, so a tiny subject
|
|
2473
|
+
* halves its effective score at most. Keeps the effective scale comparable to
|
|
2474
|
+
* raw confidence — the ABSOLUTE hysteresis margin stays meaningful — while a
|
|
2475
|
+
* bigger view still earns up to a 2× relative preference. */
|
|
2476
|
+
var SIZE_FACTOR_FLOOR = .5;
|
|
2477
|
+
/** The effective within-tier score: detector confidence scaled by the
|
|
2478
|
+
* edge-containment and (floored) subject-size factors — each neutral at 1
|
|
2479
|
+
* when absent. */
|
|
2480
|
+
function effectiveScore(c) {
|
|
2481
|
+
const sizeFactor = c.sizeScore === void 0 ? 1 : SIZE_FACTOR_FLOOR + (1 - SIZE_FACTOR_FLOOR) * c.sizeScore;
|
|
2482
|
+
return c.confidence * (c.containment ?? 1) * sizeFactor;
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* True when `candidate` beats `current` DECISIVELY — same-or-better edge tier
|
|
2486
|
+
* AND an effective score at least {@link DECISIVE_IMPROVEMENT_RATIO}× the held
|
|
2487
|
+
* peak's. Used by `BestDetectionTracker` to bypass the capture rate limit
|
|
2488
|
+
* (`minGapMs`) for genuinely better framings that land inside the rate window.
|
|
2489
|
+
*/
|
|
2490
|
+
function isDecisiveImprovement(current, candidate) {
|
|
2491
|
+
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2492
|
+
return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
|
|
2493
|
+
}
|
|
2388
2494
|
/**
|
|
2389
2495
|
* Edge-aware "is `candidate` a new best over `current`?" comparator.
|
|
2390
2496
|
*
|
|
@@ -2393,8 +2499,18 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2393
2499
|
* confidence past the `hysteresis` margin wins. The tier upgrade
|
|
2394
2500
|
* (touching → clear) bypasses hysteresis — the first clear frame is always taken.
|
|
2395
2501
|
*
|
|
2502
|
+
* EDGE-CONTAINMENT (#edge-clip): within the same tier the comparison is on the
|
|
2503
|
+
* EFFECTIVE score `confidence × containment` (see {@link bboxContainment}), not
|
|
2504
|
+
* raw confidence — so a fully-contained detection outranks a larger/higher-
|
|
2505
|
+
* confidence but border-CLIPPED one unless the confidence margin is decisive.
|
|
2506
|
+
* The binary edge-clear tier still gates first (a whole subject beats a clipped
|
|
2507
|
+
* one regardless), and within the CLEAR tier both frames have containment 1, so
|
|
2508
|
+
* this is a no-op there; it discriminates WITHIN the touching tier (feet merely
|
|
2509
|
+
* grazing the bottom vs half the body out the side). `containment` defaults to
|
|
2510
|
+
* 1 when omitted (legacy face / object-embedding callers) → identical behaviour.
|
|
2511
|
+
*
|
|
2396
2512
|
* CENTERING TIE-BREAK (#27-D): within the same tier, when neither frame clearly
|
|
2397
|
-
* wins on
|
|
2513
|
+
* wins on the effective score (the two are within the `hysteresis` band) but the
|
|
2398
2514
|
* candidate is meaningfully better CENTRED ({@link CENTER_TIE_BREAK_MARGIN}), the
|
|
2399
2515
|
* candidate wins. This only engages when both sides carry a `centerScore` (the
|
|
2400
2516
|
* best-frame path), so low-importance short tracks stop keeping an edge-of-frame
|
|
@@ -2407,9 +2523,11 @@ function bboxCenterScore(bbox, frameWidth, frameHeight) {
|
|
|
2407
2523
|
function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
2408
2524
|
if (candidate.edgeClear && !current.edgeClear) return true;
|
|
2409
2525
|
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
2410
|
-
|
|
2526
|
+
const curEffective = effectiveScore(current);
|
|
2527
|
+
const candEffective = effectiveScore(candidate);
|
|
2528
|
+
if (candEffective > curEffective + hysteresis) return true;
|
|
2411
2529
|
if (candidate.centerScore !== void 0 && current.centerScore !== void 0) {
|
|
2412
|
-
if (Math.abs(
|
|
2530
|
+
if (Math.abs(candEffective - curEffective) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
|
|
2413
2531
|
}
|
|
2414
2532
|
return false;
|
|
2415
2533
|
}
|
|
@@ -2451,6 +2569,14 @@ var BestDetectionTracker = class {
|
|
|
2451
2569
|
* does not supply centering (face / object-embedding paths) → the centering
|
|
2452
2570
|
* tie-break is disabled and the legacy confidence policy applies. */
|
|
2453
2571
|
centerScore = /* @__PURE__ */ new Map();
|
|
2572
|
+
/** Held peak's edge-containment factor (0..1), PARALLEL to `best`. Absent =
|
|
2573
|
+
* the caller does not supply containment (face / object-embedding paths) →
|
|
2574
|
+
* the within-tier comparison stays on raw confidence (containment treated as
|
|
2575
|
+
* 1). */
|
|
2576
|
+
containment = /* @__PURE__ */ new Map();
|
|
2577
|
+
/** Held peak's subject-size score (0..1), PARALLEL to `best`. Absent = the
|
|
2578
|
+
* caller does not supply size (face / object-embedding paths) → neutral. */
|
|
2579
|
+
sizeScore = /* @__PURE__ */ new Map();
|
|
2454
2580
|
constructor(options = {}) {
|
|
2455
2581
|
this.hysteresis = options.hysteresis ?? 0;
|
|
2456
2582
|
this.minGapMs = options.minGapMs ?? 0;
|
|
@@ -2467,7 +2593,7 @@ var BestDetectionTracker = class {
|
|
|
2467
2593
|
* the classic policy holds: a confidence past the `hysteresis` margin that also
|
|
2468
2594
|
* respects `minGapMs` wins. On acceptance the held peak advances.
|
|
2469
2595
|
*/
|
|
2470
|
-
observe(trackId, confidence, timestamp, edgeClear, centerScore) {
|
|
2596
|
+
observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore) {
|
|
2471
2597
|
const cur = this.best.get(trackId);
|
|
2472
2598
|
if (cur === void 0) {
|
|
2473
2599
|
this.best.set(trackId, {
|
|
@@ -2476,20 +2602,32 @@ var BestDetectionTracker = class {
|
|
|
2476
2602
|
});
|
|
2477
2603
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2478
2604
|
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2605
|
+
if (containment !== void 0) this.containment.set(trackId, containment);
|
|
2606
|
+
if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
|
|
2479
2607
|
return true;
|
|
2480
2608
|
}
|
|
2481
2609
|
const curClear = this.edgeClear.get(trackId) ?? true;
|
|
2482
2610
|
const candClear = edgeClear ?? true;
|
|
2483
2611
|
const curCenter = this.centerScore.get(trackId);
|
|
2484
|
-
const
|
|
2612
|
+
const curContainment = this.containment.get(trackId);
|
|
2613
|
+
const curSize = this.sizeScore.get(trackId);
|
|
2614
|
+
const held = {
|
|
2485
2615
|
confidence: cur.confidence,
|
|
2486
2616
|
edgeClear: curClear,
|
|
2487
|
-
...curCenter !== void 0 ? { centerScore: curCenter } : {}
|
|
2488
|
-
|
|
2617
|
+
...curCenter !== void 0 ? { centerScore: curCenter } : {},
|
|
2618
|
+
...curContainment !== void 0 ? { containment: curContainment } : {},
|
|
2619
|
+
...curSize !== void 0 ? { sizeScore: curSize } : {}
|
|
2620
|
+
};
|
|
2621
|
+
const cand = {
|
|
2489
2622
|
confidence,
|
|
2490
2623
|
edgeClear: candClear,
|
|
2491
|
-
...centerScore !== void 0 ? { centerScore } : {}
|
|
2492
|
-
|
|
2624
|
+
...centerScore !== void 0 ? { centerScore } : {},
|
|
2625
|
+
...containment !== void 0 ? { containment } : {},
|
|
2626
|
+
...sizeScore !== void 0 ? { sizeScore } : {}
|
|
2627
|
+
};
|
|
2628
|
+
const tierUpgrade = candClear && !curClear;
|
|
2629
|
+
const gapOk = timestamp - cur.atMs >= this.minGapMs || sizeScore !== void 0 && isDecisiveImprovement(held, cand);
|
|
2630
|
+
const isNewBest = tierUpgrade ? true : isEdgeAwareNewBest(held, cand, this.hysteresis) && gapOk;
|
|
2493
2631
|
if (isNewBest) {
|
|
2494
2632
|
this.best.set(trackId, {
|
|
2495
2633
|
confidence,
|
|
@@ -2497,6 +2635,8 @@ var BestDetectionTracker = class {
|
|
|
2497
2635
|
});
|
|
2498
2636
|
if (edgeClear !== void 0) this.edgeClear.set(trackId, edgeClear);
|
|
2499
2637
|
if (centerScore !== void 0) this.centerScore.set(trackId, centerScore);
|
|
2638
|
+
if (containment !== void 0) this.containment.set(trackId, containment);
|
|
2639
|
+
if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
|
|
2500
2640
|
}
|
|
2501
2641
|
return isNewBest;
|
|
2502
2642
|
}
|
|
@@ -2509,14 +2649,44 @@ var BestDetectionTracker = class {
|
|
|
2509
2649
|
this.best.delete(trackId);
|
|
2510
2650
|
this.edgeClear.delete(trackId);
|
|
2511
2651
|
this.centerScore.delete(trackId);
|
|
2652
|
+
this.containment.delete(trackId);
|
|
2653
|
+
this.sizeScore.delete(trackId);
|
|
2512
2654
|
}
|
|
2513
2655
|
clear() {
|
|
2514
2656
|
this.best.clear();
|
|
2515
2657
|
this.edgeClear.clear();
|
|
2516
2658
|
this.centerScore.clear();
|
|
2659
|
+
this.containment.clear();
|
|
2660
|
+
this.sizeScore.clear();
|
|
2517
2661
|
}
|
|
2518
2662
|
};
|
|
2519
2663
|
//#endregion
|
|
2664
|
+
//#region src/pipeline-analytics/pipeline/last-frame-promotion.ts
|
|
2665
|
+
/**
|
|
2666
|
+
* Decide whether a closing track's newest `snapshot` should be promoted to be
|
|
2667
|
+
* its `lastFrame`. Pure — see the module header for the contract.
|
|
2668
|
+
*
|
|
2669
|
+
* Promote when there is at least one `snapshot` AND either there is no
|
|
2670
|
+
* `lastFrame` yet, or the newest snapshot is strictly newer than the held
|
|
2671
|
+
* `lastFrame`. Otherwise keep the current behaviour (no promotion).
|
|
2672
|
+
*/
|
|
2673
|
+
function decideLastFramePromotion(media) {
|
|
2674
|
+
let newestSnapshot;
|
|
2675
|
+
let lastFrame;
|
|
2676
|
+
for (const m of media) if (m.kind === "snapshot") {
|
|
2677
|
+
if (newestSnapshot === void 0 || m.timestamp > newestSnapshot.timestamp) newestSnapshot = m;
|
|
2678
|
+
} else if (m.kind === "lastFrame") {
|
|
2679
|
+
if (lastFrame === void 0 || m.timestamp > lastFrame.timestamp) lastFrame = m;
|
|
2680
|
+
}
|
|
2681
|
+
if (newestSnapshot === void 0) return { promote: false };
|
|
2682
|
+
if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return { promote: false };
|
|
2683
|
+
return {
|
|
2684
|
+
promote: true,
|
|
2685
|
+
snapshotKey: newestSnapshot.key,
|
|
2686
|
+
snapshotTimestamp: newestSnapshot.timestamp
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
//#endregion
|
|
2520
2690
|
//#region src/pipeline-analytics/pipeline/track-best-detection.ts
|
|
2521
2691
|
/**
|
|
2522
2692
|
* `TrackBestSelector` — the ONE unified "best detection per track" primitive.
|
|
@@ -4681,6 +4851,31 @@ var MediaStore = class {
|
|
|
4681
4851
|
return newKey;
|
|
4682
4852
|
}
|
|
4683
4853
|
/**
|
|
4854
|
+
* Promote an existing `snapshot` blob to be the track's single `lastFrame`
|
|
4855
|
+
* (operator "genuinely-last view" policy, see `last-frame-promotion.ts`).
|
|
4856
|
+
*
|
|
4857
|
+
* Both kinds share the SAME 960-boxed rendering, so promotion re-writes the
|
|
4858
|
+
* snapshot's bytes into the single-instance `lastFrame` slot (`putReplacing`,
|
|
4859
|
+
* which overwrites any held `lastFrame` blob + row) and then removes the
|
|
4860
|
+
* promoted `snapshot` row + blob — so there is exactly ONE genuinely-last view
|
|
4861
|
+
* and no duplicate snapshot/lastFrame pair. The `lastFrame` write lands BEFORE
|
|
4862
|
+
* the snapshot delete, so the view is never momentarily absent. Returns the new
|
|
4863
|
+
* `lastFrame` key.
|
|
4864
|
+
*/
|
|
4865
|
+
async promoteToLastFrame(input) {
|
|
4866
|
+
const data = Buffer.from(input.snapshot.base64, "base64");
|
|
4867
|
+
const newKey = await this.putReplacing({
|
|
4868
|
+
deviceId: input.deviceId,
|
|
4869
|
+
ownerKind: "track",
|
|
4870
|
+
ownerId: input.trackId,
|
|
4871
|
+
kind: "lastFrame",
|
|
4872
|
+
timestamp: input.snapshot.timestamp,
|
|
4873
|
+
data
|
|
4874
|
+
});
|
|
4875
|
+
await this.deleteByKey(input.snapshot.key);
|
|
4876
|
+
return newKey;
|
|
4877
|
+
}
|
|
4878
|
+
/**
|
|
4684
4879
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
4685
4880
|
* found in the index or if the blob is missing from storage.
|
|
4686
4881
|
*/
|
|
@@ -6558,37 +6753,121 @@ function squareSubjectCropRegion(bbox, frame) {
|
|
|
6558
6753
|
};
|
|
6559
6754
|
}
|
|
6560
6755
|
/**
|
|
6561
|
-
*
|
|
6562
|
-
*
|
|
6563
|
-
*
|
|
6564
|
-
*
|
|
6565
|
-
*
|
|
6566
|
-
*
|
|
6756
|
+
* Compute the 16:9 central-square window layout for a subject bbox.
|
|
6757
|
+
*
|
|
6758
|
+
* Algorithm:
|
|
6759
|
+
* 1. central square `C = squareSubjectCropRegion(bbox, frame)` — the
|
|
6760
|
+
* subject-containing, frame-clamped square of side `c` (its contract also
|
|
6761
|
+
* handles the "subject bigger than the short edge / scale the window up"
|
|
6762
|
+
* cases). `C` becomes the middle square of the output.
|
|
6763
|
+
* 2. canvas: `canvasW = round(c × 16/9)`, `canvasH = c`.
|
|
6764
|
+
* 3. anchor: place the canvas so `C` is its horizontal middle →
|
|
6765
|
+
* `frameOriginX = C.x − (canvasW − c)/2` (frame-x mapping to canvas-x 0).
|
|
6766
|
+
* 4. in-frame slab: intersect `[frameOriginX, frameOriginX + canvasW]` with
|
|
6767
|
+
* `[0, W]`; the slab is that intersection at full height `c` (C ⊆ frame ⇒
|
|
6768
|
+
* the vertical extent is always in-frame). Its canvas offset is
|
|
6769
|
+
* `slabOffsetX = fxa − frameOriginX ≥ 0`; anything outside is padding.
|
|
6567
6770
|
*/
|
|
6568
|
-
function
|
|
6569
|
-
const
|
|
6771
|
+
function wideCentralSquareLayout(bbox, frame) {
|
|
6772
|
+
const central = squareSubjectCropRegion(bbox, frame);
|
|
6773
|
+
const c = central.w;
|
|
6774
|
+
const canvasW = Math.round(c * 16 / 9);
|
|
6775
|
+
const centralX0 = Math.round((canvasW - c) / 2);
|
|
6776
|
+
let frameOriginX = central.x - (canvasW - c) / 2;
|
|
6777
|
+
if (canvasW <= frame.W) {
|
|
6778
|
+
const slideRightMax = Math.max(0, bbox.x - central.x);
|
|
6779
|
+
const slideLeftMax = Math.max(0, central.x + c - (bbox.x + bbox.w));
|
|
6780
|
+
if (frameOriginX < 0) frameOriginX += Math.min(-frameOriginX, slideRightMax);
|
|
6781
|
+
const overRight = frameOriginX + canvasW - frame.W;
|
|
6782
|
+
if (overRight > 0) frameOriginX -= Math.min(overRight, slideLeftMax);
|
|
6783
|
+
} else frameOriginX = (frame.W - canvasW) / 2;
|
|
6784
|
+
const fxa = Math.max(0, frameOriginX);
|
|
6785
|
+
const slabW = Math.min(frame.W, frameOriginX + canvasW) - fxa;
|
|
6786
|
+
const slabOffsetX = fxa - frameOriginX;
|
|
6570
6787
|
return {
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6788
|
+
fetch: {
|
|
6789
|
+
x: fxa / frame.W,
|
|
6790
|
+
y: central.y / frame.H,
|
|
6791
|
+
w: slabW / frame.W,
|
|
6792
|
+
h: c / frame.H
|
|
6793
|
+
},
|
|
6794
|
+
canvasW,
|
|
6795
|
+
canvasH: c,
|
|
6796
|
+
slabOffsetX,
|
|
6797
|
+
slabW,
|
|
6798
|
+
slabH: c,
|
|
6799
|
+
centralX0,
|
|
6800
|
+
centralSide: c,
|
|
6801
|
+
frameOriginX
|
|
6575
6802
|
};
|
|
6576
6803
|
}
|
|
6804
|
+
//#endregion
|
|
6805
|
+
//#region src/shared/frame/subject-crop-variants.ts
|
|
6806
|
+
/**
|
|
6807
|
+
* Best-shot subject-crop variants (best-crop fast-load, 2026-07-21; 16:9
|
|
6808
|
+
* central-square reframe, 2026-07-22).
|
|
6809
|
+
*
|
|
6810
|
+
* ONE native fetch (the 16:9 central-square WINDOW — see
|
|
6811
|
+
* {@link wide-central-square-crop}) backs both persisted variants:
|
|
6812
|
+
* - `thumbnail` — the native uncapped 16:9 window JPEG (subject in the
|
|
6813
|
+
* central square, lateral scene context, never upscaled).
|
|
6814
|
+
* - `thumbnailSmall` — the SAME window downscaled to {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH}
|
|
6815
|
+
* long side (854×480), the reel / lists / grid fast-load
|
|
6816
|
+
* representative.
|
|
6817
|
+
*
|
|
6818
|
+
* The small variant is DERIVED from the already-composed native window — never a
|
|
6819
|
+
* second native round-trip and NEVER upscaled: if the window is already ≤ 854 on
|
|
6820
|
+
* its long side it is stored AS-IS (byte-identical). The window is composed from
|
|
6821
|
+
* the in-frame slab plus lateral letterbox padding for the (rare) part of the
|
|
6822
|
+
* 16:9 window that falls outside the frame at a horizontal edge.
|
|
6823
|
+
*/
|
|
6824
|
+
/** Gaussian sigma for the blurred-scene letterbox fill. */
|
|
6825
|
+
var LETTERBOX_BLUR_SIGMA = 25;
|
|
6826
|
+
/** Brightness factor for the blurred fill — dimmed so the real slab reads as
|
|
6827
|
+
* the subject surface and the fill as ambience, never as sharp scene. */
|
|
6828
|
+
var LETTERBOX_BLUR_BRIGHTNESS = .55;
|
|
6829
|
+
/**
|
|
6830
|
+
* Compose the native-resolution 16:9 central-square WINDOW (`thumbnail`) from the
|
|
6831
|
+
* in-frame slab fetched for `layout`. The slab is placed at its computed offset
|
|
6832
|
+
* on a 16:9 canvas; any lateral part of the window outside the frame (a subject
|
|
6833
|
+
* flush against a frame edge — the geometry already slides the window in-frame
|
|
6834
|
+
* wherever the subject has slack) is filled with a BLURRED, dimmed stretch of
|
|
6835
|
+
* the slab itself instead of dead black bars (operator triage 2026-07-22).
|
|
6836
|
+
* When the whole window is in-frame (`slab` already spans the full canvas) the
|
|
6837
|
+
* slab is returned VERBATIM (no re-encode). NEVER upscales the real image —
|
|
6838
|
+
* the canvas is at the slab's native scale; only the out-of-frame ambience fill
|
|
6839
|
+
* is synthesized.
|
|
6840
|
+
*
|
|
6841
|
+
* `slab` must be the JPEG of `layout.fetch`; its native pixel dimensions set the
|
|
6842
|
+
* canvas scale (native px per source px = `slabNativeHeight / layout.slabH`).
|
|
6843
|
+
*/
|
|
6844
|
+
async function composeWideCentralSquareThumbnail(slab, layout) {
|
|
6845
|
+
const meta = await sharp(slab).metadata();
|
|
6846
|
+
const nativeW = meta.width ?? 0;
|
|
6847
|
+
const nativeH = meta.height ?? 0;
|
|
6848
|
+
if (nativeW <= 0 || nativeH <= 0 || layout.slabH <= 0) return slab;
|
|
6849
|
+
const scale = nativeH / layout.slabH;
|
|
6850
|
+
const canvasNativeW = Math.max(Math.round(layout.canvasW * scale), nativeW);
|
|
6851
|
+
const leftPad = Math.max(0, Math.round(layout.slabOffsetX * scale));
|
|
6852
|
+
const rightPad = Math.max(0, canvasNativeW - nativeW - leftPad);
|
|
6853
|
+
if (leftPad === 0 && rightPad === 0) return slab;
|
|
6854
|
+
return sharp(await sharp(slab).resize(canvasNativeW, nativeH, { fit: "fill" }).blur(LETTERBOX_BLUR_SIGMA).modulate({ brightness: LETTERBOX_BLUR_BRIGHTNESS }).toBuffer()).composite([{
|
|
6855
|
+
input: slab,
|
|
6856
|
+
left: leftPad,
|
|
6857
|
+
top: 0
|
|
6858
|
+
}]).jpeg({ quality: 88 }).toBuffer();
|
|
6859
|
+
}
|
|
6577
6860
|
/**
|
|
6578
|
-
* Derive the `thumbnailSmall` JPEG from
|
|
6579
|
-
*
|
|
6580
|
-
* (aspect preserved). If the
|
|
6581
|
-
* ORIGINAL buffer is returned unchanged (store as-is, never upscale
|
|
6582
|
-
* re-encode).
|
|
6861
|
+
* Derive the `thumbnailSmall` JPEG from the composed native 16:9 window.
|
|
6862
|
+
* Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (854) on the long
|
|
6863
|
+
* side (aspect preserved → ~854×480). If the window's long side is already ≤ the
|
|
6864
|
+
* cap, the ORIGINAL buffer is returned unchanged (store as-is, never upscale).
|
|
6583
6865
|
*/
|
|
6584
|
-
async function deriveThumbnailSmall(
|
|
6585
|
-
const meta = await sharp(
|
|
6866
|
+
async function deriveThumbnailSmall(nativeWideJpeg) {
|
|
6867
|
+
const meta = await sharp(nativeWideJpeg).metadata();
|
|
6586
6868
|
const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
6587
|
-
if (longSide > 0 && longSide <=
|
|
6588
|
-
return sharp(
|
|
6589
|
-
fit: "inside",
|
|
6590
|
-
withoutEnlargement: true
|
|
6591
|
-
}).jpeg({ quality: 88 }).toBuffer();
|
|
6869
|
+
if (longSide > 0 && longSide <= 854) return nativeWideJpeg;
|
|
6870
|
+
return sharp(nativeWideJpeg).resize(854, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
|
|
6592
6871
|
}
|
|
6593
6872
|
//#endregion
|
|
6594
6873
|
//#region src/shared/frame/box-drawer.ts
|
|
@@ -6671,6 +6950,9 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
|
|
|
6671
6950
|
* upscale).
|
|
6672
6951
|
*/
|
|
6673
6952
|
var SNAPSHOT_MAX_WIDTH = 960;
|
|
6953
|
+
/** Bound the once-per-owner display-fallback memo so a long-lived process never
|
|
6954
|
+
* leaks it. */
|
|
6955
|
+
var MAX_LOGGED_DISPLAY_FALLBACKS = 5e3;
|
|
6674
6956
|
/**
|
|
6675
6957
|
* Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
|
|
6676
6958
|
* ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
|
|
@@ -6750,6 +7032,9 @@ var EventMediaDispatcher = class {
|
|
|
6750
7032
|
* {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
|
|
6751
7033
|
*/
|
|
6752
7034
|
sharedNativeFullFrame;
|
|
7035
|
+
/** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
|
|
7036
|
+
* info log — see {@link logDisplayFallbackOnce}. */
|
|
7037
|
+
loggedDisplayFallbacks = /* @__PURE__ */ new Set();
|
|
6753
7038
|
constructor(deps) {
|
|
6754
7039
|
this.deps = deps;
|
|
6755
7040
|
this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
|
|
@@ -6760,9 +7045,12 @@ var EventMediaDispatcher = class {
|
|
|
6760
7045
|
const empty = {
|
|
6761
7046
|
storedSnapshots: [],
|
|
6762
7047
|
thumbnailTrackIds: [],
|
|
7048
|
+
firstFrameTrackIds: [],
|
|
7049
|
+
lastFrameTrackIds: [],
|
|
6763
7050
|
rasterFallbacks: []
|
|
6764
7051
|
};
|
|
6765
|
-
|
|
7052
|
+
const extraCandidateCount = input.rasterFallbackCandidates?.length ?? 0;
|
|
7053
|
+
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0 && extraCandidateCount === 0) return empty;
|
|
6766
7054
|
let decoded;
|
|
6767
7055
|
try {
|
|
6768
7056
|
decoded = await resolveFrame(frameHandle, { getRemoteFrame: this.deps.getRemoteFrame });
|
|
@@ -6812,44 +7100,59 @@ var EventMediaDispatcher = class {
|
|
|
6812
7100
|
});
|
|
6813
7101
|
return empty;
|
|
6814
7102
|
}
|
|
6815
|
-
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
|
|
6816
|
-
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
|
|
6817
|
-
|
|
7103
|
+
const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
|
|
7104
|
+
for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
|
|
7105
|
+
const firstFrameTrackIds = [];
|
|
7106
|
+
for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
|
|
6818
7107
|
const storedSnapshots = [];
|
|
6819
7108
|
const thumbnailTrackIds = [];
|
|
7109
|
+
const lastFrameTrackIds = [];
|
|
6820
7110
|
for (const sn of snapshots) {
|
|
6821
7111
|
const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
|
|
6822
7112
|
if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
|
|
6823
7113
|
if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
|
|
7114
|
+
if (res.lastFrameWritten) lastFrameTrackIds.push(sn.trackId);
|
|
6824
7115
|
}
|
|
6825
7116
|
return {
|
|
6826
7117
|
storedSnapshots,
|
|
6827
7118
|
thumbnailTrackIds,
|
|
7119
|
+
firstFrameTrackIds,
|
|
7120
|
+
lastFrameTrackIds,
|
|
6828
7121
|
rasterFallbacks
|
|
6829
7122
|
};
|
|
6830
7123
|
}
|
|
6831
7124
|
/**
|
|
6832
|
-
* Cut ONE clean detection-raster subject crop per WANTED track
|
|
6833
|
-
*
|
|
6834
|
-
*
|
|
6835
|
-
*
|
|
6836
|
-
*
|
|
6837
|
-
*
|
|
7125
|
+
* Cut ONE clean detection-raster subject crop per WANTED track observed this
|
|
7126
|
+
* frame — the "first available detection-raster frame" of the zero-media
|
|
7127
|
+
* fallback. Candidate bboxes come from this frame's firstFrame/snapshot targets
|
|
7128
|
+
* AND (widened) the explicit `extraCandidates` (confirmed tracks with no such
|
|
7129
|
+
* target). Cropped from the already-resolved `frameData` at its REAL resolution
|
|
7130
|
+
* via {@link extractCrop} (extract-only — NEVER upscaled). Deduped per trackId
|
|
7131
|
+
* (first candidate wins; target-derived candidates precede the extras). A
|
|
7132
|
+
* per-track encode failure is skipped (logged) — a missing fallback simply
|
|
6838
7133
|
* leaves the track with no last-resort preview, never an error.
|
|
6839
7134
|
*/
|
|
6840
|
-
async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
|
|
7135
|
+
async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted, extraCandidates) {
|
|
6841
7136
|
if (!wanted || wanted.size === 0) return [];
|
|
6842
7137
|
const seen = /* @__PURE__ */ new Set();
|
|
6843
7138
|
const out = [];
|
|
6844
|
-
const candidates = [
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
7139
|
+
const candidates = [
|
|
7140
|
+
...trackFrames.map((t) => ({
|
|
7141
|
+
trackId: t.trackId,
|
|
7142
|
+
timestamp: t.timestamp,
|
|
7143
|
+
bbox: t.bbox
|
|
7144
|
+
})),
|
|
7145
|
+
...snapshots.map((s) => ({
|
|
7146
|
+
trackId: s.trackId,
|
|
7147
|
+
timestamp: s.timestamp,
|
|
7148
|
+
bbox: s.bbox
|
|
7149
|
+
})),
|
|
7150
|
+
...(extraCandidates ?? []).map((c) => ({
|
|
7151
|
+
trackId: c.trackId,
|
|
7152
|
+
timestamp: c.timestamp,
|
|
7153
|
+
bbox: c.bbox
|
|
7154
|
+
}))
|
|
7155
|
+
];
|
|
6853
7156
|
for (const c of candidates) {
|
|
6854
7157
|
if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
|
|
6855
7158
|
seen.add(c.trackId);
|
|
@@ -6883,11 +7186,15 @@ var EventMediaDispatcher = class {
|
|
|
6883
7186
|
* TrackStore wiring (null when `appendSnapshot` is false or the encode
|
|
6884
7187
|
* failed). `thumbnailWritten` reports whether a best `thumbnail` actually
|
|
6885
7188
|
* landed this frame (#27-A) so the caller can stop forcing retries.
|
|
7189
|
+
* `lastFrameWritten` reports whether the rolling `lastFrame` actually landed
|
|
7190
|
+
* (DEFECT B) so the caller advances its `lastFrameAt` clock ONLY on a real
|
|
7191
|
+
* write — a dropped roll leaves the clock put and retries next frame.
|
|
6886
7192
|
*/
|
|
6887
7193
|
async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
|
|
6888
7194
|
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
|
|
6889
7195
|
storedSnapshot: null,
|
|
6890
|
-
thumbnailWritten: false
|
|
7196
|
+
thumbnailWritten: false,
|
|
7197
|
+
lastFrameWritten: false
|
|
6891
7198
|
};
|
|
6892
7199
|
let boxed = null;
|
|
6893
7200
|
if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
|
|
@@ -6908,7 +7215,8 @@ var EventMediaDispatcher = class {
|
|
|
6908
7215
|
bbox: sn.bbox
|
|
6909
7216
|
};
|
|
6910
7217
|
} catch {}
|
|
6911
|
-
|
|
7218
|
+
let lastFrameWritten = false;
|
|
7219
|
+
if (sn.rollingLastFrame && boxed) lastFrameWritten = await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
6912
7220
|
let thumbnailWritten = false;
|
|
6913
7221
|
if (sn.bestThumbnail) {
|
|
6914
7222
|
const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
|
|
@@ -6919,60 +7227,94 @@ var EventMediaDispatcher = class {
|
|
|
6919
7227
|
}
|
|
6920
7228
|
return {
|
|
6921
7229
|
storedSnapshot: stored,
|
|
6922
|
-
thumbnailWritten
|
|
7230
|
+
thumbnailWritten,
|
|
7231
|
+
lastFrameWritten
|
|
6923
7232
|
};
|
|
6924
7233
|
}
|
|
6925
7234
|
/**
|
|
6926
|
-
* Clean subject-centered crop of `bbox`
|
|
6927
|
-
*
|
|
6928
|
-
*
|
|
7235
|
+
* Clean subject-centered crop of `bbox` for the DISPLAY child media
|
|
7236
|
+
* (`faceCrop`/`plateCrop`): the square-safe 16:9 region around the bbox with NO
|
|
7237
|
+
* box drawn.
|
|
6929
7238
|
*
|
|
6930
|
-
*
|
|
6931
|
-
*
|
|
6932
|
-
*
|
|
6933
|
-
*
|
|
6934
|
-
*
|
|
6935
|
-
*
|
|
6936
|
-
*
|
|
7239
|
+
* These are operator gallery images, NOT model inputs (the spec's native-source
|
|
7240
|
+
* hard rule governs model inputs, which stay native-strict). So a native-ROI
|
|
7241
|
+
* miss degrades gracefully — always honest, NEVER upscaled:
|
|
7242
|
+
* 1. native ROI crop (the runner's retained native surface) — best quality;
|
|
7243
|
+
* 2. the retained full frame (keyframe-native or the runner's ≤640 RAM tier)
|
|
7244
|
+
* cropped to the SAME region;
|
|
7245
|
+
* 3. the already-resolved ≤640 detection raster (`frameData`, in hand) cropped
|
|
7246
|
+
* to the region — the last-resort honest sub-native preview.
|
|
7247
|
+
* Only a total miss (all three unavailable) returns `null`. A fallback is
|
|
7248
|
+
* logged ONCE per owner (`${eventId}:${kind}`) at info level. Runs inside the
|
|
6937
7249
|
* live-handle window opened by `captureForFrame`.
|
|
6938
7250
|
*/
|
|
6939
|
-
async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
|
|
6940
|
-
|
|
6941
|
-
|
|
7251
|
+
async cropSubjectRegion(frameHandle, frameData, fw, fh, bbox, cropPadding, ownerId) {
|
|
7252
|
+
const norm = squareSafeCropRegionNormalized(bbox, {
|
|
7253
|
+
W: fw,
|
|
7254
|
+
H: fh
|
|
7255
|
+
}, cropPadding);
|
|
7256
|
+
if (this.deps.getNativeCropJpeg) try {
|
|
7257
|
+
const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
|
|
7258
|
+
if (native) return native;
|
|
7259
|
+
} catch (err) {
|
|
7260
|
+
this.deps.logger.debug("display child crop: native ROI fetch threw", { meta: {
|
|
6942
7261
|
shmId: frameHandle.shmId,
|
|
6943
|
-
|
|
7262
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6944
7263
|
} });
|
|
6945
|
-
return null;
|
|
6946
7264
|
}
|
|
6947
7265
|
try {
|
|
6948
|
-
const
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
7266
|
+
const full = await this.sharedNativeFullFrame(frameHandle);
|
|
7267
|
+
if (full && full.format === "rgb" && full.width > 0 && full.height > 0) {
|
|
7268
|
+
const { crop } = await extractCrop(full.data, full.width, full.height, norm);
|
|
7269
|
+
this.logDisplayFallbackOnce(ownerId, "native-full-frame");
|
|
7270
|
+
return crop;
|
|
7271
|
+
}
|
|
7272
|
+
} catch (err) {
|
|
7273
|
+
this.deps.logger.debug("display child crop: full-frame fallback threw", { meta: {
|
|
6955
7274
|
shmId: frameHandle.shmId,
|
|
6956
|
-
|
|
7275
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6957
7276
|
} });
|
|
6958
|
-
|
|
7277
|
+
}
|
|
7278
|
+
try {
|
|
7279
|
+
const { crop } = await extractCrop(frameData, fw, fh, norm);
|
|
7280
|
+
this.logDisplayFallbackOnce(ownerId, "detection-raster");
|
|
7281
|
+
return crop;
|
|
6959
7282
|
} catch (err) {
|
|
6960
|
-
this.deps.logger.warn("
|
|
7283
|
+
this.deps.logger.warn("display child crop: all sources missed", { meta: {
|
|
6961
7284
|
shmId: frameHandle.shmId,
|
|
6962
7285
|
error: err instanceof Error ? err.message : String(err)
|
|
6963
7286
|
} });
|
|
6964
7287
|
return null;
|
|
6965
7288
|
}
|
|
6966
7289
|
}
|
|
7290
|
+
/** Info-log a DISPLAY-crop fallback ONCE per owner (`${eventId}:${kind}`) so a
|
|
7291
|
+
* short native window is visible without flooding the log every frame. */
|
|
7292
|
+
logDisplayFallbackOnce(ownerId, reason) {
|
|
7293
|
+
if (this.loggedDisplayFallbacks.has(ownerId)) return;
|
|
7294
|
+
if (this.loggedDisplayFallbacks.size >= MAX_LOGGED_DISPLAY_FALLBACKS) this.loggedDisplayFallbacks.clear();
|
|
7295
|
+
this.loggedDisplayFallbacks.add(ownerId);
|
|
7296
|
+
this.deps.logger.info("display child crop fallback used (honest sub-native, not upscaled)", { meta: {
|
|
7297
|
+
ownerId,
|
|
7298
|
+
reason
|
|
7299
|
+
} });
|
|
7300
|
+
}
|
|
6967
7301
|
/**
|
|
6968
7302
|
* The best-shot subject crop as its TWO persisted variants (best-crop
|
|
6969
|
-
* fast-load, 2026-07-21
|
|
6970
|
-
*
|
|
6971
|
-
*
|
|
6972
|
-
*
|
|
6973
|
-
*
|
|
6974
|
-
*
|
|
6975
|
-
*
|
|
7303
|
+
* fast-load, 2026-07-21; 16:9 central-square reframe, 2026-07-22). ONE native
|
|
7304
|
+
* fetch: the in-frame slab of the 16:9 central-square WINDOW
|
|
7305
|
+
* ({@link wideCentralSquareLayout} — a `side × 16/9` window whose middle square
|
|
7306
|
+
* of side `max(w,h)×1.2` fully contains the subject, clamped/anchored so the
|
|
7307
|
+
* subject stays in that central square at every frame edge) is requested from
|
|
7308
|
+
* the runner's retained native surface with NO `maxWidth` (uncapped TRUE
|
|
7309
|
+
* native). It is composed into the full 16:9 window (lateral letterbox only for
|
|
7310
|
+
* the part outside the frame) → the `thumbnail`. The `thumbnailSmall` is
|
|
7311
|
+
* DERIVED by downscaling that SAME window to the 854 long-side cap (854×480) —
|
|
7312
|
+
* never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
|
|
7313
|
+
* returns the window as-is when it is already ≤ 854).
|
|
7314
|
+
*
|
|
7315
|
+
* Both variants share the SAME framing + central-square containment guarantee:
|
|
7316
|
+
* wide consumers use the 16:9 image as-is (subject centered, no cut); square
|
|
7317
|
+
* consumers center-crop the middle square (subject always inside it).
|
|
6976
7318
|
*
|
|
6977
7319
|
* NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
|
|
6978
7320
|
* `null` after a loud `logger.warn`; the caller SKIPS the write and the
|
|
@@ -6988,21 +7330,22 @@ var EventMediaDispatcher = class {
|
|
|
6988
7330
|
return null;
|
|
6989
7331
|
}
|
|
6990
7332
|
try {
|
|
6991
|
-
const
|
|
7333
|
+
const layout = wideCentralSquareLayout(bbox, {
|
|
6992
7334
|
W: fw,
|
|
6993
7335
|
H: fh
|
|
6994
7336
|
});
|
|
6995
|
-
const
|
|
6996
|
-
if (!
|
|
7337
|
+
const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch);
|
|
7338
|
+
if (!slab) {
|
|
6997
7339
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
6998
7340
|
shmId: frameHandle.shmId,
|
|
6999
7341
|
reason: "native-miss"
|
|
7000
7342
|
} });
|
|
7001
7343
|
return null;
|
|
7002
7344
|
}
|
|
7345
|
+
const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
|
|
7003
7346
|
return {
|
|
7004
|
-
thumbnail
|
|
7005
|
-
thumbnailSmall: await deriveThumbnailSmall(
|
|
7347
|
+
thumbnail,
|
|
7348
|
+
thumbnailSmall: await deriveThumbnailSmall(thumbnail)
|
|
7006
7349
|
};
|
|
7007
7350
|
} catch (err) {
|
|
7008
7351
|
this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
|
|
@@ -7091,9 +7434,9 @@ var EventMediaDispatcher = class {
|
|
|
7091
7434
|
return false;
|
|
7092
7435
|
}
|
|
7093
7436
|
}
|
|
7094
|
-
async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
|
|
7437
|
+
async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
|
|
7095
7438
|
if (ev.childCrops) for (const child of ev.childCrops) try {
|
|
7096
|
-
const childCropData = await this.cropSubjectRegion(frameHandle, fw, fh, child.bbox, cropPadding);
|
|
7439
|
+
const childCropData = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, child.bbox, cropPadding, `${ev.eventId}:${child.kind}`);
|
|
7097
7440
|
if (!childCropData) continue;
|
|
7098
7441
|
await this.deps.mediaStore.put({
|
|
7099
7442
|
deviceId,
|
|
@@ -7116,7 +7459,7 @@ var EventMediaDispatcher = class {
|
|
|
7116
7459
|
}
|
|
7117
7460
|
async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
|
|
7118
7461
|
const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
|
|
7119
|
-
if (!boxed) return;
|
|
7462
|
+
if (!boxed) return false;
|
|
7120
7463
|
try {
|
|
7121
7464
|
await this.deps.mediaStore.put({
|
|
7122
7465
|
deviceId,
|
|
@@ -7126,6 +7469,7 @@ var EventMediaDispatcher = class {
|
|
|
7126
7469
|
timestamp: tf.timestamp,
|
|
7127
7470
|
data: boxed
|
|
7128
7471
|
});
|
|
7472
|
+
return true;
|
|
7129
7473
|
} catch (err) {
|
|
7130
7474
|
this.deps.logger.warn("event media: track frame failed", {
|
|
7131
7475
|
tags: { deviceId },
|
|
@@ -7135,6 +7479,7 @@ var EventMediaDispatcher = class {
|
|
|
7135
7479
|
error: err instanceof Error ? err.message : String(err)
|
|
7136
7480
|
}
|
|
7137
7481
|
});
|
|
7482
|
+
return false;
|
|
7138
7483
|
}
|
|
7139
7484
|
}
|
|
7140
7485
|
};
|
|
@@ -8919,7 +9264,8 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
8919
9264
|
*/
|
|
8920
9265
|
function planPeriodicMedia(input) {
|
|
8921
9266
|
const appendSnapshot = input.dueSnapshot;
|
|
8922
|
-
const
|
|
9267
|
+
const lastFrameInterval = input.lastFrameIntervalMs ?? input.intervalMs;
|
|
9268
|
+
const rollingLastFrame = input.saveThumbnails && input.now - input.lastFrameAt >= lastFrameInterval && !appendSnapshot;
|
|
8923
9269
|
const thumbnailLanded = input.thumbnailLanded ?? true;
|
|
8924
9270
|
return {
|
|
8925
9271
|
appendSnapshot,
|
|
@@ -8927,7 +9273,10 @@ function planPeriodicMedia(input) {
|
|
|
8927
9273
|
bestThumbnail: input.isNewBest || !thumbnailLanded
|
|
8928
9274
|
};
|
|
8929
9275
|
}
|
|
8930
|
-
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9276
|
+
var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
|
|
9277
|
+
suppressMaxDurationMs: 1e3,
|
|
9278
|
+
nothingToShowMaxDurationMs: 1500
|
|
9279
|
+
};
|
|
8931
9280
|
/**
|
|
8932
9281
|
* Classify a closing track's persistence outcome. Pure — see the module header
|
|
8933
9282
|
* for the full contract.
|
|
@@ -8935,6 +9284,7 @@ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
|
|
|
8935
9284
|
function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
|
|
8936
9285
|
if (input.hasMedia) return "persist";
|
|
8937
9286
|
if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
|
|
9287
|
+
if (!input.hasRasterFallback && input.durationMs < config.nothingToShowMaxDurationMs) return "suppress";
|
|
8938
9288
|
return input.hasRasterFallback ? "raster-fallback" : "persist";
|
|
8939
9289
|
}
|
|
8940
9290
|
//#endregion
|
|
@@ -10110,6 +10460,7 @@ var FaceRecognizer = class {
|
|
|
10110
10460
|
embedding: input.embedding,
|
|
10111
10461
|
embeddingModelId: modelId,
|
|
10112
10462
|
...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
|
|
10463
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {},
|
|
10113
10464
|
...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
|
|
10114
10465
|
};
|
|
10115
10466
|
await this.processFrame({
|
|
@@ -10125,7 +10476,7 @@ var FaceRecognizer = class {
|
|
|
10125
10476
|
}
|
|
10126
10477
|
async processFrame(input) {
|
|
10127
10478
|
const { settings } = input;
|
|
10128
|
-
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.
|
|
10479
|
+
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
|
|
10129
10480
|
if (candidates.length === 0) return;
|
|
10130
10481
|
this.deps.logger.debug("face: frame candidates", {
|
|
10131
10482
|
tags: { deviceId: input.deviceId },
|
|
@@ -10163,7 +10514,7 @@ var FaceRecognizer = class {
|
|
|
10163
10514
|
let crop;
|
|
10164
10515
|
if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
|
|
10165
10516
|
else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
10166
|
-
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
10517
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding, c.trackId) ?? void 0;
|
|
10167
10518
|
} catch (err) {
|
|
10168
10519
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
10169
10520
|
tags: { deviceId: input.deviceId },
|
|
@@ -11694,7 +12045,7 @@ var PlateRecognizer = class {
|
|
|
11694
12045
|
if (held !== void 0 && input.score <= held.score) return;
|
|
11695
12046
|
let crop;
|
|
11696
12047
|
if (input.frameHandle !== void 0) try {
|
|
11697
|
-
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
12048
|
+
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId) ?? void 0;
|
|
11698
12049
|
} catch (err) {
|
|
11699
12050
|
this.deps.logger.debug("PlateRecognizer crop capture failed", {
|
|
11700
12051
|
tags: { deviceId: input.deviceId },
|
|
@@ -11709,6 +12060,9 @@ var PlateRecognizer = class {
|
|
|
11709
12060
|
score: input.score,
|
|
11710
12061
|
bbox: input.bbox,
|
|
11711
12062
|
timestamp: input.timestamp,
|
|
12063
|
+
frameWidth: input.frameWidth,
|
|
12064
|
+
frameHeight: input.frameHeight,
|
|
12065
|
+
cropPadding: input.cropPadding,
|
|
11712
12066
|
...crop !== void 0 ? { crop } : {}
|
|
11713
12067
|
});
|
|
11714
12068
|
}
|
|
@@ -11719,15 +12073,48 @@ var PlateRecognizer = class {
|
|
|
11719
12073
|
this.bestPlate.delete(trackId);
|
|
11720
12074
|
if (held === void 0) return;
|
|
11721
12075
|
const plateId = `plate-${trackId}`;
|
|
12076
|
+
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
12077
|
+
let cropData = held.crop;
|
|
12078
|
+
if (cropData === void 0 && keyFrameMediaKey !== void 0 && this.deps.deriveCropFromKeyFrame) try {
|
|
12079
|
+
const derived = await this.deps.deriveCropFromKeyFrame({
|
|
12080
|
+
mediaKey: keyFrameMediaKey,
|
|
12081
|
+
bbox: held.bbox,
|
|
12082
|
+
frameWidth: held.frameWidth,
|
|
12083
|
+
frameHeight: held.frameHeight,
|
|
12084
|
+
padding: held.cropPadding,
|
|
12085
|
+
timestamp: held.timestamp
|
|
12086
|
+
});
|
|
12087
|
+
if (derived) {
|
|
12088
|
+
cropData = derived.jpeg;
|
|
12089
|
+
this.deps.logger.info("plate crop recovered from keyFrame (live capture missed)", {
|
|
12090
|
+
tags: {
|
|
12091
|
+
deviceId,
|
|
12092
|
+
trackId
|
|
12093
|
+
},
|
|
12094
|
+
meta: {
|
|
12095
|
+
plateId,
|
|
12096
|
+
skewMs: derived.skewMs
|
|
12097
|
+
}
|
|
12098
|
+
});
|
|
12099
|
+
}
|
|
12100
|
+
} catch (err) {
|
|
12101
|
+
this.deps.logger.debug("PlateRecognizer keyFrame crop derive failed", {
|
|
12102
|
+
tags: { deviceId },
|
|
12103
|
+
meta: {
|
|
12104
|
+
plateId,
|
|
12105
|
+
error: String(err)
|
|
12106
|
+
}
|
|
12107
|
+
});
|
|
12108
|
+
}
|
|
11722
12109
|
let mediaKey;
|
|
11723
|
-
if (
|
|
12110
|
+
if (cropData !== void 0) try {
|
|
11724
12111
|
mediaKey = await this.deps.mediaStore.put({
|
|
11725
12112
|
deviceId,
|
|
11726
12113
|
ownerKind: "plate",
|
|
11727
12114
|
ownerId: plateId,
|
|
11728
12115
|
kind: "crop",
|
|
11729
12116
|
timestamp: held.timestamp,
|
|
11730
|
-
data:
|
|
12117
|
+
data: cropData
|
|
11731
12118
|
});
|
|
11732
12119
|
} catch (err) {
|
|
11733
12120
|
this.deps.logger.warn("PlateRecognizer plate crop put failed", {
|
|
@@ -11739,7 +12126,6 @@ var PlateRecognizer = class {
|
|
|
11739
12126
|
});
|
|
11740
12127
|
}
|
|
11741
12128
|
const match = this.matchVehicle(held.text, held.score);
|
|
11742
|
-
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
11743
12129
|
try {
|
|
11744
12130
|
await this.deps.plateStore.insert({
|
|
11745
12131
|
id: plateId,
|
|
@@ -11995,6 +12381,86 @@ function createCaptureCrop(deps) {
|
|
|
11995
12381
|
};
|
|
11996
12382
|
}
|
|
11997
12383
|
//#endregion
|
|
12384
|
+
//#region src/pipeline-analytics/pipeline/display-crop.ts
|
|
12385
|
+
/** Bound the once-per-owner memo so a long-lived process never leaks it. */
|
|
12386
|
+
var MAX_LOGGED_OWNERS = 5e3;
|
|
12387
|
+
function createDisplayCrop(deps) {
|
|
12388
|
+
const loggedOwners = /* @__PURE__ */ new Set();
|
|
12389
|
+
const logFallbackOnce = (ownerId, reason) => {
|
|
12390
|
+
if (loggedOwners.has(ownerId)) return;
|
|
12391
|
+
if (loggedOwners.size >= MAX_LOGGED_OWNERS) loggedOwners.clear();
|
|
12392
|
+
loggedOwners.add(ownerId);
|
|
12393
|
+
deps.logger.info("display crop fallback used (honest sub-native, not upscaled)", { meta: {
|
|
12394
|
+
ownerId,
|
|
12395
|
+
reason
|
|
12396
|
+
} });
|
|
12397
|
+
};
|
|
12398
|
+
return async (frameHandle, bbox, frameWidth, frameHeight, padding, ownerId, maxWidth) => {
|
|
12399
|
+
const norm = padBbox({
|
|
12400
|
+
x: bbox.x / frameWidth,
|
|
12401
|
+
y: bbox.y / frameHeight,
|
|
12402
|
+
w: bbox.w / frameWidth,
|
|
12403
|
+
h: bbox.h / frameHeight
|
|
12404
|
+
}, padding);
|
|
12405
|
+
const native = await deps.fetchNativeRoiRgb(frameHandle, norm, maxWidth);
|
|
12406
|
+
if (native) {
|
|
12407
|
+
const jpeg = await deps.encodeRgb(native.bytes, native.width, native.height);
|
|
12408
|
+
if (jpeg) return jpeg;
|
|
12409
|
+
}
|
|
12410
|
+
const fb = await deps.fetchFallbackFrame(frameHandle);
|
|
12411
|
+
if (fb) try {
|
|
12412
|
+
const jpeg = await deps.cropRegionToJpeg(fb.frame.bytes, fb.frame.width, fb.frame.height, norm);
|
|
12413
|
+
logFallbackOnce(ownerId, fb.reason);
|
|
12414
|
+
return jpeg;
|
|
12415
|
+
} catch (err) {
|
|
12416
|
+
deps.logger.debug("display crop fallback crop failed", { meta: {
|
|
12417
|
+
ownerId,
|
|
12418
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12419
|
+
} });
|
|
12420
|
+
}
|
|
12421
|
+
logFallbackOnce(ownerId, "unavailable");
|
|
12422
|
+
return null;
|
|
12423
|
+
};
|
|
12424
|
+
}
|
|
12425
|
+
//#endregion
|
|
12426
|
+
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
12427
|
+
function createKeyFrameCrop(deps) {
|
|
12428
|
+
return async (input) => {
|
|
12429
|
+
if (input.frameWidth <= 0 || input.frameHeight <= 0) return null;
|
|
12430
|
+
const media = await deps.getMedia(input.mediaKey);
|
|
12431
|
+
if (!media) return null;
|
|
12432
|
+
const skewMs = Math.abs(media.timestamp - input.timestamp);
|
|
12433
|
+
if (skewMs > deps.maxSkewMs) {
|
|
12434
|
+
deps.logger.debug("keyframe crop rejected — temporal skew too large", { meta: {
|
|
12435
|
+
mediaKey: input.mediaKey,
|
|
12436
|
+
skewMs,
|
|
12437
|
+
maxSkewMs: deps.maxSkewMs
|
|
12438
|
+
} });
|
|
12439
|
+
return null;
|
|
12440
|
+
}
|
|
12441
|
+
try {
|
|
12442
|
+
const rgb = await deps.decodeJpegToRgb(media.base64);
|
|
12443
|
+
if (rgb.width <= 0 || rgb.height <= 0) return null;
|
|
12444
|
+
const norm = padBbox({
|
|
12445
|
+
x: input.bbox.x / input.frameWidth,
|
|
12446
|
+
y: input.bbox.y / input.frameHeight,
|
|
12447
|
+
w: input.bbox.w / input.frameWidth,
|
|
12448
|
+
h: input.bbox.h / input.frameHeight
|
|
12449
|
+
}, input.padding);
|
|
12450
|
+
return {
|
|
12451
|
+
jpeg: await deps.cropRegionToJpeg(Buffer.from(rgb.bytes), rgb.width, rgb.height, norm),
|
|
12452
|
+
skewMs
|
|
12453
|
+
};
|
|
12454
|
+
} catch (err) {
|
|
12455
|
+
deps.logger.debug("keyframe crop derive failed", { meta: {
|
|
12456
|
+
mediaKey: input.mediaKey,
|
|
12457
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12458
|
+
} });
|
|
12459
|
+
return null;
|
|
12460
|
+
}
|
|
12461
|
+
};
|
|
12462
|
+
}
|
|
12463
|
+
//#endregion
|
|
11998
12464
|
//#region src/pipeline-analytics/pipeline/dropout-skip.ts
|
|
11999
12465
|
function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
|
|
12000
12466
|
if (!cfg.enabled) return false;
|
|
@@ -12241,6 +12707,16 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
|
12241
12707
|
/** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
|
|
12242
12708
|
* scheduled detail call's frameHandle lease is already gone. */
|
|
12243
12709
|
var DETAIL_FALLBACK_CROP_PADDING = .15;
|
|
12710
|
+
/** Long-side cap for the DISPLAY-crop full-frame fallback (plate/face gallery
|
|
12711
|
+
* tiles) when the native ROI missed. A generous native cap so a keyframe-native
|
|
12712
|
+
* tier still yields a legible plate/face crop; the runner's ≤640 RAM tier is
|
|
12713
|
+
* returned as-is (honest sub-native). Never triggers an upscale. */
|
|
12714
|
+
var DISPLAY_FALLBACK_MAX_WIDTH = 1920;
|
|
12715
|
+
/** Max |keyFrame − plate-read| skew (ms) accepted for the DURABLE keyFrame-
|
|
12716
|
+
* derived plate-crop last resort. A readable plate implies a slow/stopping
|
|
12717
|
+
* vehicle, so within this window the subject barely moves off its bbox;
|
|
12718
|
+
* beyond it the crop would show empty scene, so it is rejected (no image). */
|
|
12719
|
+
var KEYFRAME_CROP_MAX_SKEW_MS = 1500;
|
|
12244
12720
|
/** How long the active CLIP model id (from the embedding-encoder) is cached
|
|
12245
12721
|
* before re-reading. */
|
|
12246
12722
|
var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
|
|
@@ -12262,6 +12738,13 @@ var ZONE_SLICE_RECONCILE_MS = 3e4;
|
|
|
12262
12738
|
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
12263
12739
|
* detection confidence beats the held best by at least this margin (hysteresis
|
|
12264
12740
|
* so jitter around a plateau doesn't churn the write). */
|
|
12741
|
+
/** Rolling `lastFrame` cadence — deliberately DENSER than the 5s snapshot
|
|
12742
|
+
* interval: most real tracks live 3–9s, so on the snapshot cadence they
|
|
12743
|
+
* closed with NO `lastFrame` at all (29/68 in the 2026-07-22 4h audit) and
|
|
12744
|
+
* the close-time promotion had nothing to absorb. 1.5s bounds the "Ultimo"
|
|
12745
|
+
* tile's staleness at ~1.5s + the coast window, at one 960-boxed re-encode
|
|
12746
|
+
* per 1.5s per active track (putReplacing keeps a single row). */
|
|
12747
|
+
var LAST_FRAME_INTERVAL_MS = 1500;
|
|
12265
12748
|
var BEST_FRAME_HYSTERESIS = .05;
|
|
12266
12749
|
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
12267
12750
|
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
@@ -12516,6 +12999,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12516
12999
|
packageDropDetector = null;
|
|
12517
13000
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
12518
13001
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
13002
|
+
/** Reentrancy latch for `sweepExpiredTracks` — one sweep at a time (a stalled
|
|
13003
|
+
* sweep + queued interval ticks used to process the same close ~20×). */
|
|
13004
|
+
sweepInFlight = false;
|
|
12519
13005
|
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
12520
13006
|
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
12521
13007
|
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
@@ -12544,6 +13030,32 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12544
13030
|
* where no `snapshot` is appended, so it is never byte-identical to a stored
|
|
12545
13031
|
* `snapshot` (kills the end-of-track duplicate). Cleared on track end. */
|
|
12546
13032
|
lastFrameAtByTrack = /* @__PURE__ */ new Map();
|
|
13033
|
+
/** Tracks with a rolling-`lastFrame` capture CURRENTLY in flight (RC-1,
|
|
13034
|
+
* DEFECT B). The `lastFrameAt` clock now advances ONLY when the write lands
|
|
13035
|
+
* (dispatcher completion), not synchronously at plan time — so a dropped roll
|
|
13036
|
+
* (recycled/blank frame) leaves the clock put and re-rolls next frame. Without
|
|
13037
|
+
* an in-flight guard that re-roll would fire EVERY frame while the first
|
|
13038
|
+
* capture is still resolving (0.1–3s under the native path), stacking N
|
|
13039
|
+
* overlapping captures. While a track sits here `buildSnapshotTargets`
|
|
13040
|
+
* suppresses a new rolling-`lastFrame` request; the pending dispatch clears it
|
|
13041
|
+
* (and, if it landed, advances `lastFrameAtByTrack`). Cleared on track end +
|
|
13042
|
+
* reset. */
|
|
13043
|
+
lastFrameInFlight = /* @__PURE__ */ new Set();
|
|
13044
|
+
/** Confirmed-birth tracks whose `firstFrame` has NOT yet actually persisted
|
|
13045
|
+
* (DEFECT A). Seeded on a confirmed birth (alongside the birth firstFrame
|
|
13046
|
+
* target) and removed the moment the write lands. While a track sits here and
|
|
13047
|
+
* is matched this frame, `collectFirstFrameRetries` re-schedules a firstFrame
|
|
13048
|
+
* target so a birth capture dropped by a recycled/blank live frame is retried
|
|
13049
|
+
* on a later frame (earliest available view still beats none). Scoped to
|
|
13050
|
+
* confirmed births ONLY — a suppressed false-positive birth or resurrection
|
|
13051
|
+
* never enters, so it never gets a retro firstFrame. Cleared on track end +
|
|
13052
|
+
* reset. */
|
|
13053
|
+
firstFramePendingTracks = /* @__PURE__ */ new Set();
|
|
13054
|
+
/** Tracks with a `firstFrame` capture CURRENTLY in flight (RC-1, DEFECT A).
|
|
13055
|
+
* Mirrors `thumbnailInFlight`: while a firstFrame capture is resolving the
|
|
13056
|
+
* per-frame retry is suppressed so at most one capture is outstanding per
|
|
13057
|
+
* track. Cleared on dispatch settle (+ track end / reset). */
|
|
13058
|
+
firstFrameInFlight = /* @__PURE__ */ new Set();
|
|
12547
13059
|
/** Track ids whose best `thumbnail` has ACTUALLY been persisted (#27-A). A
|
|
12548
13060
|
* track absent here keeps forcing a best-thumbnail capture every frame until
|
|
12549
13061
|
* one lands, so a short / high-churn track whose first capture was dropped
|
|
@@ -12912,13 +13424,31 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12912
13424
|
logger: logger.child("CaptureCrop")
|
|
12913
13425
|
});
|
|
12914
13426
|
this.captureCrop = captureCrop;
|
|
13427
|
+
const captureDisplayCrop = createDisplayCrop({
|
|
13428
|
+
fetchNativeRoiRgb: (handle, norm, maxWidth) => fetchNativeCropRgb(handle, norm, maxWidth),
|
|
13429
|
+
fetchFallbackFrame: async (handle) => {
|
|
13430
|
+
const tiered = await fetchNativeFullFrameTiered(handle, DISPLAY_FALLBACK_MAX_WIDTH);
|
|
13431
|
+
if (!tiered || tiered.frame.format !== "rgb") return null;
|
|
13432
|
+
return {
|
|
13433
|
+
frame: {
|
|
13434
|
+
bytes: Buffer.from(tiered.frame.data),
|
|
13435
|
+
width: tiered.frame.width,
|
|
13436
|
+
height: tiered.frame.height
|
|
13437
|
+
},
|
|
13438
|
+
reason: tiered.tier === "ram-fullframe" ? "detection-raster" : "keyframe-native"
|
|
13439
|
+
};
|
|
13440
|
+
},
|
|
13441
|
+
encodeRgb: (bytes, w, h) => encodeRgbCropToJpeg(bytes, w, h),
|
|
13442
|
+
cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
|
|
13443
|
+
logger: logger.child("DisplayCrop")
|
|
13444
|
+
});
|
|
12915
13445
|
this.faceRecognizer = new FaceRecognizer({
|
|
12916
13446
|
identityStore: this.identityStore,
|
|
12917
13447
|
faceStore: this.faceStore,
|
|
12918
13448
|
mediaStore: this.mediaStore,
|
|
12919
13449
|
trackStore: this.trackStore,
|
|
12920
13450
|
eventStore: this.eventStore,
|
|
12921
|
-
captureCrop,
|
|
13451
|
+
captureCrop: captureDisplayCrop,
|
|
12922
13452
|
recomputeImportance: (trackId) => {
|
|
12923
13453
|
const trackStore = this.trackStore;
|
|
12924
13454
|
const eventStore = this.eventStore;
|
|
@@ -12933,11 +13463,26 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
12933
13463
|
logger: logger.child("FaceRecognizer")
|
|
12934
13464
|
});
|
|
12935
13465
|
this.faceRecognizer.refreshGallery();
|
|
13466
|
+
const plateMediaStore = this.mediaStore;
|
|
13467
|
+
const deriveCropFromKeyFrame = createKeyFrameCrop({
|
|
13468
|
+
getMedia: async (mediaKey) => {
|
|
13469
|
+
const m = await plateMediaStore.getByKey(mediaKey);
|
|
13470
|
+
return m ? {
|
|
13471
|
+
base64: m.base64,
|
|
13472
|
+
timestamp: m.timestamp
|
|
13473
|
+
} : null;
|
|
13474
|
+
},
|
|
13475
|
+
decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
|
|
13476
|
+
cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
|
|
13477
|
+
maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
|
|
13478
|
+
logger: logger.child("KeyFrameCrop")
|
|
13479
|
+
});
|
|
12936
13480
|
this.plateRecognizer = new PlateRecognizer({
|
|
12937
13481
|
plateStore: this.plateStore,
|
|
12938
13482
|
vehicleStore: this.vehicleStore,
|
|
12939
13483
|
mediaStore: this.mediaStore,
|
|
12940
|
-
captureCrop,
|
|
13484
|
+
captureCrop: captureDisplayCrop,
|
|
13485
|
+
deriveCropFromKeyFrame,
|
|
12941
13486
|
getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
|
|
12942
13487
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
12943
13488
|
logger: logger.child("PlateRecognizer")
|
|
@@ -13438,6 +13983,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13438
13983
|
this.dropoutSkipsByKey.clear();
|
|
13439
13984
|
this.bestFrameTracker.clear();
|
|
13440
13985
|
this.lastFrameAtByTrack.clear();
|
|
13986
|
+
this.lastFrameInFlight.clear();
|
|
13987
|
+
this.firstFramePendingTracks.clear();
|
|
13988
|
+
this.firstFrameInFlight.clear();
|
|
13441
13989
|
this.thumbnailLandedTracks.clear();
|
|
13442
13990
|
this.thumbnailInFlight.clear();
|
|
13443
13991
|
this.keyFrameInFlight.clear();
|
|
@@ -13584,6 +14132,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13584
14132
|
source,
|
|
13585
14133
|
resurrected: true
|
|
13586
14134
|
} });
|
|
14135
|
+
if (this.eventMediaDispatcher) this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13587
14136
|
continue;
|
|
13588
14137
|
}
|
|
13589
14138
|
bornCandidates.push({
|
|
@@ -13609,15 +14158,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13609
14158
|
className: t.className,
|
|
13610
14159
|
source
|
|
13611
14160
|
} });
|
|
13612
|
-
if (this.eventMediaDispatcher
|
|
13613
|
-
|
|
14161
|
+
if (this.eventMediaDispatcher) {
|
|
14162
|
+
this.firstFramePendingTracks.add(id);
|
|
14163
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
14164
|
+
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
14165
|
+
if (frameHandle) firstFrameTargets.push({
|
|
13614
14166
|
trackId: id,
|
|
13615
14167
|
timestamp: result.timestamp,
|
|
13616
14168
|
bbox: { ...t.bbox },
|
|
13617
14169
|
...t.label ? { label: t.label } : {}
|
|
13618
14170
|
});
|
|
13619
|
-
this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
|
|
13620
|
-
this.lastFrameAtByTrack.set(id, result.timestamp);
|
|
13621
14171
|
}
|
|
13622
14172
|
this.ctx.eventBus.emit({
|
|
13623
14173
|
id: `pa-${randomUUID()}`,
|
|
@@ -13766,9 +14316,26 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13766
14316
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
13767
14317
|
else plateCrops += 1;
|
|
13768
14318
|
const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
|
|
14319
|
+
for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
|
|
13769
14320
|
const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
|
|
13770
14321
|
if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
|
|
13771
|
-
|
|
14322
|
+
const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
|
|
14323
|
+
const rasterFallbackCandidates = [];
|
|
14324
|
+
const widenedRasterWantedIds = /* @__PURE__ */ new Set();
|
|
14325
|
+
for (const t of result.tracked) {
|
|
14326
|
+
if (t.matchedThisFrame === false) continue;
|
|
14327
|
+
const closure = this.trackClosureState.get(t.trackId);
|
|
14328
|
+
if (!closure?.confirmed) continue;
|
|
14329
|
+
if (closure.rasterFallback) continue;
|
|
14330
|
+
if (targetedThisFrame.has(t.trackId)) continue;
|
|
14331
|
+
widenedRasterWantedIds.add(t.trackId);
|
|
14332
|
+
rasterFallbackCandidates.push({
|
|
14333
|
+
trackId: t.trackId,
|
|
14334
|
+
timestamp: result.timestamp,
|
|
14335
|
+
bbox: { ...t.bbox }
|
|
14336
|
+
});
|
|
14337
|
+
}
|
|
14338
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0 || widenedRasterWantedIds.size > 0) {
|
|
13772
14339
|
const captureCounts = {
|
|
13773
14340
|
events: eventTargets.length,
|
|
13774
14341
|
trackFrames: firstFrameTargets.length,
|
|
@@ -13789,7 +14356,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13789
14356
|
} });
|
|
13790
14357
|
const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
|
|
13791
14358
|
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
|
|
13792
|
-
const
|
|
14359
|
+
const firstFrameInFlightTrackIds = firstFrameTargets.map((t) => t.trackId);
|
|
14360
|
+
for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.add(trackId);
|
|
14361
|
+
const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
|
|
14362
|
+
for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
|
|
14363
|
+
const dispatchTimestamp = result.timestamp;
|
|
14364
|
+
const rasterFallbackWantedTrackIds = new Set(widenedRasterWantedIds);
|
|
13793
14365
|
for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
|
|
13794
14366
|
for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
|
|
13795
14367
|
this.eventMediaDispatcher.captureForFrame({
|
|
@@ -13799,7 +14371,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13799
14371
|
trackFrames: firstFrameTargets,
|
|
13800
14372
|
snapshots: snapshotTargets,
|
|
13801
14373
|
cropPadding: mediaSettings.cropPadding,
|
|
13802
|
-
rasterFallbackWantedTrackIds
|
|
14374
|
+
rasterFallbackWantedTrackIds,
|
|
14375
|
+
rasterFallbackCandidates
|
|
13803
14376
|
}).then((res) => {
|
|
13804
14377
|
for (const rf of res.rasterFallbacks) {
|
|
13805
14378
|
const st = this.ensureTrackClosureState(deviceId, rf.trackId);
|
|
@@ -13819,8 +14392,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13819
14392
|
mediaKey: s.mediaKey
|
|
13820
14393
|
});
|
|
13821
14394
|
for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
|
|
14395
|
+
for (const trackId of res.firstFrameTrackIds) this.firstFramePendingTracks.delete(trackId);
|
|
14396
|
+
for (const trackId of res.lastFrameTrackIds) this.lastFrameAtByTrack.set(trackId, dispatchTimestamp);
|
|
13822
14397
|
}).catch(() => {}).finally(() => {
|
|
13823
14398
|
for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.delete(trackId);
|
|
14399
|
+
for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.delete(trackId);
|
|
14400
|
+
for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.delete(trackId);
|
|
13824
14401
|
});
|
|
13825
14402
|
}
|
|
13826
14403
|
}
|
|
@@ -13884,6 +14461,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13884
14461
|
}
|
|
13885
14462
|
overlayDetections = frame.detections;
|
|
13886
14463
|
}
|
|
14464
|
+
const hasMovingTrack = result.tracked.some((t) => t.state === "moving" || t.state === "entered" || t.state === "left");
|
|
13887
14465
|
this.ctx.eventBus.emit({
|
|
13888
14466
|
id: `pa-${randomUUID()}`,
|
|
13889
14467
|
timestamp: new Date(result.timestamp),
|
|
@@ -13898,7 +14476,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
13898
14476
|
timestamp: result.timestamp,
|
|
13899
14477
|
frameWidth: result.frameWidth,
|
|
13900
14478
|
frameHeight: result.frameHeight,
|
|
13901
|
-
detections: overlayDetections
|
|
14479
|
+
detections: overlayDetections,
|
|
14480
|
+
hasMovingTrack
|
|
13902
14481
|
}
|
|
13903
14482
|
});
|
|
13904
14483
|
}
|
|
@@ -14226,7 +14805,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14226
14805
|
if (!this.faceRecognizer || detail.embedding === void 0) return;
|
|
14227
14806
|
if (!await this.resolveGlobalFaceEnabled()) return;
|
|
14228
14807
|
const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
|
|
14229
|
-
|
|
14808
|
+
const faceShortSidePx = detail.nativeFaceShortSidePx ?? (detail.bbox !== void 0 ? Math.min(detail.bbox.w, detail.bbox.h) : void 0);
|
|
14809
|
+
if (faceShortSidePx !== void 0 && faceShortSidePx < settings.minFacePx) return;
|
|
14230
14810
|
await this.faceRecognizer.ingestFaceDetail({
|
|
14231
14811
|
deviceId,
|
|
14232
14812
|
trackId,
|
|
@@ -14522,6 +15102,34 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14522
15102
|
});
|
|
14523
15103
|
this.emitTrackLifecycle(payload, timestamp);
|
|
14524
15104
|
}
|
|
15105
|
+
/**
|
|
15106
|
+
* DEFECT A: build `firstFrame` RETRY targets for confirmed-birth tracks whose
|
|
15107
|
+
* birth capture never landed. A track qualifies when it is still pending
|
|
15108
|
+
* (`firstFramePendingTracks`), is OBSERVED this frame (`matchedThisFrame` — a
|
|
15109
|
+
* coasted/stale box would crop the empty scene), is NOT already targeted this
|
|
15110
|
+
* frame (its birth target), and has no capture in flight (RC-1). Each target is
|
|
15111
|
+
* stamped with the CURRENT frame timestamp so the media carries the real
|
|
15112
|
+
* capture instant, not the birth ts — the earliest AVAILABLE view still beats
|
|
15113
|
+
* no firstFrame at all.
|
|
15114
|
+
*/
|
|
15115
|
+
collectFirstFrameRetries(tracked, timestamp, alreadyTargeted) {
|
|
15116
|
+
if (this.firstFramePendingTracks.size === 0) return [];
|
|
15117
|
+
const bornThisFrame = new Set(alreadyTargeted.map((t) => t.trackId));
|
|
15118
|
+
const retries = [];
|
|
15119
|
+
for (const t of tracked) {
|
|
15120
|
+
if (t.matchedThisFrame === false) continue;
|
|
15121
|
+
if (!this.firstFramePendingTracks.has(t.trackId)) continue;
|
|
15122
|
+
if (bornThisFrame.has(t.trackId)) continue;
|
|
15123
|
+
if (this.firstFrameInFlight.has(t.trackId)) continue;
|
|
15124
|
+
retries.push({
|
|
15125
|
+
trackId: t.trackId,
|
|
15126
|
+
timestamp,
|
|
15127
|
+
bbox: { ...t.bbox },
|
|
15128
|
+
...t.label ? { label: t.label } : {}
|
|
15129
|
+
});
|
|
15130
|
+
}
|
|
15131
|
+
return retries;
|
|
15132
|
+
}
|
|
14525
15133
|
buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
|
|
14526
15134
|
const targets = [];
|
|
14527
15135
|
for (const t of tracked) {
|
|
@@ -14544,7 +15152,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14544
15152
|
frameHeight
|
|
14545
15153
|
});
|
|
14546
15154
|
const centerScore = bboxCenterScore(t.bbox, frameWidth, frameHeight);
|
|
14547
|
-
const
|
|
15155
|
+
const containment = bboxContainment(t.bbox, frameWidth, frameHeight);
|
|
15156
|
+
const sizeScore = bboxSizeScore(t.bbox, frameWidth, frameHeight);
|
|
15157
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore);
|
|
14548
15158
|
this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
|
|
14549
15159
|
const plan = planPeriodicMedia({
|
|
14550
15160
|
saveThumbnails: media.saveThumbnails,
|
|
@@ -14553,21 +15163,22 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14553
15163
|
thumbnailLanded: this.thumbnailLandedTracks.has(t.trackId),
|
|
14554
15164
|
lastFrameAt: this.lastFrameAtByTrack.get(t.trackId) ?? 0,
|
|
14555
15165
|
now: timestamp,
|
|
14556
|
-
intervalMs: media.snapshotIntervalMs
|
|
15166
|
+
intervalMs: media.snapshotIntervalMs,
|
|
15167
|
+
lastFrameIntervalMs: Math.min(LAST_FRAME_INTERVAL_MS, media.snapshotIntervalMs)
|
|
14557
15168
|
});
|
|
14558
|
-
|
|
15169
|
+
const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
|
|
14559
15170
|
if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
|
|
14560
15171
|
const plausibleBox = isPlausibleThumbnailBox(t.bbox, frameWidth, frameHeight);
|
|
14561
15172
|
const bestThumbnail = plan.bestThumbnail && plausibleBox && !this.thumbnailInFlight.has(t.trackId);
|
|
14562
15173
|
const keyFrame = isNewBest && plausibleBox;
|
|
14563
|
-
if (!plan.appendSnapshot && !
|
|
15174
|
+
if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && !keyFrame) continue;
|
|
14564
15175
|
targets.push({
|
|
14565
15176
|
trackId: t.trackId,
|
|
14566
15177
|
timestamp,
|
|
14567
15178
|
bbox: { ...t.bbox },
|
|
14568
15179
|
...t.label ? { label: t.label } : {},
|
|
14569
15180
|
appendSnapshot: plan.appendSnapshot,
|
|
14570
|
-
rollingLastFrame
|
|
15181
|
+
rollingLastFrame,
|
|
14571
15182
|
bestThumbnail,
|
|
14572
15183
|
keyFrame
|
|
14573
15184
|
});
|
|
@@ -14814,14 +15425,17 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14814
15425
|
}
|
|
14815
15426
|
async sweepExpiredTracks() {
|
|
14816
15427
|
if (this.shuttingDown || !this.trackStore) return;
|
|
15428
|
+
if (this.sweepInFlight) return;
|
|
15429
|
+
this.sweepInFlight = true;
|
|
14817
15430
|
try {
|
|
14818
15431
|
const expired = await this.trackStore.expireStale(Date.now());
|
|
14819
15432
|
for (const t of expired) {
|
|
14820
15433
|
const duration = t.lastSeen - t.firstSeen;
|
|
14821
15434
|
const closure = this.trackClosureState.get(t.trackId);
|
|
15435
|
+
const ownedMedia = await this.mediaStore?.listByOwner("track", t.trackId) ?? [];
|
|
14822
15436
|
const outcome = decideZeroMediaPolicy({
|
|
14823
15437
|
durationMs: duration,
|
|
14824
|
-
hasMedia:
|
|
15438
|
+
hasMedia: ownedMedia.length > 0,
|
|
14825
15439
|
confirmed: closure?.confirmed ?? false,
|
|
14826
15440
|
hasRasterFallback: closure?.rasterFallback !== void 0
|
|
14827
15441
|
});
|
|
@@ -14856,6 +15470,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14856
15470
|
}
|
|
14857
15471
|
});
|
|
14858
15472
|
}
|
|
15473
|
+
const promotion = decideLastFramePromotion(ownedMedia);
|
|
15474
|
+
if (promotion.promote) {
|
|
15475
|
+
const snapshot = ownedMedia.find((m) => m.key === promotion.snapshotKey);
|
|
15476
|
+
if (snapshot) try {
|
|
15477
|
+
await this.mediaStore?.promoteToLastFrame({
|
|
15478
|
+
deviceId: t.deviceId,
|
|
15479
|
+
trackId: t.trackId,
|
|
15480
|
+
snapshot
|
|
15481
|
+
});
|
|
15482
|
+
} catch (err) {
|
|
15483
|
+
this.ctx.logger.debug("lastFrame promotion failed", {
|
|
15484
|
+
tags: { deviceId: t.deviceId },
|
|
15485
|
+
meta: {
|
|
15486
|
+
trackId: t.trackId,
|
|
15487
|
+
error: String(err)
|
|
15488
|
+
}
|
|
15489
|
+
});
|
|
15490
|
+
}
|
|
15491
|
+
}
|
|
14859
15492
|
this.ctx.logger.info("track ended", {
|
|
14860
15493
|
tags: { deviceId: t.deviceId },
|
|
14861
15494
|
meta: {
|
|
@@ -14913,6 +15546,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14913
15546
|
this.bestFrameTracker.delete(t.trackId);
|
|
14914
15547
|
this.objectEmbeddingBestSelector.delete(t.trackId);
|
|
14915
15548
|
this.lastFrameAtByTrack.delete(t.trackId);
|
|
15549
|
+
this.lastFrameInFlight.delete(t.trackId);
|
|
15550
|
+
this.firstFramePendingTracks.delete(t.trackId);
|
|
15551
|
+
this.firstFrameInFlight.delete(t.trackId);
|
|
14916
15552
|
this.thumbnailLandedTracks.delete(t.trackId);
|
|
14917
15553
|
this.thumbnailInFlight.delete(t.trackId);
|
|
14918
15554
|
this.keyFrameInFlight.delete(t.trackId);
|
|
@@ -14971,6 +15607,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14971
15607
|
} catch (err) {
|
|
14972
15608
|
if (this.shuttingDown) return;
|
|
14973
15609
|
this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
|
|
15610
|
+
} finally {
|
|
15611
|
+
this.sweepInFlight = false;
|
|
14974
15612
|
}
|
|
14975
15613
|
}
|
|
14976
15614
|
/**
|
|
@@ -14990,6 +15628,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
14990
15628
|
this.bestFrameTracker.delete(trackId);
|
|
14991
15629
|
this.objectEmbeddingBestSelector.delete(trackId);
|
|
14992
15630
|
this.lastFrameAtByTrack.delete(trackId);
|
|
15631
|
+
this.lastFrameInFlight.delete(trackId);
|
|
15632
|
+
this.firstFramePendingTracks.delete(trackId);
|
|
15633
|
+
this.firstFrameInFlight.delete(trackId);
|
|
14993
15634
|
this.thumbnailLandedTracks.delete(trackId);
|
|
14994
15635
|
this.thumbnailInFlight.delete(trackId);
|
|
14995
15636
|
this.keyFrameInFlight.delete(trackId);
|
|
@@ -15218,6 +15859,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15218
15859
|
this.bestFrameTracker.delete(track.trackId);
|
|
15219
15860
|
this.objectEmbeddingBestSelector.delete(track.trackId);
|
|
15220
15861
|
this.lastFrameAtByTrack.delete(track.trackId);
|
|
15862
|
+
this.lastFrameInFlight.delete(track.trackId);
|
|
15863
|
+
this.firstFramePendingTracks.delete(track.trackId);
|
|
15864
|
+
this.firstFrameInFlight.delete(track.trackId);
|
|
15221
15865
|
this.thumbnailLandedTracks.delete(track.trackId);
|
|
15222
15866
|
this.thumbnailInFlight.delete(track.trackId);
|
|
15223
15867
|
this.keyFrameInFlight.delete(track.trackId);
|
|
@@ -15577,7 +16221,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
15577
16221
|
return input.kind ? all.filter((m) => m.kind === input.kind) : all;
|
|
15578
16222
|
}
|
|
15579
16223
|
async getTrackMedia(input) {
|
|
15580
|
-
|
|
16224
|
+
const all = await (this.mediaStore?.listByOwner("track", input.trackId) ?? Promise.resolve([]));
|
|
16225
|
+
const kinds = input.kinds;
|
|
16226
|
+
return kinds && kinds.length > 0 ? all.filter((m) => kinds.includes(m.kind)) : all;
|
|
15581
16227
|
}
|
|
15582
16228
|
/**
|
|
15583
16229
|
* Search object events by text using CLIP cosine similarity.
|