@camstack/addon-post-analysis 1.2.369 → 1.2.370

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.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-E4CGZ8NR.js → clip-model-registry-C3AUTkBR.js} +1618 -45
  2. package/dist/{clip-model-registry-C_-tWzrX.mjs → clip-model-registry-CCwyUQ7k.mjs} +1595 -46
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +752 -631
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bac17NNv.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C1gR-74b.mjs} +3 -3
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BRMRfKVO.mjs +26 -0
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CkaRW2Sy.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-CMFNXHOl.mjs → hostInit-s-NoTpFK.mjs} +3 -3
  10. package/dist/pipeline-analytics/index.js +982 -82
  11. package/dist/pipeline-analytics/index.mjs +1013 -113
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +5 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DOGD-EFh.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bb1tsxlC.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { $ as NC_ALARM_DEFAULT_PROFILE_ID, $n as describeSystemEventFamilyProblem, $r as ruleKindOf, $t as RECORDING_EXPORT_MAX_READ_BYTES, A as EMBEDDING_MODEL_FAILED, Ai as isDeviceScopedCap, An as alarmDeviceSecureOnArm, Ar as linkedStepOf, At as NcConditionsSchema, B as LABEL_SUPPLY_MACRO, Bi as looseObject, Bn as buildViewerLink, Br as readTimelapseGeneratedAt, Bt as NcSharedSequenceSchema, C as DEFAULT_DETAIL_CROP_CONVENTION, Ci as errMsg, Cn as TrackPuritySchema, Cr as isRetiredClearKey, Ct as NC_TEMPLATE_VARS, D as DETECTION_PIPELINE_CAP_NAME, Di as customAction, Dn as TrackSourceSchema, Dr as isSystemEventSummaryRule, Dt as NcAlarmSensorsSchema, E as DETECTION_MACRO_CLASSES, Ei as createEvent, En as TrackSideSchema, Er as isSystemDelivery, Et as NcAlarmSensorsInputSchema, F as EVENT_OWNER_TYPES, Fi as _enum, Fn as applySecureOnArmEdit, Ft as NcRulePatchSchema, G as MAX_ARCHIVED_DEBUG_NOTES, Gi as strictObject, Gn as coreBlockIdFromAddonId, Gr as resolveModeNotifyPolicy, Gt as NcSystemEventKindSchema, H as ListTrackRelationsInputSchema, Hi as object, Hn as classifyRetrainReview, Hr as resolveClipSearchMinScore, Ht as NcSnoozeSchema, I as EXPORT_DENSE_MAX_RANGES, Ii as array, In as assertTimelapseCadences, Ir as pipelineAnalyticsCapability, It as NcRuleSchema, J as MAX_TRACK_PURITIES, Ji as tuple, Jn as deleteAlarmProfile, Jr as rootSupplyServes, Jt as NcTemplatePreviewSchema, K as MAX_BIRTH_DECISION_RECORDS, Ki as string, Kn as cosineSimilarity$1, Kt as NcTaxonomySchema, L as FIRST_LEVEL_MACRO_CLASSES, Li as boolean, Ln as audioMetricsCapability, Lr as plateGalleryCapability, Lt as NcRuleTargetSchema, M as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Mn as applyBlockingEdit, Mr as memoizePinnedFfmpeg, Mt as NcRuleActionSchema, N as EVENT_DENSITY_BATCH_MAX, Ni as sleep$1, Nn as applyModeDelaysEdits, Nr as notificationRulesCapability, Nt as NcRuleActionsSchema, O as DeclaredDevices, Oi as defineCustomActions, On as activateAlarmProfile, Or as isVectorIndexDimConflict, Ot as NcAlarmSettingsPatchSchema, P as EVENT_KIND_BY_CAP, Pi as ZodObject, Pn as applyNonBlockingEdit, Pr as occupancyScope, Pt as NcRuleInputSchema, Q as MotionTriggeredBySchema, Qn as describeSystemEventDeliveryProblem, Qr as ruleGroupingApplies, R as FULL_IMAGE_BBOX, Ri as discriminatedUnion, Rn as audioModeOf, Rr as readDeviceStateFrom, Rt as NcRuleUpdateInputSchema, S as CoverStateSchema, Si as DeviceType, Sn as TimelapseRuleSchema, Sr as isMediaOwnerType, St as NC_TAXONOMY, T as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Ti as CamProfileSchema, Tn as TrackRelationSchema, Tr as isScheduleActive, Tt as NcAlarmPostTriggeredActionSchema, U as ListTrackRelationsResultSchema, Ui as partialRecord, Un as cloneAlarmSettings, Ur as resolveLocationMode, Ut as NcSnoozeSuppressedSchema, V as LabelAttributionSchema, Vi as number, Vn as calendarEventLocalSpan, Vr as renameAlarmProfile, Vt as NcSnoozeInputSchema, W as LockStateSchema, Wi as record, Wn as consumableLowPercentOf, Wr as resolveModeDelays, Wt as NcStateSummarySectionIdSchema, X as MOTION_CLOSE_AFTER_MS, Xi as unknown, Xn as derivePostTriggeredAction, Xr as ruleBelongsToAlarm$1, Xt as OPS_LOG_DEFAULT_LIMIT, Y as MAX_TRACK_RELATIONS, Yi as union, Yn as deriveDetailCropRect, Yr as ruleAutoTriggersAlarm, Yt as NcTemplateVarDescriptorSchema, Z as MediaFileKindEnum, Zi as EventCategory, Zn as deriveRecordingMode, Zr as ruleDeviceCompatCapQueries, Zt as OpsLogEntrySchema, _ as BaseDevice, _i as zoneAnalyticsCapability, _n as SetTrackRelationInputSchema, _r as isDetectionMacroClass, _t as NC_GROUPING_MIN_SEC, a as CLIP_EMBEDDING_STEP_ID, ai as subKindsOf, an as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ar as encodeVectorBase64, at as NC_ALARM_TRIGGERED_MAX_SEC, b as ClearTrackRelationInputSchema, bi as evaluateSensorEdge, bn as TimelapseRuleInputSchema, br as isKnownTemplateVar, bt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, c as resolveClusterModelPin, ci as systemEventDeliveryProblems, cn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, cr as evictionPolicyOfLocation, ct as NC_CLIP_POST_ROLL_DEFAULT_SEC, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as systemEventSummaryScheduleOf, dn as SCENE_DEFAULT_UNCOVERED_POLICY, dr as gifDeriveSpeed, dt as NC_CONDITION_CATALOG, ei as sceneMonitorCapability, en as RECORDING_SOURCE_CAMSTACK_ADDON, et as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as systemEventSummarySplitOf, fn as SCENE_DIVERGED, fr as gifFromMp4Args, ft as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as viewerLinkBaseOf, gn as SetTrackPurityResultSchema, gr as isDefaultAlarmProfileId, gt as NC_GROUPING_MAX_SEC, h as ArchivedDebugNoteSchema, hi as vectorDimFromBase64, hn as SetTrackPurityInputSchema, hr as isAudioClassName, ht as NC_GROUPING_DEFAULT_SEC, ii as storageOccupancyCapability, in as RetrainStatusSchema, it as NC_ALARM_SYSTEM_EVENT_KINDS, j as EMBEDDING_REQUIREMENTS_FAILED, ji as nodePin, jn as alarmPanelCapability, jr as mayWriteToLocation, jt as NcLegacyRuleActionSequenceSchema, k as DetectionSourceSchema, ki as hydrateSchema, kn as addonWidgetsSourceCapability, kr as kebabToCamel, kt as NcConditionDescriptorSchema, l as APPEARANCE_BIN_COUNT, li as systemEventFamilyProblems, ln as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, lr as faceGalleryCapability, lt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, m as AlarmArmModeSchema, mi as templateVarsFor, mn as SceneMonitorSchema, mr as isAppLinkBase, mt as NC_DEFAULT_SNOOZE_MINUTES, ni as sensorRaisedOf, nn as RetrainDismissedSchema, nr as embeddingRefusalOf, nt as NC_ALARM_DELAY_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, oi as summarizeReview, on as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, or as ensureAlarmProfiles, ot as NC_AUDIO_CONFIRM_HITS_DEFAULT, p as AUDIO_MACRO_LABELS, pi as templateCarriesSummary, pn as SCENE_RESET_RECAPTURES, pr as isAlarmControllableCap, pt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, q as MAX_BIRTH_LATENCY_PROXY_MS, qi as templateLiteral, qn as createAlarmProfile, qr as retrainRevisionConflictMessage, qt as NcTemplatePreviewInputSchema, ri as servedTemplateVars, rn as RetrainOriginSchema, rt as NC_ALARM_NOTIFY_FIELDS, s as readClusterModelPinStrict, si as syncActiveAlarmProfileSettings, sn as SCENE_DEFAULT_ANCHOR_THRESHOLD, sr as eventKindLabel, st as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as secureActuationFor, tn as RebuildObjectEmbeddingsRefusalSchema, tt as NC_ALARM_DELAY_FIELDS, u as APPEARANCE_BLOCKS, ui as systemEventFilterApplies, un as SCENE_DEFAULT_QUIET_SECONDS, ur as failureContributionCapability, ut as NC_CLIP_RETENTION_MAX_SEC, v as BirthDecisionRecordSchema, vi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, vn as TIMELAPSE_DENSE_FLOOR_SEC, vr as isEventKindLabelKey, vt as NC_SENSOR_TRANSITION_KINDS, w as DEFAULT_EVENT_COLOR, wi as BaseAddon, wn as TrackRelationKindSchema, wr as isRootSuppliableStepId, wt as NcAlarmConfigSchema, x as ClearTrackRelationResultSchema, xi as isSourceCap, xn as TimelapseRulePatchSchema, xr as isLabelSupplyStepId, xt as NC_TANK_SYSTEM_EVENT_KINDS, y as COCO_TO_MACRO, yi as canonicalHash, yn as TRACK_RELATIONS_DEFAULT_LIMIT, yr as isEventOwnerType, yt as NC_SNOOZE_MAX_MINUTES, z as FailureCounters, zi as literal, zn as buildEventKindDescriptor, zr as readMotionTriggeredBy, zt as NcScheduleSchema } from "../clip-model-registry-C_-tWzrX.mjs";
1
+ import { $ as MotionTriggeredBySchema, $i as union, $n as deriveRecordingMode, $r as ruleAutoTriggersAlarm, A as DetectionSourceSchema, Ai as createEvent, An as addonWidgetsSourceCapability, Ar as isSystemEventSummaryRule, At as NcConditionDescriptorSchema, B as FailureCounters, Bi as array, Bn as audioModeOf, Br as pipelineAnalyticsCapability, Bt as NcScheduleSchema, C as CoverStateSchema, Ci as canonicalHash, Cn as TimelapseRuleSchema, Cr as isKnownTemplateVar, Ct as NC_TAXONOMY, D as DETECTION_MACRO_CLASSES, Di as errMsg, Dn as TrackSideSchema, Dr as isRootSuppliableStepId, Dt as NcAlarmSensorsInputSchema, E as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Ei as DeviceType, En as TrackRelationSchema, Er as isRetiredClearKey, Et as NcAlarmPostTriggeredActionSchema, F as EVENT_KIND_BY_CAP, Fi as nodePin, Fn as applyNonBlockingEdit, Fr as memoizePinnedFfmpeg, Ft as NcRuleInputSchema, G as LockStateSchema, Gi as number, Gn as cloneAlarmSettings, Gr as renameAlarmProfile, Gt as NcStateSummarySectionIdSchema, H as LabelAttributionSchema, Hi as discriminatedUnion, Hn as buildViewerLink, Hr as readDeviceStateFrom, Ht as NcSnoozeInputSchema, I as EVENT_OWNER_TYPES, In as applySecureOnArmEdit, Ir as notificationRulesCapability, It as NcRulePatchSchema, J as MAX_BIRTH_LATENCY_PROXY_MS, Ji as record, Jn as cosineSimilarity$1, Jr as resolveModeDelays, Jt as NcTemplatePreviewInputSchema, K as MAX_ARCHIVED_DEBUG_NOTES, Ki as object, Kn as consumableLowPercentOf, Kr as resolveClipSearchMinScore, Kt as NcSystemEventKindSchema, L as EXPORT_DENSE_MAX_RANGES, Li as sleep$1, Ln as armRemovalReconcile, Lr as occupancyScope, Lt as NcRuleSchema, M as EMBEDDING_REQUIREMENTS_FAILED, Mi as defineCustomActions, Mn as alarmPanelCapability, Mr as kebabToCamel, Mt as NcLegacyRuleActionSequenceSchema, N as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Ni as hydrateSchema, Nn as applyBlockingEdit, Nr as linkedStepOf, Nt as NcRuleActionSchema, O as DETECTION_PIPELINE_CAP_NAME, Oi as BaseAddon, On as TrackSourceSchema, Or as isScheduleActive, Ot as NcAlarmSensorsSchema, P as EVENT_DENSITY_BATCH_MAX, Pi as isDeviceScopedCap, Pn as applyModeDelaysEdits, Pr as mayWriteToLocation, Pt as NcRuleActionsSchema, Q as MediaFileKindEnum, Qi as tuple, Qn as derivePostTriggeredAction, Qr as rootSupplyServes, Qt as OpsLogEntrySchema, R as FIRST_LEVEL_MACRO_CLASSES, Ri as ZodObject, Rn as assertTimelapseCadences, Rt as NcRuleTargetSchema, S as ClearTrackRelationResultSchema, Si as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Sn as TimelapseRulePatchSchema, Sr as isEventOwnerType, St as NC_TANK_SYSTEM_EVENT_KINDS, T as DEFAULT_EVENT_COLOR, Ti as isSourceCap, Tn as TrackRelationKindSchema, Tr as isMediaOwnerType, Tt as NcAlarmConfigSchema, U as ListTrackRelationsInputSchema, Ui as literal, Un as calendarEventLocalSpan, Ur as readMotionTriggeredBy, Ut as NcSnoozeSchema, V as LABEL_SUPPLY_MACRO, Vi as boolean, Vn as buildEventKindDescriptor, Vr as plateGalleryCapability, Vt as NcSharedSequenceSchema, W as ListTrackRelationsResultSchema, Wi as looseObject, Wn as classifyRetrainReview, Wr as readTimelapseGeneratedAt, Wt as NcSnoozeSuppressedSchema, X as MAX_TRACK_RELATIONS, Xi as string, Xn as deleteAlarmProfile, Xt as NcTemplateVarDescriptorSchema, Y as MAX_TRACK_PURITIES, Yi as strictObject, Yn as createAlarmProfile, Yr as resolveModeNotifyPolicy, Yt as NcTemplatePreviewSchema, Z as MOTION_CLOSE_AFTER_MS, Zi as templateLiteral, Zn as deriveDetailCropRect, Zr as retrainRevisionConflictMessage, Zt as OPS_LOG_DEFAULT_LIMIT, _ as BIRTH_DECISION_DEFAULT_LIMIT, _i as templateCarriesSummary, _n as SetTrackPurityResultSchema, _r as isAppLinkBase, _t as NC_GROUPING_MAX_SEC, a as CLIP_EMBEDDING_STEP_ID, ai as secureActuationFor, an as RetrainStatusSchema, ar as embeddingRefusalOf, at as NC_ALARM_SYSTEM_EVENT_KINDS, b as COCO_TO_MACRO, bi as viewerLinkBaseOf, bn as TRACK_RELATIONS_DEFAULT_LIMIT, br as isDetectionMacroClass, bt as NC_SNOOZE_MAX_MINUTES, c as readClusterModelPinStrict, ci as storageOccupancyCapability, cn as SCENE_DEFAULT_ANCHOR_THRESHOLD, cr as encodeVectorBase64, ct as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, d as APPEARANCE_BLOCKS, di as syncActiveAlarmProfileSettings, dn as SCENE_DEFAULT_QUIET_SECONDS, dr as evictionPolicyOfLocation, dt as NC_CLIP_RETENTION_MAX_SEC, ea as unknown, ei as ruleBelongsToAlarm$1, en as RECORDING_EXPORT_MAX_READ_BYTES, er as describeSystemEventDeliveryProblem, et as NC_ALARM_DEFAULT_PROFILE_ID, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fi as systemEventDeliveryProblems, fn as SCENE_DEFAULT_UNCOVERED_POLICY, fr as faceGalleryCapability, ft as NC_CONDITION_CATALOG, g as ArchivedDebugNoteSchema, gi as systemEventSummarySplitOf, gn as SetTrackPurityInputSchema, gr as isAlarmControllableCap, gt as NC_GROUPING_DEFAULT_SEC, h as AlarmArmModeSchema, hi as systemEventSummaryScheduleOf, hn as SceneMonitorSchema, hr as gifFromMp4Args, ht as NC_DEFAULT_SNOOZE_MINUTES, ii as sceneMonitorCapability, in as RetrainOriginSchema, it as NC_ALARM_NOTIFY_FIELDS, j as EMBEDDING_MODEL_FAILED, ji as customAction, jn as alarmDeviceSecureOnArm, jr as isVectorIndexDimConflict, jt as NcConditionsSchema, k as DeclaredDevices, ki as CamProfileSchema, kn as activateAlarmProfile, kr as isSystemDelivery, kt as NcAlarmSettingsPatchSchema, l as resolveClusterModelPin, li as subKindsOf, ln as SCENE_DEFAULT_CHECK_INTERVAL_SEC, lr as ensureAlarmProfiles, lt as NC_CLIP_POST_ROLL_DEFAULT_SEC, m as AUDIO_MACRO_LABELS, mi as systemEventFilterApplies, mn as SCENE_RESET_RECAPTURES, mr as gifDeriveSpeed, mt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, ni as ruleGroupingApplies, nn as RebuildObjectEmbeddingsRefusalSchema, nr as deviceRemovalHookCapability, nt as NC_ALARM_DELAY_FIELDS, o as CLUSTER_MODEL_OWNER_ADDON_ID, oi as sensorRaisedOf, on as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ot as NC_ALARM_TRIGGERED_MAX_SEC, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pi as systemEventFamilyProblems, pn as SCENE_DIVERGED, pr as failureContributionCapability, pt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, q as MAX_BIRTH_DECISION_RECORDS, qi as partialRecord, qn as coreBlockIdFromAddonId, qr as resolveLocationMode, qt as NcTaxonomySchema, ri as ruleKindOf, rn as RetrainDismissedSchema, rt as NC_ALARM_DELAY_MAX_SEC, s as FACE_EMBEDDING_STEP_ID, si as servedTemplateVars, sn as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, st as NC_AUDIO_CONFIRM_HITS_DEFAULT, t as BUILTIN_CLIP_MODELS, ta as EventCategory, ti as ruleDeviceCompatCapQueries, tn as RECORDING_SOURCE_CAMSTACK_ADDON, tr as describeSystemEventFamilyProblem, tt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, u as APPEARANCE_BIN_COUNT, ui as summarizeReview, un as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, ur as eventKindLabel, ut as NC_CLIP_PRE_ROLL_DEFAULT_SEC, v as BaseDevice, vi as templateVarsFor, vn as SetTrackRelationInputSchema, vr as isAudioClassName, vt as NC_GROUPING_MIN_SEC, w as DEFAULT_DETAIL_CROP_CONVENTION, wi as evaluateSensorEdge, wn as TrackPuritySchema, wr as isLabelSupplyStepId, wt as NC_TEMPLATE_VARS, x as ClearTrackRelationInputSchema, xi as zoneAnalyticsCapability, xn as TimelapseRuleInputSchema, xr as isEventKindLabelKey, xt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, y as BirthDecisionRecordSchema, yi as vectorDimFromBase64, yn as TIMELAPSE_DENSE_FLOOR_SEC, yr as isDefaultAlarmProfileId, yt as NC_SENSOR_TRANSITION_KINDS, z as FULL_IMAGE_BBOX, zi as _enum, zn as audioMetricsCapability, zr as pickFaceRescoreIntervalMinutes, zt as NcRuleUpdateInputSchema } from "../clip-model-registry-CCwyUQ7k.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs$1 from "node:fs";
4
4
  import { createWriteStream, promises } from "node:fs";
