@camstack/addon-post-analysis 1.2.128 → 1.2.130

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 { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-CRDS5ykk.mjs";
1
+ import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-CBCs0-eP.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -2439,6 +2439,45 @@ var NcArtifactPlane = class {
2439
2439
  }
2440
2440
  }
2441
2441
  /**
2442
+ * Mint a FRESH signed URL for an artefact already sitting on the shelf —
2443
+ * no bytes read, no re-store. The other half of {@link publishIndexed}: that
2444
+ * one stores AND mints once at publish time; this one re-mints on demand for
2445
+ * an id a caller already holds (a history row's `artifactIds`), because the
2446
+ * link `publishIndexed` returned has its own TTL and a caller reaching for
2447
+ * the artefact hours later needs a new one.
2448
+ *
2449
+ * `null` on any of three causes, deliberately indistinguishable to the
2450
+ * caller: no index is wired (nothing to resolve against), the id is unknown
2451
+ * or its artefact has expired past the retained shelf's own age bound, or no
2452
+ * externally-reachable base URL exists for this install. A caller (the Home
2453
+ * Assistant export) must render all three as "no image right now" — never as
2454
+ * a broken link, and never by reusing a URL this call refused to mint.
2455
+ */
2456
+ async mintUrl(artifactId) {
2457
+ const index = this.deps.index;
2458
+ if (index === void 0) return null;
2459
+ try {
2460
+ const record = await index.resolve(artifactId);
2461
+ if (record === null) return null;
2462
+ if (record.expiresAt <= this.now()) return null;
2463
+ const baseUrl = await this.resolveBaseUrl();
2464
+ if (baseUrl === null) return null;
2465
+ return buildArtifactUrl({
2466
+ baseUrl,
2467
+ routePrefix: this.deps.routePrefix,
2468
+ id: artifactId,
2469
+ secret: this.deps.secret,
2470
+ expMs: this.now() + (this.deps.ttlMs ?? DEFAULT_TTL_MS)
2471
+ });
2472
+ } catch (err) {
2473
+ this.deps.logger.debug("artifact url mint failed", { meta: {
2474
+ artifactId,
2475
+ error: String(err)
2476
+ } });
2477
+ return null;
2478
+ }
2479
+ }
2480
+ /**
2442
2481
  * Absolutize a path against the SAME base URL an artifact link would use.
2443
2482
  *
2444
2483
  * For links this plane does not host but a RECIPIENT still has to open — a
@@ -8925,7 +8964,7 @@ var NcDispatcher = class {
8925
8964
  return { ok: true };
8926
8965
  }
8927
8966
  }
8928
- const notification = await this.buildNotification(entry, target);
8967
+ const { notification, artifactIds } = await this.buildNotification(entry, target);
8929
8968
  const verdict = await this.runConfirmGate(entry, notification);
8930
8969
  if (verdict !== null && verdict.decision === "suppressed") {
8931
8970
  this.deps.logger.info("notification suppressed by AI confirm", {
@@ -8983,10 +9022,11 @@ var NcDispatcher = class {
8983
9022
  ...dropped.length > 0 ? { dropped } : {}
8984
9023
  }
8985
9024
  });
8986
- return verdict !== null ? {
9025
+ return {
8987
9026
  ok: true,
8988
- confirm: verdict
8989
- } : { ok: true };
9027
+ ...verdict !== null ? { confirm: verdict } : {},
9028
+ ...artifactIds.length > 0 ? { artifactIds } : {}
9029
+ };
8990
9030
  } catch (err) {
8991
9031
  return {
8992
9032
  ok: false,
@@ -9297,6 +9337,12 @@ var NcDispatcher = class {
9297
9337
  });
9298
9338
  return input.frozen ?? "";
9299
9339
  }
9340
+ /**
9341
+ * Builds the notification AND returns the artefact ids its media resolve
9342
+ * indexed — see {@link withArtifactUrls}. A tuple rather than a field on
9343
+ * `NcNotificationInput`: the ids are internal (outbox provenance), never
9344
+ * part of what `notification-output` sends to a backend.
9345
+ */
9300
9346
  async buildNotification(entry, target) {
9301
9347
  const subject = entry.payload.subject;
9302
9348
  const systemEvent = subject.systemEvent;
@@ -9325,7 +9371,8 @@ var NcDispatcher = class {
9325
9371
  vars: { body: rendered },
9326
9372
  frozen: rendered
9327
9373
  }) : rendered;
