@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.
@@ -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-gQ5DHTYd.mjs";
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 confidence (the two are within the `hysteresis` band) but the
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
- if (candidate.confidence > current.confidence + hysteresis) return true;
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(candidate.confidence - current.confidence) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
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 isNewBest = candClear && !curClear ? true : isEdgeAwareNewBest({
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
- }, this.hysteresis) && timestamp - cur.atMs >= this.minGapMs;
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
- * The same SQUARE region as {@link squareSubjectCropRegion}, expressed in
6562
- * NORMALIZED [0,1]×[0,1] coordinates. A normalized box maps DIRECTLY onto a
6563
- * native-resolution surface of the SAME aspect ratio, so the region computed
6564
- * from the detection-frame dimensions addresses the exact same ROI on the
6565
- * runner's retained native frame. Reuses the pixel geometry verbatim (single
6566
- * source of truth) and divides by the frame dimensions.
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 squareSubjectCropRegionNormalized(bbox, frame) {
6569
- const region = squareSubjectCropRegion(bbox, frame);
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
- x: region.x / frame.W,
6572
- y: region.y / frame.H,
6573
- w: region.w / frame.W,
6574
- h: region.h / frame.H
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 an already-encoded native subject-crop
6579
- * JPEG. Downscales to at most {@link THUMBNAIL_SMALL_MAX_WIDTH} on the long side
6580
- * (aspect preserved). If the native crop's long side is already ≤ the cap, the
6581
- * ORIGINAL buffer is returned unchanged (store as-is, never upscale, no wasted
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(nativeJpeg) {
6585
- const meta = await sharp(nativeJpeg).metadata();
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 <= 480) return nativeJpeg;
6588
- return sharp(nativeJpeg).resize(480, 480, {
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
- if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
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
- for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
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 that has a
6833
- * target (firstFrame or snapshot) in this frame the "first available
6834
- * detection-raster frame" of the zero-media fallback. Cropped from the
6835
- * already-resolved `frameData` at its REAL resolution via {@link extractCrop}
6836
- * (extract-only NEVER upscaled). Deduped per trackId (first target wins).
6837
- * A per-track encode failure is skipped (logged) a missing fallback simply
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 = [...trackFrames.map((t) => ({
6845
- trackId: t.trackId,
6846
- timestamp: t.timestamp,
6847
- bbox: t.bbox
6848
- })), ...snapshots.map((s) => ({
6849
- trackId: s.trackId,
6850
- timestamp: s.timestamp,
6851
- bbox: s.bbox
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
- if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
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` the shared output contract of the
6927
- * object-event `crop` kind, the child `faceCrop`/`plateCrop`, and the track
6928
- * `thumbnail`: the square-safe 16:9 region around the bbox with NO box drawn.
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
- * NATIVE-OR-NOTHING: the region is requested from the runner's retained native
6931
- * surface (normalized [0,1] coords map directly onto it) with NO `maxWidth` —
6932
- * the crop at TRUE native resolution (decision #3). On any miss/error (or a
6933
- * runner without the method) it returns `null` after a loud `logger.warn`; the
6934
- * caller SKIPS the write and the per-frame retry lands a real native crop
6935
- * later. It is NEVER upscaled — an upscaled ≤640 tile is a blurred lie
6936
- * (case-study lapVar 5–7), so no local resize fallback exists. Runs inside the
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
- if (!this.deps.getNativeCropJpeg) {
6941
- this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
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
- reason: "no-native-cap"
7262
+ error: err instanceof Error ? err.message : String(err)
6944
7263
  } });
6945
- return null;
6946
7264
  }
6947
7265
  try {
6948
- const norm = squareSafeCropRegionNormalized(bbox, {
6949
- W: fw,
6950
- H: fh
6951
- }, cropPadding);
6952
- const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
6953
- if (native) return native;
6954
- this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
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
- reason: "native-miss"
7275
+ error: err instanceof Error ? err.message : String(err)
6957
7276
  } });
6958
- return null;
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("native subject crop miss will retry, no upscale", { meta: {
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). ONE native fetch: the SQUARE-framed subject ROI
6970
- * (side = max(w,h)×1.2, clamped to the frame {@link squareSubjectCropRegionNormalized})
6971
- * is requested from the runner's retained native surface with NO `maxWidth`
6972
- * (uncapped TRUE native, decision #3) the `thumbnail`. The `thumbnailSmall`
6973
- * is DERIVED by downscaling that SAME JPEG to the 480 long-side cap never a
6974
- * second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
6975
- * returns the native buffer as-is when it is already 480).
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 norm = squareSubjectCropRegionNormalized(bbox, {
7333
+ const layout = wideCentralSquareLayout(bbox, {
6992
7334
  W: fw,
6993
7335
  H: fh
6994
7336
  });
6995
- const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
6996
- if (!native) {
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: native,
7005
- thumbnailSmall: await deriveThumbnailSmall(native)
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 rollingLastFrame = input.saveThumbnails && input.now - input.lastFrameAt >= input.intervalMs && !appendSnapshot;
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 = { suppressMaxDurationMs: 1e3 };
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.faceBbox === void 0 || Math.min(t.faceBbox.w, t.faceBbox.h) >= settings.minFacePx));
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 (held.crop !== void 0) try {
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: held.crop
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 && frameHandle) {
13613
- firstFrameTargets.push({
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
- if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
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 rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
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
- if (detail.bbox !== void 0 && Math.min(detail.bbox.w, detail.bbox.h) < settings.minFacePx) return;
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 isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp, edgeClear, centerScore);
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
- if (plan.rollingLastFrame) this.lastFrameAtByTrack.set(t.trackId, timestamp);
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 && !plan.rollingLastFrame && !bestThumbnail && !keyFrame) continue;
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: plan.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: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
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
- return this.mediaStore?.listByOwner("track", input.trackId) ?? [];
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.