@camstack/addon-post-analysis 1.1.24 → 1.1.26

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,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B0VgBdBN.js");
6
- const require_resolve_frame = require("../resolve-frame-DBXwF5fk.js");
5
+ const require_dist = require("../dist-BEx5ST1W.js");
6
+ const require_resolve_frame = require("../resolve-frame-Cbm_NFuq.js");
7
7
  let _camstack_shm_ring = require("@camstack/shm-ring");
8
8
  let sharp = require("sharp");
9
9
  sharp = require_dist.__toESM(sharp);
@@ -124,6 +124,138 @@ function clusterByEmbedding(items, threshold = .5) {
124
124
  })).toSorted((a, b) => b.size - a.size || b.cohesion - a.cohesion);
125
125
  }
126
126
  //#endregion
127
+ //#region src/pipeline-analytics/pipeline/key-event-scoring.ts
128
+ /**
129
+ * key-event-scoring — the SINGLE deterministic "importance" scorer for a
130
+ * completed track. A value in [0,1] the viewer sorts events by (events page)
131
+ * and a future reels feature pulls highlights from.
132
+ *
133
+ * PURE + DETERMINISTIC: no Date.now / no random. Time enters ONLY as the
134
+ * precomputed `durationMs` (the caller already knows a track's dwell). Given
135
+ * the same `ScoringInput`, `computeImportance` always returns the same
136
+ * `{ importance, reason }`.
137
+ *
138
+ * The score is a weighted sum of seven independent, individually-saturated
139
+ * signals. Every weight and knee is a NAMED EXPORT so the formula is auditable
140
+ * and the tests can assert exact wiring. The weights sum to exactly 1.0, so an
141
+ * all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
142
+ * all-zero track scores 0.
143
+ */
144
+ var WEIGHT_CONFIDENCE = .3;
145
+ var WEIGHT_CLASS = .2;
146
+ var WEIGHT_DWELL = .2;
147
+ var WEIGHT_PROXIMITY = .12;
148
+ var WEIGHT_TRAVEL = .08;
149
+ var WEIGHT_ZONE = .05;
150
+ var WEIGHT_IDENTITY = .05;
151
+ /** Dwell (ms) that saturates the dwell term. */
152
+ var DWELL_FULL_MS = 6e4;
153
+ /** Peak bbox area (as a fraction of frame area) that saturates the size term. */
154
+ var SIZE_FULL = .15;
155
+ var CLASS_RANK_VEHICLE = .8;
156
+ var CLASS_RANK_ANIMAL = .5;
157
+ var CLASS_RANK_DEFAULT = .25;
158
+ var PERSON_CLASSES = new Set(["person", "face"]);
159
+ var VEHICLE_CLASSES = new Set([
160
+ "vehicle",
161
+ "car",
162
+ "truck",
163
+ "bus"
164
+ ]);
165
+ var ANIMAL_CLASSES = new Set([
166
+ "animal",
167
+ "dog",
168
+ "cat"
169
+ ]);
170
+ /** Clamp to [0,1]. */
171
+ function clamp01(x) {
172
+ if (Number.isNaN(x)) return 0;
173
+ if (x < 0) return 0;
174
+ if (x > 1) return 1;
175
+ return x;
176
+ }
177
+ /** Rank a class name into [0,1] by how event-worthy it is. Case-insensitive. */
178
+ function classRank(className) {
179
+ const c = className.toLowerCase();
180
+ if (PERSON_CLASSES.has(c)) return 1;
181
+ if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
182
+ if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
183
+ return CLASS_RANK_DEFAULT;
184
+ }
185
+ /**
186
+ * Compute a deterministic importance score for a completed track.
187
+ *
188
+ * `reason` is the tag of the single largest weighted term. Ties resolve to the
189
+ * first term in a fixed, weight-descending order (confidence → class → dwell →
190
+ * proximity → travel → zone → identity), so the output stays deterministic.
191
+ */
192
+ function computeImportance(input) {
193
+ const terms = [
194
+ {
195
+ reason: "confidence",
196
+ value: WEIGHT_CONFIDENCE * clamp01(input.peakConfidence)
197
+ },
198
+ {
199
+ reason: "class",
200
+ value: WEIGHT_CLASS * classRank(input.className)
201
+ },
202
+ {
203
+ reason: "dwell",
204
+ value: WEIGHT_DWELL * clamp01(input.durationMs / DWELL_FULL_MS)
205
+ },
206
+ {
207
+ reason: "proximity",
208
+ value: WEIGHT_PROXIMITY * clamp01(input.peakBboxAreaFrac / SIZE_FULL)
209
+ },
210
+ {
211
+ reason: "travel",
212
+ value: WEIGHT_TRAVEL * clamp01(input.totalDistance / 2)
213
+ },
214
+ {
215
+ reason: "zone",
216
+ value: WEIGHT_ZONE * (input.zonesVisited.length > 0 ? 1 : 0)
217
+ },
218
+ {
219
+ reason: "identity",
220
+ value: WEIGHT_IDENTITY * (input.label ? 1 : 0)
221
+ }
222
+ ];
223
+ let sum = 0;
224
+ let best = terms[0];
225
+ for (const term of terms) {
226
+ sum += term.value;
227
+ if (term.value > best.value) best = term;
228
+ }
229
+ return {
230
+ importance: clamp01(sum),
231
+ reason: best.reason
232
+ };
233
+ }
234
+ //#endregion
235
+ //#region src/pipeline-analytics/pipeline/key-event-recompute.ts
236
+ /**
237
+ * Recompute + persist a persisted track's importance from its current fields
238
+ * (including any newly-assigned label) and re-stamp its object events. No-op
239
+ * when the track is not (yet) persisted.
240
+ */
241
+ async function recomputeTrackImportance(deps, trackId) {
242
+ const track = await deps.trackStore.getPersistedByTrackId(trackId);
243
+ if (track === null) return;
244
+ const peak = await deps.eventStore.peakForTrack(trackId);
245
+ const { importance, reason } = computeImportance({
246
+ peakConfidence: peak.peakConfidence,
247
+ className: track.className,
248
+ durationMs: track.lastSeen - track.firstSeen,
249
+ peakBboxAreaFrac: peak.peakBboxAreaFrac,
250
+ totalDistance: track.totalDistance,
251
+ zonesVisited: track.zonesVisited,
252
+ ...track.label !== void 0 ? { label: track.label } : {}
253
+ });
254
+ const bestEventId = track.bestEventId ?? peak.bestEventId;
255
+ await deps.trackStore.setImportance(trackId, importance, reason, bestEventId);
256
+ await deps.eventStore.setImportanceForTrack(trackId, importance);
257
+ }
258
+ //#endregion
127
259
  //#region src/pipeline-analytics/face-gallery-provider.ts
128
260
  /** Embedding model id used when enrolling face crops as identity samples. */
129
261
  var MODEL_ID = "arcface-r100";
@@ -244,7 +376,9 @@ var FaceGalleryProvider = class {
244
376
  ...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
245
377
  ...identityName !== void 0 ? { identityName } : {},
246
378
  assigned: face.assigned,
247
- ...base64 !== void 0 ? { base64 } : {}
379
+ ...base64 !== void 0 ? { base64 } : {},
380
+ ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
381
+ ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {}
248
382
  });
249
383
  }
250
384
  return result;
@@ -272,7 +406,9 @@ var FaceGalleryProvider = class {
272
406
  ...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
273
407
  ...identityName !== void 0 ? { identityName } : {},
274
408
  assigned: face.assigned,
275
- ...base64 !== void 0 ? { base64 } : {}
409
+ ...base64 !== void 0 ? { base64 } : {},
410
+ ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
411
+ ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {}
276
412
  };
277
413
  }
