@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.
@@ -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-Db0CsDGK.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
6577
6806
  /**
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).
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.
6583
6823
  */
6584
- async function deriveThumbnailSmall(nativeJpeg) {
6585
- const meta = await sharp(nativeJpeg).metadata();
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
+ }
6860
+ /**
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).
6865
+ */
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));
@@ -6816,7 +7101,7 @@ var EventMediaDispatcher = class {
6816
7101
  return empty;
6817
7102
  }
6818
7103
  const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
6819
- for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
7104
+ for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
6820
7105
  const firstFrameTrackIds = [];
6821
7106
  for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
6822
7107
  const storedSnapshots = [];
@@ -6947,56 +7232,89 @@ var EventMediaDispatcher = class {
6947
7232
  };
6948
7233
  }
6949
7234
  /**
6950
- * Clean subject-centered crop of `bbox` the shared output contract of the
6951
- * object-event `crop` kind, the child `faceCrop`/`plateCrop`, and the track
6952
- * `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.
6953
7238
  *
6954
- * NATIVE-OR-NOTHING: the region is requested from the runner's retained native
6955
- * surface (normalized [0,1] coords map directly onto it) with NO `maxWidth` —
6956
- * the crop at TRUE native resolution (decision #3). On any miss/error (or a
6957
- * runner without the method) it returns `null` after a loud `logger.warn`; the
6958
- * caller SKIPS the write and the per-frame retry lands a real native crop
6959
- * later. It is NEVER upscaled — an upscaled ≤640 tile is a blurred lie
6960
- * (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
6961
7249
  * live-handle window opened by `captureForFrame`.
6962
7250
  */
6963
- async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
6964
- if (!this.deps.getNativeCropJpeg) {
6965
- 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: {
6966
7261
  shmId: frameHandle.shmId,
6967
- reason: "no-native-cap"
7262
+ error: err instanceof Error ? err.message : String(err)
6968
7263
  } });
6969
- return null;
6970
7264
  }
6971
7265
  try {
6972
- const norm = squareSafeCropRegionNormalized(bbox, {
6973
- W: fw,
6974
- H: fh
6975
- }, cropPadding);
6976
- const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
6977
- if (native) return native;
6978
- 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: {
6979
7274
  shmId: frameHandle.shmId,
6980
- reason: "native-miss"
7275
+ error: err instanceof Error ? err.message : String(err)
6981
7276
  } });
6982
- return null;
7277
+ }
7278
+ try {
7279
+ const { crop } = await extractCrop(frameData, fw, fh, norm);
7280
+ this.logDisplayFallbackOnce(ownerId, "detection-raster");
7281
+ return crop;
6983
7282
  } catch (err) {
6984
- 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: {
6985
7284
  shmId: frameHandle.shmId,
6986
7285
  error: err instanceof Error ? err.message : String(err)
6987
7286
  } });
6988
7287
  return null;
6989
7288
  }
6990
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
+ }
6991
7301
  /**
6992
7302
  * The best-shot subject crop as its TWO persisted variants (best-crop
6993
- * fast-load, 2026-07-21). ONE native fetch: the SQUARE-framed subject ROI
6994
- * (side = max(w,h)×1.2, clamped to the frame {@link squareSubjectCropRegionNormalized})
6995
- * is requested from the runner's retained native surface with NO `maxWidth`
6996
- * (uncapped TRUE native, decision #3) the `thumbnail`. The `thumbnailSmall`
6997
- * is DERIVED by downscaling that SAME JPEG to the 480 long-side cap never a
6998
- * second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
6999
- * 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).
7000
7318
  *
7001
7319
  * NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
7002
7320
  * `null` after a loud `logger.warn`; the caller SKIPS the write and the
@@ -7012,21 +7330,22 @@ var EventMediaDispatcher = class {
7012
7330
  return null;
7013
7331
  }
7014
7332
  try {
7015
- const norm = squareSubjectCropRegionNormalized(bbox, {
7333
+ const layout = wideCentralSquareLayout(bbox, {
7016
7334
  W: fw,
7017
7335
  H: fh
7018
7336
  });
7019
- const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
7020
- if (!native) {
7337
+ const slab = await this.deps.getNativeCropJpeg(frameHandle, layout.fetch);
7338
+ if (!slab) {
7021
7339
  this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
7022
7340
  shmId: frameHandle.shmId,
7023
7341
  reason: "native-miss"
7024
7342
  } });
7025
7343
  return null;
7026
7344
  }
7345
+ const thumbnail = await composeWideCentralSquareThumbnail(slab, layout);
7027
7346
  return {
7028
- thumbnail: native,
7029
- thumbnailSmall: await deriveThumbnailSmall(native)
7347
+ thumbnail,
7348
+ thumbnailSmall: await deriveThumbnailSmall(thumbnail)
7030
7349
  };
7031
7350
  } catch (err) {
7032
7351
  this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
@@ -7115,9 +7434,9 @@ var EventMediaDispatcher = class {
7115
7434
  return false;
7116
7435
  }
7117
7436
  }
7118
- async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
7437
+ async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
7119
7438
  if (ev.childCrops) for (const child of ev.childCrops) try {
7120
- 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}`);
7121
7440
  if (!childCropData) continue;
7122
7441
  await this.deps.mediaStore.put({
7123
7442
  deviceId,
@@ -8945,7 +9264,8 @@ function evaluatePeriodicSnapshot(input) {
8945
9264
  */