@@ -45936,6 +45936,22 @@ var MediaStore = class MediaStore {
45936
45936
  * happens HERE and nowhere else.
45937
45937
  */
45938
45938
  async getById(mediaId) {
45939
+ const found = await this.getBytesById(mediaId);
45940
+ if (found === null) return null;
45941
+ const { bytes, ...rest } = found;
45942
+ return {
45943
+ ...rest,
45944
+ base64: bytes.toString("base64")
45945
+ };
45946
+ }
45947
+ /**
45948
+ * {@link getById} WITHOUT the base64 round trip: the blob as the storage
45949
+ * engine read it. Every in-process reader (key-frame derive, retrain and
45950
+ * export readers, face re-embed) used to take `base64` and decode it straight
45951
+ * back — a 0.4–0.8 MB string plus a second buffer per read (D867). `getById`
45952
+ * stays for the callers that cross a wire.
45953
+ */
45954
+ async getBytesById(mediaId) {
45939
45955
  const row = await this.store.get.query({
45940
45956
  collection: MEDIA_COLLECTION,
45941
45957
  key: String(mediaId)
@@ -45950,10 +45966,10 @@ var MediaStore = class MediaStore {
45950
45966
  return {
45951
45967
  mediaId,
45952
45968
  kind,
45953
- base64: (await this.storage.read({
45969
+ bytes: await this.storage.read({
45954
45970
  location: mediaRowLocation(data),
45955
45971
  relativePath: path
45956
- })).toString("base64"),
45972
+ }),
45957
45973
  sizeBytes,
45958
45974
  timestamp,
45959
45975
  deviceId: Number(data["deviceId"])
@@ -47686,7 +47702,7 @@ function previewBlurSigma(frameWidth) {
47686
47702
  }
47687
47703
  /** Break a word too long for one line into line-width pieces. Without this a
47688
47704
  * single 200-character token wraps to nothing and overflows both edges. */
47689
- function chunk(word, size) {
47705
+ function chunk$1(word, size) {
47690
47706
  const pieces = [];
47691
47707
  for (let i = 0; i < word.length; i += size) pieces.push(word.slice(i, i + size));
47692
47708
  return pieces;
@@ -47694,7 +47710,7 @@ function chunk(word, size) {
47694
47710
  /** Greedy word wrap at a character budget — the only measurement available,
47695
47711
  * since librsvg lays the text out long after this string is written. */
47696
47712
  function wrapLines(text, maxChars) {
47697
- return text.split(" ").filter((w) => w.length > 0).reduce((lines, word) => (word.length > maxChars ? chunk(word, maxChars) : [word]).reduce((acc, piece) => {
47713
+ return text.split(" ").filter((w) => w.length > 0).reduce((lines, word) => (word.length > maxChars ? chunk$1(word, maxChars) : [word]).reduce((acc, piece) => {
47698
47714
  const last = acc.at(-1);
47699
47715
  if (last === void 0) return [piece];
47700
47716
  const merged = `${last} ${piece}`;
@@ -52541,6 +52557,17 @@ function choosePhotoFace(input) {
52541
52557
  //#endregion
52542
52558
  //#region src/pipeline-analytics/pipeline/face-matcher.ts
52543
52559
  /**
52560
+ * The scoring view of a sample's vector. A `Float32Array` is returned as is —
52561
+ * no per-probe, per-sample allocation (~3200 x 2 KB = 6.5 MB per probe on the
52562
+ * live gallery); a plain array is wrapped, exactly as before. The scores are
52563
+ * bit-identical either way: wrapping a `number[]` and storing a `Float32Array`
52564
+ * produce the same floats.
52565
+ */
52566
+ function galleryVector(sample) {
52567
+ const e = sample.embedding;
52568
+ return e instanceof Float32Array ? e : new Float32Array(e);
52569
+ }
52570
+ /**
52544
52571
  * Count enrolled samples per identity for probes of a matching model+dimension.
52545
52572
  * Only identities meeting `minIdentitySamples` are eligible auto-match targets.
52546
52573
  */
@@ -52574,7 +52601,7 @@ function rankIdentities(probe, gallery, minIdentitySamples) {
52574
52601
  for (const s of gallery) {
52575
52602
  if (s.modelId !== probe.modelId) continue;
52576
52603
  if (s.embedding.length !== probe.embedding.length) continue;
52577
- const score = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
52604
+ const score = cosineSimilarity$1(probeVec, galleryVector(s));
52578
52605
  const prev = bestByIdentity.get(s.identityId);
52579
52606
  if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
52580
52607
  countByIdentity.set(s.identityId, (countByIdentity.get(s.identityId) ?? 0) + 1);
@@ -52643,7 +52670,7 @@ function enrollmentPromiscuity(embedding, modelId, targetIdentityId, gallery, bo
52643
52670
  if (s.identityId === targetIdentityId) continue;
52644
52671
  if (s.modelId !== modelId) continue;
52645
52672
  if (s.embedding.length !== embedding.length) continue;
52646
- const cosine = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
52673
+ const cosine = cosineSimilarity$1(probeVec, galleryVector(s));
52647
52674
  if (cosine >= bound && (worst === null || cosine > worst.cosine)) worst = {
52648
52675
  identityId: s.identityId,
52649
52676
  cosine
@@ -52661,7 +52688,7 @@ function assignUniquePerFrame(candidates, gallery, opts) {
52661
52688
  if (s.modelId !== c.modelId) continue;
52662
52689
  if (s.embedding.length !== c.embedding.length) continue;
52663
52690
  if (!eligible.has(s.identityId)) continue;
52664
- const score = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
52691
+ const score = cosineSimilarity$1(probeVec, galleryVector(s));
52665
52692
  const prev = bestByIdentity.get(s.identityId);
52666
52693
  if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
52667
52694
  }
@@ -54891,6 +54918,20 @@ var EmbeddingVectorIndex = class {
54891
54918
  * carries no stamp, which no current writer produces.
54892
54919
  */
54893
54920
  async loadStamped(ids) {
54921
+ const f32 = await this.loadStampedFloat32(ids);
54922
+ return new Map([...f32].map(([id, row]) => [id, {
54923
+ embedding: Array.from(row.embedding),
54924
+ modelId: row.modelId
54925
+ }]));
54926
+ }
54927
+ /**
54928
+ * {@link loadStamped} without widening to `number[]`: the vector as the
54929
+ * `Float32Array` it was stored as, in a buffer of its own (a view over the
54930
+ * base64 decode would pin a pooled slab). For the matching gallery, which is
54931
+ * scored against on every probe and used to be re-wrapped per sample per
54932
+ * probe (D867). Same read-failure contract as {@link loadStamped}.
54933
+ */
54934
+ async loadStampedFloat32(ids) {
54894
54935
  const out = /* @__PURE__ */ new Map();
54895
54936
  if (ids.length === 0) return out;
54896
54937
  try {
@@ -54904,7 +54945,7 @@ var EmbeddingVectorIndex = class {
54904
54945
  const view = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
54905
54946
  const modelId = item.metadata["modelId"];
54906
54947
  out.set(item.id, {
54907
- embedding: Array.from(view),
54948
+ embedding: new Float32Array(view),
54908
54949
  modelId: typeof modelId === "string" && modelId.length > 0 ? modelId : null
54909
54950
  });
54910
54951
  }
@@ -55451,6 +55492,15 @@ function rowToFace(id, raw) {
55451
55492
  sourceFrameLandmarks: data.sourceFrameLandmarks ?? void 0
55452
55493
  };
55453
55494
  }
55495
+ /** The columns a whole-archive rescore reads per face (no vector, no crop). */
55496
+ var RESCORE_COLUMNS = [
55497
+ "deviceId",
55498
+ "assigned",
55499
+ "recognizedIdentityId",
55500
+ "embeddingModelId",
55501
+ "suggestedIdentityId",
55502
+ "suggestedMatchScore"
55503
+ ];
55454
55504
  var FaceStore = class {
55455
55505
  store;
55456
55506
  vectors;
@@ -56269,6 +56319,107 @@ var FaceStore = class {
56269
56319
  return out;
56270
56320
  }
56271
56321
  /**
56322
+ * The ids a whole-archive rescore will visit, SNAPSHOT at the start of the
56323
+ * run — the same population as {@link listPendingFaces} (unassigned AND
56324
+ * carrying no identity), so the two never disagree about "pending". Ids only:
56325
+ * the job pages the rows themselves ({@link loadRescoreBatch}) so the
56326
+ * settings thread never serves one big read of vectors.
56327
+ */
56328
+ async listPendingFaceIds() {
56329
+ return (await this.listPendingFaces()).map((r) => r.faceId);
56330
+ }
56331
+ /**
56332
+ * One page of a whole-archive rescore: the rows of `ids` that are STILL
56333
+ * pending (the operator may have assigned one since the snapshot), each with
56334
+ * its vector. Ids whose vector is in neither home are counted, not dropped
56335
+ * silently — "no vector" must not read as "scored, no match".
56336
+ */
56337
+ async loadRescoreBatch(ids) {
56338
+ if (ids.length === 0) return {
56339
+ rows: [],
56340
+ noVector: 0,
56341
+ gone: 0
56342
+ };
56343
+ const records = await this.store.query.query({
56344
+ collection: FACES_COLLECTION,
56345
+ columns: RESCORE_COLUMNS,
56346
+ filter: {
56347
+ whereIn: { id: [...ids] },
56348
+ limit: ids.length
56349
+ }
56350
+ });
56351
+ const stillPending = records.filter((r) => !r.data["assigned"] && (r.data["recognizedIdentityId"] ?? null) === null);
56352
+ const vectors = await this.loadEmbeddings(stillPending.map((r) => r.id));
56353
+ const rows = [];
56354
+ let noVector = 0;
56355
+ for (const r of stillPending) {
56356
+ const embedding = vectors.get(r.id);
56357
+ if (embedding === void 0 || embedding.length === 0) {
56358
+ noVector += 1;
56359
+ continue;
56360
+ }
56361
+ const suggestedIdentityId = r.data["suggestedIdentityId"];
56362
+ const suggestedMatchScore = r.data["suggestedMatchScore"];
56363
+ const embeddingModelId = r.data["embeddingModelId"];
56364
+ rows.push({
56365
+ id: r.id,
56366
+ deviceId: Number(r.data["deviceId"] ?? -1),
56367
+ embedding,
56368
+ modelId: typeof embeddingModelId === "string" ? embeddingModelId : LEGACY_FACE_MODEL_ID,
56369
+ ...typeof suggestedIdentityId === "string" ? { suggestedIdentityId } : {},
56370
+ ...typeof suggestedMatchScore === "number" ? { suggestedMatchScore } : {}
56371
+ });
56372
+ }
56373
+ return {
56374
+ rows,
56375
+ noVector,
56376
+ gone: ids.length - records.length
56377
+ };
56378
+ }
56379
+ /**
56380
+ * Write a batch of suggestion changes in ONE transaction (`transact`, all or
56381
+ * nothing). THROWS: a batch that did not land must not be counted as
56382
+ * applied. Touches only the two suggestion columns — never an assignment.
56383
+ */
56384
+ async applySuggestionBatch(updates) {
56385
+ if (updates.length === 0) return;
56386
+ await this.store.transact.mutate({ ops: updates.map((u) => ({
56387
+ op: "update",
56388
+ collection: FACES_COLLECTION,
56389
+ id: u.faceId,
56390
+ data: u.suggestion === null ? {
56391
+ suggestedIdentityId: null,
56392
+ suggestedMatchScore: null
56393
+ } : {
56394
+ suggestedIdentityId: u.suggestion.identityId,
56395
+ suggestedMatchScore: u.suggestion.score
56396
+ }
56397
+ })) });
56398
+ }
56399
+ /**
56400
+ * The unassigned-face set marker for change detection: how many unassigned
56401
+ * rows exist and the newest one's timestamp. ONE `COUNT(*)`+`MAX` statement
56402
+ * over `assigned = 0`, answered from `idx_faces_assigned_ts (assigned,
56403
+ * timestamp)` alone — covering, no table row fetched (proved by EXPLAIN in
56404
+ * `face-rescore-marker-plan-over-real-sqlite.spec.ts`, D447). A face
56405
+ * arriving moves the max, a face leaving or being assigned moves the count.
56406
+ */
56407
+ async pendingMarker() {
56408
+ const result = await this.store.aggregate.query({
56409
+ collection: FACES_COLLECTION,
56410
+ fields: [{
56411
+ as: "maxTimestamp",
56412
+ field: "timestamp",
56413
+ op: "max"
56414
+ }],
56415
+ filter: { where: { assigned: false } }
56416
+ });
56417
+ return {
56418
+ count: result.count,
56419
+ maxTimestamp: result.values["maxTimestamp"] ?? null
56420
+ };
56421
+ }
56422
+ /**
56272
56423
  * A face's enrolled sample is going away (D643 prune, or the operator's
56273
56424
  * Remove): forget the sample, and the crop too when it is the blob leaving
56274
56425
  * with it. The face itself — and who it was assigned to — stays. THROWS on
@@ -57216,10 +57367,15 @@ var FaceGalleryProvider = class {
57216
57367
  * real workflow is the exact case this breaks: assign ten faces to build a
57217
57368
  * person up, then open an older unassigned track and ask "does it match NOW?"
57218
57369
  * — and read a number that was decided before any of those ten existed. There
57219
- * is no background job that refreshes it and there should not be one (the
57370
+ * is no refresh on every assign, and there should not be one (the
57220
57371
  * gallery changes on every assign; re-scoring every buffered face each time is
57221
- * work nobody asked for). The honest fix is to make the question askable on
57222
- * demand. That is this module, and that is the button.
57372
+ * work nobody asked for). Two answers exist instead. On demand: this module,
57373
+ * the single-face button, and the whole-archive button (`face-rescore-job.ts`).
57374
+ * And D866 added a SLOW, change-gated job (`face-rescore-scheduler.ts`): hourly
57375
+ * by default, and only when a fingerprint of the gallery and of the unassigned
57376
+ * faces differs from the last completed run's — so an idle archive costs two
57377
+ * narrow reads, not a rescore. That supersedes this header's earlier "there is
57378
+ * no background job" statement; the objection to per-assign refresh stands.
57223
57379
  *
57224
57380
  * ## What it does NOT do
57225
57381
  *
@@ -57373,6 +57529,47 @@ async function persistRescoreResult(faceId, report, ports) {
57373
57529
  return candidate !== null ? "written" : "cleared";
57374
57530
  }
57375
57531
  //#endregion
57532
+ //#region src/pipeline-analytics/face-rescore-job-status.ts
57533
+ /**
57534
+ * The wire/storage shape of a whole-archive face rescore job (D866) — one Zod
57535
+ * schema for the action output, the persisted last report and the in-process
57536
+ * status, so the three cannot drift.
57537
+ */
57538
+ var FaceRescoreJobStateSchema = _enum([
57539
+ "idle",
57540
+ "running",
57541
+ "completed",
57542
+ "failed"
57543
+ ]);
57544
+ var FaceRescoreJobStatusSchema = object({
57545
+ /** `null` while nothing ever ran in this process. */
57546
+ jobId: string().nullable(),
57547
+ state: FaceRescoreJobStateSchema,
57548
+ /** Faces visited so far (rescored or deliberately skipped). */
57549
+ scanned: number().int(),
57550
+ /** Faces in the snapshot taken at the start; 0 until it is taken. */
57551
+ total: number().int(),
57552
+ /** newlySuggested + raised + lowered + cleared — rows actually written. */
57553
+ changed: number().int(),
57554
+ newlySuggested: number().int(),
57555
+ raised: number().int(),
57556
+ lowered: number().int(),
57557
+ cleared: number().int(),
57558
+ /** Faces in an old feature space: skipped, their suggestion untouched. */
57559
+ modelMismatch: number().int(),
57560
+ /** Faces with no vector in either home: skipped, counted. */
57561
+ noVector: number().int(),
57562
+ batches: number().int(),
57563
+ /** Slowest single batch's STORE time (read + transaction), ms. */
57564
+ maxBatchStoreMs: number(),
57565
+ durationMs: number(),
57566
+ galleryFingerprint: string(),
57567
+ /** Combined fingerprint taken at the start; what the scheduler persists. */
57568
+ fingerprint: string(),
57569
+ startedAt: number().nullable(),
57570
+ error: string().optional()
57571
+ });
57572
+ //#endregion
57376
57573
  //#region src/pipeline-analytics/face-rescore-actions.ts
57377
57574
  /**
57378
57575
  * face-rescore-actions — "score this face against the gallery AS IT IS NOW".
@@ -57455,52 +57652,549 @@ var FaceReembedReportSchema = object({
57455
57652
  /** The model the re-embed pinned — the cluster row's, never a substitute. */
57456
57653
  modelId: string().optional()
57457
57654
  });
57458
- var faceRescoreActions = defineCustomActions({
57655
+ var faceRescoreActions = defineCustomActions({
57656
+ /**
57657
+ * Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
57658
+ * `suggestedMatchScore`) — never an assignment, never automatic; see
57659
+ * `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
57660
+ * `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
57661
+ * also names every enrolled person and how well an arbitrary face matches
57662
+ * them — the gallery's shape, which is not a viewer-level fact.
57663
+ */
57664
+ "face.rescoreTrack": customAction(object({
57665
+ deviceId: number().int(),
57666
+ trackId: string().min(1),
57667
+ /**
57668
+ * Run `face-embedding` over the face's stored aligned template first, so a
57669
+ * face left in a superseded feature space becomes comparable again. Costs a
57670
+ * node round-trip and WRITES the face row. Never implicit.
57671
+ */
57672
+ reembed: boolean().default(false)
57673
+ }), object({
57674
+ faceId: string(),
57675
+ deviceId: number().int(),
57676
+ trackId: string(),
57677
+ /** The face's current assignment, so the UI can mark "already this person". */
57678
+ assigned: boolean(),
57679
+ assignedIdentityId: string().optional(),
57680
+ /** What the track has been SHOWING until now — read BEFORE this call
57681
+ * writes, so it is whatever a previous track close or a previous
57682
+ * `face.rescoreTrack` last persisted. Returned so the UI can put the stale
57683
+ * number beside the fresh one. */
57684
+ storedSuggestedIdentityId: string().optional(),
57685
+ storedSuggestedMatchScore: number().optional(),
57686
+ verdict: FaceRescoreVerdictSchema,
57687
+ comparable: boolean(),
57688
+ probeModelId: string(),
57689
+ clusterModelId: string(),
57690
+ params: FaceRescoreParamsSchema,
57691
+ identities: array(FaceRescoreIdentityRowSchema).readonly(),
57692
+ runnerUpGap: number().optional(),
57693
+ /** Whether a `reembed: true` retry could actually do anything. Lets the UI
57694
+ * offer the escape hatch only when it exists. */
57695
+ reembedAvailable: boolean(),
57696
+ reembed: FaceReembedReportSchema
57697
+ }), {
57698
+ kind: "mutation",
57699
+ auth: "admin"
57700
+ }),
57701
+ /**
57702
+ * Whole-archive rescore (D866): EVERY unassigned face against ONE gallery
57703
+ * snapshot, as a paced single-flight job. Returns at once with the running
57704
+ * job's status (a second call while one runs returns THAT job); poll
57705
+ * `face.rescoreStatus` for progress and the final report. Same persistence
57706
+ * rules as `face.rescoreTrack` — suggestions only, never an assignment, never
57707
+ * a `model-mismatch` write — and a weaker or vanished match LOWERS or CLEARS
57708
+ * the stored suggestion (see `face-rescore-job.ts`).
57709
+ */
57710
+ "face.rescoreAll": customAction(object({}), FaceRescoreJobStatusSchema, {
57711
+ kind: "mutation",
57712
+ auth: "admin"
57713
+ }),
57714
+ /** Progress of the running whole-archive rescore, else the last report. */
57715
+ "face.rescoreStatus": customAction(object({}), FaceRescoreJobStatusSchema, {
57716
+ kind: "query",
57717
+ auth: "admin"
57718
+ })
57719
+ });
57720
+ //#endregion
57721
+ //#region src/pipeline-analytics/face-rescore-job.ts
57722
+ /** Faces per batch: ~200 narrow updates in one transaction is tens of ms. */
57723
+ var RESCORE_BATCH_SIZE = 200;
57724
+ /** Pause after a batch = this × the batch's store time, clamped below. */
57725
+ var RESCORE_PAUSE_FACTOR = 2;
57726
+ var RESCORE_PAUSE_MIN_MS = 25;
57727
+ var RESCORE_PAUSE_MAX_MS = 500;
57728
+ /** Two scores closer than this are the same score (REAL round trip). */
57729
+ var SCORE_EPSILON = 1e-6;
57459
57730
  /**
57460
- * Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
57461
- * `suggestedMatchScore`) — never an assignment, never automatic; see
57462
- * `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
57463
- * `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
57464
- * also names every enrolled person and how well an arbitrary face matches
57465
- * them — the gallery's shape, which is not a viewer-level fact.
57731
+ * How the fresh answer relates to what the row holds. A switch to another
57732
+ * identity counts by its score: higher ⇒ raised, otherwise lowered (a tie with
57733
+ * a different person is `raised` — it is a change and must be written).
57466
57734
  */
57467
- "face.rescoreTrack": customAction(object({
57468
- deviceId: number().int(),
57469
- trackId: string().min(1),
57735
+ function classifyRescoreChange(stored, next) {
57736
+ if (stored === null && next === null) return "unchanged";
57737
+ if (stored === null) return "new";
57738
+ if (next === null) return "cleared";
57739
+ const delta = next.score - stored.score;
57740
+ if (next.identityId === stored.identityId && Math.abs(delta) <= SCORE_EPSILON) return "unchanged";
57741
+ return delta >= 0 ? "raised" : "lowered";
57742
+ }
57743
+ function storedSuggestion(row) {
57744
+ return row.suggestedIdentityId === void 0 ? null : {
57745
+ identityId: row.suggestedIdentityId,
57746
+ score: row.suggestedMatchScore ?? 0
57747
+ };
57748
+ }
57749
+ function sha(parts) {
57750
+ return createHash("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
57751
+ }
57752
+ /**
57753
+ * Pure. The gallery part changes when a sample is added, removed or restamped
57754
+ * to another model, or the cluster model flips; the pending part when a face
57755
+ * arrives, leaves or is assigned. Sorted, so read order cannot move it.
57756
+ */
57757
+ function computeRescoreFingerprint(input) {
57758
+ const lines = input.manifest.map((m) => `${m.sampleId}|${m.identityId}|${m.modelId}`).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
57759
+ const gallery = sha([`cluster:${input.clusterModelId}`, ...lines]);
57760
+ const pending = `${String(input.marker.count)}@${String(input.marker.maxTimestamp ?? "none")}`;
57761
+ return {
57762
+ gallery,
57763
+ pending,
57764
+ combined: sha([gallery, pending])
57765
+ };
57766
+ }
57767
+ var IDLE_STATUS = {
57768
+ jobId: null,
57769
+ state: "idle",
57770
+ scanned: 0,
57771
+ total: 0,
57772
+ changed: 0,
57773
+ newlySuggested: 0,
57774
+ raised: 0,
57775
+ lowered: 0,
57776
+ cleared: 0,
57777
+ modelMismatch: 0,
57778
+ noVector: 0,
57779
+ batches: 0,
57780
+ maxBatchStoreMs: 0,
57781
+ durationMs: 0,
57782
+ galleryFingerprint: "",
57783
+ fingerprint: "",
57784
+ startedAt: null
57785
+ };
57786
+ /** The fingerprint of NOW, plus the cluster model it was taken under. */
57787
+ async function readRescoreFingerprint(ports) {
57788
+ const clusterModelId = await ports.resolveClusterModelId();
57789
+ if (clusterModelId === null) return null;
57790
+ const [manifest, marker] = await Promise.all([ports.galleryManifest(), ports.pendingMarker()]);
57791
+ return {
57792
+ fingerprint: computeRescoreFingerprint({
57793
+ manifest,
57794
+ marker,
57795
+ clusterModelId
57796
+ }),
57797
+ clusterModelId
57798
+ };
57799
+ }
57800
+ function chunk(items, size) {
57801
+ const out = [];
57802
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
57803
+ return out;
57804
+ }
57805
+ function pauseFor(storeMs) {
57806
+ return Math.min(RESCORE_PAUSE_MAX_MS, Math.max(RESCORE_PAUSE_MIN_MS, Math.round(storeMs * RESCORE_PAUSE_FACTOR)));
57807
+ }
57808
+ var FaceRescoreJob = class {
57809
+ ports;
57810
+ current = IDLE_STATUS;
57811
+ running = null;
57812
+ stopped = false;
57813
+ constructor(ports) {
57814
+ this.ports = ports;
57815
+ }
57816
+ /** Progress of the running job, else the last finished one, else idle. */
57817
+ status() {
57818
+ return this.current;
57819
+ }
57820
+ isRunning() {
57821
+ return this.running !== null;
57822
+ }
57823
+ /** Resolves when no job is running. Never rejects: failure is in `status()`. */
57824
+ async whenSettled() {
57825
+ await this.running;
57826
+ }
57827
+ /** Ask a running job to stop after its current batch (addon shutdown). */
57828
+ stop() {
57829
+ this.stopped = true;
57830
+ }
57470
57831
  /**
57471
- * Run `face-embedding` over the face's stored aligned template first, so a
57472
- * face left in a superseded feature space becomes comparable again. Costs a
57473
- * node round-trip and WRITES the face row. Never implicit.
57832
+ * Start a job — or return the one already running (single-flight). The
57833
+ * returned status is a snapshot; poll {@link status} for progress.
57474
57834
  */
57475
- reembed: boolean().default(false)
57476
- }), object({
57477
- faceId: string(),
57478
- deviceId: number().int(),
57479
- trackId: string(),
57480
- /** The face's current assignment, so the UI can mark "already this person". */
57481
- assigned: boolean(),
57482
- assignedIdentityId: string().optional(),
57483
- /** What the track has been SHOWING until now — read BEFORE this call
57484
- * writes, so it is whatever a previous track close or a previous
57485
- * `face.rescoreTrack` last persisted. Returned so the UI can put the stale
57486
- * number beside the fresh one. */
57487
- storedSuggestedIdentityId: string().optional(),
57488
- storedSuggestedMatchScore: number().optional(),
57489
- verdict: FaceRescoreVerdictSchema,
57490
- comparable: boolean(),
57491
- probeModelId: string(),
57492
- clusterModelId: string(),
57493
- params: FaceRescoreParamsSchema,
57494
- identities: array(FaceRescoreIdentityRowSchema).readonly(),
57495
- runnerUpGap: number().optional(),
57496
- /** Whether a `reembed: true` retry could actually do anything. Lets the UI
57497
- * offer the escape hatch only when it exists. */
57498
- reembedAvailable: boolean(),
57499
- reembed: FaceReembedReportSchema
57500
- }), {
57501
- kind: "mutation",
57502
- auth: "admin"
57503
- }) });
57835
+ start() {
57836
+ if (this.running !== null) return this.current;
57837
+ this.stopped = false;
57838
+ const startedAt = this.ports.now();
57839
+ this.current = {
57840
+ ...IDLE_STATUS,
57841
+ jobId: this.ports.newJobId(),
57842
+ state: "running",
57843
+ startedAt
57844
+ };
57845
+ this.running = this.run(startedAt).finally(() => {
57846
+ this.running = null;
57847
+ });
57848
+ return this.current;
57849
+ }
57850
+ patch(change) {
57851
+ this.current = {
57852
+ ...this.current,
57853
+ ...change
57854
+ };
57855
+ }
57856
+ async run(startedAt) {
57857
+ try {
57858
+ await this.runBatches(startedAt);
57859
+ this.patch({
57860
+ state: "completed",
57861
+ durationMs: this.ports.now() - startedAt
57862
+ });
57863
+ this.logSummary("face rescore job finished");
57864
+ await this.recordCompletion();
57865
+ } catch (err) {
57866
+ const error = err instanceof Error ? err.message : String(err);
57867
+ this.patch({
57868
+ state: "failed",
57869
+ error,
57870
+ durationMs: this.ports.now() - startedAt
57871
+ });
57872
+ this.ports.logger.warn("face rescore job FAILED — rows written so far stay written", { meta: {
57873
+ ...this.summaryMeta(),
57874
+ error
57875
+ } });
57876
+ }
57877
+ }
57878
+ async recordCompletion() {
57879
+ try {
57880
+ await this.ports.recordCompletion(this.current);
57881
+ } catch (err) {
57882
+ this.ports.logger.warn("face rescore result could not be recorded — the next tick may rerun", { meta: {
57883
+ jobId: this.current.jobId,
57884
+ error: err instanceof Error ? err.message : String(err)
57885
+ } });
57886
+ }
57887
+ }
57888
+ summaryMeta() {
57889
+ const s = this.current;
57890
+ return {
57891
+ jobId: s.jobId,
57892
+ scanned: s.scanned,
57893
+ total: s.total,
57894
+ changed: s.changed,
57895
+ newlySuggested: s.newlySuggested,
57896
+ raised: s.raised,
57897
+ lowered: s.lowered,
57898
+ cleared: s.cleared,
57899
+ modelMismatch: s.modelMismatch,
57900
+ noVector: s.noVector,
57901
+ batches: s.batches,
57902
+ maxBatchStoreMs: s.maxBatchStoreMs,
57903
+ durationMs: s.durationMs,
57904
+ galleryFingerprint: s.galleryFingerprint
57905
+ };
57906
+ }
57907
+ logSummary(message) {
57908
+ this.ports.logger.info(message, { meta: this.summaryMeta() });
57909
+ }
57910
+ async runBatches(startedAt) {
57911
+ const observed = await readRescoreFingerprint(this.ports);
57912
+ if (observed === null) throw new Error("the cluster face-embedding model could not be resolved");
57913
+ const { clusterModelId, fingerprint } = observed;
57914
+ const [gallery, identityNames] = await Promise.all([this.ports.loadGallery(), this.ports.identityNames()]);
57915
+ const ids = await this.ports.listPendingFaceIds();
57916
+ this.patch({
57917
+ total: ids.length,
57918
+ galleryFingerprint: fingerprint.gallery,
57919
+ fingerprint: fingerprint.combined
57920
+ });
57921
+ const paramsByDevice = /* @__PURE__ */ new Map();
57922
+ for (const batchIds of chunk(ids, RESCORE_BATCH_SIZE)) {
57923
+ if (this.stopped) throw new Error("stopped before completion");
57924
+ const storeStart = this.ports.now();
57925
+ const batch = await this.ports.loadRescoreBatch(batchIds);
57926
+ const readMs = this.ports.now() - storeStart;
57927
+ const outcome = await this.scoreBatch(batch.rows, {
57928
+ gallery,
57929
+ identityNames,
57930
+ clusterModelId,
57931
+ paramsByDevice
57932
+ });
57933
+ const writeStart = this.ports.now();
57934
+ await this.ports.applySuggestionBatch(outcome.updates);
57935
+ const storeMs = readMs + (this.ports.now() - writeStart);
57936
+ const s = this.current;
57937
+ this.patch({
57938
+ scanned: s.scanned + batchIds.length,
57939
+ changed: s.changed + outcome.updates.length,
57940
+ newlySuggested: s.newlySuggested + outcome.tally.new,
57941
+ raised: s.raised + outcome.tally.raised,
57942
+ lowered: s.lowered + outcome.tally.lowered,
57943
+ cleared: s.cleared + outcome.tally.cleared,
57944
+ modelMismatch: s.modelMismatch + outcome.modelMismatch,
57945
+ noVector: s.noVector + batch.noVector,
57946
+ batches: s.batches + 1,
57947
+ maxBatchStoreMs: Math.max(s.maxBatchStoreMs, storeMs),
57948
+ durationMs: this.ports.now() - startedAt
57949
+ });
57950
+ this.ports.logger.debug("face rescore batch", { meta: {
57951
+ jobId: s.jobId,
57952
+ size: batchIds.length,
57953
+ written: outcome.updates.length,
57954
+ storeMs
57955
+ } });
57956
+ await this.ports.pause(pauseFor(storeMs));
57957
+ }
57958
+ }
57959
+ async scoreBatch(rows, ctx) {
57960
+ const updates = [];
57961
+ const tally = {
57962
+ new: 0,
57963
+ raised: 0,
57964
+ lowered: 0,
57965
+ cleared: 0
57966
+ };
57967
+ let modelMismatch = 0;
57968
+ for (const row of rows) {
57969
+ let params = ctx.paramsByDevice.get(row.deviceId);
57970
+ if (params === void 0) {
57971
+ params = await this.ports.paramsForDevice(row.deviceId);
57972
+ ctx.paramsByDevice.set(row.deviceId, params);
57973
+ }
57974
+ const report = buildFaceRescoreReport({
57975
+ probe: {
57976
+ embedding: row.embedding,
57977
+ modelId: row.modelId
57978
+ },
57979
+ gallery: ctx.gallery,
57980
+ identityNames: ctx.identityNames,
57981
+ clusterModelId: ctx.clusterModelId,
57982
+ params
57983
+ });
57984
+ const captured = { value: null };
57985
+ if (await persistRescoreResult(row.id, report, { setSuggestion: (_id, suggestion) => {
57986
+ captured.value = suggestion;
57987
+ return Promise.resolve();
57988
+ } }) === "not-comparable") {
57989
+ modelMismatch += 1;
57990
+ continue;
57991
+ }
57992
+ const change = classifyRescoreChange(storedSuggestion(row), captured.value);
57993
+ if (change === "unchanged") continue;
57994
+ tally[change] += 1;
57995
+ updates.push({
57996
+ faceId: row.id,
57997
+ suggestion: captured.value
57998
+ });
57999
+ this.ports.logger.debug("face rescore changed a suggestion", {
58000
+ tags: { deviceId: row.deviceId },
58001
+ meta: {
58002
+ faceId: row.id,
58003
+ change,
58004
+ from: row.suggestedIdentityId ?? null,
58005
+ fromScore: row.suggestedMatchScore ?? null,
58006
+ to: decideRescorePersistence(report)?.identityId ?? null
58007
+ }
58008
+ });
58009
+ }
58010
+ return {
58011
+ updates,
58012
+ tally,
58013
+ modelMismatch
58014
+ };
58015
+ }
58016
+ };
58017
+ //#endregion
58018
+ //#region src/pipeline-analytics/face-rescore-scheduler.ts
58019
+ var FACE_RESCORE_TICK_MS = 6e4;
58020
+ var FaceRescoreScheduler = class {
58021
+ deps;
58022
+ lastCheckAt;
58023
+ ticking = false;
58024
+ constructor(deps, startedAt) {
58025
+ this.deps = deps;
58026
+ this.lastCheckAt = startedAt;
58027
+ }
58028
+ async tick() {
58029
+ if (this.ticking) return "busy";
58030
+ this.ticking = true;
58031
+ try {
58032
+ return await this.decideAndRun();
58033
+ } finally {
58034
+ this.ticking = false;
58035
+ }
58036
+ }
58037
+ async decideAndRun() {
58038
+ const minutes = await this.readInterval();
58039
+ if (minutes === null) return "setting-unreadable";
58040
+ if (minutes === 0) return "disabled";
58041
+ const now = this.deps.now();
58042
+ if (now - this.lastCheckAt < minutes * 6e4) return "not-due";
58043
+ this.lastCheckAt = now;
58044
+ if (this.deps.job.isRunning()) return "busy";
58045
+ const observed = await this.observe();
58046
+ if (observed === null) return "unreadable";
58047
+ const previous = await this.deps.state.load();
58048
+ if (previous !== null && previous.fingerprint === observed) {
58049
+ this.deps.logger.info("face rescore skipped, unchanged", { meta: {
58050
+ fingerprint: observed,
58051
+ lastRunAt: previous.finishedAt
58052
+ } });
58053
+ return "unchanged";
58054
+ }
58055
+ this.deps.job.start();
58056
+ await this.deps.job.whenSettled();
58057
+ return this.deps.job.status().state === "completed" ? "ran" : "failed";
58058
+ }
58059
+ async readInterval() {
58060
+ try {
58061
+ return await this.deps.readIntervalMinutes();
58062
+ } catch (err) {
58063
+ this.deps.logger.warn("face rescore interval unreadable — tick skipped", { meta: { error: err instanceof Error ? err.message : String(err) } });
58064
+ return null;
58065
+ }
58066
+ }
58067
+ async observe() {
58068
+ try {
58069
+ const seen = await readRescoreFingerprint(this.deps.fingerprintPorts);
58070
+ if (seen === null) {
58071
+ this.deps.logger.warn("face rescore tick skipped — cluster face model unresolved");
58072
+ return null;
58073
+ }
58074
+ return seen.fingerprint.combined;
58075
+ } catch (err) {
58076
+ this.deps.logger.warn("face rescore fingerprint unreadable — tick skipped", { meta: { error: err instanceof Error ? err.message : String(err) } });
58077
+ return null;
58078
+ }
58079
+ }
58080
+ };
58081
+ //#endregion
58082
+ //#region src/pipeline-analytics/face-rescore-wiring.ts
58083
+ /**
58084
+ * face-rescore-wiring — binds the whole-archive rescore job (D866) to the real
58085
+ * stores, the cluster model pin and the orchestrator's interval setting.
58086
+ *
58087
+ * Kept out of `index.ts` (20 000 lines): the addon holds the returned runtime
58088
+ * and forwards three things to it — the two actions, the timer tick, stop.
58089
+ */
58090
+ function buildFaceRescoreRuntime(deps, startedAt) {
58091
+ const { faceStore, identityStore, logger } = deps;
58092
+ const resolveClusterModelId = () => resolveClusterModelPin(deps.api, FACE_EMBEDDING_STEP_ID, { warn: (m, e) => logger.warn(m, e) });
58093
+ const galleryManifest = () => identityStore.galleryManifest();
58094
+ const pendingMarker = () => faceStore.pendingMarker();
58095
+ const job = new FaceRescoreJob({
58096
+ resolveClusterModelId,
58097
+ galleryManifest,
58098
+ pendingMarker,
58099
+ loadGallery: () => identityStore.loadGallery(),
58100
+ identityNames: async () => new Map((await identityStore.listIdentities()).map((i) => [i.id, i.name])),
58101
+ paramsForDevice: deps.paramsForDevice,
58102
+ listPendingFaceIds: () => faceStore.listPendingFaceIds(),
58103
+ loadRescoreBatch: (ids) => faceStore.loadRescoreBatch(ids),
58104
+ applySuggestionBatch: (updates) => faceStore.applySuggestionBatch(updates),
58105
+ recordCompletion: (status) => deps.state.save({
58106
+ fingerprint: status.fingerprint,
58107
+ finishedAt: Date.now(),
58108
+ report: status
58109
+ }),
58110
+ pause: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
58111
+ now: () => Date.now(),
58112
+ newJobId: () => randomUUID(),
58113
+ logger
58114
+ });
58115
+ return {
58116
+ job,
58117
+ scheduler: new FaceRescoreScheduler({
58118
+ readIntervalMinutes: async () => pickFaceRescoreIntervalMinutes(await deps.api.addonSettings.getGlobalSettings.query({ addonId: CLUSTER_MODEL_OWNER_ADDON_ID })),
58119
+ job,
58120
+ fingerprintPorts: {
58121
+ resolveClusterModelId,
58122
+ galleryManifest,
58123
+ pendingMarker
58124
+ },
58125
+ state: deps.state,
58126
+ now: () => Date.now(),
58127
+ logger
58128
+ }, startedAt)
58129
+ };
58130
+ }
58131
+ //#endregion
58132
+ //#region src/pipeline-analytics/store/face-rescore-state-store.ts
58133
+ /**
58134
+ * @durable class=config owner=pipeline-analytics
58135
+ * write="one row, rewritten when a whole-archive face rescore completes"
58136
+ * loss="the next scheduler tick reruns the rescore once — idempotent over an
58137
+ * unchanged gallery"
58138
+ * retention="never pruned: one row"
58139
+ */
58140
+ var FACE_RESCORE_STATE_COLLECTION = "pipeline-analytics:face-rescore-state";
58141
+ var STATE_KEY = "last-run";
58142
+ var FaceRescoreStateStore = class {
58143
+ deps;
58144
+ constructor(deps) {
58145
+ this.deps = deps;
58146
+ }
58147
+ static async declare(store) {
58148
+ await store.declareCollection.mutate({
58149
+ collection: FACE_RESCORE_STATE_COLLECTION,
58150
+ columns: [{
58151
+ name: "data",
58152
+ type: "TEXT",
58153
+ notNull: true
58154
+ }]
58155
+ });
58156
+ }
58157
+ /** `null` on no row OR an unreadable one: either way the next tick reruns. */
58158
+ load = async () => {
58159
+ try {
58160
+ return decodeRunState(await this.deps.store.get.query({
58161
+ collection: FACE_RESCORE_STATE_COLLECTION,
58162
+ key: STATE_KEY
58163
+ }) ?? null);
58164
+ } catch (err) {
58165
+ this.deps.logger.warn("face rescore state unreadable — treating as never run", { meta: { error: String(err) } });
58166
+ return null;
58167
+ }
58168
+ };
58169
+ /** THROWS: a state that did not land must be visible to the caller. */
58170
+ save = async (state) => {
58171
+ await this.deps.store.set.mutate({
58172
+ collection: FACE_RESCORE_STATE_COLLECTION,
58173
+ key: STATE_KEY,
58174
+ value: {
58175
+ fingerprint: state.fingerprint,
58176
+ finishedAt: state.finishedAt,
58177
+ report: state.report
58178
+ }
58179
+ });
58180
+ };
58181
+ };
58182
+ /** Narrow a stored row; anything malformed is "no previous run". */
58183
+ function decodeRunState(value) {
58184
+ if (value === null || typeof value !== "object") return null;
58185
+ const row = { ...value };
58186
+ const fingerprint = row["fingerprint"];
58187
+ const finishedAt = row["finishedAt"];
58188
+ const report = row["report"];
58189
+ if (typeof fingerprint !== "string" || fingerprint === "") return null;
58190
+ if (typeof finishedAt !== "number") return null;
58191
+ const parsed = FaceRescoreJobStatusSchema.safeParse(report);
58192
+ return parsed.success ? {
58193
+ fingerprint,
58194
+ finishedAt,
58195
+ report: parsed.data
58196
+ } : null;
58197
+ }
57504
58198
  //#endregion
57505
58199
  //#region src/pipeline-analytics/face-sample-reembed.ts
57506
58200
  /** Pacing between samples — off hub-main's thread, not a throttle. */
@@ -76517,6 +77211,39 @@ function clampBox(b, frameWidth, frameHeight) {
76517
77211
  h: Math.max(1, Math.min(Math.round(b.h), frameHeight - y))
76518
77212
  };
76519
77213
  }
77214
+ /** The SVG overlay: one `<rect>` per box plus an optional caption. */
77215
+ function buildBoxOverlaySvg(boxes, frameWidth, frameHeight) {
77216
+ const fontSize = Math.max(12, Math.round(frameHeight / 30));
77217
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}">${boxes.map((b) => {
77218
+ const c = clampBox(b, frameWidth, frameHeight);
77219
+ const color = b.color ?? DEFAULT_COLOR;
77220
+ const rect = `<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" fill="none" stroke="${color}" stroke-width="${STROKE_WIDTH}"/>`;
77221
+ if (!b.label) return rect;
77222
+ const ty = Math.max(fontSize, c.y - 4);
77223
+ return rect + `<text x="${c.x}" y="${ty}" font-family="sans-serif" font-size="${fontSize}" fill="${color}" stroke="black" stroke-width="0.5">${escapeXml(b.label)}</text>`;
77224
+ }).join("")}</svg>`;
77225
+ }
77226
+ /**
77227
+ * Draw bounding boxes over a frame that is still a JPEG and re-encode it in ONE
77228
+ * libvips pipeline (decode, composite, encode). The raw RGB surface never
77229
+ * exists in the JS process: {@link drawBoxedFrame} needs it handed in, which for
77230
+ * a consumer that only re-encodes meant a 1-6 MB V8-external buffer per tile
77231
+ * (D867). Same overlay and quality contract; no `maxWidth` — the caller asked
77232
+ * the owning runner for the size it wants.
77233
+ */
77234
+ async function drawBoxedJpeg(jpeg, frameWidth, frameHeight, boxes, opts = {}) {
77235
+ const quality = opts.quality ?? DEFAULT_QUALITY;
77236
+ let pipeline = sharp(jpeg);
77237
+ if (boxes.length > 0) {
77238
+ const svg = buildBoxOverlaySvg(boxes, frameWidth, frameHeight);
77239
+ pipeline = pipeline.composite([{
77240
+ input: Buffer.from(svg),
77241
+ top: 0,
77242
+ left: 0
77243
+ }]);
77244
+ }
77245
+ return pipeline.jpeg({ quality }).toBuffer();
77246
+ }
76520
77247
  /**
76521
77248
  * Draw bounding boxes over a raw RGB frame and JPEG-encode it.
76522
77249
  *
@@ -76530,15 +77257,7 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
76530
77257
  let base = frameData;
76531
77258
  let baseIsRaw = true;
76532
77259
  if (boxes.length > 0) {
76533
- const fontSize = Math.max(12, Math.round(frameHeight / 30));
76534
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}">${boxes.map((b) => {
76535
- const c = clampBox(b, frameWidth, frameHeight);
76536
- const color = b.color ?? DEFAULT_COLOR;
76537
- const rect = `<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" fill="none" stroke="${color}" stroke-width="${STROKE_WIDTH}"/>`;
76538
- if (!b.label) return rect;
76539
- const ty = Math.max(fontSize, c.y - 4);
76540
- return rect + `<text x="${c.x}" y="${ty}" font-family="sans-serif" font-size="${fontSize}" fill="${color}" stroke="black" stroke-width="0.5">${escapeXml(b.label)}</text>`;
76541
- }).join("")}</svg>`;
77260
+ const svg = buildBoxOverlaySvg(boxes, frameWidth, frameHeight);
76542
77261
  base = await sharp(frameData, { raw: {
76543
77262
  width: frameWidth,
76544
77263
  height: frameHeight,
@@ -77112,7 +77831,7 @@ function createDebugCropCapture(deps) {
77112
77831
  * `extractCrop` (which crops a FULL frame) so the native path never re-crops.
77113
77832
  */
77114
77833
  async function encodeRgbCropToJpeg(bytes, width, height) {
77115
- return sharp(Buffer.from(bytes), { raw: {
77834
+ return sharp(bytes, { raw: {
77116
77835
  width,
77117
77836
  height,
77118
77837
  channels: 3
@@ -77131,7 +77850,7 @@ async function encodeRgbCropToJpeg(bytes, width, height) {
77131
77850
  * what lets the caller stop base64-ing purely to be understood.
77132
77851
  */
77133
77852
  async function decodeJpegToRgb(jpeg) {
77134
- const { data, info } = await sharp(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : Buffer.from(jpeg)).raw().toBuffer({ resolveWithObject: true });
77853
+ const { data, info } = await sharp(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : jpeg).raw().toBuffer({ resolveWithObject: true });
77135
77854
  return {
77136
77855
  bytes: data,
77137
77856
  width: info.width,
@@ -77776,13 +78495,14 @@ var AnalyticsQueryFacade = class {
77776
78495
  cutoffMs: Number.MAX_SAFE_INTEGER
77777
78496
  });
77778
78497
  const total = counts.motion + counts.object + counts.audio;
78498
+ const byRemoval = input.reason !== void 0;
77779
78499
  await this.deps.opsLog()?.append({
77780
- op: "manual-delete",
77781
- reason: "manual",
78500
+ op: byRemoval ? "prune" : "manual-delete",
78501
+ reason: input.reason ?? "manual",
77782
78502
  deviceId: input.deviceId,
77783
78503
  itemsAffected: total,
77784
78504
  bytesReclaimed: 0,
77785
- detail: "deleted all events for device"
78505
+ detail: byRemoval ? "the device was deleted — its events were removed with it" : "deleted all events for device"
77786
78506
  });
77787
78507
  return counts;
77788
78508
  }
@@ -79854,6 +80574,53 @@ var DeviceAlertSettingsSchema = object({
79854
80574
  /** The shipped defaults, for the settings-schema form and for tests. */
79855
80575
  var DEVICE_ALERT_DEFAULTS = DeviceAlertSettingsSchema.parse({});
79856
80576
  //#endregion
80577
+ //#region src/pipeline-analytics/device-removal-hook.ts
80578
+ function createAnalyticsRemovalHook(deps) {
80579
+ return {
80580
+ onDeviceRemoved: async (input) => {
80581
+ const { deviceId } = input;
80582
+ const events = await deps.deleteDeviceEvents({
80583
+ deviceId,
80584
+ reason: "orphaned-device"
80585
+ });
80586
+ const tracks = await deps.wipeAllAnalytics({ deviceId });
80587
+ const sensor = await deps.evictSensorEvents(deviceId, Number.MAX_SAFE_INTEGER);
80588
+ const counts = {
80589
+ events: events.motion + events.object + events.audio,
80590
+ tracks: tracks.tracks,
80591
+ "track-events": tracks.events,
80592
+ media: tracks.media,
80593
+ faces: tracks.faces,
80594
+ plates: tracks.plates,
80595
+ embeddings: tracks.embeddings,
80596
+ "sensor-events": sensor
80597
+ };
80598
+ if (Object.values(counts).reduce((sum, n) => sum + n, 0) > 0) deps.logger.info("pipeline-analytics: removed the data of a deleted device", {
80599
+ tags: { deviceId },
80600
+ meta: {
80601
+ counts,
80602
+ deletedAt: input.deletedAt
80603
+ }
80604
+ });
80605
+ return { counts };
80606
+ },
80607
+ listDeviceData: async () => {
80608
+ const out = [];
80609
+ for (const [deviceId, count] of await deps.eventCountsByDevice()) if (count > 0 && Number.isInteger(deviceId) && deviceId > 0) out.push({
80610
+ store: "events",
80611
+ deviceId,
80612
+ count
80613
+ });
80614
+ for (const deviceId of await deps.trackDeviceIds()) if (Number.isInteger(deviceId) && deviceId > 0) out.push({
80615
+ store: "tracks",
80616
+ deviceId,
80617
+ count: 1
80618
+ });
80619
+ return out;
80620
+ }
80621
+ };
80622
+ }
80623
+ //#endregion
79857
80624
  //#region src/pipeline-analytics/device-settings-cache.ts
79858
80625
  /**
79859
80626
  * TTL cache for ONE settings kind, keyed by deviceId. Obtain instances via
@@ -89339,7 +90106,7 @@ function createKeyFrameCrop(deps) {
89339
90106
  return null;
89340
90107
  }
89341
90108
  try {
89342
- const rgb = await deps.decodeJpegToRgb(media.base64);
90109
+ const rgb = await deps.decodeJpegToRgb(media.bytes);
89343
90110
  if (rgb.width <= 0 || rgb.height <= 0) return null;
89344
90111
  const norm = padBbox({
89345
90112
  x: input.bbox.x / input.frameWidth,
@@ -89347,7 +90114,7 @@ function createKeyFrameCrop(deps) {
89347
90114
  w: input.bbox.w / input.frameWidth,
89348
90115
  h: input.bbox.h / input.frameHeight
89349
90116
  }, input.padding);
89350
- const jpeg = await deps.cropRegionToJpeg(Buffer.from(rgb.bytes), rgb.width, rgb.height, norm);
90117
+ const jpeg = await deps.cropRegionToJpeg(rgb.bytes, rgb.width, rgb.height, norm);
89351
90118
  return verdict.kind === "agrees" ? {
89352
90119
  jpeg,
89353
90120
  skewMs,
@@ -97651,6 +98418,29 @@ var IdentityStore = class {
97651
98418
  };
97652
98419
  });
97653
98420
  }
98421
+ /**
98422
+ * The gallery's IDENTITY of the moment — sample id, owner and model of every
98423
+ * sample, no vectors. What the rescore change detector hashes; projected so a
98424
+ * periodic look costs a few narrow columns, not the gallery's floats.
98425
+ */
98426
+ async galleryManifest() {
98427
+ const rows = await this.store.query.query({
98428
+ collection: IDENTITY_SAMPLES_COLLECTION,
98429
+ columns: ["identityId", "modelId"],
98430
+ filter: {}
98431
+ });
98432
+ const out = [];
98433
+ for (const r of rows) {
98434
+ const identityId = r.data["identityId"];
98435
+ const modelId = r.data["modelId"];
98436
+ if (typeof identityId === "string" && typeof modelId === "string") out.push({
98437
+ sampleId: r.id,
98438
+ identityId,
98439
+ modelId
98440
+ });
98441
+ }
98442
+ return out;
98443
+ }
97654
98444
  async addSample(input) {
97655
98445
  const sample = {
97656
98446
  id: randomUUID(),
@@ -97949,7 +98739,7 @@ var IdentityStore = class {
97949
98739
  const d = r.data;
97950
98740
  if (typeof d.modelId === "string" && typeof d.identityId === "string") wanted.push(r.id);
97951
98741
  }
97952
- const vectors = await this.vectors.loadStamped(wanted);
98742
+ const vectors = await this.vectors.loadStampedFloat32(wanted);
97953
98743
  const gallery = [];
97954
98744
  let fromLegacyColumn = 0;
97955
98745
  let missing = 0;
@@ -97979,7 +98769,7 @@ var IdentityStore = class {
97979
98769
  fromLegacyColumn += 1;
97980
98770
  gallery.push({
97981
98771
  identityId: d.identityId,
97982
- embedding: legacy,
98772
+ embedding: new Float32Array(legacy),
97983
98773
  modelId: d.modelId
97984
98774
  });
97985
98775
  continue;
@@ -101903,6 +102693,7 @@ function createNativeFrameTransport(deps) {
101903
102693
  };
101904
102694
  const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
101905
102695
  const getNativeFullFrameRgb = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToFullFrame));
102696
+ const getNativeFullFrameJpeg = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, cropReplyToJpeg));
101906
102697
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
101907
102698
  const cropMetricLogger = logger.child("NativeCrop");
101908
102699
  let nativeHits = 0;
@@ -102011,6 +102802,7 @@ function createNativeFrameTransport(deps) {
102011
102802
  return {
102012
102803
  getRemoteFrame,
102013
102804
  getNativeFullFrameRgb,
102805
+ getNativeFullFrameJpeg,
102014
102806
  getNativeCropJpeg,
102015
102807
  captureCrop: createCaptureCrop({
102016
102808
  tryNativeCrop,
@@ -103829,6 +104621,9 @@ var EventMediaDispatcher = class {
103829
104621
  * {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
103830
104622
  */
103831
104623
  sharedNativeFullFrame;
104624
+ /** The JPEG twin of {@link sharedNativeFullFrame} — the boxed tiles' source
104625
+ * (a few tens of kB held instead of a 1.5 MB RGB surface). */
104626
+ sharedNativeFullJpeg;
103832
104627
  /** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
103833
104628
  * info log — see {@link logDisplayFallbackOnce}. */
103834
104629
  loggedDisplayFallbacks = /* @__PURE__ */ new Set();
@@ -103837,11 +104632,12 @@ var EventMediaDispatcher = class {
103837
104632
  constructor(deps) {
103838
104633
  this.deps = deps;
103839
104634
  this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
104635
+ this.sharedNativeFullJpeg = createSharedFrameResolver(async (handle) => this.deps.getNativeFullFrameJpeg ? this.deps.getNativeFullFrameJpeg(handle, SNAPSHOT_MAX_WIDTH) : null);
103840
104636
  }
103841
104637
  /**
103842
104638
  * Resolve the ≤640 detection frame by handle NOW — in the caller's LIVE frame
103843
- * window — and COPY its pixel data (`Buffer.from`) so ring recycling can never
103844
- * touch it. This is the seam the per-frame batch dispatch uses to pin the
104639
+ * window — and hold the privately owned pixel buffer the fetch returned (no
104640
+ * copy — D867), so ring recycling can never touch it. This is the seam the per-frame batch dispatch uses to pin the
103845
104641
  * frame BEFORE queueing on the CaptureScheduler: since S3 the batch is one
103846
104642
  * scheduler request against a 3-slot device lane and can start seconds late,
103847
104643
  * by which time the handle's ring slot is usually recycled and the by-handle
@@ -103869,10 +104665,7 @@ var EventMediaDispatcher = class {
103869
104665
  this.warnPinnedFrameMiss(deviceId, frameHandle, "recycled-before-pin");
103870
104666
  return null;
103871
104667
  }
103872
- return {
103873
- ...decoded,
103874
- data: Buffer.from(decoded.data)
103875
- };
104668
+ return decoded;
103876
104669
  } catch (err) {
103877
104670
  this.warnPinnedFrameMiss(deviceId, frameHandle, "threw", String(err));
103878
104671
  return null;
@@ -103945,7 +104738,7 @@ var EventMediaDispatcher = class {
103945
104738
  this.warnBatchLost("resolved frame is not RGB", input, { format: decoded.format });
103946
104739
  return empty;
103947
104740
  }
103948
- const frameData = input.pinnedFrame !== void 0 ? decoded.data : Buffer.from(decoded.data);
104741
+ const frameData = decoded.data;
103949
104742
  const fw = decoded.width;
103950
104743
  const fh = decoded.height;
103951
104744
  if (isUniformRgbFrame(frameData, fw, fh)) {
@@ -104273,9 +105066,11 @@ var EventMediaDispatcher = class {
104273
105066
  */
104274
105067
  async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
104275
105068
  let nf = null;
105069
+ let nfJpeg = null;
104276
105070
  let fetchThrew = false;
104277
105071
  try {
104278
- nf = await this.sharedNativeFullFrame(frameHandle);
105072
+ if (this.deps.getNativeFullFrameJpeg) nfJpeg = await this.sharedNativeFullJpeg(frameHandle);
105073
+ else nf = await this.sharedNativeFullFrame(frameHandle);
104279
105074
  } catch (err) {
104280
105075
  fetchThrew = true;
104281
105076
  this.deps.logger.warn("native full frame fetch threw — 640 boxed fallback", {
@@ -104288,6 +105083,15 @@ var EventMediaDispatcher = class {
104288
105083
  });
104289
105084
  }
104290
105085
  try {
105086
+ if (nfJpeg && nfJpeg.width > 0 && nfJpeg.height > 0) {
105087
+ const box = scaleBox(bbox, fw, fh, nfJpeg.width, nfJpeg.height, label);
105088
+ const opts = { quality: MEDIA_QUALITY };
105089
+ const [boxed, clean] = await Promise.all([drawBoxedJpeg(nfJpeg.jpeg, nfJpeg.width, nfJpeg.height, [box], opts), drawBoxedJpeg(nfJpeg.jpeg, nfJpeg.width, nfJpeg.height, [], opts)]);
105090
+ return {
105091
+ boxed,
105092
+ clean
105093
+ };
105094
+ }
104291
105095
  if (nf && nf.format === "rgb" && nf.width > 0 && nf.height > 0) {
104292
105096
  const box = scaleBox(bbox, fw, fh, nf.width, nf.height, label);
104293
105097
  const opts = { quality: MEDIA_QUALITY };
@@ -104736,6 +105540,17 @@ Object.freeze({
104736
105540
  "page": 1,
104737
105541
  "pageSize": 50
104738
105542
  } },
105543
+ "device-credentials": { "listDeviceCredentials": { "includeRevoked": false } },
105544
+ "device-history": {
105545
+ "query": {
105546
+ "resolution": "auto",
105547
+ "maxPoints": 1e3
105548
+ },
105549
+ "queryNode": {
105550
+ "resolution": "auto",
105551
+ "maxPoints": 1e3
105552
+ }
105553
+ },
104739
105554
  "device-manager": { "adoptionListCandidates": {
104740
105555
  "page": 1,
104741
105556
  "pageSize": 50
@@ -104988,6 +105803,7 @@ Object.freeze({
104988
105803
  "device-export": ["listExposedDevices", "listSupportedDeviceKinds"],
104989
105804
  "device-extension": ["listSlots", "listSources"],
104990
105805
  "device-provider": ["discoverDevices", "getDevices"],
105806
+ "device-removal-hook": ["listDeviceData"],
104991
105807
  "failure-contribution": ["list"],
104992
105808
  "llm": [
104993
105809
  "getDefaults",
@@ -111560,6 +112376,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
111560
112376
  */
111561
112377
  faceAlignmentJobs = null;
111562
112378
  faceGalleryPruneTimer = null;
112379
+ /** D866: the whole-archive rescore job + its change-gated hourly scheduler. */
112380
+ faceRescoreRuntime = null;
112381
+ faceRescoreState = null;
112382
+ faceRescoreTimer = null;
111563
112383
  /**
111564
112384
  * Analysed photographs waiting to be named, held between `photo.analyze` and
111565
112385
  * `photo.enroll`. Bounded and expiring — the value held is a whole
@@ -112501,13 +113321,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
112501
113321
  if (!this._keyFrameThumbnailDerive) {
112502
113322
  const deriveKeyFrameThumbnailJpeg = createKeyFrameCrop({
112503
113323
  getMedia: async (mediaKey) => {
112504
- const m = await this.mediaStore?.getById(mediaKey);
113324
+ const m = await this.mediaStore?.getBytesById(mediaKey);
112505
113325
  return m ? {
112506
- base64: m.base64,
113326
+ bytes: m.bytes,
112507
113327
  timestamp: m.timestamp
112508
113328
  } : null;
112509
113329
  },
112510
- decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
113330
+ decodeJpegToRgb: (jpeg) => decodeJpegToRgb(jpeg),
112511
113331
  cropRegionToJpeg: (bytes, w, h, norm) => cropWideCentralSquareFromRgb(bytes, w, h, norm),
112512
113332
  maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
112513
113333
  minSubjectIou: KEYFRAME_CROP_MIN_SUBJECT_IOU,
@@ -113200,6 +114020,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113200
114020
  getRemoteFrame: transport.getRemoteFrame,
113201
114021
  getNativeCropJpeg: transport.getNativeCropJpeg,
113202
114022
  getNativeFullFrameRgb: transport.getNativeFullFrameRgb,
114023
+ getNativeFullFrameJpeg: transport.getNativeFullFrameJpeg,
113203
114024
  getParkedSubjectSlab: (deviceId, trackId) => this.fetchParkedSubjectSlab(deviceId, trackId),
113204
114025
  mediaStore: stores.mediaStore,
113205
114026
  logger: logger.child("EventMediaDispatcher"),
@@ -113527,8 +114348,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113527
114348
  NotificationCenter.declare(api.settingsStore),
113528
114349
  ViewerSettingsSnapshotStore.declare(api.settingsStore),
113529
114350
  declareMigrationMarkers(api.settingsStore),
113530
- SweepCursorStore.declare(api.settingsStore)
114351
+ SweepCursorStore.declare(api.settingsStore),
114352
+ FaceRescoreStateStore.declare(api.settingsStore)
113531
114353
  ]);
114354
+ this.faceRescoreState = new FaceRescoreStateStore({
114355
+ store: api.settingsStore,
114356
+ logger: this.ctx.logger.child("FaceRescoreState")
114357
+ });
113532
114358
  this.sweepCursors = new SweepCursorStore({
113533
114359
  store: api.settingsStore,
113534
114360
  logger: this.ctx.logger.child("SweepCursors")
@@ -113761,8 +114587,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113761
114587
  writeGate: this.mediaWriteGate,
113762
114588
  measureChroma: (bytes) => chromaSpread(bytes),
113763
114589
  readSource: async (mediaKey) => {
113764
- const file = await mediaStore.getById(mediaKey);
113765
- return file === null ? null : Buffer.from(file.base64, "base64");
114590
+ const file = await mediaStore.getBytesById(mediaKey);
114591
+ return file === null ? null : file.bytes;
113766
114592
  }
113767
114593
  });
113768
114594
  const retrainAnnotations = new RetrainAnnotationStore({
@@ -113791,8 +114617,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113791
114617
  media: {
113792
114618
  listTrackMedia: (trackId) => mediaStore.listInfoByOwner("track", trackId),
113793
114619
  readBlob: async (mediaKey) => {
113794
- const file = await mediaStore.getById(mediaKey);
113795
- return file === null ? null : Buffer.from(file.base64, "base64");
114620
+ const file = await mediaStore.getBytesById(mediaKey);
114621
+ return file === null ? null : file.bytes;
113796
114622
  }
113797
114623
  },
113798
114624
  frames: retrainFrames,
@@ -114131,13 +114957,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114131
114957
  const plateMediaStore = stores.mediaStore;
114132
114958
  const deriveCropFromKeyFrame = createKeyFrameCrop({
114133
114959
  getMedia: async (mediaKey) => {
114134
- const m = await plateMediaStore.getById(mediaKey);
114960
+ const m = await plateMediaStore.getBytesById(mediaKey);
114135
114961
  return m ? {
114136
- base64: m.base64,
114962
+ bytes: m.bytes,
114137
114963
  timestamp: m.timestamp
114138
114964
  } : null;
114139
114965
  },
114140
- decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
114966
+ decodeJpegToRgb: (jpeg) => decodeJpegToRgb(jpeg),
114141
114967
  cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
114142
114968
  maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
114143
114969
  minSubjectIou: KEYFRAME_CROP_MIN_SUBJECT_IOU,
@@ -114680,9 +115506,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114680
115506
  }));
114681
115507
  },
114682
115508
  readMedia: async (mediaId) => {
114683
- const file = await stores.mediaStore.getById(mediaId);
115509
+ const file = await stores.mediaStore.getBytesById(mediaId);
114684
115510
  if (file === null) return null;
114685
- return Buffer.from(file.base64, "base64");
115511
+ return file.bytes;
114686
115512
  },
114687
115513
  locationDescriptions: async () => {
114688
115514
  const rows = await api.deviceManager.listLocationDescriptions.query();
@@ -114935,8 +115761,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114935
115761
  const handler = createTrainingExportHandler({
114936
115762
  buildPlan: (deviceIds) => this.buildTrainingExportPlan(deviceIds),
114937
115763
  readBlob: async (mediaId) => {
114938
- const file = await (this.mediaStore?.getById(mediaId) ?? Promise.resolve(null));
114939
- return file === null ? null : Buffer.from(file.base64, "base64");
115764
+ const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
115765
+ return file === null ? null : file.bytes;
114940
115766
  },
114941
115767
  logger: this.ctx.logger,
114942
115768
  now: () => Date.now()
@@ -115406,8 +116232,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115406
116232
  const handler = createDebugExportHandler({
115407
116233
  buildPlan: (deviceIds) => buildDebugBundlePlan(this.buildDebugServiceDeps(), deviceIds),
115408
116234
  readMedia: async (key) => {
115409
- const file = await this.mediaStore?.getById(key);
115410
- return file === null || file === void 0 ? null : Buffer.from(file.base64, "base64");
116235
+ const file = await this.mediaStore?.getBytesById(key);
116236
+ return file === null || file === void 0 ? null : file.bytes;
115411
116237
  },
115412
116238
  logger: this.ctx.logger.child("debug-export"),
115413
116239
  now: () => Date.now()
@@ -115802,7 +116628,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115802
116628
  trackId: o.trackId,
115803
116629
  deviceId: o.deviceId
115804
116630
  })),
115805
- externalNonBufferMb: Math.max(0, Math.round((mem.external - mem.arrayBuffers) / 1048576))
116631
+ externalNonBufferMb: Math.max(0, Math.round((mem.external - mem.arrayBuffers) / 1048576)),
116632
+ mapSizes: {
116633
+ processors: this.processors.size,
116634
+ processorInputs: this.processorInputs.size,
116635
+ lastActiveTrackIds: this.lastActiveTrackIds.size,
116636
+ shortMotionlessCells: this.shortMotionlessCells.size,
116637
+ proxies: this.proxies.size,
116638
+ lastRunnerNodeByDevice: this.lastRunnerNodeByDevice.size,
116639
+ pendingConfirmationLooks: this.pendingConfirmationLooks.size,
116640
+ audioEpisodeTracks: this.audioEpisodeTracks.size,
116641
+ detailStepsByDevice: this.detailStepsByDevice.size
116642
+ }
115806
116643
  } });
115807
116644
  }
115808
116645
  /** Residents neither active in the TrackStore nor carried by the tracker
@@ -115825,6 +116662,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115825
116662
  this.faceReembedAutoTimer = setInterval(() => {
115826
116663
  this.maybeAutoReembedFaces().then(() => this.faceAlignmentJobs?.maybeAutoRunPhotoPass());
115827
116664
  }, FACE_REEMBED_AUTO_CHECK_MS);
116665
+ this.faceRescoreTimer = setInterval(() => {
116666
+ this.faceRescoreRuntimeOrNull()?.scheduler.tick().catch((err) => {
116667
+ this.ctx.logger.warn("face rescore tick failed", { meta: { error: String(err) } });
116668
+ });
116669
+ }, FACE_RESCORE_TICK_MS);
115828
116670
  this.faceGalleryPruneTimer = setInterval(() => {
115829
116671
  this.faceAlignmentJobs?.prune();
115830
116672
  }, FACE_GALLERY_PRUNE_INTERVAL_MS);
@@ -116237,9 +117079,35 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116237
117079
  ...this.sceneProvider !== null ? [{
116238
117080
  capability: sceneMonitorCapability,
116239
117081
  provider: this.sceneProvider
116240
- }] : []
117082
+ }] : [],
117083
+ {
117084
+ capability: deviceRemovalHookCapability,
117085
+ provider: this.removalHook
117086
+ }
116241
117087
  ];
116242
117088
  }
117089
+ /** The `device-removal-hook` provider (D865): told by device-manager, caught up from its tombstones. */
117090
+ get removalHook() {
117091
+ this.removalHookImpl ??= createAnalyticsRemovalHook({
117092
+ logger: this.ctx.logger,
117093
+ deleteDeviceEvents: (input) => this.deleteDeviceEvents(input),
117094
+ wipeAllAnalytics: (input) => this.wipeAllAnalytics(input),
117095
+ evictSensorEvents: async (deviceId, cutoffMs) => await this.sensorEventStore?.evictBefore(deviceId, cutoffMs) ?? 0,
117096
+ eventCountsByDevice: async () => await this.eventStore?.countByDevice() ?? /* @__PURE__ */ new Map(),
117097
+ trackDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
117098
+ });
117099
+ return this.removalHookImpl;
117100
+ }
117101
+ removalHookImpl = null;
117102
+ /** Catch up on deletions missed while this addon was down (D865) — tombstones only. */
117103
+ async onHubReachable() {
117104
+ await super.onHubReachable();
117105
+ this.ctx.addDisposer(armRemovalReconcile({
117106
+ api: this.ctx.api,
117107
+ logger: this.ctx.logger,
117108
+ remove: (input) => this.removalHook.onDeviceRemoved(input)
117109
+ }));
117110
+ }
116243
117111
  /**
116244
117112
  * `BaseAddon.updateGlobalSettings` calls this after the write lands —
116245
117113
  * gated to the LOCAL node's own write (a sibling-node write returns before
@@ -116301,6 +117169,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116301
117169
  clearInterval(this.faceGalleryPruneTimer);
116302
117170
  this.faceGalleryPruneTimer = null;
116303
117171
  }
117172
+ if (this.faceRescoreTimer) {
117173
+ clearInterval(this.faceRescoreTimer);
117174
+ this.faceRescoreTimer = null;
117175
+ }
117176
+ this.faceRescoreRuntime?.job.stop();
116304
117177
  if (this.orphanAuditTimer) {
116305
117178
  clearInterval(this.orphanAuditTimer);
116306
117179
  this.orphanAuditTimer = null;
@@ -122246,8 +123119,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
122246
123119
  loadAlignedCrop: async (sample) => {
122247
123120
  if (sample.mediaId === void 0) return null;
122248
123121
  try {
122249
- const file = await mediaStore.getById(sample.mediaId);
122250
- return file === null ? null : Buffer.from(file.base64, "base64");
123122
+ const file = await mediaStore.getBytesById(sample.mediaId);
123123
+ return file === null ? null : file.bytes;
122251
123124
  } catch (err) {
122252
123125
  this.ctx.logger.warn("face re-embed: aligned crop read threw", { meta: {
122253
123126
  sampleId: sample.sampleId,
@@ -122311,8 +123184,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
122311
123184
  loadAlignedCrop: async (sample) => {
122312
123185
  if (sample.mediaId === void 0) return null;
122313
123186
  try {
122314
- const file = await mediaStore.getById(sample.mediaId);
122315
- return file === null ? null : Buffer.from(file.base64, "base64");
123187
+ const file = await mediaStore.getBytesById(sample.mediaId);
123188
+ return file === null ? null : file.bytes;
122316
123189
  } catch (err) {
122317
123190
  this.ctx.logger.warn("face sample re-embed: aligned crop read threw", { meta: {
122318
123191
  sampleId: sample.sampleId,
@@ -122372,8 +123245,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
122372
123245
  loadAlignedCrop: async (sample) => {
122373
123246
  if (sample.mediaId === void 0) return null;
122374
123247
  try {
122375
- const file = await mediaStore.getById(sample.mediaId);
122376
- return file === null ? null : Buffer.from(file.base64, "base64");
123248
+ const file = await mediaStore.getBytesById(sample.mediaId);
123249
+ return file === null ? null : file.bytes;
122377
123250
  } catch (err) {
122378
123251
  this.ctx.logger.warn("face embedding audit: aligned crop read threw", { meta: {
122379
123252
  sampleId: sample.sampleId,
@@ -122519,9 +123392,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
122519
123392
  };
122520
123393
  if (!api || !faceStore || !mediaStore) return refuse("failed", "the addon API or the face/media stores are not ready");
122521
123394
  if (face.mediaId === void 0) return refuse("no-pixels", "this face kept no crop — there is nothing to re-embed from");
122522
- const file = await mediaStore.getById(face.mediaId);
123395
+ const file = await mediaStore.getBytesById(face.mediaId);
122523
123396
  if (file === null) return refuse("no-pixels", `the crop blob ${face.mediaId} is gone`);
122524
- const raster = await decodeJpegToRgb(file.base64);
123397
+ const raster = await decodeJpegToRgb(file.bytes);
122525
123398
  const templatePx = PipelineAnalyticsAddon.ARCFACE_TEMPLATE_PX;
122526
123399
  if (raster.width !== templatePx || raster.height !== templatePx) return refuse("not-a-template", `the stored crop is ${raster.width}×${raster.height}, not the ${templatePx}² aligned template — this face was captured without landmarks, so re-embedding it would produce a plausible and meaningless vector`);
122527
123400
  const outcome = await new FaceReembedEmbedder({
@@ -122534,7 +123407,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
122534
123407
  modelId: face.embeddingModelId ?? LEGACY_MODEL_ID,
122535
123408
  dim: face.embedding.length,
122536
123409
  mediaId: face.mediaId
122537
- }, Buffer.from(file.base64, "base64"));
123410
+ }, file.bytes);
122538
123411
  if (outcome.kind === "no-capable-node") return refuse("no-capable-node", outcome.reason);
122539
123412
  if (outcome.kind === "failed") return refuse("failed", outcome.reason);
122540
123413
  if (!await faceStore.restampEmbedding({
@@ -122676,7 +123549,34 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
122676
123549
  };
122677
123550
  }
122678
123551
  buildFaceRescoreActionHandlers() {
122679
- return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
123552
+ return {
123553
+ "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input),
123554
+ "face.rescoreAll": () => Promise.resolve(this.requireFaceRescoreRuntime("face.rescoreAll").job.start()),
123555
+ "face.rescoreStatus": () => Promise.resolve(this.requireFaceRescoreRuntime("face.rescoreStatus").job.status())
123556
+ };
123557
+ }
123558
+ /** The whole-archive rescore runtime, built once the stores exist; else null. */
123559
+ faceRescoreRuntimeOrNull() {
123560
+ if (this.faceRescoreRuntime) return this.faceRescoreRuntime;
123561
+ const api = this.ctx.api;
123562
+ const faceStore = this.faceStore;
123563
+ const identityStore = this.identityStore;
123564
+ const state = this.faceRescoreState;
123565
+ if (!api || !faceStore || !identityStore || !state) return null;
123566
+ this.faceRescoreRuntime = buildFaceRescoreRuntime({
123567
+ api,
123568
+ faceStore,
123569
+ identityStore,
123570
+ state,
123571
+ logger: this.ctx.logger.child("FaceRescore"),
123572
+ paramsForDevice: async (deviceId) => this.faceRescoreParams(await this.resolveDeviceFaceSettings(deviceId))
123573
+ }, Date.now());
123574
+ return this.faceRescoreRuntime;
123575
+ }
123576
+ requireFaceRescoreRuntime(action) {
123577
+ const runtime = this.faceRescoreRuntimeOrNull();
123578
+ if (!runtime) throw new Error(`${action}: the face stores are not ready`);
123579
+ return runtime;
122680
123580
  }
122681
123581
  buildStillnessCalibrationActionHandlers() {
122682
123582
  return { "stillness.calibrate": (input) => this.calibrateStillnessForDevice(input) };
@@ -124295,10 +125195,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
124295
125195
  return this.applyThumbVariant(base, variant);
124296
125196
  }
124297
125197
  async readMediaById(mediaId) {
124298
- const file = await (this.mediaStore?.getById(mediaId) ?? Promise.resolve(null));
125198
+ const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
124299
125199
  if (!file) return null;
124300
125200
  return {
124301
- bytes: Buffer.from(file.base64, "base64"),
125201
+ bytes: file.bytes,
124302
125202
  mediaId: file.mediaId,
124303
125203
  kind: file.kind,
124304
125204
  deviceId: file.deviceId
@@ -124353,9 +125253,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
124353
125253
  const listInfo = (ownerKind, id, deviceId) => this.mediaStore?.listInfoByOwner(ownerKind, id, deviceId) ?? Promise.resolve([]);
124354
125254
  const getTrackIdForEvent = (eventId) => isEventOwnerType(ownerType) ? this.eventStore?.getTrackIdForEvent(eventId, ownerType) ?? Promise.resolve(null) : Promise.resolve(null);
124355
125255
  const readBlob = async (mediaId) => {
124356
- const file = await (this.mediaStore?.getById(mediaId) ?? Promise.resolve(null));
125256
+ const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
124357
125257
  return file === null ? null : {
124358
- bytes: Buffer.from(file.base64, "base64"),
125258
+ bytes: file.bytes,
124359
125259
  mediaId: file.mediaId,
124360
125260
  kind: file.kind,
124361
125261
  deviceId: file.deviceId