278
414
  /**
@@ -349,6 +485,10 @@ var FaceGalleryProvider = class {
349
485
  if (identityName !== void 0) {
350
486
  await this.trackStore.setLabel(currentFace.trackId, identityName);
351
487
  await this.eventStore.setLabelForTrack(currentFace.trackId, identityName);
488
+ await recomputeTrackImportance({
489
+ trackStore: this.trackStore,
490
+ eventStore: this.eventStore
491
+ }, currentFace.trackId);
352
492
  }
353
493
  this.refreshGallery();
354
494
  }
@@ -1429,6 +1569,10 @@ var FrameProcessor = class {
1429
1569
  w: det.bbox.width,
1430
1570
  h: det.bbox.height
1431
1571
  });
1572
+ if (det.embedding !== void 0 && !embeddingByBbox.has(parentBbox)) embeddingByBbox.set(parentBbox, {
1573
+ embedding: det.embedding,
1574
+ ...det.embeddingModelId !== void 0 ? { embeddingModelId: det.embeddingModelId } : {}
1575
+ });
1432
1576
  }
1433
1577
  for (const det of frame.detections) {
1434
1578
  if (det.kind !== "detail" || det.macroClass !== "plate" || !det.parentId) continue;
@@ -1532,6 +1676,152 @@ var FrameProcessor = class {
1532
1676
  }
1533
1677
  };
1534
1678
  //#endregion
1679
+ //#region src/pipeline-analytics/pipeline/best-detection-tracker.ts
1680
+ var BestDetectionTracker = class {
1681
+ hysteresis;
1682
+ minGapMs;
1683
+ best = /* @__PURE__ */ new Map();
1684
+ constructor(options = {}) {
1685
+ this.hysteresis = options.hysteresis ?? 0;
1686
+ this.minGapMs = options.minGapMs ?? 0;
1687
+ }
1688
+ /**
1689
+ * Record a detection's `confidence` (at wall-clock `timestamp`) for `trackId`.
1690
+ * Returns true when it becomes the track's new best — the first sighting, or a
1691
+ * confidence that beats the held peak by more than `hysteresis` AND respects
1692
+ * `minGapMs`. On acceptance the held peak is advanced to this observation.
1693
+ */
1694
+ observe(trackId, confidence, timestamp) {
1695
+ const cur = this.best.get(trackId);
1696
+ const isNewBest = cur === void 0 || confidence > cur.confidence + this.hysteresis && timestamp - cur.atMs >= this.minGapMs;
1697
+ if (isNewBest) this.best.set(trackId, {
1698
+ confidence,
1699
+ atMs: timestamp
1700
+ });
1701
+ return isNewBest;
1702
+ }
1703
+ /** The held peak for a track (undefined if never observed). */
1704
+ peak(trackId) {
1705
+ return this.best.get(trackId);
1706
+ }
1707
+ /** Drop a track's peak (call at track end). */
1708
+ delete(trackId) {
1709
+ this.best.delete(trackId);
1710
+ }
1711
+ clear() {
1712
+ this.best.clear();
1713
+ }
1714
+ };
1715
+ //#endregion
1716
+ //#region src/pipeline-analytics/pipeline/track-best-detection.ts
1717
+ /**
1718
+ * `TrackBestSelector` — the ONE unified "best detection per track" primitive.
1719
+ *
1720
+ * Post-analysis derives several per-track "best" artefacts (best FRAME thumbnail,
1721
+ * best OBJECT/CLIP embedding + its crop, best FACE crop). They all rank the same
1722
+ * way: highest detector confidence per `trackId`. This selector composes the
1723
+ * canonical {@link BestDetectionTracker} ranking with the RESOLVED payload the
1724
+ * consumers need at the peak (bbox / className / embedding / faceBbox), so a
1725
+ * single "new best" DECISION can drive one capture whose output is shared:
1726
+ * - the boxed best-frame `thumbnail`,
1727
+ * - the tight object crop written onto the CLIP embedding row's `mediaKey`
1728
+ * (so a search hit's thumbnail IS the embedded crop).
1729
+ *
1730
+ * It is deliberately in-memory (the peak is per live track, dropped at track
1731
+ * end). The CLIP embedding's cross-restart persistence stays a SEPARATE store-
1732
+ * side gate (`ObjectEmbeddingStore.upsertIfBetter`): this selector unifies the
1733
+ * DECISION, not the durable store (see best-detection-tracker.ts docstring).
1734
+ *
1735
+ * The face path keeps its own hold buffer because a face crop can only come
1736
+ * from a face-bearing frame (the documented FRAME↔FACE seam) — but it shares
1737
+ * this ranking so best-frame and best-face agree on which frame is "best".
1738
+ */
1739
+ var TrackBestSelector = class {
1740
+ tracker;
1741
+ payloads = /* @__PURE__ */ new Map();
1742
+ constructor(options = {}) {
1743
+ this.tracker = new BestDetectionTracker(options);
1744
+ }
1745
+ /**
1746
+ * Record an observation for its track. Returns true when it becomes the
1747
+ * track's new best (first sighting, or a confidence that beats the held peak
1748
+ * per the tracker's hysteresis / minGap rules). On acceptance the held payload
1749
+ * advances to this observation so `peak(trackId)` returns the winning frame's
1750
+ * bbox / embedding / faceBbox.
1751
+ */
1752
+ observe(obs) {
1753
+ const isNewBest = this.tracker.observe(obs.trackId, obs.confidence, obs.atMs);
1754
+ if (isNewBest) {
1755
+ const { trackId, ...payload } = obs;
1756
+ this.payloads.set(trackId, payload);
1757
+ }
1758
+ return isNewBest;
1759
+ }
1760
+ /** The held best payload for a track (undefined if never observed). */
1761
+ peak(trackId) {
1762
+ return this.payloads.get(trackId);
1763
+ }
1764
+ /** The held peak confidence for a track (undefined if never observed). */
1765
+ peakConfidence(trackId) {
1766
+ return this.tracker.peak(trackId)?.confidence;
1767
+ }
1768
+ /** Drop a track's peak + payload (call at track end). */
1769
+ delete(trackId) {
1770
+ this.tracker.delete(trackId);
1771
+ this.payloads.delete(trackId);
1772
+ }
1773
+ clear() {
1774
+ this.tracker.clear();
1775
+ this.payloads.clear();
1776
+ }
1777
+ };
1778
+ //#endregion
1779
+ //#region src/pipeline-analytics/pipeline/object-embedding-selection.ts
1780
+ function isClipObjectEmbedding(t) {
1781
+ return Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.embeddingModelId.startsWith("mobileclip-");
1782
+ }
1783
+ function resolveSearchThumbnailUrl(input) {
1784
+ if (input.baseUrl === null) return void 0;
1785
+ const id = input.embeddingMediaKey ?? input.eventId;
1786
+ return `${input.baseUrl}/${encodeURIComponent(id)}`;
1787
+ }
1788
+ //#endregion
1789
+ //#region src/pipeline-analytics/pipeline/key-event-query.ts
1790
+ async function rankKeyEvents(candidates, options, peakLookup) {
1791
+ const scored = [];
1792
+ for (const t of candidates) {
1793
+ if (options.classFilter !== void 0 && t.className !== options.classFilter) continue;
1794
+ let importance = t.importance;
1795
+ let bestEventId = t.bestEventId;
1796
+ if (importance === void 0) {
1797
+ const peak = await peakLookup(t.trackId);
1798
+ importance = computeImportance({
1799
+ peakConfidence: peak.peakConfidence,
1800
+ className: t.className,
1801
+ durationMs: t.lastSeen - t.firstSeen,
1802
+ peakBboxAreaFrac: peak.peakBboxAreaFrac,
1803
+ totalDistance: t.totalDistance,
1804
+ zonesVisited: t.zonesVisited,
1805
+ ...t.label !== void 0 ? { label: t.label } : {}
1806
+ }).importance;
1807
+ bestEventId = bestEventId ?? peak.bestEventId;
1808
+ }
1809
+ if (options.minImportance !== void 0 && importance < options.minImportance) continue;
1810
+ scored.push({
1811
+ id: bestEventId ?? t.trackId,
1812
+ trackId: t.trackId,
1813
+ timestamp: t.firstSeen,
1814
+ className: t.className,
1815
+ ...t.label !== void 0 ? { label: t.label } : {},
1816
+ importance,
1817
+ bestEventId: bestEventId ?? "",
1818
+ windowMs: t.lastSeen - t.firstSeen
1819
+ });
1820
+ }
1821
+ scored.sort((a, b) => b.importance - a.importance);
1822
+ return scored.slice(0, options.limit);
1823
+ }
1824
+ //#endregion
1535
1825
  //#region src/pipeline-analytics/pipeline/native-detection.ts
1536
1826
  /**
1537
1827
  * Nominal frame size used to denormalize native `[0,1]` boxes when a
@@ -1701,6 +1991,18 @@ var TRACKS_COLUMNS = [
1701
1991
  {
1702
1992
  name: "state",
1703
1993
  type: "TEXT"
1994
+ },
1995
+ {
1996
+ name: "importance",
1997
+ type: "REAL"
1998
+ },
1999
+ {
2000
+ name: "bestEventId",
2001
+ type: "TEXT"
2002
+ },
2003
+ {
2004
+ name: "importanceReason",
2005
+ type: "TEXT"
1704
2006
  }
1705
2007
  ];
1706
2008
  var TRACKS_INDEXES = [{
@@ -1732,7 +2034,10 @@ function cloneTrack(t) {
1732
2034
  zonesVisited: [...t.zonesVisited],
1733
2035
  totalDistance: t.totalDistance,
1734
2036
  state: t.state,
1735
- active: t.active
2037
+ active: t.active,
2038
+ ...t.importance !== void 0 ? { importance: t.importance } : {},
2039
+ ...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
2040
+ ...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
1736
2041
  };
1737
2042
  }
1738
2043
  var TrackStore = class {
@@ -1801,6 +2106,16 @@ var TrackStore = class {
1801
2106
  lastSnapshotAt(trackId) {
1802
2107
  return this.active.get(trackId)?.lastSnapshotAt ?? 0;
1803
2108
  }
2109
+ /**
2110
+ * Seed the snapshot cadence clock (once, at track start) WITHOUT appending a
2111
+ * snapshot, so the first periodic snapshot fires ~snapshotIntervalMs after the
2112
+ * track begins rather than immediately — the `firstFrame` already covers the
2113
+ * track's start. No-op if a snapshot was already taken (clock already set).
2114
+ */
2115
+ seedSnapshotClock(trackId, timestamp) {
2116
+ const t = this.active.get(trackId);
2117
+ if (t && t.lastSnapshotAt === 0) t.lastSnapshotAt = timestamp;
2118
+ }
1804
2119
  getActive(deviceId) {
1805
2120
  const out = [];
1806
2121
  for (const t of this.active.values()) if (t.deviceId === deviceId && t.active) out.push(cloneTrack(t));
@@ -1856,6 +2171,37 @@ var TrackStore = class {
1856
2171
  }
1857
2172
  }
