@camstack/addon-post-analysis 1.2.5 → 1.2.6

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