9328
- const attachments = [...await this.withArtifactUrls(entry, await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
9374
+ const resolvedMedia = await this.withArtifactUrls(entry, await this.resolveAttachments(entry));
9375
+ const attachments = [...resolvedMedia.attachments, ...frozenArtifactAttachments(entry)];
9329
9376
  const params = pickParams(entry.payload.params);
9330
9377
  const tapUrl = params.clickUrl === void 0 && this.deps.viewerUrlBase !== void 0 ? defaultTapUrl({
9331
9378
  deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
@@ -9335,21 +9382,24 @@ var NcDispatcher = class {
9335
9382
  }) : null;
9336
9383
  const actions = await this.mintActions(entry, target);
9337
9384
  return {
9338
- body,
9339
- title,
9340
- priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
9341
- ...params,
9342
- ...tapUrl !== null ? { clickUrl: tapUrl } : {},
9343
- tag: entry.payload.group?.key ?? entry.ruleId,
9344
- ...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
9345
- ...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
9346
- ...systemEvent !== void 0 ? { metadata: {
9347
- systemEventKind: systemEvent.kind,
9348
- subject: systemEvent.subject,
9349
- ...systemEvent.data
9350
- } } : {},
9351
- ...attachments.length > 0 ? { attachments } : {},
9352
- ...actions.length > 0 ? { actions } : {}
9385
+ notification: {
9386
+ body,
9387
+ title,
9388
+ priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
9389
+ ...params,
9390
+ ...tapUrl !== null ? { clickUrl: tapUrl } : {},
9391
+ tag: entry.payload.group?.key ?? entry.ruleId,
9392
+ ...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
9393
+ ...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
9394
+ ...systemEvent !== void 0 ? { metadata: {
9395
+ systemEventKind: systemEvent.kind,
9396
+ subject: systemEvent.subject,
9397
+ ...systemEvent.data
9398
+ } } : {},
9399
+ ...attachments.length > 0 ? { attachments } : {},
9400
+ ...actions.length > 0 ? { actions } : {}
9401
+ },
9402
+ artifactIds: resolvedMedia.artifactIds
9353
9403
  };
9354
9404
  }
9355
9405
  /**
@@ -9393,11 +9443,28 @@ var NcDispatcher = class {
9393
9443
  }
9394
9444
  });
9395
9445
  }
9446
+ /**
9447
+ * What one delivery's media resolve produced: the attachments ready to ship
9448
+ * (bytes plus a signed url when the plane could mint one) and the ids the
9449
+ * artefact INDEX assigned them, in the same order — still first, then gif,
9450
+ * then clip. The ids never ride on the attachment objects themselves: those
9451
+ * cross to `notification-output` and a notifier backend has no use for an
9452
+ * internal store id, only for `url`/`bytes`.
9453
+ *
9454
+ * `artifactIds` is what lets a delivery's outbox row later answer "which
9455
+ * artefact was this trigger's picture" (`getHistory.artifactIds`) without a
9456
+ * second write path — the outbox writes back exactly the ids this method
9457
+ * handed to a SUCCESSFUL send, never re-deriving them.
9458
+ */
9396
9459
  async withArtifactUrls(entry, attachments) {
9397
9460
  const index = this.deps.indexArtifact;
9398
9461
  const publish = this.deps.publishArtifact;
9399
- if (index === void 0 && publish === void 0) return [...attachments];
9462
+ if (index === void 0 && publish === void 0) return {
9463
+ attachments: [...attachments],
9464
+ artifactIds: []
9465
+ };
9400
9466
  const out = [];
9467
+ const artifactIds = [];
9401
9468
  let posterId = null;
9402
9469
  const deviceId = entry.payload.subject.deviceId;
9403
9470
  const common = {
@@ -9420,6 +9487,7 @@ var NcDispatcher = class {
9420
9487
  artifactId: null
9421
9488
  }));
9422
9489
  if (kind === "event-still" && pub.artifactId !== null) posterId = pub.artifactId;
9490
+ if (pub.artifactId !== null) artifactIds.push(pub.artifactId);
9423
9491
  out.push(pub.url === null ? { ...att } : {
9424
9492
  ...att,
9425
9493
  url: pub.url
@@ -9432,7 +9500,10 @@ var NcDispatcher = class {
9432
9500
  url
9433
9501
  });
9434
9502
  }
9435
- return out;
9503
+ return {
9504
+ attachments: out,
9505
+ artifactIds
9506
+ };
9436
9507
  }
9437
9508
  /** The full attachment list: the policy still (zone-cropped when the rule
9438
9509
  * froze zone ids) plus the footage cut from the broker's clip ring when
@@ -12536,13 +12607,17 @@ var NcOutbox = class {
12536
12607
  }
12537
12608
  const now = this.now();
12538
12609
  if (result.ok) {
12610
+ const payloadPatch = {
12611
+ ...result.confirm !== void 0 ? { confirmVerdict: result.confirm } : {},
12612
+ ...result.artifactIds !== void 0 && result.artifactIds.length > 0 ? { artifactIds: result.artifactIds } : {}
12613
+ };
12539
12614
  const sent = {
12540
12615
  ...entry,
12541
12616
  status: "sent",
12542
12617
  updatedAt: now,
12543
- ...result.confirm !== void 0 ? { payload: {
12618
+ ...Object.keys(payloadPatch).length > 0 ? { payload: {
12544
12619
  ...entry.payload,
12545
- confirmVerdict: result.confirm
12620
+ ...payloadPatch
12546
12621
  } } : {}
12547
12622
  };
12548
12623
  this.pending.delete(entry.id);
@@ -19561,7 +19636,8 @@ function outboxEntryToHistory(texts, entry) {
19561
19636
  zones: [...s.zones],
19562
19637
  timestamp: s.timestamp,
19563
19638
  ...s.systemEvent !== void 0 ? { systemEvent: historySystemEvent(texts, s.systemEvent) } : {}
19564
- }
19639
+ },
19640
+ ...entry.payload.artifactIds !== void 0 && entry.payload.artifactIds.length > 0 ? { artifactIds: [...entry.payload.artifactIds] } : {}
19565
19641
  };
19566
19642
  }
19567
19643
  /** The watched question in one grep-able token. */
@@ -21100,6 +21176,7 @@ var NotificationCenter = class NotificationCenter {
21100
21176
  }
21101
21177
  return { entries: mapped };
21102
21178
  },