1858
2173
  /**
2174
+ * Stamp a track's importance score (+ dominant reason and best-event pointer).
2175
+ * Updates the in-memory active entry (so the value is carried into an eventual
2176
+ * (re)persist) AND patches the already-persisted row. Mirrors `setLabel`.
2177
+ * Forward-only — never rewrites history beyond these fields.
2178
+ */
2179
+ async setImportance(trackId, importance, reason, bestEventId) {
2180
+ const active = this.active.get(trackId);
2181
+ if (active) {
2182
+ active.importance = importance;
2183
+ active.importanceReason = reason;
2184
+ if (bestEventId !== void 0) active.bestEventId = bestEventId;
2185
+ }
2186
+ const data = {
2187
+ importance,
2188
+ importanceReason: reason
2189
+ };
2190
+ if (bestEventId !== void 0) data["bestEventId"] = bestEventId;
2191
+ try {
2192
+ await this.store.update.mutate({
2193
+ collection: TRACKS_COLLECTION,
2194
+ id: trackId,
2195
+ data
2196
+ });
2197
+ } catch (err) {
2198
+ this.logger.warn("setImportance persist failed", { meta: {
2199
+ trackId,
2200
+ error: String(err)
2201
+ } });
2202
+ }
2203
+ }
2204
+ /**
1859
2205
  * Clear a track's label. Sets label to null in the persisted row (so
1860
2206
  * rowToTrack's `typeof label === 'string'` guard omits it on read → label
1861
2207
  * is absent/undefined). Also clears the in-memory active entry if present.
@@ -1926,7 +2272,10 @@ var TrackStore = class {
1926
2272
  snapshots: [...t.snapshots],
1927
2273
  zonesVisited: [...t.zonesVisited],
1928
2274
  totalDistance: t.totalDistance,
1929
- state: t.state
2275
+ state: t.state,
2276
+ ...t.importance !== void 0 ? { importance: t.importance } : {},
2277
+ ...t.bestEventId !== void 0 ? { bestEventId: t.bestEventId } : {},
2278
+ ...t.importanceReason !== void 0 ? { importanceReason: t.importanceReason } : {}
1930
2279
  }
1931
2280
  });
1932
2281
  }
@@ -1935,6 +2284,9 @@ var TrackStore = class {
1935
2284
  const snapshots = data["snapshots"] ?? [];
1936
2285
  const zones = data["zonesVisited"] ?? [];
1937
2286
  const label = data["label"];
2287
+ const importance = data["importance"];
2288
+ const bestEventId = data["bestEventId"];
2289
+ const importanceReason = data["importanceReason"];
1938
2290
  return {
1939
2291
  trackId: id,
1940
2292
  deviceId: Number(data["deviceId"]),
@@ -1947,7 +2299,10 @@ var TrackStore = class {
1947
2299
  zonesVisited: zones,
1948
2300
  totalDistance: Number(data["totalDistance"] ?? 0),
1949
2301
  state: data["state"] ?? "idle",
1950
- active: false
2302
+ active: false,
2303
+ ...typeof importance === "number" ? { importance } : {},
2304
+ ...typeof bestEventId === "string" ? { bestEventId } : {},
2305
+ ...typeof importanceReason === "string" ? { importanceReason } : {}
1951
2306
  };
1952
2307
  }
1953
2308
  };
@@ -2065,6 +2420,46 @@ var MediaStore = class {
2065
2420
  }
2066
2421
  }
2067
2422
  /**
2423
+ * Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
2424
+ * kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
2425
+ * each new capture replaces the previous one (blob + index row) rather than
2426
+ * accumulating a filmstrip the way `put` does. Deletes any existing rows of
2427
+ * that (owner, kind) first, then writes the fresh one. Returns the new key.
2428
+ */
2429
+ async putReplacing(params) {
2430
+ const existing = await this.store.query.query({
2431
+ collection: MEDIA_COLLECTION,
2432
+ filter: { where: {
2433
+ ownerKind: params.ownerKind,
2434
+ ownerId: params.ownerId,
2435
+ kind: params.kind
2436
+ } }
2437
+ });
2438
+ const newKey = await this.put(params);
2439
+ for (const row of existing) {
2440
+ if (row.id === newKey) continue;
2441
+ const path = String(row.data["path"] ?? "");
2442
+ if (path) try {
2443
+ await this.storage.delete({
2444
+ location: "eventMedia",
2445
+ relativePath: path
2446
+ });
2447
+ } catch {}
2448
+ try {
2449
+ await this.store.delete.mutate({
2450
+ collection: MEDIA_COLLECTION,
2451
+ key: row.id
2452
+ });
2453
+ } catch (err) {
2454
+ this.logger.debug("media putReplacing: stale row delete failed", { meta: {
2455
+ key: row.id,
2456
+ error: String(err)
2457
+ } });
2458
+ }
2459
+ }
2460
+ return newKey;
2461
+ }
2462
+ /**
2068
2463
  * Fetch one media entry by its key (id). Returns null if the key is not
2069
2464
  * found in the index or if the blob is missing from storage.
2070
2465
  */
@@ -2410,6 +2805,10 @@ var OBJECT_COLUMNS = [
2410
2805
  {
2411
2806
  name: "mediaKey",
2412
2807
  type: "TEXT"
2808
+ },
2809
+ {
2810
+ name: "importance",
2811
+ type: "REAL"
2413
2812
  }
2414
2813
  ];