8946
9265
  function planPeriodicMedia(input) {
8947
9266
  const appendSnapshot = input.dueSnapshot;
8948
- 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;
8949
9269
  const thumbnailLanded = input.thumbnailLanded ?? true;
8950
9270
  return {
8951
9271
  appendSnapshot,
@@ -8953,7 +9273,10 @@ function planPeriodicMedia(input) {
8953
9273
  bestThumbnail: input.isNewBest || !thumbnailLanded
8954
9274
  };
8955
9275
  }
8956
- var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
9276
+ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = {
9277
+ suppressMaxDurationMs: 1e3,
9278
+ nothingToShowMaxDurationMs: 1500
9279
+ };
8957
9280
  /**
8958
9281
  * Classify a closing track's persistence outcome. Pure — see the module header
8959
9282
  * for the full contract.
@@ -8961,6 +9284,7 @@ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
8961
9284
  function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
8962
9285
  if (input.hasMedia) return "persist";
8963
9286
  if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
9287
+ if (!input.hasRasterFallback && input.durationMs < config.nothingToShowMaxDurationMs) return "suppress";
8964
9288
  return input.hasRasterFallback ? "raster-fallback" : "persist";
8965
9289
  }
8966
9290
  //#endregion
@@ -10136,6 +10460,7 @@ var FaceRecognizer = class {
10136
10460
  embedding: input.embedding,
10137
10461
  embeddingModelId: modelId,
10138
10462
  ...input.faceBbox !== void 0 ? { faceBbox: input.faceBbox } : {},
10463
+ ...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {},
10139
10464
  ...input.alignedCropJpeg !== void 0 ? { faceAlignedCrop: input.alignedCropJpeg } : {}
10140
10465
  };
10141
10466
  await this.processFrame({
@@ -10151,7 +10476,7 @@ var FaceRecognizer = class {
10151
10476
  }
10152
10477
  async processFrame(input) {
10153
10478
  const { settings } = input;
10154
- 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);
10155
10480
  if (candidates.length === 0) return;
10156
10481
  this.deps.logger.debug("face: frame candidates", {
10157
10482
  tags: { deviceId: input.deviceId },
@@ -10189,7 +10514,7 @@ var FaceRecognizer = class {
10189
10514
  let crop;
10190
10515
  if (c.faceAlignedCrop !== void 0) crop = Buffer.from(c.faceAlignedCrop, "base64");
10191
10516
  else if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
10192
- 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;
10193
10518
  } catch (err) {
10194
10519
  this.deps.logger.debug("FaceRecognizer crop capture failed", {
10195
10520
  tags: { deviceId: input.deviceId },
@@ -11720,7 +12045,7 @@ var PlateRecognizer = class {
11720
12045
  if (held !== void 0 && input.score <= held.score) return;
11721
12046
  let crop;
11722
12047
  if (input.frameHandle !== void 0) try {
11723
- 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;
11724
12049
  } catch (err) {
11725
12050
  this.deps.logger.debug("PlateRecognizer crop capture failed", {
11726
12051
  tags: { deviceId: input.deviceId },
@@ -11735,6 +12060,9 @@ var PlateRecognizer = class {
11735
12060
  score: input.score,
11736
12061
  bbox: input.bbox,
11737
12062
  timestamp: input.timestamp,
12063
+ frameWidth: input.frameWidth,
12064
+ frameHeight: input.frameHeight,
12065
+ cropPadding: input.cropPadding,
11738
12066
  ...crop !== void 0 ? { crop } : {}
11739
12067
  });
11740
12068
  }
@@ -11745,15 +12073,48 @@ var PlateRecognizer = class {
11745
12073
  this.bestPlate.delete(trackId);
11746
12074
  if (held === void 0) return;
11747
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
+ }
11748
12109
  let mediaKey;
11749
- if (held.crop !== void 0) try {
12110
+ if (cropData !== void 0) try {
11750
12111
  mediaKey = await this.deps.mediaStore.put({
11751
12112
  deviceId,
11752
12113
  ownerKind: "plate",
11753
12114
  ownerId: plateId,
11754
12115
  kind: "crop",
11755
12116
  timestamp: held.timestamp,
11756
- data: held.crop
12117
+ data: cropData
11757
12118
  });
11758
12119
  } catch (err) {
11759
12120
  this.deps.logger.warn("PlateRecognizer plate crop put failed", {
@@ -11765,7 +12126,6 @@ var PlateRecognizer = class {
11765
12126
  });
11766
12127
  }
11767
12128
  const match = this.matchVehicle(held.text, held.score);
11768
- const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
11769
12129
  try {
11770
12130
  await this.deps.plateStore.insert({
11771
12131
  id: plateId,
@@ -12021,6 +12381,86 @@ function createCaptureCrop(deps) {
12021
12381
  };
12022
12382
  }
12023
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
12024
12464
  //#region src/pipeline-analytics/pipeline/dropout-skip.ts
12025
12465
  function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
12026
12466
  if (!cfg.enabled) return false;
@@ -12267,6 +12707,16 @@ var TTL_SWEEP_INTERVAL_MS = 5e3;
12267
12707
  /** Padding applied to the parent bbox for the `cropJpeg` retry fallback when a
12268
12708
  * scheduled detail call's frameHandle lease is already gone. */
12269
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;
12270
12720
  /** How long the active CLIP model id (from the embedding-encoder) is cached
12271
12721
  * before re-reading. */
12272
12722
  var CLIP_MODEL_ID_CACHE_TTL_MS = 6e4;
@@ -12288,6 +12738,13 @@ var ZONE_SLICE_RECONCILE_MS = 3e4;
12288
12738
  /** §5 best-frame: a track's `thumbnail` is overwritten only when the current
12289
12739
  * detection confidence beats the held best by at least this margin (hysteresis
12290
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;
12291
12748
  var BEST_FRAME_HYSTERESIS = .05;
12292
12749
  /** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
12293
12750
  var BEST_FRAME_MIN_GAP_MS = 2e3;
@@ -12542,6 +12999,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12542
12999
  packageDropDetector = null;
12543
13000
  /** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
12544
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;
12545
13005
  /** Best (highest-confidence) frame per track — drives the single overwrite
12546
13006
  * `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
12547
13007
  * face path (`face-recognizer.ts`); rate-limited here since each best-frame
@@ -12964,13 +13424,31 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12964
13424
  logger: logger.child("CaptureCrop")
12965
13425
  });
12966
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
+ });
12967
13445
  this.faceRecognizer = new FaceRecognizer({
12968
13446
  identityStore: this.identityStore,
12969
13447
  faceStore: this.faceStore,
12970
13448
  mediaStore: this.mediaStore,
12971
13449
  trackStore: this.trackStore,
12972
13450
  eventStore: this.eventStore,
12973
- captureCrop,
13451
+ captureCrop: captureDisplayCrop,
12974
13452
  recomputeImportance: (trackId) => {
12975
13453
  const trackStore = this.trackStore;
12976
13454
  const eventStore = this.eventStore;
@@ -12985,11 +13463,26 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12985
13463
  logger: logger.child("FaceRecognizer")
12986
13464
  });
12987
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
+ });
12988
13480
  this.plateRecognizer = new PlateRecognizer({
12989
13481
  plateStore: this.plateStore,
12990
13482
  vehicleStore: this.vehicleStore,
12991
13483
  mediaStore: this.mediaStore,
12992
- captureCrop,
13484
+ captureCrop: captureDisplayCrop,
13485
+ deriveCropFromKeyFrame,
12993
13486
  getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
12994
13487
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
12995
13488
  logger: logger.child("PlateRecognizer")
@@ -13639,6 +14132,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13639
14132
  source,
13640
14133
  resurrected: true
13641
14134
  } });
14135
+ if (this.eventMediaDispatcher) this.lastFrameAtByTrack.set(id, result.timestamp);
13642
14136
  continue;
13643
14137
  }
13644
14138
  bornCandidates.push({
@@ -13825,7 +14319,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13825
14319
  for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
13826
14320
  const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
13827
14321
  if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
13828
- 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) {
13829
14339
  const captureCounts = {
13830
14340
  events: eventTargets.length,
13831
14341
  trackFrames: firstFrameTargets.length,
@@ -13851,24 +14361,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13851
14361
  const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
13852
14362
  for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
13853
14363
  const dispatchTimestamp = result.timestamp;
13854
- const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
14364
+ const rasterFallbackWantedTrackIds = new Set(widenedRasterWantedIds);
13855
14365
  for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
13856
14366
  for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
13857
- const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
13858
- const rasterFallbackCandidates = [];
13859
- for (const t of result.tracked) {
13860
- if (t.matchedThisFrame === false) continue;
13861
- const closure = this.trackClosureState.get(t.trackId);
13862
- if (!closure?.confirmed) continue;
13863
- if (closure.rasterFallback) continue;
13864
- if (targetedThisFrame.has(t.trackId)) continue;
13865
- rasterFallbackWantedTrackIds.add(t.trackId);
13866
- rasterFallbackCandidates.push({
13867
- trackId: t.trackId,
13868
- timestamp: result.timestamp,
13869
- bbox: { ...t.bbox }
13870
- });
13871
- }
13872
14367
  this.eventMediaDispatcher.captureForFrame({
13873
14368
  deviceId,
13874
14369
  frameHandle,
@@ -14310,7 +14805,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14310
14805
  if (!this.faceRecognizer || detail.embedding === void 0) return;
14311
14806
  if (!await this.resolveGlobalFaceEnabled()) return;
14312
14807
  const [settings, media] = await Promise.all([this.resolveDeviceFaceSettings(deviceId), this.resolveDeviceMediaSettings(deviceId)]);
14313
- 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;
14314
14810
  await this.faceRecognizer.ingestFaceDetail({
14315
14811
  deviceId,
14316
14812
  trackId,
@@ -14656,7 +15152,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14656
15152
  frameHeight
14657
15153
  });
14658
15154
  const centerScore = bboxCenterScore(t.bbox, frameWidth, frameHeight);
14659
- 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);
14660
15158
  this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
14661
15159
  const plan = planPeriodicMedia({
14662
15160
  saveThumbnails: media.saveThumbnails,
@@ -14665,7 +15163,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14665
15163
  thumbnailLanded: this.thumbnailLandedTracks.has(t.trackId),
14666
15164
  lastFrameAt: this.lastFrameAtByTrack.get(t.trackId) ?? 0,
14667
15165
  now: timestamp,
14668
- intervalMs: media.snapshotIntervalMs
15166
+ intervalMs: media.snapshotIntervalMs,
15167
+ lastFrameIntervalMs: Math.min(LAST_FRAME_INTERVAL_MS, media.snapshotIntervalMs)
14669
15168
  });
14670
15169
  const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
14671
15170
  if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
@@ -14926,14 +15425,17 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14926
15425
  }
14927
15426
  async sweepExpiredTracks() {
14928
15427
  if (this.shuttingDown || !this.trackStore) return;
15428
+ if (this.sweepInFlight) return;
15429
+ this.sweepInFlight = true;
14929
15430
  try {
14930
15431
  const expired = await this.trackStore.expireStale(Date.now());
14931
15432
  for (const t of expired) {
14932
15433
  const duration = t.lastSeen - t.firstSeen;
14933
15434
  const closure = this.trackClosureState.get(t.trackId);
15435
+ const ownedMedia = await this.mediaStore?.listByOwner("track", t.trackId) ?? [];
14934
15436
  const outcome = decideZeroMediaPolicy({
14935
15437
  durationMs: duration,
14936
- hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
15438
+ hasMedia: ownedMedia.length > 0,
14937
15439
  confirmed: closure?.confirmed ?? false,
14938
15440
  hasRasterFallback: closure?.rasterFallback !== void 0
14939
15441
  });
@@ -14968,6 +15470,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14968
15470
  }
14969
15471
  });
14970
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
+ }
14971
15492
  this.ctx.logger.info("track ended", {
14972
15493
  tags: { deviceId: t.deviceId },
14973
15494
  meta: {
@@ -15086,6 +15607,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15086
15607
  } catch (err) {
15087
15608
  if (this.shuttingDown) return;
15088
15609
  this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
15610
+ } finally {
15611
+ this.sweepInFlight = false;
15089
15612
  }
15090
15613
  }
15091
15614
  /**