21179
+ resolveArtifactUrl: async ({ artifactId }) => ({ url: await this.deps.resolveArtifactUrl?.(artifactId) ?? null }),
21103
21180
  listDeviceMutes: async () => ({ mutedDeviceIds: this.deviceMutes.listMuted() }),
21104
21181
  setDeviceMuted: async ({ deviceId, muted }) => {
21105
21182
  await this.deviceMutes.setMuted(deviceId, muted);
@@ -27195,6 +27272,18 @@ var FaceGalleryProvider = class {
27195
27272
  * This module names that state (`model-mismatch`) so the UI can say "this face
27196
27273
  * is in an old feature space" instead of showing 0% and letting the operator
27197
27274
  * conclude the person is not there.
27275
+ *
27276
+ * ## `buildFaceRescoreReport` stays pure — the CALLER persists
27277
+ *
27278
+ * This function never touches the store; every input is passed in and nothing
27279
+ * is read or written (see its own doc below). The operator asked for this
27280
+ * exact recompute, though, and wants the answer to stick — so the ACTION that
27281
+ * wraps this report (`rescoreFaceForTrack` in `pipeline-analytics/index.ts`)
27282
+ * writes the result onto `Face.suggestedIdentityId` / `suggestedMatchScore`,
27283
+ * through `decideRescorePersistence` / `persistRescoreResult` below. That is
27284
+ * still not a background job — it is the SAME explicitly-requested call,
27285
+ * writing once. A `model-mismatch` report (`comparable: false`) is never
27286
+ * written: see `persistRescoreResult`.
27198
27287
  */
27199
27288
  /** The empty report every "cannot score" path returns, so shape never varies. */
27200
27289
  function emptyReport(verdict, input, comparable) {
@@ -27270,6 +27359,52 @@ function buildFaceRescoreReport(input) {
27270
27359
  ...best !== void 0 && second !== void 0 ? { runnerUpGap: best.score - second.score } : {}
27271
27360
  };
27272
27361
  }
27362
+ /**
27363
+ * What `face.rescoreTrack` should PERSIST for a COMPARABLE report, decided as
27364
+ * its own pure function so the rule is directly testable — separated from the
27365
+ * class that owns the store I/O (`rescoreFaceForTrack` in
27366
+ * `pipeline-analytics/index.ts`), which only wires this answer to
27367
+ * `FaceStore.setSuggestion`.
27368
+ *
27369
+ * `null` covers every verdict OTHER than `auto-assignable` / `suggestion`, and
27370
+ * also an `auto-assignable` / `suggestion` verdict whose best-ranked row is
27371
+ * not itself ELIGIBLE (`identities` is ranked by raw score across every
27372
+ * comparable identity, eligible or not — see `buildFaceRescoreReport`). Every
27373
+ * one of those is a genuine "nothing to suggest right now": never an
27374
+ * assignment, and never the winner of an `ambiguous` verdict — the matcher
27375
+ * refused to pick one, so this function does not pick one either.
27376
+ *
27377
+ * Callers MUST gate on `report.comparable` before calling this — a
27378
+ * `model-mismatch` report has no verdict worth acting on; see
27379
+ * {@link persistRescoreResult}, which does that gating and is the one thing
27380
+ * `rescoreFaceForTrack` actually calls.
27381
+ */
27382
+ function decideRescorePersistence(report) {
27383
+ if (report.verdict !== "auto-assignable" && report.verdict !== "suggestion") return null;
27384
+ const candidate = report.identities.find((row) => row.eligible);
27385
+ return candidate === void 0 ? null : {
27386
+ identityId: candidate.identityId,
27387
+ score: candidate.score
27388
+ };
27389
+ }
27390
+ /**
27391
+ * Persist `face.rescoreTrack`'s result onto the SUGGESTED tier — the ONE
27392
+ * write this explicitly-requested action makes, and the only place that
27393
+ * decides whether it happens at all.
27394
+ *
27395
+ * A non-comparable report (`model-mismatch`) short-circuits BEFORE
27396
+ * `ports.setSuggestion` is ever called: a cosine across model spaces is
27397
+ * well-formed noise, and persisting it would look exactly like a fresh,
27398
+ * meaningful read to every future reader of the row. This is the single most
27399
+ * load-bearing branch in this file — the regression it guards against writes
27400
+ * garbage that reads as data.
27401
+ */
27402
+ async function persistRescoreResult(faceId, report, ports) {
27403
+ if (!report.comparable) return "not-comparable";
27404
+ const candidate = decideRescorePersistence(report);
27405
+ await ports.setSuggestion(faceId, candidate);
27406
+ return candidate !== null ? "written" : "cleared";
27407
+ }
27273
27408
  //#endregion
27274
27409
  //#region src/pipeline-analytics/face-rescore-actions.ts
27275
27410
  /**
@@ -27355,9 +27490,12 @@ var FaceReembedReportSchema = object({
27355
27490
  });
27356
27491
  var faceRescoreActions = defineCustomActions({
27357
27492
  /**
27358
- * Read-only by default. `admin` because the report names every enrolled
27359
- * person and how well an arbitrary face matches them — the gallery's shape,
27360
- * which is not a viewer-level fact.
27493
+ * Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
27494
+ * `suggestedMatchScore`) — never an assignment, never automatic; see
27495
+ * `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
27496
+ * `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
27497
+ * also names every enrolled person and how well an arbitrary face matches
27498
+ * them — the gallery's shape, which is not a viewer-level fact.
27361
27499
  */
27362
27500
  "face.rescoreTrack": customAction(object({
27363
27501
  deviceId: number().int(),
@@ -27375,8 +27513,10 @@ var faceRescoreActions = defineCustomActions({
27375
27513
  /** The face's current assignment, so the UI can mark "already this person". */
27376
27514
  assigned: boolean(),
27377
27515
  assignedIdentityId: string().optional(),
27378
- /** What the track has been SHOWING until now — persisted at detection time.
27379
- * Returned so the UI can put the stale number beside the fresh one. */
27516
+ /** What the track has been SHOWING until now — read BEFORE this call
27517
+ * writes, so it is whatever a previous track close or a previous
27518
+ * `face.rescoreTrack` last persisted. Returned so the UI can put the stale
27519
+ * number beside the fresh one. */
27380
27520
  storedSuggestedIdentityId: string().optional(),
27381
27521
  storedSuggestedMatchScore: number().optional(),
27382
27522
  verdict: FaceRescoreVerdictSchema,
@@ -29383,6 +29523,51 @@ var FrameProcessor = class {
29383
29523
  }
29384
29524
  };
29385
29525
  //#endregion
29526
+ //#region src/pipeline-analytics/replay/frame-processor-pass.ts
29527
+ /**
29528
+ * Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
29529
+ * return the tracks it produced.
29530
+ *
29531
+ * No `TrackStore`, no bus, no media — a track here is a summary built
29532
+ * ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
29533
+ */
29534
+ function runFrameProcessorPass(input) {
29535
+ const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
29536
+ if (input.zones !== void 0) processor.setZones(input.zones);
29537
+ if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
29538
+ if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
29539
+ const accum = /* @__PURE__ */ new Map();
29540
+ for (const { timestamp, frame } of input.frames) {
29541
+ const result = processor.process({
29542
+ timestamp,
29543
+ frame
29544
+ });
29545
+ for (const td of result.tracked) {
29546
+ const existing = accum.get(td.trackId);
29547
+ const matched = td.matchedThisFrame ? 1 : 0;
29548
+ if (existing === void 0) accum.set(td.trackId, {
29549
+ className: td.className,
29550
+ firstSeenMs: timestamp,
29551
+ lastSeenMs: timestamp,
29552
+ bbox: { ...td.bbox },
29553
+ framesMatched: matched
29554
+ });
29555
+ else {
29556
+ existing.className = td.className;
29557
+ existing.lastSeenMs = timestamp;
29558
+ existing.framesMatched += matched;
29559
+ }
29560
+ }
29561
+ }
29562
+ return { tracks: [...accum.values()].map((t) => ({
29563
+ className: t.className,
29564
+ firstSeenMs: t.firstSeenMs,
29565
+ lastSeenMs: t.lastSeenMs,
29566
+ bbox: t.bbox,
29567
+ framesMatched: t.framesMatched
29568
+ })) };
29569
+ }
29570
+ //#endregion
29386
29571
  //#region src/pipeline-analytics/pipeline/semantic-detail-bbox.ts
29387
29572
  /** Which box the semantic best-shot should crop: a plate/face DETAIL box
29388
29573
  * when we have one, otherwise the subject box. */
@@ -47566,6 +47751,37 @@ var FaceStore = class {
47566
47751
  }
47567
47752
  }
47568
47753
  /**
47754
+ * Persist a `face.rescoreTrack` result onto the SUGGESTED tier — the ONE
47755
+ * write that explicitly-requested action makes. `suggestion: null` means
47756
+ * the recompute found no candidate worth offering and CLEARS both columns
47757
+ * (a genuine "the gallery has nothing for this face right now" result, not
47758
+ * a no-op left at whatever they held before). Never touches `assigned` /
47759
+ * `recognizedIdentityId` — a suggestion is never an assignment. The caller
47760
+ * is responsible for never calling this with a non-comparable (`model-
47761
+ * mismatch`) report; this method persists whatever it is given.
47762
+ */
47763
+ async setSuggestion(faceId, suggestion) {
47764
+ const data = suggestion === null ? {
47765
+ suggestedIdentityId: null,
47766
+ suggestedMatchScore: null
47767
+ } : {
47768
+ suggestedIdentityId: suggestion.identityId,
47769
+ suggestedMatchScore: suggestion.score
47770
+ };
47771
+ try {
47772
+ await this.store.update.mutate({
47773
+ collection: FACES_COLLECTION,
47774
+ id: faceId,
47775
+ data
47776
+ });
47777
+ } catch (err) {
47778
+ this.logger.warn("FaceStore.setSuggestion failed", { meta: {
47779
+ faceId,
47780
+ error: String(err)
47781
+ } });
47782
+ }
47783
+ }
47784
+ /**
47569
47785
  * `TrackScopedStore` contract for the retention cascade: delete the buffered
47570
47786
  * face rows of the given tracks that are NOT assigned. Reuses the EXACT
47571
47787
  * `assigned` exemption that `prune`/`pruneAll` apply (see the
@@ -59957,6 +60173,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
59957
60173
  cancelVision: async (requestId) => {
59958
60174
  await api.llm.cancel.mutate({ requestId });
59959
60175
  },
60176
+ resolveArtifactUrl: async (artifactId) => await this.ncArtifactPlane?.mintUrl(artifactId) ?? null,
59960
60177
  mintActionUrl: (input) => this.ncActionMintUrl?.(input) ?? Promise.resolve(null),
59961
60178
  readDeviceStates: async (ids) => {
59962
60179
  const out = /* @__PURE__ */ new Map();
@@ -60475,6 +60692,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
60475
60692
  ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
60476
60693
  });
60477
60694
  }
60695
+ /**
60696
+ * The FrameProcessor pass of a replay run — see the cap doc on
60697
+ * `runReplayFrameProcessor` for why this is not the `processFrame` method
60698
+ * this addon deliberately does not have.
60699
+ *
60700
+ * `runFrameProcessorPass` is pure w.r.t. this addon's own state: it
60701
+ * constructs a FRESH `FrameProcessor` and does not touch `this.trackStore`,
60702
+ * `this.stationaryRegistry` or any other instance field — unlike
60703
+ * `getOrCreateProcessor` (the production path), which wires both.
60704
+ * `scripts/check-replay-isolation.ts` holds that boundary on the
60705
+ * `replay/` directory this delegates into.
60706
+ */
60707
+ async runReplayFrameProcessor(input) {
60708
+ return runFrameProcessorPass(input);
60709
+ }
60478
60710
  async listRetrainAnnotations(input) {
60479
60711
  return this.requireRetrain().listAnnotations(input.trackId);
60480
60712
  }
@@ -65424,6 +65656,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
65424
65656
  * See `face-rescore.ts` for why the number already on the track cannot answer
65425
65657
  * that: it was computed during live detection and persisted at track close,
65426
65658
  * so every enrolment made since is invisible to it.
65659
+ *
65660
+ * The report's result is PERSISTED (`persistRescoreResult`, below) onto
65661
+ * `Face.suggestedIdentityId` / `suggestedMatchScore` — the SAME columns the
65662
+ * live recognizer's suggestion band writes, so a reader never has to know
65663
+ * whether a suggestion came from track close or from this button. Still
65664
+ * never a background job: this write happens once, only because the
65665
+ * operator asked THIS question right now.
65427
65666
  */
65428
65667
  async rescoreFaceForTrack(input) {
65429
65668
  const api = this.ctx.api;
@@ -65466,6 +65705,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
65466
65705
  clusterModelId,
65467
65706
  params
65468
65707
  });
65708
+ const persisted = await persistRescoreResult(faceId, report, { setSuggestion: (id, suggestion) => faceStore.setSuggestion(id, suggestion) });
65469
65709
  this.ctx.logger.info("face rescore", {
65470
65710
  tags: { deviceId: input.deviceId },
65471
65711
  meta: {
@@ -65479,7 +65719,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
65479
65719
  best: report.identities[0]?.name ?? null,
65480
65720
  bestScore: report.identities[0]?.score ?? null,
65481
65721
  storedSuggestion: face.suggestedMatchScore ?? null,
65482
- reembedded: reembed.succeeded
65722
+ reembedded: reembed.succeeded,
65723
+ persisted
65483
65724
  }
65484
65725
  });
65485
65726
  return {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BZm5TUIO.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CMk3S5nA.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.128",
3
+ "version": "1.2.130",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",