2415
2814
  var AUDIO_COLUMNS = [
@@ -2607,6 +3006,63 @@ var EventStore = class {
2607
3006
  return updated;
2608
3007
  }
2609
3008
  /**
3009
+ * Forward-only: stamp `importance` on every already-emitted object event of a
3010
+ * track. Returns the number of events updated. Best-effort per row. Mirrors
3011
+ * `setLabelForTrack`. Called when a track's key-event score is computed (at
3012
+ * expiry) or recomputed (late label) so an event row carries the parent
3013
+ * track's importance without a join.
3014
+ */
3015
+ async setImportanceForTrack(trackId, importance) {
3016
+ const rows = await this.store.query.query({
3017
+ collection: OBJECT_EVENTS_COLLECTION,
3018
+ filter: { where: { trackId } }
3019
+ });
3020
+ let updated = 0;
3021
+ for (const row of rows) try {
3022
+ await this.store.update.mutate({
3023
+ collection: OBJECT_EVENTS_COLLECTION,
3024
+ id: row.id,
3025
+ data: { importance }
3026
+ });
3027
+ updated++;
3028
+ } catch (err) {
3029
+ this.logger.warn("setImportanceForTrack update failed", { meta: {
3030
+ trackId,
3031
+ eventId: row.id,
3032
+ error: String(err)
3033
+ } });
3034
+ }
3035
+ return updated;
3036
+ }
3037
+ /**
3038
+ * The track's highest-confidence object event, its bbox area (as a fraction of
3039
+ * frame area), and that event's id — the SHARED per-track ranking already used
3040
+ * for the best frame, read back from the persisted object events (index
3041
+ * `idx_object_track`). Returns zeros + undefined id when the track has none.
3042
+ * Used by the importance scorer at expiry and by `getKeyEvents` compute-on-read.
3043
+ */
3044
+ async peakForTrack(trackId) {
3045
+ const rows = await this.store.query.query({
3046
+ collection: OBJECT_EVENTS_COLLECTION,
3047
+ filter: { where: { trackId } }
3048
+ });
3049
+ let bestConf = -1;
3050
+ let bestEventId;
3051
+ let peakBboxAreaFrac = 0;
3052
+ for (const row of rows) {
3053
+ const conf = typeof row.data["confidence"] === "number" ? row.data["confidence"] : 0;
3054
+ if (conf <= bestConf) continue;
3055
+ bestConf = conf;
3056
+ bestEventId = row.id;
3057
+ peakBboxAreaFrac = bboxAreaFrac(row.data);
3058
+ }
3059
+ return {
3060
+ peakConfidence: bestConf < 0 ? 0 : bestConf,
3061
+ peakBboxAreaFrac,
3062
+ bestEventId
3063
+ };
3064
+ }
3065
+ /**
2610
3066
  * Clear `label` on every already-emitted object event of a track (sets label
2611
3067
  * to null so stripNulls/slimObject omit it on read → label is absent/undefined).
2612
3068
  * Returns the number of events updated. Best-effort per row. Mirrors
@@ -2801,7 +3257,8 @@ function slimObject(id, data) {
2801
3257
  timestamp: data["timestamp"],
2802
3258
  className: data["className"],
2803
3259
  ...typeof data["frameId"] === "string" ? { frameId: data["frameId"] } : {},
2804
- ...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {}
3260
+ ...typeof data["trackId"] === "string" ? { trackId: data["trackId"] } : {},
3261
+ ...typeof data["importance"] === "number" ? { importance: data["importance"] } : {}
2805
3262
  };
2806
3263
  if (typeof data["label"] === "string") return {
2807
3264
  ...base,
@@ -2831,6 +3288,17 @@ function slimAudio(id, data) {
2831
3288
  }
2832
3289
  return base;
2833
3290
  }
3291
+ function bboxAreaFrac(data) {
3292
+ const bbox = data["bbox"];
3293
+ const fw = data["frameWidth"];
3294
+ const fh = data["frameHeight"];
3295
+ if (bbox === null || typeof bbox !== "object") return 0;
3296
+ if (typeof fw !== "number" || typeof fh !== "number" || fw <= 0 || fh <= 0) return 0;
3297
+ const w = "w" in bbox && typeof bbox.w === "number" ? bbox.w : 0;
3298
+ const h = "h" in bbox && typeof bbox.h === "number" ? bbox.h : 0;
3299
+ if (w <= 0 || h <= 0) return 0;
3300
+ return w * h / (fw * fh);
3301
+ }
2834
3302
  function stripNulls(data) {
2835
3303
  const out = {};
2836
3304
  for (const [k, v] of Object.entries(data)) if (v !== null) out[k] = v;
@@ -2977,7 +3445,9 @@ var EventMediaDispatcher = class {
2977
3445
  }
2978
3446
  async captureForFrame(input) {
2979
3447
  const { deviceId, frameHandle, events, trackFrames } = input;
2980
- if (events.length === 0 && trackFrames.length === 0) return;
3448
+ const snapshots = input.snapshots ?? [];
3449
+ const empty = { storedSnapshots: [] };
3450
+ if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
2981
3451
  let decoded;
2982
3452
  try {
2983
3453
  decoded = await require_resolve_frame.resolveFrame(frameHandle, {
@@ -2994,7 +3464,7 @@ var EventMediaDispatcher = class {
2994
3464
  error: String(err)
2995
3465
  }
2996
3466
  });
2997
- return;
3467
+ return empty;
2998
3468
  }
2999
3469
  if (!decoded) {
3000
3470
  this.deps.logger.debug("event media: frame recycled before resolve", {
@@ -3004,7 +3474,7 @@ var EventMediaDispatcher = class {
3004
3474
  shmId: frameHandle.shmId
3005
3475
  }
3006
3476
  });
3007
- return;
3477
+ return empty;
3008
3478
  }
3009
3479
  if (decoded.format !== "rgb") {
3010
3480
  this.deps.logger.debug("event media: resolved frame is not RGB", {
@@ -3014,13 +3484,87 @@ var EventMediaDispatcher = class {
3014
3484
  format: decoded.format
3015
3485
  }
3016
3486
  });
3017
- return;
3487
+ return empty;
3018
3488
  }
3019
3489
  const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
3020
3490
  const fw = decoded.width;
3021
3491
  const fh = decoded.height;
3022
3492
  for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
3023
3493
  for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
3494
+ const storedSnapshots = [];
3495
+ for (const sn of snapshots) {
3496
+ const stored = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn);
3497
+ if (stored) storedSnapshots.push(stored);
3498
+ }
3499
+ return { storedSnapshots };
3500
+ }
3501
+ /**
3502
+ * Periodic per-track media (§5). Encodes the boxed frame ONCE and fans it out
3503
+ * to whichever of the three destinations is requested: an appended `snapshot`
3504
+ * (timeline filmstrip), the rolling `lastFrame` (overwrite), and the best
3505
+ * `thumbnail` (overwrite). Returns the appended snapshot for TrackStore wiring
3506
+ * (null when `appendSnapshot` is false or the encode failed).
3507
+ */
3508
+ async writeTrackSnapshot(deviceId, frameData, fw, fh, sn) {
3509
+ if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return null;
3510
+ let boxed;
3511
+ try {
3512
+ boxed = await drawBoxedFrame(frameData, fw, fh, [{
3513
+ ...sn.bbox,
3514
+ ...sn.label ? { label: sn.label } : {}
3515
+ }], { quality: MEDIA_QUALITY });
3516
+ } catch (err) {
3517
+ this.deps.logger.warn("event media: track snapshot encode failed", {
3518
+ tags: { deviceId },
3519
+ meta: {
3520
+ deviceId,
3521
+ trackId: sn.trackId,
3522
+ error: err instanceof Error ? err.message : String(err)
3523
+ }
3524
+ });
3525
+ return null;
3526
+ }
3527
+ let stored = null;
3528
+ if (sn.appendSnapshot) try {
3529
+ const mediaKey = await this.deps.mediaStore.put({
3530
+ deviceId,
3531
+ ownerKind: "track",
3532
+ ownerId: sn.trackId,
3533
+ kind: "snapshot",
3534
+ timestamp: sn.timestamp,
3535
+ data: boxed
3536
+ });
3537
+ stored = {
3538
+ trackId: sn.trackId,
3539
+ mediaKey,
3540
+ timestamp: sn.timestamp,
3541
+ bbox: sn.bbox
3542
+ };
3543
+ } catch {}
3544
+ if (sn.rollingLastFrame) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
3545
+ if (sn.bestThumbnail) await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
3546
+ return stored;
3547
+ }
3548
+ async replaceKind(deviceId, trackId, kind, timestamp, data) {
3549
+ try {
3550
+ await this.deps.mediaStore.putReplacing({
3551
+ deviceId,
3552
+ ownerKind: "track",
3553
+ ownerId: trackId,
3554
+ kind,
3555
+ timestamp,
3556
+ data
3557
+ });
3558
+ } catch (err) {
3559
+ this.deps.logger.debug(`event media: ${kind} replace failed`, {
3560
+ tags: { deviceId },
3561
+ meta: {
3562
+ deviceId,
3563
+ trackId,
3564
+ error: err instanceof Error ? err.message : String(err)
3565
+ }
3566
+ });
3567
+ }
3024
3568
  }
3025
3569
  async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
3026
3570
  const box = {
@@ -4015,11 +4559,20 @@ function resolveFaceSettings(raw) {
4015
4559
  * its default — parse never throws). Mirrors `face-settings` /
4016
4560
  * `audio-detection-settings`.
4017
4561
  */
4018
- var MediaSettingsSchema = require_dist.object({
4019
- /** Fractional padding added around a detection bbox before cropping.
4020
- * 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
4021
- * max 2 (200%). */
4022
- cropPadding: require_dist.number().min(0).max(2).default(.15) });
4562
+ var MediaSettingsSchema = require_dist.object({
4563
+ /** Fractional padding added around a detection bbox before cropping.
4564
+ * 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
4565
+ * max 2 (200%). */
4566
+ cropPadding: require_dist.number().min(0).max(2).default(.15),
4567
+ /** Master switch for periodic per-track snapshots (the timeline filmstrip +
4568
+ * the rolling `lastFrame` + the best `thumbnail`). When false, only the
4569
+ * per-track `firstFrame` and per-event media are produced. */
4570
+ saveThumbnails: require_dist.boolean().default(true),
4571
+ /** Cadence (ms) for the periodic per-track `snapshot` + rolling `lastFrame`.
4572
+ * A snapshot is captured for an active track only after this much wall-clock
4573
+ * has elapsed since its previous one. */
4574
+ snapshotIntervalMs: require_dist.number().int().min(500).max(6e4).default(5e3)
4575
+ });
4023
4576
  var MEDIA_DEFAULTS = MediaSettingsSchema.parse({});
4024
4577
  /**
4025
4578
  * Resolve a per-device store blob into typed media settings. Unknown/invalid
@@ -4030,7 +4583,11 @@ function resolveMediaSettings(raw) {
4030
4583
  const parsed = MediaSettingsSchema.shape[key].safeParse(raw[key]);
4031
4584
  return parsed.success ? parsed.data : MEDIA_DEFAULTS[key];
4032
4585
  };
4033
- return { cropPadding: pick("cropPadding") };
4586
+ return {
4587
+ cropPadding: pick("cropPadding"),
4588
+ saveThumbnails: pick("saveThumbnails"),
4589
+ snapshotIntervalMs: pick("snapshotIntervalMs")
4590
+ };
4034
4591
  }
4035
4592
  //#endregion
4036
4593
  //#region src/pipeline-analytics/store/identity-store.ts
@@ -4361,6 +4918,14 @@ var FACE_COLUMNS = [
4361
4918
  {
4362
4919
  name: "assignedSampleId",
4363
4920
  type: "TEXT"
4921
+ },
4922
+ {
4923
+ name: "keyFrameMediaKey",
4924
+ type: "TEXT"
4925
+ },
4926
+ {
4927
+ name: "faceBbox",
4928
+ type: "JSON"
4364
4929
  }
4365
4930
  ];
4366
4931
  var FACE_INDEXES = [{
@@ -4433,7 +4998,9 @@ var FaceStore = class {
4433
4998
  assigned: Boolean(r.data.assigned),
4434
4999
  mediaKey: data.mediaKey ?? void 0,
4435
5000
  recognizedIdentityId: data.recognizedIdentityId ?? void 0,
4436
- assignedSampleId: data.assignedSampleId ?? void 0
5001
+ assignedSampleId: data.assignedSampleId ?? void 0,
5002
+ keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
5003
+ faceBbox: data.faceBbox ?? void 0
4437
5004
  };
4438
5005
  }).filter((f) => !f.assigned);
4439
5006
  }
@@ -4597,8 +5164,11 @@ var FaceStore = class {
4597
5164
  id: faceId,
4598
5165
  ...data,
4599
5166
  assigned: Boolean(raw.assigned),
5167
+ mediaKey: data.mediaKey ?? void 0,
4600
5168
  recognizedIdentityId: data.recognizedIdentityId ?? void 0,
4601
- assignedSampleId: data.assignedSampleId ?? void 0
5169
+ assignedSampleId: data.assignedSampleId ?? void 0,
5170
+ keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
5171
+ faceBbox: data.faceBbox ?? void 0
4602
5172
  };
4603
5173
  }
4604
5174
  /**
@@ -4642,7 +5212,9 @@ var FaceStore = class {
4642
5212
  assigned: Boolean(r.data.assigned),
4643
5213
  mediaKey: data.mediaKey ?? void 0,
4644
5214
  recognizedIdentityId: data.recognizedIdentityId ?? void 0,
4645
- assignedSampleId: data.assignedSampleId ?? void 0
5215
+ assignedSampleId: data.assignedSampleId ?? void 0,
5216
+ keyFrameMediaKey: data.keyFrameMediaKey ?? void 0,
5217
+ faceBbox: data.faceBbox ?? void 0
4646
5218
  };
4647
5219
  });
4648
5220
  const filterMode = input.filter ?? "all";
@@ -4707,6 +5279,10 @@ var OBJECT_EMBEDDING_COLUMNS = [
4707
5279
  {
4708
5280
  name: "mediaKey",
4709
5281
  type: "TEXT"
5282
+ },
5283
+ {
5284
+ name: "keyFrameMediaKey",
5285
+ type: "TEXT"
4710
5286
  }
4711
5287
  ];
4712
5288
  var ObjectEmbeddingStore = class {
@@ -4753,7 +5329,8 @@ var ObjectEmbeddingStore = class {
4753
5329
  modelId: input.modelId,
4754
5330
  dim: input.embedding.length,
4755
5331
  confidence: input.confidence,
4756
- ...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {}
5332
+ ...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
5333
+ ...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
4757
5334
  };
4758
5335
  try {
4759
5336
  await this.store.set.mutate({
@@ -4791,7 +5368,8 @@ var ObjectEmbeddingStore = class {
4791
5368
  return {
4792
5369
  id: r.id,
4793
5370
  ...data,
4794
- mediaKey: data.mediaKey ?? void 0
5371
+ mediaKey: data.mediaKey ?? void 0,
5372
+ keyFrameMediaKey: data.keyFrameMediaKey ?? void 0
4795
5373
  };
4796
5374
  });
4797
5375
  }
@@ -4920,12 +5498,22 @@ function updateTrackAggregate(prev, match, opts) {
4920
5498
  }
4921
5499
  //#endregion
4922
5500
  //#region src/pipeline-analytics/face-recognizer.ts
5501
+ /** At most one "dropping imageless track" log per this interval, per recognizer. */
5502
+ var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
4923
5503
  var FaceRecognizer = class {
4924
5504
  deps;
4925
5505
  gallery = [];
4926
5506
  names = /* @__PURE__ */ new Map();
4927
5507
  aggregates = /* @__PURE__ */ new Map();
4928
5508
  bestFace = /* @__PURE__ */ new Map();
5509
+ /** The ONE best-detection-per-track policy, shared with the best-frame path
5510
+ * (`index.ts`). Face params: no hysteresis / no rate-limit — always keep the
5511
+ * true highest-confidence face (holding a buffer in memory is cheap, and a
5512
+ * track may last well under the best-frame rate-limit window). */
5513
+ bestTracker = new BestDetectionTracker();
5514
+ /** Throttle for the "dropping imageless track" log — one line per minute at
5515
+ * most, so a busy scene that never produces a face crop can't flood logs. */
5516
+ lastImagelessLogAt = 0;
4929
5517
  constructor(deps) {
4930
5518
  this.deps = deps;
4931
5519
  }
@@ -4959,55 +5547,25 @@ var FaceRecognizer = class {
4959
5547
  threshold: settings.similarityThreshold,
4960
5548
  margin: settings.margin
4961
5549
  }) : /* @__PURE__ */ new Map();
5550
+ const labelWork = [];
4962
5551
  for (const c of candidates) {
4963
5552
  const match = matches.get(c.trackId) ?? null;
4964
5553
  const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
4965
5554
  this.aggregates.set(c.trackId, state);
4966
- if (changed && state.assignedIdentityId !== null) {
4967
- const name = this.names.get(state.assignedIdentityId);
4968
- this.deps.logger.info("face: identity assigned", {
4969
- tags: {
4970
- deviceId: input.deviceId,
4971
- trackId: c.trackId
4972
- },
4973
- meta: {
4974
- identityId: state.assignedIdentityId,
4975
- name: name ?? null,
4976
- score: match?.score ?? null
4977
- }
4978
- });
4979
- if (name !== void 0) {
4980
- try {
4981
- await this.deps.trackStore.setLabel(c.trackId, name);
4982
- } catch (err) {
4983
- this.deps.logger.warn("setLabel failed", {
4984
- tags: { deviceId: input.deviceId },
4985
- meta: {
4986
- trackId: c.trackId,
4987
- error: String(err)
4988
- }
4989
- });
4990
- }
4991
- try {
4992
- await this.deps.eventStore.setLabelForTrack(c.trackId, name);
4993
- } catch (err) {
4994
- this.deps.logger.warn("setLabelForTrack failed", {
4995
- tags: { deviceId: input.deviceId },
4996
- meta: {
4997
- trackId: c.trackId,
4998
- error: String(err)
4999
- }
5000
- });
5001
- }
5002
- }
5003
- }
5555
+ if (changed && state.assignedIdentityId !== null) labelWork.push({
5556
+ trackId: c.trackId,
5557
+ assignedIdentityId: state.assignedIdentityId,
5558
+ matchScore: match?.score ?? null
5559
+ });
5004
5560
  const recognizedIdentityId = state.assignedIdentityId ?? match?.identityId ?? void 0;
5005
5561
  const held = this.bestFace.get(c.trackId);
5006
- if (held === void 0 || c.confidence > held.score) {
5562
+ const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp);
5563
+ const needsCrop = held !== void 0 && held.crop === void 0;
5564
+ if (isNewBest || needsCrop) {
5007
5565
  const cropBbox = c.faceBbox ?? c.bbox;
5008
5566
  let crop;
5009
- if (input.frameHandle !== void 0) try {
5010
- crop = await this.deps.captureCrop(input.frameHandle, cropBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
5567
+ if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
5568
+ crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
5011
5569
  } catch (err) {
5012
5570
  this.deps.logger.debug("FaceRecognizer crop capture failed", {
5013
5571
  tags: { deviceId: input.deviceId },
@@ -5017,14 +5575,20 @@ var FaceRecognizer = class {
5017
5575
  }
5018
5576
  });
5019
5577
  }
5020
- this.bestFace.set(c.trackId, {
5021
- score: c.confidence,
5022
- embedding: c.embedding,
5023
- embeddingModelId: c.embeddingModelId,
5024
- bbox: cropBbox,
5025
- timestamp: input.timestamp,
5026
- ...crop !== void 0 ? { crop } : {},
5027
- ...recognizedIdentityId !== void 0 ? { recognizedIdentityId } : {}
5578
+ if (isNewBest) {
5579
+ const bestCrop = crop ?? held?.crop;
5580
+ this.bestFace.set(c.trackId, {
5581
+ score: c.confidence,
5582
+ embedding: c.embedding,
5583
+ embeddingModelId: c.embeddingModelId,
5584
+ bbox: cropBbox,
5585
+ timestamp: input.timestamp,
5586
+ ...bestCrop !== void 0 ? { crop: bestCrop } : {},
5587
+ ...recognizedIdentityId !== void 0 ? { recognizedIdentityId } : {}
5588
+ });
5589
+ } else if (crop !== void 0 && held !== void 0) this.bestFace.set(c.trackId, {
5590
+ ...held,
5591
+ crop
5028
5592
  });
5029
5593
  this.deps.logger.debug("face: best-face held", {
5030
5594
  tags: {
@@ -5033,15 +5597,64 @@ var FaceRecognizer = class {
5033
5597
  },
5034
5598
  meta: {
5035
5599
  score: c.confidence,
5036
- hasCrop: crop !== void 0,
5600
+ isNewBest,
5601
+ hasCrop: this.bestFace.get(c.trackId)?.crop !== void 0,
5037
5602
  recognizedIdentityId: recognizedIdentityId ?? null
5038
5603
  }
5039
5604
  });
5040
- } else if (recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
5605
+ } else if (held !== void 0 && recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
5041
5606
  ...held,
5042
5607
  recognizedIdentityId
5043
5608
  });
5044
5609
  }
5610
+ for (const work of labelWork) {
5611
+ const name = this.names.get(work.assignedIdentityId);
5612
+ this.deps.logger.info("face: identity assigned", {
5613
+ tags: {
5614
+ deviceId: input.deviceId,
5615
+ trackId: work.trackId
5616
+ },
5617
+ meta: {
5618
+ identityId: work.assignedIdentityId,
5619
+ name: name ?? null,
5620
+ score: work.matchScore
5621
+ }
5622
+ });
5623
+ if (name === void 0) continue;
5624
+ try {
5625
+ await this.deps.trackStore.setLabel(work.trackId, name);
5626
+ } catch (err) {
5627
+ this.deps.logger.warn("setLabel failed", {
5628
+ tags: { deviceId: input.deviceId },
5629
+ meta: {
5630
+ trackId: work.trackId,
5631
+ error: String(err)
5632
+ }
5633
+ });
5634
+ }
5635
+ try {
5636
+ await this.deps.eventStore.setLabelForTrack(work.trackId, name);
5637
+ } catch (err) {
5638
+ this.deps.logger.warn("setLabelForTrack failed", {
5639
+ tags: { deviceId: input.deviceId },
5640
+ meta: {
5641
+ trackId: work.trackId,
5642
+ error: String(err)
5643
+ }
5644
+ });
5645
+ }
5646
+ try {
5647
+ await this.deps.recomputeImportance?.(work.trackId);
5648
+ } catch (err) {
5649
+ this.deps.logger.warn("recomputeImportance failed", {
5650
+ tags: { deviceId: input.deviceId },
5651
+ meta: {
5652
+ trackId: work.trackId,
5653
+ error: String(err)
5654
+ }
5655
+ });
5656
+ }
5657
+ }
5045
5658
  }
5046
5659
  /**
5047
5660
  * Persist the held best face for a finished track as ONE FaceStore buffer
@@ -5052,6 +5665,7 @@ var FaceRecognizer = class {
5052
5665
  const held = this.bestFace.get(trackId);
5053
5666
  this.aggregates.delete(trackId);
5054
5667
  this.bestFace.delete(trackId);
5668
+ this.bestTracker.delete(trackId);
5055
5669
  if (held === void 0) {
5056
5670
  this.deps.logger.debug("face: track ended without a held face", { tags: {
5057
5671
  deviceId,
@@ -5059,9 +5673,23 @@ var FaceRecognizer = class {
5059
5673
  } });
5060
5674
  return;
5061
5675
  }
5676
+ if (held.crop === void 0) {
5677
+ const now = Date.now();
5678
+ if (now - this.lastImagelessLogAt >= FACE_IMAGELESS_LOG_THROTTLE_MS) {
5679
+ this.lastImagelessLogAt = now;
5680
+ this.deps.logger.info("face: dropping imageless track (no crop captured)", {
5681
+ tags: {
5682
+ deviceId,
5683
+ trackId
5684
+ },
5685
+ meta: { score: held.score }
5686
+ });
5687
+ }
5688
+ return;
5689
+ }
5062
5690
  const faceId = `face-${trackId}`;
5063
5691
  let mediaKey;
5064
- if (held.crop !== void 0) try {
5692
+ try {
5065
5693
  mediaKey = await this.deps.mediaStore.put({
5066
5694
  deviceId,
5067
5695
  ownerKind: "face",
@@ -5079,6 +5707,17 @@ var FaceRecognizer = class {
5079
5707
  }
5080
5708
  });
5081
5709
  }
5710
+ if (mediaKey === void 0) {
5711
+ this.deps.logger.warn("face: crop store failed — dropping face row", {
5712
+ tags: {
5713
+ deviceId,
5714
+ trackId
5715
+ },
5716
+ meta: { faceId }
5717
+ });
5718
+ return;
5719
+ }
5720
+ const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
5082
5721
  try {
5083
5722
  await this.deps.faceStore.insert({
5084
5723
  id: faceId,
@@ -5086,9 +5725,11 @@ var FaceRecognizer = class {
5086
5725
  trackId,
5087
5726
  timestamp: held.timestamp,
5088
5727
  embedding: held.embedding,
5089
- ...mediaKey !== void 0 ? { mediaKey } : {},
5728
+ mediaKey,
5090
5729
  ...held.recognizedIdentityId !== void 0 ? { recognizedIdentityId: held.recognizedIdentityId } : {},
5091
- assigned: false
5730
+ assigned: false,
5731
+ faceBbox: held.bbox,
5732
+ ...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
5092
5733
  });
5093
5734
  this.deps.logger.info("face: buffered to gallery", {
5094
5735
  tags: {
@@ -5097,7 +5738,7 @@ var FaceRecognizer = class {
5097
5738
  },
5098
5739
  meta: {
5099
5740
  faceId,
5100
- hasCrop: mediaKey !== void 0,
5741
+ hasCrop: true,
5101
5742
  recognizedIdentityId: held.recognizedIdentityId ?? null,
5102
5743
  score: held.score
5103
5744
  }
@@ -5429,6 +6070,38 @@ var PlateRecognizer = class {
5429
6070
  }
5430
6071
  };
5431
6072
  //#endregion
6073
+ //#region src/shared/frame/shared-frame-resolver.ts
6074
+ function frameHandleKey(h) {
6075
+ return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
6076
+ }
6077
+ function createSharedFrameResolver(resolve) {
6078
+ let cache = null;
6079
+ return (handle) => {
6080
+ const key = frameHandleKey(handle);
6081
+ if (cache === null || cache.key !== key) cache = {
6082
+ key,
6083
+ value: resolve(handle)
6084
+ };
6085
+ return cache.value;
6086
+ };
6087
+ }
6088
+ //#endregion
6089
+ //#region src/shared/frame/encode-crop.ts
6090
+ /**
6091
+ * JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
6092
+ * NATIVE-resolution crop path: the decode worker returns the exact ROI pixels
6093
+ * at native res, so there is nothing left to crop — only to encode to the same
6094
+ * JPEG contract `extractCrop` produces (quality 90). Kept separate from
6095
+ * `extractCrop` (which crops a FULL frame) so the native path never re-crops.
6096
+ */
6097
+ async function encodeRgbCropToJpeg(bytes, width, height) {
6098
+ return (0, sharp.default)(Buffer.from(bytes), { raw: {
6099
+ width,
6100
+ height,
6101
+ channels: 3
6102
+ } }).jpeg({ quality: 90 }).toBuffer();
6103
+ }
6104
+ //#endregion
5432
6105
  //#region src/pipeline-analytics/pipeline/event-child-crops.ts
5433
6106
  /**
5434
6107
  * Compute Intersection-over-Union between two (x,y,w,h) bounding boxes.
@@ -5727,6 +6400,24 @@ function createEventMediaHandler(deps) {
5727
6400
  */
5728
6401
  var TTL_SWEEP_INTERVAL_MS = 5e3;
5729
6402
  var SETTINGS_CACHE_TTL_MS = 5e3;
6403
+ /** §5 best-frame: a track's `thumbnail` is overwritten only when the current
6404
+ * detection confidence beats the held best by at least this margin (hysteresis
6405
+ * so jitter around a plateau doesn't churn the write). */
6406
+ var BEST_FRAME_HYSTERESIS = .05;
6407
+ /** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
6408
+ var BEST_FRAME_MIN_GAP_MS = 2e3;
6409
+ /** Design B: cap (px) on the width of the native KEY FRAME (full-frame native
6410
+ * capture). Native resolution is the point, but a full 4K RGB surface over the
6411
+ * transport per new-best is wasteful for a web detail view — 1920px keeps a
6412
+ * sharp native frame while bounding the copy (a miss falls back to the
6413
+ * detection-res frame, which is already ≤640px). */
6414
+ var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
6415
+ /** getKeyEvents: max completed tracks pulled from a window before importance
6416
+ * ranking. Ordering is by importance (not firstSeen) and legacy rows score on
6417
+ * read, so we over-fetch candidates and trim to `limit` after sorting. */
6418
+ var KEY_EVENT_CANDIDATE_CAP = 500;
6419
+ /** getKeyEvents: default page size when the caller omits `limit`. */
6420
+ var KEY_EVENT_DEFAULT_LIMIT = 50;
5730
6421
  /** Cluster setting key (in the centralized addon store) selecting the SINGLE
5731
6422
  * node that runs post-analysis (event/media/audio/motion generation). All
5732
6423
  * other nodes are fully inert. No multi-node balancing. Default: the hub. */
@@ -5838,6 +6529,31 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
5838
6529
  mediaCacheByDevice = /* @__PURE__ */ new Map();
5839
6530
  /** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
5840
6531
  dropoutSkipsByKey = /* @__PURE__ */ new Map();
6532
+ /** Best (highest-confidence) frame per track — drives the single overwrite
6533
+ * `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
6534
+ * face path (`face-recognizer.ts`); rate-limited here since each best-frame
6535
+ * capture re-encodes a full boxed frame. */
6536
+ bestFrameTracker = new BestDetectionTracker({
6537
+ hysteresis: BEST_FRAME_HYSTERESIS,
6538
+ minGapMs: BEST_FRAME_MIN_GAP_MS
6539
+ });
6540
+ /** Best (highest-confidence) CLIP-object detection per track — drives ONE
6541
+ * tight object-crop capture whose media key is written onto the embedding
6542
+ * row (so a semantic-search hit's thumbnail IS the embedded crop). Shares the
6543
+ * unified best-per-track decision (`TrackBestSelector`); the persistent
6544
+ * cross-restart "best" gate stays in `ObjectEmbeddingStore.upsertIfBetter`. */
6545
+ objectEmbeddingBestSelector = new TrackBestSelector();
6546
+ /** Design B: the track's shared native key-frame media key, captured at the
6547
+ * best-detection moment (object-embedding best path). Read by the face path
6548
+ * at track end so a face row links to the SAME single key frame. Cleared on
6549
+ * track end. */
6550
+ keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
6551
+ /** The shared crop extractor (native-res first, detection-frame fallback),
6552
+ * captured in the constructor so `processFrame` can crop object thumbnails in
6553
+ * the same live-frame window as the face/plate/event-media captures. The
6554
+ * optional `maxWidth` caps the native crop width (used for the full-frame key
6555
+ * frame so a 4K native surface never floods the transport). */
6556
+ captureCrop = null;
5841
6557
  shuttingDown = false;
5842
6558
  /** True only on the cluster's designated post-processing node. When false the
5843
6559
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
@@ -5871,7 +6587,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
5871
6587
  let storage = this.ctx.kernel.storage;
5872
6588
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
5873
6589
  if (mediaRoot) {
5874
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-Cx0LM0Sq.js"));
6590
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-Cg_cGqs0.js"));
5875
6591
  storage = new FilesystemStorageProvider(mediaRoot);
5876
6592
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
5877
6593
  }
@@ -5937,23 +6653,62 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
5937
6653
  });
5938
6654
  const ownNodeIdForFaces = ownNodeId;
5939
6655
  const frameReadersForFaces = this.frameReaders;
5940
- const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding) => {
5941
- const decoded = await require_resolve_frame.resolveFrame(frameHandle, {
5942
- ownNodeId: ownNodeIdForFaces,
5943
- readers: frameReadersForFaces,
5944
- getRemoteFrame
5945
- });
5946
- if (!decoded || decoded.format !== "rgb") return null;
5947
- const data = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
6656
+ const pipelineRunnerApi = api.pipelineRunner;
6657
+ const cropMetricLogger = logger.child("NativeCrop");
6658
+ let nativeHits = 0;
6659
+ let nativeFallbacks = 0;
6660
+ let lastCropMetricAt = 0;
6661
+ const NATIVE_CROP_METRIC_INTERVAL_MS = 3e4;
6662
+ const bumpCropMetric = (hit) => {
6663
+ if (hit) nativeHits += 1;
6664
+ else nativeFallbacks += 1;
6665
+ const now = Date.now();
6666
+ if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
6667
+ lastCropMetricAt = now;
6668
+ cropMetricLogger.info("native-crop window", { meta: {
6669
+ nativeHits,
6670
+ detectionFrameFallbacks: nativeFallbacks
6671
+ } });
6672
+ };
6673
+ const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
6674
+ if (!pipelineRunnerApi?.getNativeCrop) return null;
6675
+ try {
6676
+ const native = await pipelineRunnerApi.getNativeCrop.query({
6677
+ handle: frameHandle,
6678
+ bbox: paddedNorm,
6679
+ ...maxWidth !== void 0 ? { maxWidth } : {}
6680
+ }, require_dist.nodePin(frameHandle.nodeId));
6681
+ if (!native || native.width <= 0 || native.height <= 0) return null;
6682
+ return await encodeRgbCropToJpeg(Buffer.from(native.bytes), native.width, native.height);
6683
+ } catch (err) {
6684
+ cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: require_dist.errMsg(err) } });
6685
+ return null;
6686
+ }
6687
+ };
6688
+ const resolveFrameShared = createSharedFrameResolver((frameHandle) => require_resolve_frame.resolveFrame(frameHandle, {
6689
+ ownNodeId: ownNodeIdForFaces,
6690
+ readers: frameReadersForFaces,
6691
+ getRemoteFrame
6692
+ }));
6693
+ const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
5948
6694
  const paddedNorm = padBbox({
5949
6695
  x: bbox.x / frameWidth,
5950
6696
  y: bbox.y / frameHeight,
5951
6697
  w: bbox.w / frameWidth,
5952
6698
  h: bbox.h / frameHeight
5953
6699
  }, padding);
5954
- const { crop } = await require_resolve_frame.extractCrop(data, decoded.width, decoded.height, paddedNorm);
6700
+ const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
6701
+ if (nativeCrop) {
6702
+ bumpCropMetric(true);
6703
+ return nativeCrop;
6704
+ }
6705
+ bumpCropMetric(false);
6706
+ const decoded = await resolveFrameShared(frameHandle);
6707
+ if (!decoded || decoded.format !== "rgb") return null;
6708
+ const { crop } = await require_resolve_frame.extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
5955
6709
  return crop;
5956
6710
  };
6711
+ this.captureCrop = captureCrop;
5957
6712
  this.faceRecognizer = new FaceRecognizer({
5958
6713
  identityStore: this.identityStore,
5959
6714
  faceStore: this.faceStore,
@@ -5961,6 +6716,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
5961
6716
  trackStore: this.trackStore,
5962
6717
  eventStore: this.eventStore,
5963
6718
  captureCrop,
6719
+ recomputeImportance: (trackId) => {
6720
+ const trackStore = this.trackStore;
6721
+ const eventStore = this.eventStore;
6722
+ if (!trackStore || !eventStore) return Promise.resolve();
6723
+ return recomputeTrackImportance({
6724
+ trackStore,
6725
+ eventStore
6726
+ }, trackId);
6727
+ },
6728
+ getKeyFrameMediaKey: (trackId) => this.keyFrameKeyByTrackId.get(trackId),
5964
6729
  logger: logger.child("FaceRecognizer")
5965
6730
  });
5966
6731
  this.faceRecognizer.refreshGallery();
@@ -5998,7 +6763,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
5998
6763
  try {
5999
6764
  const handler = createEventMediaHandler({ getMedia: async (id) => {
6000
6765
  try {
6001
- return await this.readEventThumbnail(id);
6766
+ return await this.readMediaByEventOrKey(id);
6002
6767
  } catch (err) {
6003
6768
  this.ctx.logger.warn("readEventThumbnail failed", { meta: {
6004
6769
  eventId: id,
@@ -6381,6 +7146,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6381
7146
  this.processors.clear();
6382
7147
  this.lastActiveTrackIds.clear();
6383
7148
  this.dropoutSkipsByKey.clear();
7149
+ this.bestFrameTracker.clear();
7150
+ this.objectEmbeddingBestSelector.clear();
6384
7151
  this.levelStateByDevice.clear();
6385
7152
  this.settingsCacheByDevice.clear();
6386
7153
  this.sensitivityCacheByDevice.clear();
@@ -6487,12 +7254,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6487
7254
  className: t.className,
6488
7255
  source
6489
7256
  } });
6490
- if (this.eventMediaDispatcher && frameHandle) firstFrameTargets.push({
6491
- trackId: id,
6492
- timestamp: result.timestamp,
6493
- bbox: { ...t.bbox },
6494
- ...t.label ? { label: t.label } : {}
6495
- });
7257
+ if (this.eventMediaDispatcher && frameHandle) {
7258
+ firstFrameTargets.push({
7259
+ trackId: id,
7260
+ timestamp: result.timestamp,
7261
+ bbox: { ...t.bbox },
7262
+ ...t.label ? { label: t.label } : {}
7263
+ });
7264
+ this.trackStore.seedSnapshotClock(id, result.timestamp);
7265
+ }
6496
7266
  this.ctx.eventBus.emit({
6497
7267
  id: `pa-${(0, node_crypto.randomUUID)()}`,
6498
7268
  timestamp: new Date(result.timestamp),
@@ -6533,16 +7303,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6533
7303
  } });
6534
7304
  }
6535
7305
  await Promise.all(result.objectEvents.map((e) => this.eventStore.insertObject(e)));
6536
- if (this.objectEmbeddingStore) {
6537
- for (const t of result.tracked) if (t.embedding !== void 0 && t.embeddingModelId !== void 0 && t.embeddingModelId.startsWith("mobileclip-")) this.objectEmbeddingStore.upsertIfBetter({
7306
+ const objectEmbeddingBests = [];
7307
+ if (this.objectEmbeddingStore) for (const t of result.tracked) {
7308
+ if (!isClipObjectEmbedding(t)) continue;
7309
+ if (this.objectEmbeddingBestSelector.observe({
6538
7310
  trackId: t.trackId,
6539
- deviceId,
6540
- timestamp: result.timestamp,
7311
+ confidence: t.confidence,
7312
+ atMs: result.timestamp,
6541
7313
  className: t.className,
7314
+ bbox: t.bbox,
6542
7315
  embedding: t.embedding,
6543
- modelId: t.embeddingModelId,
6544
- confidence: t.confidence
6545
- });
7316
+ embeddingModelId: t.embeddingModelId
7317
+ })) objectEmbeddingBests.push(t);
6546
7318
  }
6547
7319
  const faceSettings = this.faceRecognizer ? await this.resolveDeviceFaceSettings(deviceId) : null;
6548
7320
  const faceGloballyEnabled = this.faceRecognizer ? await this.resolveGlobalFaceEnabled() : false;
@@ -6565,11 +7337,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6565
7337
  let plateCrops = 0;
6566
7338
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
6567
7339
  else plateCrops += 1;
6568
- if (eventTargets.length > 0 || firstFrameTargets.length > 0) {
7340
+ const snapshotTargets = this.buildSnapshotTargets(result.tracked, result.timestamp, mediaSettings);
7341
+ if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
6569
7342
  log.info("media capture", { meta: {
6570
7343
  source,
6571
7344
  events: eventTargets.length,
6572
7345
  trackFrames: firstFrameTargets.length,
7346
+ snapshots: snapshotTargets.length,
6573
7347
  faceCrops,
6574
7348
  plateCrops
6575
7349
  } });
@@ -6578,8 +7352,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6578
7352
  frameHandle,
6579
7353
  events: eventTargets,
6580
7354
  trackFrames: firstFrameTargets,
7355
+ snapshots: snapshotTargets,
6581
7356
  cropPadding: mediaSettings.cropPadding
6582
- });
7357
+ }).then((res) => {
7358
+ for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
7359
+ timestamp: s.timestamp,
7360
+ position: {
7361
+ x: s.bbox.x + s.bbox.w / 2,
7362
+ y: s.bbox.y + s.bbox.h / 2,
7363
+ timestamp: s.timestamp,
7364
+ bbox: s.bbox
7365
+ },
7366
+ mediaKey: s.mediaKey
7367
+ });
7368
+ }).catch(() => {});
6583
7369
  }
6584
7370
  }
6585
7371
  if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
@@ -6601,6 +7387,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6601
7387
  cropPadding: mediaSettings.cropPadding,
6602
7388
  ...frameHandle !== void 0 ? { frameHandle } : {}
6603
7389
  });
7390
+ if (objectEmbeddingBests.length > 0) this.persistObjectEmbeddingBests(deviceId, result.timestamp, objectEmbeddingBests, frameHandle, result.frameWidth, result.frameHeight, mediaSettings.cropPadding);
6604
7391
  for (const e of result.objectEvents) this.ctx.eventBus.emit({
6605
7392
  id: `pa-${e.id}`,
6606
7393
  timestamp: new Date(e.timestamp),
@@ -6712,6 +7499,112 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6712
7499
  });
6713
7500
  return settings;
6714
7501
  }
7502
+ /**
7503
+ * §5 — decide which active tracks need periodic media THIS frame. Pure over
7504
+ * TrackStore.lastSnapshotAt + the per-track best-confidence map:
7505
+ * • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
7506
+ * the snapshotIntervalMs cadence, gated by `saveThumbnails`;
7507
+ * • `thumbnail` (best) fires when confidence beats the held best by the
7508
+ * hysteresis margin, rate-limited to one per BEST_FRAME_MIN_GAP_MS, and is
7509
+ * NOT gated by saveThumbnails (a single best still-frame is always useful).
7510
+ * The per-track best-confidence map is updated here as a side effect.
7511
+ */
7512
+ /**
7513
+ * Capture the tight object crop for each new-best CLIP track (native-res via
7514
+ * the shared extractor) and upsert the embedding row with that crop's media
7515
+ * key. The crop uses `putReplacing` so exactly ONE object crop is kept per
7516
+ * track (the current peak). The embedding is upserted even when no crop was
7517
+ * captured (no frame handle) so semantic search still works — the crop just
7518
+ * enhances the search-hit thumbnail. `upsertIfBetter` remains the durable
7519
+ * cross-restart best gate (R3). Best-effort; issued in the live-frame window.
7520
+ */
7521
+ async persistObjectEmbeddingBests(deviceId, timestamp, bests, frameHandle, frameWidth, frameHeight, cropPadding) {
7522
+ const store = this.objectEmbeddingStore;
7523
+ if (!store) return;
7524
+ await Promise.all(bests.map(async (t) => {
7525
+ if (!isClipObjectEmbedding(t)) return;
7526
+ let mediaKey;
7527
+ let keyFrameMediaKey;
7528
+ if (frameHandle !== void 0 && this.captureCrop && this.mediaStore) {
7529
+ try {
7530
+ const crop = await this.captureCrop(frameHandle, t.bbox, frameWidth, frameHeight, cropPadding);
7531
+ if (crop) mediaKey = await this.mediaStore.putReplacing({
7532
+ deviceId,
7533
+ ownerKind: "track",
7534
+ ownerId: t.trackId,
7535
+ kind: "crop",
7536
+ timestamp,
7537
+ data: crop
7538
+ });
7539
+ } catch (err) {
7540
+ this.ctx.logger.debug("object-embedding crop capture failed", {
7541
+ tags: { deviceId },
7542
+ meta: {
7543
+ trackId: t.trackId,
7544
+ error: require_dist.errMsg(err)
7545
+ }
7546
+ });
7547
+ }
7548
+ try {
7549
+ const keyFrame = await this.captureCrop(frameHandle, {
7550
+ x: 0,
7551
+ y: 0,
7552
+ w: frameWidth,
7553
+ h: frameHeight
7554
+ }, frameWidth, frameHeight, 0, KEYFRAME_NATIVE_MAX_WIDTH);
7555
+ if (keyFrame) {
7556
+ keyFrameMediaKey = await this.mediaStore.putReplacing({
7557
+ deviceId,
7558
+ ownerKind: "track",
7559
+ ownerId: t.trackId,
7560
+ kind: "keyFrame",
7561
+ timestamp,
7562
+ data: keyFrame
7563
+ });
7564
+ this.keyFrameKeyByTrackId.set(t.trackId, keyFrameMediaKey);
7565
+ }
7566
+ } catch (err) {
7567
+ this.ctx.logger.debug("key-frame capture failed", {
7568
+ tags: { deviceId },
7569
+ meta: {
7570
+ trackId: t.trackId,
7571
+ error: require_dist.errMsg(err)
7572
+ }
7573
+ });
7574
+ }
7575
+ }
7576
+ await store.upsertIfBetter({
7577
+ trackId: t.trackId,
7578
+ deviceId,
7579
+ timestamp,
7580
+ className: t.className,
7581
+ embedding: t.embedding,
7582
+ modelId: t.embeddingModelId,
7583
+ confidence: t.confidence,
7584
+ ...mediaKey !== void 0 ? { mediaKey } : {},
7585
+ ...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
7586
+ });
7587
+ }));
7588
+ }
7589
+ buildSnapshotTargets(tracked, timestamp, media) {
7590
+ const targets = [];
7591
+ for (const t of tracked) {
7592
+ const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
7593
+ const dueSnapshot = media.saveThumbnails && lastSnap > 0 && timestamp - lastSnap >= media.snapshotIntervalMs;
7594
+ const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp);
7595
+ if (!dueSnapshot && !isNewBest) continue;
7596
+ targets.push({
7597
+ trackId: t.trackId,
7598
+ timestamp,
7599
+ bbox: { ...t.bbox },
7600
+ ...t.label ? { label: t.label } : {},
7601
+ appendSnapshot: dueSnapshot,
7602
+ rollingLastFrame: dueSnapshot,
7603
+ bestThumbnail: isNewBest
7604
+ });
7605
+ }
7606
+ return targets;
7607
+ }
6715
7608
  async handleAudioResult(data) {
6716
7609
  if (this.shuttingDown) return;
6717
7610
  const { deviceId, frame } = data;
@@ -6964,8 +7857,36 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6964
7857
  positions: t.positions.length
6965
7858
  }
6966
7859
  });
6967
- this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
7860
+ const faceEnd = this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
7861
+ const dropKeyFrameKey = () => {
7862
+ this.keyFrameKeyByTrackId.delete(t.trackId);
7863
+ };
7864
+ if (faceEnd) faceEnd.finally(dropKeyFrameKey);
7865
+ else dropKeyFrameKey();
6968
7866
  this.plateRecognizer?.onTrackEnd(t.deviceId, t.trackId);
7867
+ try {
7868
+ const peak = await this.eventStore?.peakForTrack(t.trackId);
7869
+ if (peak) {
7870
+ const { importance, reason } = computeImportance({
7871
+ peakConfidence: this.bestFrameTracker.peak(t.trackId)?.confidence ?? peak.peakConfidence,
7872
+ className: t.className,
7873
+ durationMs: duration,
7874
+ peakBboxAreaFrac: peak.peakBboxAreaFrac,
7875
+ totalDistance: t.totalDistance,
7876
+ zonesVisited: t.zonesVisited,
7877
+ ...t.label !== void 0 ? { label: t.label } : {}
7878
+ });
7879
+ await this.trackStore?.setImportance(t.trackId, importance, reason, peak.bestEventId);
7880
+ if (peak.bestEventId !== void 0) await this.eventStore?.setImportanceForTrack(t.trackId, importance);
7881
+ }
7882
+ } catch (err) {
7883
+ this.ctx.logger.debug("importance scoring failed", { meta: {
7884
+ trackId: t.trackId,
7885
+ error: String(err)
7886
+ } });
7887
+ }
7888
+ this.bestFrameTracker.delete(t.trackId);
7889
+ this.objectEmbeddingBestSelector.delete(t.trackId);
6969
7890
  this.ctx.eventBus.emit({
6970
7891
  id: `pa-end-${t.trackId}`,
6971
7892
  timestamp: new Date(t.lastSeen),
@@ -7188,6 +8109,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7188
8109
  if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
7189
8110
  return this.withMediaUrl(events);
7190
8111
  }
8112
+ async getKeyEvents(input) {
8113
+ const trackStore = this.trackStore;
8114
+ if (!trackStore) return [];
8115
+ const eventStore = this.eventStore;
8116
+ try {
8117
+ const candidates = await trackStore.queryHistorical({
8118
+ deviceId: input.deviceId,
8119
+ since: input.since,
8120
+ until: input.until,
8121
+ limit: KEY_EVENT_CANDIDATE_CAP
8122
+ });
8123
+ const peakLookup = (trackId) => eventStore ? eventStore.peakForTrack(trackId) : Promise.resolve({
8124
+ peakConfidence: 0,
8125
+ peakBboxAreaFrac: 0,
8126
+ bestEventId: void 0
8127
+ });
8128
+ return await rankKeyEvents(candidates, {
8129
+ limit: input.limit ?? KEY_EVENT_DEFAULT_LIMIT,
8130
+ ...input.minImportance !== void 0 ? { minImportance: input.minImportance } : {},
8131
+ ...input.classFilter !== void 0 ? { classFilter: input.classFilter } : {}
8132
+ }, peakLookup);
8133
+ } catch (err) {
8134
+ this.ctx.logger.debug("getKeyEvents failed", { meta: { error: String(err) } });
8135
+ return [];
8136
+ }
8137
+ }
7191
8138
  async getAudioEvents(input) {
7192
8139
  const events = await (this.eventStore?.queryAudio(input) ?? Promise.resolve([]));
7193
8140
  if (input.projection !== "slim" || this.eventMediaBaseUrl === null) return events;
@@ -7230,6 +8177,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7230
8177
  className: input.classFilter
7231
8178
  });
7232
8179
  if (embeddingRows.length === 0) return [];
8180
+ const embeddingMediaKeyByTrackId = /* @__PURE__ */ new Map();
8181
+ const keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
8182
+ for (const row of embeddingRows) {
8183
+ if (row.mediaKey !== void 0) embeddingMediaKeyByTrackId.set(row.trackId, row.mediaKey);
8184
+ if (row.keyFrameMediaKey !== void 0) keyFrameKeyByTrackId.set(row.trackId, row.keyFrameMediaKey);
8185
+ }
7233
8186
  const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
7234
8187
  this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
7235
8188
  return null;
@@ -7276,10 +8229,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7276
8229
  score
7277
8230
  });
7278
8231
  }
7279
- for (const { event, score } of bestEventByTrackId.values()) scored.push({
7280
- ...event,
7281
- score
7282
- });
8232
+ for (const { event, score } of bestEventByTrackId.values()) {
8233
+ const mediaUrl = resolveSearchThumbnailUrl({
8234
+ baseUrl: this.eventMediaBaseUrl,
8235
+ eventId: event.id,
8236
+ ...event.trackId !== void 0 && embeddingMediaKeyByTrackId.has(event.trackId) ? { embeddingMediaKey: embeddingMediaKeyByTrackId.get(event.trackId) } : {}
8237
+ });
8238
+ const keyFrameMediaKey = event.trackId !== void 0 ? keyFrameKeyByTrackId.get(event.trackId) : void 0;
8239
+ scored.push({
8240
+ ...event,
8241
+ score,
8242
+ ...mediaUrl !== void 0 ? { mediaUrl } : {},
8243
+ ...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
8244
+ });
8245
+ }
7283
8246
  }
7284
8247
  scored.sort((a, b) => b.score - a.score);
7285
8248
  return scored.slice(0, input.limit);
@@ -7311,6 +8274,24 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7311
8274
  * (native-res boxed frame) → any available file. Returns `null` if the
7312
8275
  * event has no media at all.
7313
8276
  */
8277
+ /**
8278
+ * Data-plane resolver: an id is EITHER a bare event id (a UUID → the event's
8279
+ * crop, today's behaviour) OR a MediaStore key (`ownerKind:ownerId:kind:ts`,
8280
+ * contains ':' → served directly by key). The object-embedding search hit
8281
+ * points its thumbnail at the embedding row's crop key (a `track:…:crop:…`
8282
+ * key), so this resolves that crop; event ids stay on the event-crop path.
8283
+ */
8284
+ async readMediaByEventOrKey(id) {
8285
+ if (id.includes(":")) {
8286
+ const file = await (this.mediaStore?.getByKey(id) ?? Promise.resolve(null));
8287
+ if (!file) return null;
8288
+ return {
8289
+ bytes: Buffer.from(file.base64, "base64"),
8290
+ key: file.key
8291
+ };
8292
+ }
8293
+ return this.readEventThumbnail(id);
8294
+ }
7314
8295
  async readEventThumbnail(eventId) {
7315
8296
  const files = await (this.mediaStore?.listByOwner("event", eventId) ?? Promise.resolve([]));
7316
8297
  const chosen = files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
@@ -7399,7 +8380,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7399
8380
  min: 500,
7400
8381
  max: 6e4,
7401
8382
  step: 500,
7402
- default: 2e3,
8383
+ default: 5e3,
7403
8384
  showValue: true,
7404
8385
  unit: "s",
7405
8386
  displayScale: 1e3