@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
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-E4CGZ8NR.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-C3AUTkBR.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -45959,6 +45959,22 @@ var MediaStore = class MediaStore {
45959
45959
  * happens HERE and nowhere else.
45960
45960
  */
45961
45961
  async getById(mediaId) {
45962
+ const found = await this.getBytesById(mediaId);
45963
+ if (found === null) return null;
45964
+ const { bytes, ...rest } = found;
45965
+ return {
45966
+ ...rest,
45967
+ base64: bytes.toString("base64")
45968
+ };
45969
+ }
45970
+ /**
45971
+ * {@link getById} WITHOUT the base64 round trip: the blob as the storage
45972
+ * engine read it. Every in-process reader (key-frame derive, retrain and
45973
+ * export readers, face re-embed) used to take `base64` and decode it straight
45974
+ * back — a 0.4–0.8 MB string plus a second buffer per read (D867). `getById`
45975
+ * stays for the callers that cross a wire.
45976
+ */
45977
+ async getBytesById(mediaId) {
45962
45978
  const row = await this.store.get.query({
45963
45979
  collection: MEDIA_COLLECTION,
45964
45980
  key: String(mediaId)
@@ -45973,10 +45989,10 @@ var MediaStore = class MediaStore {
45973
45989
  return {
45974
45990
  mediaId,
45975
45991
  kind,
45976
- base64: (await this.storage.read({
45992
+ bytes: await this.storage.read({
45977
45993
  location: mediaRowLocation(data),
45978
45994
  relativePath: path
45979
- })).toString("base64"),
45995
+ }),
45980
45996
  sizeBytes,
45981
45997
  timestamp,
45982
45998
  deviceId: Number(data["deviceId"])
@@ -47709,7 +47725,7 @@ function previewBlurSigma(frameWidth) {
47709
47725
  }
47710
47726
  /** Break a word too long for one line into line-width pieces. Without this a
47711
47727
  * single 200-character token wraps to nothing and overflows both edges. */
47712
- function chunk(word, size) {
47728
+ function chunk$1(word, size) {
47713
47729
  const pieces = [];
47714
47730
  for (let i = 0; i < word.length; i += size) pieces.push(word.slice(i, i + size));
47715
47731
  return pieces;
@@ -47717,7 +47733,7 @@ function chunk(word, size) {
47717
47733
  /** Greedy word wrap at a character budget — the only measurement available,
47718
47734
  * since librsvg lays the text out long after this string is written. */
47719
47735
  function wrapLines(text, maxChars) {
47720
- return text.split(" ").filter((w) => w.length > 0).reduce((lines, word) => (word.length > maxChars ? chunk(word, maxChars) : [word]).reduce((acc, piece) => {
47736
+ return text.split(" ").filter((w) => w.length > 0).reduce((lines, word) => (word.length > maxChars ? chunk$1(word, maxChars) : [word]).reduce((acc, piece) => {
47721
47737
  const last = acc.at(-1);
47722
47738
  if (last === void 0) return [piece];
47723
47739
  const merged = `${last} ${piece}`;
@@ -52583,6 +52599,17 @@ function choosePhotoFace(input) {
52583
52599
  //#endregion
52584
52600
  //#region src/pipeline-analytics/pipeline/face-matcher.ts
52585
52601
  /**
52602
+ * The scoring view of a sample's vector. A `Float32Array` is returned as is —
52603
+ * no per-probe, per-sample allocation (~3200 x 2 KB = 6.5 MB per probe on the
52604
+ * live gallery); a plain array is wrapped, exactly as before. The scores are
52605
+ * bit-identical either way: wrapping a `number[]` and storing a `Float32Array`
52606
+ * produce the same floats.
52607
+ */
52608
+ function galleryVector(sample) {
52609
+ const e = sample.embedding;
52610
+ return e instanceof Float32Array ? e : new Float32Array(e);
52611
+ }
52612
+ /**
52586
52613
  * Count enrolled samples per identity for probes of a matching model+dimension.
52587
52614
  * Only identities meeting `minIdentitySamples` are eligible auto-match targets.
52588
52615
  */
@@ -52616,7 +52643,7 @@ function rankIdentities(probe, gallery, minIdentitySamples) {
52616
52643
  for (const s of gallery) {
52617
52644
  if (s.modelId !== probe.modelId) continue;
52618
52645
  if (s.embedding.length !== probe.embedding.length) continue;
52619
- const score = require_clip_model_registry.cosineSimilarity(probeVec, new Float32Array(s.embedding));
52646
+ const score = require_clip_model_registry.cosineSimilarity(probeVec, galleryVector(s));
52620
52647
  const prev = bestByIdentity.get(s.identityId);
52621
52648
  if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
52622
52649
  countByIdentity.set(s.identityId, (countByIdentity.get(s.identityId) ?? 0) + 1);
@@ -52685,7 +52712,7 @@ function enrollmentPromiscuity(embedding, modelId, targetIdentityId, gallery, bo
52685
52712
  if (s.identityId === targetIdentityId) continue;
52686
52713
  if (s.modelId !== modelId) continue;
52687
52714
  if (s.embedding.length !== embedding.length) continue;
52688
- const cosine = require_clip_model_registry.cosineSimilarity(probeVec, new Float32Array(s.embedding));
52715
+ const cosine = require_clip_model_registry.cosineSimilarity(probeVec, galleryVector(s));
52689
52716
  if (cosine >= bound && (worst === null || cosine > worst.cosine)) worst = {
52690
52717
  identityId: s.identityId,
52691
52718
  cosine
@@ -52703,7 +52730,7 @@ function assignUniquePerFrame(candidates, gallery, opts) {
52703
52730
  if (s.modelId !== c.modelId) continue;
52704
52731
  if (s.embedding.length !== c.embedding.length) continue;
52705
52732
  if (!eligible.has(s.identityId)) continue;
52706
- const score = require_clip_model_registry.cosineSimilarity(probeVec, new Float32Array(s.embedding));
52733
+ const score = require_clip_model_registry.cosineSimilarity(probeVec, galleryVector(s));
52707
52734
  const prev = bestByIdentity.get(s.identityId);
52708
52735
  if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
52709
52736
  }
@@ -54933,6 +54960,20 @@ var EmbeddingVectorIndex = class {
54933
54960
  * carries no stamp, which no current writer produces.
54934
54961
  */
54935
54962
  async loadStamped(ids) {
54963
+ const f32 = await this.loadStampedFloat32(ids);
54964
+ return new Map([...f32].map(([id, row]) => [id, {
54965
+ embedding: Array.from(row.embedding),
54966
+ modelId: row.modelId
54967
+ }]));
54968
+ }
54969
+ /**
54970
+ * {@link loadStamped} without widening to `number[]`: the vector as the
54971
+ * `Float32Array` it was stored as, in a buffer of its own (a view over the
54972
+ * base64 decode would pin a pooled slab). For the matching gallery, which is
54973
+ * scored against on every probe and used to be re-wrapped per sample per
54974
+ * probe (D867). Same read-failure contract as {@link loadStamped}.
54975
+ */
54976
+ async loadStampedFloat32(ids) {
54936
54977
  const out = /* @__PURE__ */ new Map();
54937
54978
  if (ids.length === 0) return out;
54938
54979
  try {
@@ -54946,7 +54987,7 @@ var EmbeddingVectorIndex = class {
54946
54987
  const view = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
54947
54988
  const modelId = item.metadata["modelId"];
54948
54989
  out.set(item.id, {
54949
- embedding: Array.from(view),
54990
+ embedding: new Float32Array(view),
54950
54991
  modelId: typeof modelId === "string" && modelId.length > 0 ? modelId : null
54951
54992
  });
54952
54993
  }
@@ -55493,6 +55534,15 @@ function rowToFace(id, raw) {
55493
55534
  sourceFrameLandmarks: data.sourceFrameLandmarks ?? void 0
55494
55535
  };
55495
55536
  }
55537
+ /** The columns a whole-archive rescore reads per face (no vector, no crop). */
55538
+ var RESCORE_COLUMNS = [
55539
+ "deviceId",
55540
+ "assigned",
55541
+ "recognizedIdentityId",
55542
+ "embeddingModelId",
55543
+ "suggestedIdentityId",
55544
+ "suggestedMatchScore"
55545
+ ];
55496
55546
  var FaceStore = class {
55497
55547
  store;
55498
55548
  vectors;
@@ -56311,6 +56361,107 @@ var FaceStore = class {
56311
56361
  return out;
56312
56362
  }
56313
56363
  /**
56364
+ * The ids a whole-archive rescore will visit, SNAPSHOT at the start of the
56365
+ * run — the same population as {@link listPendingFaces} (unassigned AND
56366
+ * carrying no identity), so the two never disagree about "pending". Ids only:
56367
+ * the job pages the rows themselves ({@link loadRescoreBatch}) so the
56368
+ * settings thread never serves one big read of vectors.
56369
+ */
56370
+ async listPendingFaceIds() {
56371
+ return (await this.listPendingFaces()).map((r) => r.faceId);
56372
+ }
56373
+ /**
56374
+ * One page of a whole-archive rescore: the rows of `ids` that are STILL
56375
+ * pending (the operator may have assigned one since the snapshot), each with
56376
+ * its vector. Ids whose vector is in neither home are counted, not dropped
56377
+ * silently — "no vector" must not read as "scored, no match".
56378
+ */
56379
+ async loadRescoreBatch(ids) {
56380
+ if (ids.length === 0) return {
56381
+ rows: [],
56382
+ noVector: 0,
56383
+ gone: 0
56384
+ };
56385
+ const records = await this.store.query.query({
56386
+ collection: FACES_COLLECTION,
56387
+ columns: RESCORE_COLUMNS,
56388
+ filter: {
56389
+ whereIn: { id: [...ids] },
56390
+ limit: ids.length
56391
+ }
56392
+ });
56393
+ const stillPending = records.filter((r) => !r.data["assigned"] && (r.data["recognizedIdentityId"] ?? null) === null);
56394
+ const vectors = await this.loadEmbeddings(stillPending.map((r) => r.id));
56395
+ const rows = [];
56396
+ let noVector = 0;
56397
+ for (const r of stillPending) {
56398
+ const embedding = vectors.get(r.id);
56399
+ if (embedding === void 0 || embedding.length === 0) {
56400
+ noVector += 1;
56401
+ continue;
56402
+ }
56403
+ const suggestedIdentityId = r.data["suggestedIdentityId"];
56404
+ const suggestedMatchScore = r.data["suggestedMatchScore"];
56405
+ const embeddingModelId = r.data["embeddingModelId"];
56406
+ rows.push({
56407
+ id: r.id,
56408
+ deviceId: Number(r.data["deviceId"] ?? -1),
56409
+ embedding,
56410
+ modelId: typeof embeddingModelId === "string" ? embeddingModelId : LEGACY_FACE_MODEL_ID,
56411
+ ...typeof suggestedIdentityId === "string" ? { suggestedIdentityId } : {},
56412
+ ...typeof suggestedMatchScore === "number" ? { suggestedMatchScore } : {}
56413
+ });
56414
+ }
56415
+ return {
56416
+ rows,
56417
+ noVector,
56418
+ gone: ids.length - records.length
56419
+ };
56420
+ }
56421
+ /**
56422
+ * Write a batch of suggestion changes in ONE transaction (`transact`, all or
56423
+ * nothing). THROWS: a batch that did not land must not be counted as
56424
+ * applied. Touches only the two suggestion columns — never an assignment.
56425
+ */
56426
+ async applySuggestionBatch(updates) {
56427
+ if (updates.length === 0) return;
56428
+ await this.store.transact.mutate({ ops: updates.map((u) => ({
56429
+ op: "update",
56430
+ collection: FACES_COLLECTION,
56431
+ id: u.faceId,
56432
+ data: u.suggestion === null ? {
56433
+ suggestedIdentityId: null,
56434
+ suggestedMatchScore: null
56435
+ } : {
56436
+ suggestedIdentityId: u.suggestion.identityId,
56437
+ suggestedMatchScore: u.suggestion.score
56438
+ }
56439
+ })) });
56440
+ }
56441
+ /**
56442
+ * The unassigned-face set marker for change detection: how many unassigned
56443
+ * rows exist and the newest one's timestamp. ONE `COUNT(*)`+`MAX` statement
56444
+ * over `assigned = 0`, answered from `idx_faces_assigned_ts (assigned,
56445
+ * timestamp)` alone — covering, no table row fetched (proved by EXPLAIN in
56446
+ * `face-rescore-marker-plan-over-real-sqlite.spec.ts`, D447). A face
56447
+ * arriving moves the max, a face leaving or being assigned moves the count.
56448
+ */
56449
+ async pendingMarker() {
56450
+ const result = await this.store.aggregate.query({
56451
+ collection: FACES_COLLECTION,
56452
+ fields: [{
56453
+ as: "maxTimestamp",
56454
+ field: "timestamp",
56455
+ op: "max"
56456
+ }],
56457
+ filter: { where: { assigned: false } }
56458
+ });
56459
+ return {
56460
+ count: result.count,
56461
+ maxTimestamp: result.values["maxTimestamp"] ?? null
56462
+ };
56463
+ }
56464
+ /**
56314
56465
  * A face's enrolled sample is going away (D643 prune, or the operator's
56315
56466
  * Remove): forget the sample, and the crop too when it is the blob leaving
56316
56467
  * with it. The face itself — and who it was assigned to — stays. THROWS on
@@ -57258,10 +57409,15 @@ var FaceGalleryProvider = class {
57258
57409
  * real workflow is the exact case this breaks: assign ten faces to build a
57259
57410
  * person up, then open an older unassigned track and ask "does it match NOW?"
57260
57411
  * — and read a number that was decided before any of those ten existed. There
57261
- * is no background job that refreshes it and there should not be one (the
57412
+ * is no refresh on every assign, and there should not be one (the
57262
57413
  * gallery changes on every assign; re-scoring every buffered face each time is
57263
- * work nobody asked for). The honest fix is to make the question askable on
57264
- * demand. That is this module, and that is the button.
57414
+ * work nobody asked for). Two answers exist instead. On demand: this module,
57415
+ * the single-face button, and the whole-archive button (`face-rescore-job.ts`).
57416
+ * And D866 added a SLOW, change-gated job (`face-rescore-scheduler.ts`): hourly
57417
+ * by default, and only when a fingerprint of the gallery and of the unassigned
57418
+ * faces differs from the last completed run's — so an idle archive costs two
57419
+ * narrow reads, not a rescore. That supersedes this header's earlier "there is
57420
+ * no background job" statement; the objection to per-assign refresh stands.
57265
57421
  *
57266
57422
  * ## What it does NOT do
57267
57423
  *
@@ -57415,6 +57571,47 @@ async function persistRescoreResult(faceId, report, ports) {
57415
57571
  return candidate !== null ? "written" : "cleared";
57416
57572
  }
57417
57573
  //#endregion
57574
+ //#region src/pipeline-analytics/face-rescore-job-status.ts
57575
+ /**
57576
+ * The wire/storage shape of a whole-archive face rescore job (D866) — one Zod
57577
+ * schema for the action output, the persisted last report and the in-process
57578
+ * status, so the three cannot drift.
57579
+ */
57580
+ var FaceRescoreJobStateSchema = require_clip_model_registry._enum([
57581
+ "idle",
57582
+ "running",
57583
+ "completed",
57584
+ "failed"
57585
+ ]);
57586
+ var FaceRescoreJobStatusSchema = require_clip_model_registry.object({
57587
+ /** `null` while nothing ever ran in this process. */
57588
+ jobId: require_clip_model_registry.string().nullable(),
57589
+ state: FaceRescoreJobStateSchema,
57590
+ /** Faces visited so far (rescored or deliberately skipped). */
57591
+ scanned: require_clip_model_registry.number().int(),
57592
+ /** Faces in the snapshot taken at the start; 0 until it is taken. */
57593
+ total: require_clip_model_registry.number().int(),
57594
+ /** newlySuggested + raised + lowered + cleared — rows actually written. */
57595
+ changed: require_clip_model_registry.number().int(),
57596
+ newlySuggested: require_clip_model_registry.number().int(),
57597
+ raised: require_clip_model_registry.number().int(),
57598
+ lowered: require_clip_model_registry.number().int(),
57599
+ cleared: require_clip_model_registry.number().int(),
57600
+ /** Faces in an old feature space: skipped, their suggestion untouched. */
57601
+ modelMismatch: require_clip_model_registry.number().int(),
57602
+ /** Faces with no vector in either home: skipped, counted. */
57603
+ noVector: require_clip_model_registry.number().int(),
57604
+ batches: require_clip_model_registry.number().int(),
57605
+ /** Slowest single batch's STORE time (read + transaction), ms. */
57606
+ maxBatchStoreMs: require_clip_model_registry.number(),
57607
+ durationMs: require_clip_model_registry.number(),
57608
+ galleryFingerprint: require_clip_model_registry.string(),
57609
+ /** Combined fingerprint taken at the start; what the scheduler persists. */
57610
+ fingerprint: require_clip_model_registry.string(),
57611
+ startedAt: require_clip_model_registry.number().nullable(),
57612
+ error: require_clip_model_registry.string().optional()
57613
+ });
57614
+ //#endregion
57418
57615
  //#region src/pipeline-analytics/face-rescore-actions.ts
57419
57616
  /**
57420
57617
  * face-rescore-actions — "score this face against the gallery AS IT IS NOW".
@@ -57532,19 +57729,516 @@ var FaceRescoreResultSchema = require_clip_model_registry.object({
57532
57729
  reembedAvailable: require_clip_model_registry.boolean(),
57533
57730
  reembed: FaceReembedReportSchema
57534
57731
  });
57535
- var faceRescoreActions = require_clip_model_registry.defineCustomActions({
57732
+ var faceRescoreActions = require_clip_model_registry.defineCustomActions({
57733
+ /**
57734
+ * Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
57735
+ * `suggestedMatchScore`) — never an assignment, never automatic; see
57736
+ * `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
57737
+ * `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
57738
+ * also names every enrolled person and how well an arbitrary face matches
57739
+ * them — the gallery's shape, which is not a viewer-level fact.
57740
+ */
57741
+ "face.rescoreTrack": require_clip_model_registry.customAction(FaceRescoreInputSchema, FaceRescoreResultSchema, {
57742
+ kind: "mutation",
57743
+ auth: "admin"
57744
+ }),
57745
+ /**
57746
+ * Whole-archive rescore (D866): EVERY unassigned face against ONE gallery
57747
+ * snapshot, as a paced single-flight job. Returns at once with the running
57748
+ * job's status (a second call while one runs returns THAT job); poll
57749
+ * `face.rescoreStatus` for progress and the final report. Same persistence
57750
+ * rules as `face.rescoreTrack` — suggestions only, never an assignment, never
57751
+ * a `model-mismatch` write — and a weaker or vanished match LOWERS or CLEARS
57752
+ * the stored suggestion (see `face-rescore-job.ts`).
57753
+ */
57754
+ "face.rescoreAll": require_clip_model_registry.customAction(require_clip_model_registry.object({}), FaceRescoreJobStatusSchema, {
57755
+ kind: "mutation",
57756
+ auth: "admin"
57757
+ }),
57758
+ /** Progress of the running whole-archive rescore, else the last report. */
57759
+ "face.rescoreStatus": require_clip_model_registry.customAction(require_clip_model_registry.object({}), FaceRescoreJobStatusSchema, {
57760
+ kind: "query",
57761
+ auth: "admin"
57762
+ })
57763
+ });
57764
+ //#endregion
57765
+ //#region src/pipeline-analytics/face-rescore-job.ts
57766
+ /** Faces per batch: ~200 narrow updates in one transaction is tens of ms. */
57767
+ var RESCORE_BATCH_SIZE = 200;
57768
+ /** Pause after a batch = this × the batch's store time, clamped below. */
57769
+ var RESCORE_PAUSE_FACTOR = 2;
57770
+ var RESCORE_PAUSE_MIN_MS = 25;
57771
+ var RESCORE_PAUSE_MAX_MS = 500;
57772
+ /** Two scores closer than this are the same score (REAL round trip). */
57773
+ var SCORE_EPSILON = 1e-6;
57536
57774
  /**
57537
- * Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
57538
- * `suggestedMatchScore`) — never an assignment, never automatic; see
57539
- * `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
57540
- * `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
57541
- * also names every enrolled person and how well an arbitrary face matches
57542
- * them — the gallery's shape, which is not a viewer-level fact.
57775
+ * How the fresh answer relates to what the row holds. A switch to another
57776
+ * identity counts by its score: higher ⇒ raised, otherwise lowered (a tie with
57777
+ * a different person is `raised` — it is a change and must be written).
57543
57778
  */
57544
- "face.rescoreTrack": require_clip_model_registry.customAction(FaceRescoreInputSchema, FaceRescoreResultSchema, {
57545
- kind: "mutation",
57546
- auth: "admin"
57547
- }) });
57779
+ function classifyRescoreChange(stored, next) {
57780
+ if (stored === null && next === null) return "unchanged";
57781
+ if (stored === null) return "new";
57782
+ if (next === null) return "cleared";
57783
+ const delta = next.score - stored.score;
57784
+ if (next.identityId === stored.identityId && Math.abs(delta) <= SCORE_EPSILON) return "unchanged";
57785
+ return delta >= 0 ? "raised" : "lowered";
57786
+ }
57787
+ function storedSuggestion(row) {
57788
+ return row.suggestedIdentityId === void 0 ? null : {
57789
+ identityId: row.suggestedIdentityId,
57790
+ score: row.suggestedMatchScore ?? 0
57791
+ };
57792
+ }
57793
+ function sha(parts) {
57794
+ return (0, node_crypto.createHash)("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
57795
+ }
57796
+ /**
57797
+ * Pure. The gallery part changes when a sample is added, removed or restamped
57798
+ * to another model, or the cluster model flips; the pending part when a face
57799
+ * arrives, leaves or is assigned. Sorted, so read order cannot move it.
57800
+ */
57801
+ function computeRescoreFingerprint(input) {
57802
+ const lines = input.manifest.map((m) => `${m.sampleId}|${m.identityId}|${m.modelId}`).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
57803
+ const gallery = sha([`cluster:${input.clusterModelId}`, ...lines]);
57804
+ const pending = `${String(input.marker.count)}@${String(input.marker.maxTimestamp ?? "none")}`;
57805
+ return {
57806
+ gallery,
57807
+ pending,
57808
+ combined: sha([gallery, pending])
57809
+ };
57810
+ }
57811
+ var IDLE_STATUS = {
57812
+ jobId: null,
57813
+ state: "idle",
57814
+ scanned: 0,
57815
+ total: 0,
57816
+ changed: 0,
57817
+ newlySuggested: 0,
57818
+ raised: 0,
57819
+ lowered: 0,
57820
+ cleared: 0,
57821
+ modelMismatch: 0,
57822
+ noVector: 0,
57823
+ batches: 0,
57824
+ maxBatchStoreMs: 0,
57825
+ durationMs: 0,
57826
+ galleryFingerprint: "",
57827
+ fingerprint: "",
57828
+ startedAt: null
57829
+ };
57830
+ /** The fingerprint of NOW, plus the cluster model it was taken under. */
57831
+ async function readRescoreFingerprint(ports) {
57832
+ const clusterModelId = await ports.resolveClusterModelId();
57833
+ if (clusterModelId === null) return null;
57834
+ const [manifest, marker] = await Promise.all([ports.galleryManifest(), ports.pendingMarker()]);
57835
+ return {
57836
+ fingerprint: computeRescoreFingerprint({
57837
+ manifest,
57838
+ marker,
57839
+ clusterModelId
57840
+ }),
57841
+ clusterModelId
57842
+ };
57843
+ }
57844
+ function chunk(items, size) {
57845
+ const out = [];
57846
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
57847
+ return out;
57848
+ }
57849
+ function pauseFor(storeMs) {
57850
+ return Math.min(RESCORE_PAUSE_MAX_MS, Math.max(RESCORE_PAUSE_MIN_MS, Math.round(storeMs * RESCORE_PAUSE_FACTOR)));
57851
+ }
57852
+ var FaceRescoreJob = class {
57853
+ ports;
57854
+ current = IDLE_STATUS;
57855
+ running = null;
57856
+ stopped = false;
57857
+ constructor(ports) {
57858
+ this.ports = ports;
57859
+ }
57860
+ /** Progress of the running job, else the last finished one, else idle. */
57861
+ status() {
57862
+ return this.current;
57863
+ }
57864
+ isRunning() {
57865
+ return this.running !== null;
57866
+ }
57867
+ /** Resolves when no job is running. Never rejects: failure is in `status()`. */
57868
+ async whenSettled() {
57869
+ await this.running;
57870
+ }
57871
+ /** Ask a running job to stop after its current batch (addon shutdown). */
57872
+ stop() {
57873
+ this.stopped = true;
57874
+ }
57875
+ /**
57876
+ * Start a job — or return the one already running (single-flight). The
57877
+ * returned status is a snapshot; poll {@link status} for progress.
57878
+ */
57879
+ start() {
57880
+ if (this.running !== null) return this.current;
57881
+ this.stopped = false;
57882
+ const startedAt = this.ports.now();
57883
+ this.current = {
57884
+ ...IDLE_STATUS,
57885
+ jobId: this.ports.newJobId(),
57886
+ state: "running",
57887
+ startedAt
57888
+ };
57889
+ this.running = this.run(startedAt).finally(() => {
57890
+ this.running = null;
57891
+ });
57892
+ return this.current;
57893
+ }
57894
+ patch(change) {
57895
+ this.current = {
57896
+ ...this.current,
57897
+ ...change
57898
+ };
57899
+ }
57900
+ async run(startedAt) {
57901
+ try {
57902
+ await this.runBatches(startedAt);
57903
+ this.patch({
57904
+ state: "completed",
57905
+ durationMs: this.ports.now() - startedAt
57906
+ });
57907
+ this.logSummary("face rescore job finished");
57908
+ await this.recordCompletion();
57909
+ } catch (err) {
57910
+ const error = err instanceof Error ? err.message : String(err);
57911
+ this.patch({
57912
+ state: "failed",
57913
+ error,
57914
+ durationMs: this.ports.now() - startedAt
57915
+ });
57916
+ this.ports.logger.warn("face rescore job FAILED — rows written so far stay written", { meta: {
57917
+ ...this.summaryMeta(),
57918
+ error
57919
+ } });
57920
+ }
57921
+ }
57922
+ async recordCompletion() {
57923
+ try {
57924
+ await this.ports.recordCompletion(this.current);
57925
+ } catch (err) {
57926
+ this.ports.logger.warn("face rescore result could not be recorded — the next tick may rerun", { meta: {
57927
+ jobId: this.current.jobId,
57928
+ error: err instanceof Error ? err.message : String(err)
57929
+ } });
57930
+ }
57931
+ }
57932
+ summaryMeta() {
57933
+ const s = this.current;
57934
+ return {
57935
+ jobId: s.jobId,
57936
+ scanned: s.scanned,
57937
+ total: s.total,
57938
+ changed: s.changed,
57939
+ newlySuggested: s.newlySuggested,
57940
+ raised: s.raised,
57941
+ lowered: s.lowered,
57942
+ cleared: s.cleared,
57943
+ modelMismatch: s.modelMismatch,
57944
+ noVector: s.noVector,
57945
+ batches: s.batches,
57946
+ maxBatchStoreMs: s.maxBatchStoreMs,
57947
+ durationMs: s.durationMs,
57948
+ galleryFingerprint: s.galleryFingerprint
57949
+ };
57950
+ }
57951
+ logSummary(message) {
57952
+ this.ports.logger.info(message, { meta: this.summaryMeta() });
57953
+ }
57954
+ async runBatches(startedAt) {
57955
+ const observed = await readRescoreFingerprint(this.ports);
57956
+ if (observed === null) throw new Error("the cluster face-embedding model could not be resolved");
57957
+ const { clusterModelId, fingerprint } = observed;
57958
+ const [gallery, identityNames] = await Promise.all([this.ports.loadGallery(), this.ports.identityNames()]);
57959
+ const ids = await this.ports.listPendingFaceIds();
57960
+ this.patch({
57961
+ total: ids.length,
57962
+ galleryFingerprint: fingerprint.gallery,
57963
+ fingerprint: fingerprint.combined
57964
+ });
57965
+ const paramsByDevice = /* @__PURE__ */ new Map();
57966
+ for (const batchIds of chunk(ids, RESCORE_BATCH_SIZE)) {
57967
+ if (this.stopped) throw new Error("stopped before completion");
57968
+ const storeStart = this.ports.now();
57969
+ const batch = await this.ports.loadRescoreBatch(batchIds);
57970
+ const readMs = this.ports.now() - storeStart;
57971
+ const outcome = await this.scoreBatch(batch.rows, {
57972
+ gallery,
57973
+ identityNames,
57974
+ clusterModelId,
57975
+ paramsByDevice
57976
+ });
57977
+ const writeStart = this.ports.now();
57978
+ await this.ports.applySuggestionBatch(outcome.updates);
57979
+ const storeMs = readMs + (this.ports.now() - writeStart);
57980
+ const s = this.current;
57981
+ this.patch({
57982
+ scanned: s.scanned + batchIds.length,
57983
+ changed: s.changed + outcome.updates.length,
57984
+ newlySuggested: s.newlySuggested + outcome.tally.new,
57985
+ raised: s.raised + outcome.tally.raised,
57986
+ lowered: s.lowered + outcome.tally.lowered,
57987
+ cleared: s.cleared + outcome.tally.cleared,
57988
+ modelMismatch: s.modelMismatch + outcome.modelMismatch,
57989
+ noVector: s.noVector + batch.noVector,
57990
+ batches: s.batches + 1,
57991
+ maxBatchStoreMs: Math.max(s.maxBatchStoreMs, storeMs),
57992
+ durationMs: this.ports.now() - startedAt
57993
+ });
57994
+ this.ports.logger.debug("face rescore batch", { meta: {
57995
+ jobId: s.jobId,
57996
+ size: batchIds.length,
57997
+ written: outcome.updates.length,
57998
+ storeMs
57999
+ } });
58000
+ await this.ports.pause(pauseFor(storeMs));
58001
+ }
58002
+ }
58003
+ async scoreBatch(rows, ctx) {
58004
+ const updates = [];
58005
+ const tally = {
58006
+ new: 0,
58007
+ raised: 0,
58008
+ lowered: 0,
58009
+ cleared: 0
58010
+ };
58011
+ let modelMismatch = 0;
58012
+ for (const row of rows) {
58013
+ let params = ctx.paramsByDevice.get(row.deviceId);
58014
+ if (params === void 0) {
58015
+ params = await this.ports.paramsForDevice(row.deviceId);
58016
+ ctx.paramsByDevice.set(row.deviceId, params);
58017
+ }
58018
+ const report = buildFaceRescoreReport({
58019
+ probe: {
58020
+ embedding: row.embedding,
58021
+ modelId: row.modelId
58022
+ },
58023
+ gallery: ctx.gallery,
58024
+ identityNames: ctx.identityNames,
58025
+ clusterModelId: ctx.clusterModelId,
58026
+ params
58027
+ });
58028
+ const captured = { value: null };
58029
+ if (await persistRescoreResult(row.id, report, { setSuggestion: (_id, suggestion) => {
58030
+ captured.value = suggestion;
58031
+ return Promise.resolve();
58032
+ } }) === "not-comparable") {
58033
+ modelMismatch += 1;
58034
+ continue;
58035
+ }
58036
+ const change = classifyRescoreChange(storedSuggestion(row), captured.value);
58037
+ if (change === "unchanged") continue;
58038
+ tally[change] += 1;
58039
+ updates.push({
58040
+ faceId: row.id,
58041
+ suggestion: captured.value
58042
+ });
58043
+ this.ports.logger.debug("face rescore changed a suggestion", {
58044
+ tags: { deviceId: row.deviceId },
58045
+ meta: {
58046
+ faceId: row.id,
58047
+ change,
58048
+ from: row.suggestedIdentityId ?? null,
58049
+ fromScore: row.suggestedMatchScore ?? null,
58050
+ to: decideRescorePersistence(report)?.identityId ?? null
58051
+ }
58052
+ });
58053
+ }
58054
+ return {
58055
+ updates,
58056
+ tally,
58057
+ modelMismatch
58058
+ };
58059
+ }
58060
+ };
58061
+ //#endregion
58062
+ //#region src/pipeline-analytics/face-rescore-scheduler.ts
58063
+ var FACE_RESCORE_TICK_MS = 6e4;
58064
+ var FaceRescoreScheduler = class {
58065
+ deps;
58066
+ lastCheckAt;
58067
+ ticking = false;
58068
+ constructor(deps, startedAt) {
58069
+ this.deps = deps;
58070
+ this.lastCheckAt = startedAt;
58071
+ }
58072
+ async tick() {
58073
+ if (this.ticking) return "busy";
58074
+ this.ticking = true;
58075
+ try {
58076
+ return await this.decideAndRun();
58077
+ } finally {
58078
+ this.ticking = false;
58079
+ }
58080
+ }
58081
+ async decideAndRun() {
58082
+ const minutes = await this.readInterval();
58083
+ if (minutes === null) return "setting-unreadable";
58084
+ if (minutes === 0) return "disabled";
58085
+ const now = this.deps.now();
58086
+ if (now - this.lastCheckAt < minutes * 6e4) return "not-due";
58087
+ this.lastCheckAt = now;
58088
+ if (this.deps.job.isRunning()) return "busy";
58089
+ const observed = await this.observe();
58090
+ if (observed === null) return "unreadable";
58091
+ const previous = await this.deps.state.load();
58092
+ if (previous !== null && previous.fingerprint === observed) {
58093
+ this.deps.logger.info("face rescore skipped, unchanged", { meta: {
58094
+ fingerprint: observed,
58095
+ lastRunAt: previous.finishedAt
58096
+ } });
58097
+ return "unchanged";
58098
+ }
58099
+ this.deps.job.start();
58100
+ await this.deps.job.whenSettled();
58101
+ return this.deps.job.status().state === "completed" ? "ran" : "failed";
58102
+ }
58103
+ async readInterval() {
58104
+ try {
58105
+ return await this.deps.readIntervalMinutes();
58106
+ } catch (err) {
58107
+ this.deps.logger.warn("face rescore interval unreadable — tick skipped", { meta: { error: err instanceof Error ? err.message : String(err) } });
58108
+ return null;
58109
+ }
58110
+ }
58111
+ async observe() {
58112
+ try {
58113
+ const seen = await readRescoreFingerprint(this.deps.fingerprintPorts);
58114
+ if (seen === null) {
58115
+ this.deps.logger.warn("face rescore tick skipped — cluster face model unresolved");
58116
+ return null;
58117
+ }
58118
+ return seen.fingerprint.combined;
58119
+ } catch (err) {
58120
+ this.deps.logger.warn("face rescore fingerprint unreadable — tick skipped", { meta: { error: err instanceof Error ? err.message : String(err) } });
58121
+ return null;
58122
+ }
58123
+ }
58124
+ };
58125
+ //#endregion
58126
+ //#region src/pipeline-analytics/face-rescore-wiring.ts
58127
+ /**
58128
+ * face-rescore-wiring — binds the whole-archive rescore job (D866) to the real
58129
+ * stores, the cluster model pin and the orchestrator's interval setting.
58130
+ *
58131
+ * Kept out of `index.ts` (20 000 lines): the addon holds the returned runtime
58132
+ * and forwards three things to it — the two actions, the timer tick, stop.
58133
+ */
58134
+ function buildFaceRescoreRuntime(deps, startedAt) {
58135
+ const { faceStore, identityStore, logger } = deps;
58136
+ const resolveClusterModelId = () => require_clip_model_registry.resolveClusterModelPin(deps.api, require_clip_model_registry.FACE_EMBEDDING_STEP_ID, { warn: (m, e) => logger.warn(m, e) });
58137
+ const galleryManifest = () => identityStore.galleryManifest();
58138
+ const pendingMarker = () => faceStore.pendingMarker();
58139
+ const job = new FaceRescoreJob({
58140
+ resolveClusterModelId,
58141
+ galleryManifest,
58142
+ pendingMarker,
58143
+ loadGallery: () => identityStore.loadGallery(),
58144
+ identityNames: async () => new Map((await identityStore.listIdentities()).map((i) => [i.id, i.name])),
58145
+ paramsForDevice: deps.paramsForDevice,
58146
+ listPendingFaceIds: () => faceStore.listPendingFaceIds(),
58147
+ loadRescoreBatch: (ids) => faceStore.loadRescoreBatch(ids),
58148
+ applySuggestionBatch: (updates) => faceStore.applySuggestionBatch(updates),
58149
+ recordCompletion: (status) => deps.state.save({
58150
+ fingerprint: status.fingerprint,
58151
+ finishedAt: Date.now(),
58152
+ report: status
58153
+ }),
58154
+ pause: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
58155
+ now: () => Date.now(),
58156
+ newJobId: () => (0, node_crypto.randomUUID)(),
58157
+ logger
58158
+ });
58159
+ return {
58160
+ job,
58161
+ scheduler: new FaceRescoreScheduler({
58162
+ readIntervalMinutes: async () => require_clip_model_registry.pickFaceRescoreIntervalMinutes(await deps.api.addonSettings.getGlobalSettings.query({ addonId: require_clip_model_registry.CLUSTER_MODEL_OWNER_ADDON_ID })),
58163
+ job,
58164
+ fingerprintPorts: {
58165
+ resolveClusterModelId,
58166
+ galleryManifest,
58167
+ pendingMarker
58168
+ },
58169
+ state: deps.state,
58170
+ now: () => Date.now(),
58171
+ logger
58172
+ }, startedAt)
58173
+ };
58174
+ }
58175
+ //#endregion
58176
+ //#region src/pipeline-analytics/store/face-rescore-state-store.ts
58177
+ /**
58178
+ * @durable class=config owner=pipeline-analytics
58179
+ * write="one row, rewritten when a whole-archive face rescore completes"
58180
+ * loss="the next scheduler tick reruns the rescore once — idempotent over an
58181
+ * unchanged gallery"
58182
+ * retention="never pruned: one row"
58183
+ */
58184
+ var FACE_RESCORE_STATE_COLLECTION = "pipeline-analytics:face-rescore-state";
58185
+ var STATE_KEY = "last-run";
58186
+ var FaceRescoreStateStore = class {
58187
+ deps;
58188
+ constructor(deps) {
58189
+ this.deps = deps;
58190
+ }
58191
+ static async declare(store) {
58192
+ await store.declareCollection.mutate({
58193
+ collection: FACE_RESCORE_STATE_COLLECTION,
58194
+ columns: [{
58195
+ name: "data",
58196
+ type: "TEXT",
58197
+ notNull: true
58198
+ }]
58199
+ });
58200
+ }
58201
+ /** `null` on no row OR an unreadable one: either way the next tick reruns. */
58202
+ load = async () => {
58203
+ try {
58204
+ return decodeRunState(await this.deps.store.get.query({
58205
+ collection: FACE_RESCORE_STATE_COLLECTION,
58206
+ key: STATE_KEY
58207
+ }) ?? null);
58208
+ } catch (err) {
58209
+ this.deps.logger.warn("face rescore state unreadable — treating as never run", { meta: { error: String(err) } });
58210
+ return null;
58211
+ }
58212
+ };
58213
+ /** THROWS: a state that did not land must be visible to the caller. */
58214
+ save = async (state) => {
58215
+ await this.deps.store.set.mutate({
58216
+ collection: FACE_RESCORE_STATE_COLLECTION,
58217
+ key: STATE_KEY,
58218
+ value: {
58219
+ fingerprint: state.fingerprint,
58220
+ finishedAt: state.finishedAt,
58221
+ report: state.report
58222
+ }
58223
+ });
58224
+ };
58225
+ };
58226
+ /** Narrow a stored row; anything malformed is "no previous run". */
58227
+ function decodeRunState(value) {
58228
+ if (value === null || typeof value !== "object") return null;
58229
+ const row = { ...value };
58230
+ const fingerprint = row["fingerprint"];
58231
+ const finishedAt = row["finishedAt"];
58232
+ const report = row["report"];
58233
+ if (typeof fingerprint !== "string" || fingerprint === "") return null;
58234
+ if (typeof finishedAt !== "number") return null;
58235
+ const parsed = FaceRescoreJobStatusSchema.safeParse(report);
58236
+ return parsed.success ? {
58237
+ fingerprint,
58238
+ finishedAt,
58239
+ report: parsed.data
58240
+ } : null;
58241
+ }
57548
58242
  //#endregion
57549
58243
  //#region src/pipeline-analytics/face-sample-reembed.ts
57550
58244
  /** Pacing between samples — off hub-main's thread, not a throttle. */
@@ -76612,6 +77306,39 @@ function clampBox(b, frameWidth, frameHeight) {
76612
77306
  h: Math.max(1, Math.min(Math.round(b.h), frameHeight - y))
76613
77307
  };
76614
77308
  }
77309
+ /** The SVG overlay: one `<rect>` per box plus an optional caption. */
77310
+ function buildBoxOverlaySvg(boxes, frameWidth, frameHeight) {
77311
+ const fontSize = Math.max(12, Math.round(frameHeight / 30));
77312
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}">${boxes.map((b) => {
77313
+ const c = clampBox(b, frameWidth, frameHeight);
77314
+ const color = b.color ?? DEFAULT_COLOR;
77315
+ const rect = `<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" fill="none" stroke="${color}" stroke-width="${STROKE_WIDTH}"/>`;
77316
+ if (!b.label) return rect;
77317
+ const ty = Math.max(fontSize, c.y - 4);
77318
+ 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>`;
77319
+ }).join("")}</svg>`;
77320
+ }
77321
+ /**
77322
+ * Draw bounding boxes over a frame that is still a JPEG and re-encode it in ONE
77323
+ * libvips pipeline (decode, composite, encode). The raw RGB surface never
77324
+ * exists in the JS process: {@link drawBoxedFrame} needs it handed in, which for
77325
+ * a consumer that only re-encodes meant a 1-6 MB V8-external buffer per tile
77326
+ * (D867). Same overlay and quality contract; no `maxWidth` — the caller asked
77327
+ * the owning runner for the size it wants.
77328
+ */
77329
+ async function drawBoxedJpeg(jpeg, frameWidth, frameHeight, boxes, opts = {}) {
77330
+ const quality = opts.quality ?? DEFAULT_QUALITY;
77331
+ let pipeline = (0, sharp.default)(jpeg);
77332
+ if (boxes.length > 0) {
77333
+ const svg = buildBoxOverlaySvg(boxes, frameWidth, frameHeight);
77334
+ pipeline = pipeline.composite([{
77335
+ input: Buffer.from(svg),
77336
+ top: 0,
77337
+ left: 0
77338
+ }]);
77339
+ }
77340
+ return pipeline.jpeg({ quality }).toBuffer();
77341
+ }
76615
77342
  /**
76616
77343
  * Draw bounding boxes over a raw RGB frame and JPEG-encode it.
76617
77344
  *
@@ -76625,15 +77352,7 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
76625
77352
  let base = frameData;
76626
77353
  let baseIsRaw = true;
76627
77354
  if (boxes.length > 0) {
76628
- const fontSize = Math.max(12, Math.round(frameHeight / 30));
76629
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}">${boxes.map((b) => {
76630
- const c = clampBox(b, frameWidth, frameHeight);
76631
- const color = b.color ?? DEFAULT_COLOR;
76632
- const rect = `<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" fill="none" stroke="${color}" stroke-width="${STROKE_WIDTH}"/>`;
76633
- if (!b.label) return rect;
76634
- const ty = Math.max(fontSize, c.y - 4);
76635
- 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>`;
76636
- }).join("")}</svg>`;
77355
+ const svg = buildBoxOverlaySvg(boxes, frameWidth, frameHeight);
76637
77356
  base = await (0, sharp.default)(frameData, { raw: {
76638
77357
  width: frameWidth,
76639
77358
  height: frameHeight,
@@ -77207,7 +77926,7 @@ function createDebugCropCapture(deps) {
77207
77926
  * `extractCrop` (which crops a FULL frame) so the native path never re-crops.
77208
77927
  */
77209
77928
  async function encodeRgbCropToJpeg(bytes, width, height) {
77210
- return (0, sharp.default)(Buffer.from(bytes), { raw: {
77929
+ return (0, sharp.default)(bytes, { raw: {
77211
77930
  width,
77212
77931
  height,
77213
77932
  channels: 3
@@ -77226,7 +77945,7 @@ async function encodeRgbCropToJpeg(bytes, width, height) {
77226
77945
  * what lets the caller stop base64-ing purely to be understood.
77227
77946
  */
77228
77947
  async function decodeJpegToRgb(jpeg) {
77229
- const { data, info } = await (0, sharp.default)(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : Buffer.from(jpeg)).raw().toBuffer({ resolveWithObject: true });
77948
+ const { data, info } = await (0, sharp.default)(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : jpeg).raw().toBuffer({ resolveWithObject: true });
77230
77949
  return {
77231
77950
  bytes: data,
77232
77951
  width: info.width,
@@ -77871,13 +78590,14 @@ var AnalyticsQueryFacade = class {
77871
78590
  cutoffMs: Number.MAX_SAFE_INTEGER
77872
78591
  });
77873
78592
  const total = counts.motion + counts.object + counts.audio;
78593
+ const byRemoval = input.reason !== void 0;
77874
78594
  await this.deps.opsLog()?.append({
77875
- op: "manual-delete",
77876
- reason: "manual",
78595
+ op: byRemoval ? "prune" : "manual-delete",
78596
+ reason: input.reason ?? "manual",
77877
78597
  deviceId: input.deviceId,
77878
78598
  itemsAffected: total,
77879
78599
  bytesReclaimed: 0,
77880
- detail: "deleted all events for device"
78600
+ detail: byRemoval ? "the device was deleted — its events were removed with it" : "deleted all events for device"
77881
78601
  });
77882
78602
  return counts;
77883
78603
  }
@@ -79949,6 +80669,53 @@ var DeviceAlertSettingsSchema = require_clip_model_registry.object({
79949
80669
  /** The shipped defaults, for the settings-schema form and for tests. */
79950
80670
  var DEVICE_ALERT_DEFAULTS = DeviceAlertSettingsSchema.parse({});
79951
80671
  //#endregion
80672
+ //#region src/pipeline-analytics/device-removal-hook.ts
80673
+ function createAnalyticsRemovalHook(deps) {
80674
+ return {
80675
+ onDeviceRemoved: async (input) => {
80676
+ const { deviceId } = input;
80677
+ const events = await deps.deleteDeviceEvents({
80678
+ deviceId,
80679
+ reason: "orphaned-device"
80680
+ });
80681
+ const tracks = await deps.wipeAllAnalytics({ deviceId });
80682
+ const sensor = await deps.evictSensorEvents(deviceId, Number.MAX_SAFE_INTEGER);
80683
+ const counts = {
80684
+ events: events.motion + events.object + events.audio,
80685
+ tracks: tracks.tracks,
80686
+ "track-events": tracks.events,
80687
+ media: tracks.media,
80688
+ faces: tracks.faces,
80689
+ plates: tracks.plates,
80690
+ embeddings: tracks.embeddings,
80691
+ "sensor-events": sensor
80692
+ };
80693
+ if (Object.values(counts).reduce((sum, n) => sum + n, 0) > 0) deps.logger.info("pipeline-analytics: removed the data of a deleted device", {
80694
+ tags: { deviceId },
80695
+ meta: {
80696
+ counts,
80697
+ deletedAt: input.deletedAt
80698
+ }
80699
+ });
80700
+ return { counts };
80701
+ },
80702
+ listDeviceData: async () => {
80703
+ const out = [];
80704
+ for (const [deviceId, count] of await deps.eventCountsByDevice()) if (count > 0 && Number.isInteger(deviceId) && deviceId > 0) out.push({
80705
+ store: "events",
80706
+ deviceId,
80707
+ count
80708
+ });
80709
+ for (const deviceId of await deps.trackDeviceIds()) if (Number.isInteger(deviceId) && deviceId > 0) out.push({
80710
+ store: "tracks",
80711
+ deviceId,
80712
+ count: 1
80713
+ });
80714
+ return out;
80715
+ }
80716
+ };
80717
+ }
80718
+ //#endregion
79952
80719
  //#region src/pipeline-analytics/device-settings-cache.ts
79953
80720
  /**
79954
80721
  * TTL cache for ONE settings kind, keyed by deviceId. Obtain instances via
@@ -89434,7 +90201,7 @@ function createKeyFrameCrop(deps) {
89434
90201
  return null;
89435
90202
  }
89436
90203
  try {
89437
- const rgb = await deps.decodeJpegToRgb(media.base64);
90204
+ const rgb = await deps.decodeJpegToRgb(media.bytes);
89438
90205
  if (rgb.width <= 0 || rgb.height <= 0) return null;
89439
90206
  const norm = padBbox({
89440
90207
  x: input.bbox.x / input.frameWidth,
@@ -89442,7 +90209,7 @@ function createKeyFrameCrop(deps) {
89442
90209
  w: input.bbox.w / input.frameWidth,
89443
90210
  h: input.bbox.h / input.frameHeight
89444
90211
  }, input.padding);
89445
- const jpeg = await deps.cropRegionToJpeg(Buffer.from(rgb.bytes), rgb.width, rgb.height, norm);
90212
+ const jpeg = await deps.cropRegionToJpeg(rgb.bytes, rgb.width, rgb.height, norm);
89446
90213
  return verdict.kind === "agrees" ? {
89447
90214
  jpeg,
89448
90215
  skewMs,
@@ -97746,6 +98513,29 @@ var IdentityStore = class {
97746
98513
  };
97747
98514
  });
97748
98515
  }
98516
+ /**
98517
+ * The gallery's IDENTITY of the moment — sample id, owner and model of every
98518
+ * sample, no vectors. What the rescore change detector hashes; projected so a
98519
+ * periodic look costs a few narrow columns, not the gallery's floats.
98520
+ */
98521
+ async galleryManifest() {
98522
+ const rows = await this.store.query.query({
98523
+ collection: IDENTITY_SAMPLES_COLLECTION,
98524
+ columns: ["identityId", "modelId"],
98525
+ filter: {}
98526
+ });
98527
+ const out = [];
98528
+ for (const r of rows) {
98529
+ const identityId = r.data["identityId"];
98530
+ const modelId = r.data["modelId"];
98531
+ if (typeof identityId === "string" && typeof modelId === "string") out.push({
98532
+ sampleId: r.id,
98533
+ identityId,
98534
+ modelId
98535
+ });
98536
+ }
98537
+ return out;
98538
+ }
97749
98539
  async addSample(input) {
97750
98540
  const sample = {
97751
98541
  id: (0, node_crypto.randomUUID)(),
@@ -98044,7 +98834,7 @@ var IdentityStore = class {
98044
98834
  const d = r.data;
98045
98835
  if (typeof d.modelId === "string" && typeof d.identityId === "string") wanted.push(r.id);
98046
98836
  }
98047
- const vectors = await this.vectors.loadStamped(wanted);
98837
+ const vectors = await this.vectors.loadStampedFloat32(wanted);
98048
98838
  const gallery = [];
98049
98839
  let fromLegacyColumn = 0;
98050
98840
  let missing = 0;
@@ -98074,7 +98864,7 @@ var IdentityStore = class {
98074
98864
  fromLegacyColumn += 1;
98075
98865
  gallery.push({
98076
98866
  identityId: d.identityId,
98077
- embedding: legacy,
98867
+ embedding: new Float32Array(legacy),
98078
98868
  modelId: d.modelId
98079
98869
  });
98080
98870
  continue;
@@ -101998,6 +102788,7 @@ function createNativeFrameTransport(deps) {
101998
102788
  };
101999
102789
  const getRemoteFrame = async (handle) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, handle.width, replyToFullFrame));
102000
102790
  const getNativeFullFrameRgb = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToFullFrame));
102791
+ const getNativeFullFrameJpeg = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, cropReplyToJpeg));
102001
102792
  const fetchNativeFullFrameTiered = async (handle, maxWidth) => closeWindowAfter(requestNativeCrop(handle, FULL_FRAME_BBOX, maxWidth, replyToTieredFullFrame));
102002
102793
  const cropMetricLogger = logger.child("NativeCrop");
102003
102794
  let nativeHits = 0;
@@ -102106,6 +102897,7 @@ function createNativeFrameTransport(deps) {
102106
102897
  return {
102107
102898
  getRemoteFrame,
102108
102899
  getNativeFullFrameRgb,
102900
+ getNativeFullFrameJpeg,
102109
102901
  getNativeCropJpeg,
102110
102902
  captureCrop: createCaptureCrop({
102111
102903
  tryNativeCrop,
@@ -103924,6 +104716,9 @@ var EventMediaDispatcher = class {
103924
104716
  * {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
103925
104717
  */
103926
104718
  sharedNativeFullFrame;
104719
+ /** The JPEG twin of {@link sharedNativeFullFrame} — the boxed tiles' source
104720
+ * (a few tens of kB held instead of a 1.5 MB RGB surface). */
104721
+ sharedNativeFullJpeg;
103927
104722
  /** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
103928
104723
  * info log — see {@link logDisplayFallbackOnce}. */
103929
104724
  loggedDisplayFallbacks = /* @__PURE__ */ new Set();
@@ -103932,11 +104727,12 @@ var EventMediaDispatcher = class {
103932
104727
  constructor(deps) {
103933
104728
  this.deps = deps;
103934
104729
  this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
104730
+ this.sharedNativeFullJpeg = createSharedFrameResolver(async (handle) => this.deps.getNativeFullFrameJpeg ? this.deps.getNativeFullFrameJpeg(handle, SNAPSHOT_MAX_WIDTH) : null);
103935
104731
  }
103936
104732
  /**
103937
104733
  * Resolve the ≤640 detection frame by handle NOW — in the caller's LIVE frame
103938
- * window — and COPY its pixel data (`Buffer.from`) so ring recycling can never
103939
- * touch it. This is the seam the per-frame batch dispatch uses to pin the
104734
+ * window — and hold the privately owned pixel buffer the fetch returned (no
104735
+ * copy — D867), so ring recycling can never touch it. This is the seam the per-frame batch dispatch uses to pin the
103940
104736
  * frame BEFORE queueing on the CaptureScheduler: since S3 the batch is one
103941
104737
  * scheduler request against a 3-slot device lane and can start seconds late,
103942
104738
  * by which time the handle's ring slot is usually recycled and the by-handle
@@ -103964,10 +104760,7 @@ var EventMediaDispatcher = class {
103964
104760
  this.warnPinnedFrameMiss(deviceId, frameHandle, "recycled-before-pin");
103965
104761
  return null;
103966
104762
  }
103967
- return {
103968
- ...decoded,
103969
- data: Buffer.from(decoded.data)
103970
- };
104763
+ return decoded;
103971
104764
  } catch (err) {
103972
104765
  this.warnPinnedFrameMiss(deviceId, frameHandle, "threw", String(err));
103973
104766
  return null;
@@ -104040,7 +104833,7 @@ var EventMediaDispatcher = class {
104040
104833
  this.warnBatchLost("resolved frame is not RGB", input, { format: decoded.format });
104041
104834
  return empty;
104042
104835
  }
104043
- const frameData = input.pinnedFrame !== void 0 ? decoded.data : Buffer.from(decoded.data);
104836
+ const frameData = decoded.data;
104044
104837
  const fw = decoded.width;
104045
104838
  const fh = decoded.height;
104046
104839
  if (isUniformRgbFrame(frameData, fw, fh)) {
@@ -104368,9 +105161,11 @@ var EventMediaDispatcher = class {
104368
105161
  */
104369
105162
  async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
104370
105163
  let nf = null;
105164
+ let nfJpeg = null;
104371
105165
  let fetchThrew = false;
104372
105166
  try {
104373
- nf = await this.sharedNativeFullFrame(frameHandle);
105167
+ if (this.deps.getNativeFullFrameJpeg) nfJpeg = await this.sharedNativeFullJpeg(frameHandle);
105168
+ else nf = await this.sharedNativeFullFrame(frameHandle);
104374
105169
  } catch (err) {
104375
105170
  fetchThrew = true;
104376
105171
  this.deps.logger.warn("native full frame fetch threw — 640 boxed fallback", {
@@ -104383,6 +105178,15 @@ var EventMediaDispatcher = class {
104383
105178
  });
104384
105179
  }
104385
105180
  try {
105181
+ if (nfJpeg && nfJpeg.width > 0 && nfJpeg.height > 0) {
105182
+ const box = scaleBox(bbox, fw, fh, nfJpeg.width, nfJpeg.height, label);
105183
+ const opts = { quality: MEDIA_QUALITY };
105184
+ const [boxed, clean] = await Promise.all([drawBoxedJpeg(nfJpeg.jpeg, nfJpeg.width, nfJpeg.height, [box], opts), drawBoxedJpeg(nfJpeg.jpeg, nfJpeg.width, nfJpeg.height, [], opts)]);
105185
+ return {
105186
+ boxed,
105187
+ clean
105188
+ };
105189
+ }
104386
105190
  if (nf && nf.format === "rgb" && nf.width > 0 && nf.height > 0) {
104387
105191
  const box = scaleBox(bbox, fw, fh, nf.width, nf.height, label);
104388
105192
  const opts = { quality: MEDIA_QUALITY };
@@ -104831,6 +105635,17 @@ Object.freeze({
104831
105635
  "page": 1,
104832
105636
  "pageSize": 50
104833
105637
  } },
105638
+ "device-credentials": { "listDeviceCredentials": { "includeRevoked": false } },
105639
+ "device-history": {
105640
+ "query": {
105641
+ "resolution": "auto",
105642
+ "maxPoints": 1e3
105643
+ },
105644
+ "queryNode": {
105645
+ "resolution": "auto",
105646
+ "maxPoints": 1e3
105647
+ }
105648
+ },
104834
105649
  "device-manager": { "adoptionListCandidates": {
104835
105650
  "page": 1,
104836
105651
  "pageSize": 50
@@ -105083,6 +105898,7 @@ Object.freeze({
105083
105898
  "device-export": ["listExposedDevices", "listSupportedDeviceKinds"],
105084
105899
  "device-extension": ["listSlots", "listSources"],
105085
105900
  "device-provider": ["discoverDevices", "getDevices"],
105901
+ "device-removal-hook": ["listDeviceData"],
105086
105902
  "failure-contribution": ["list"],
105087
105903
  "llm": [
105088
105904
  "getDefaults",
@@ -111655,6 +112471,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
111655
112471
  */
111656
112472
  faceAlignmentJobs = null;
111657
112473
  faceGalleryPruneTimer = null;
112474
+ /** D866: the whole-archive rescore job + its change-gated hourly scheduler. */
112475
+ faceRescoreRuntime = null;
112476
+ faceRescoreState = null;
112477
+ faceRescoreTimer = null;
111658
112478
  /**
111659
112479
  * Analysed photographs waiting to be named, held between `photo.analyze` and
111660
112480
  * `photo.enroll`. Bounded and expiring — the value held is a whole
@@ -112596,13 +113416,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
112596
113416
  if (!this._keyFrameThumbnailDerive) {
112597
113417
  const deriveKeyFrameThumbnailJpeg = createKeyFrameCrop({
112598
113418
  getMedia: async (mediaKey) => {
112599
- const m = await this.mediaStore?.getById(mediaKey);
113419
+ const m = await this.mediaStore?.getBytesById(mediaKey);
112600
113420
  return m ? {
112601
- base64: m.base64,
113421
+ bytes: m.bytes,
112602
113422
  timestamp: m.timestamp
112603
113423
  } : null;
112604
113424
  },
112605
- decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
113425
+ decodeJpegToRgb: (jpeg) => decodeJpegToRgb(jpeg),
112606
113426
  cropRegionToJpeg: (bytes, w, h, norm) => cropWideCentralSquareFromRgb(bytes, w, h, norm),
112607
113427
  maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
112608
113428
  minSubjectIou: KEYFRAME_CROP_MIN_SUBJECT_IOU,
@@ -113295,6 +114115,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113295
114115
  getRemoteFrame: transport.getRemoteFrame,
113296
114116
  getNativeCropJpeg: transport.getNativeCropJpeg,
113297
114117
  getNativeFullFrameRgb: transport.getNativeFullFrameRgb,
114118
+ getNativeFullFrameJpeg: transport.getNativeFullFrameJpeg,
113298
114119
  getParkedSubjectSlab: (deviceId, trackId) => this.fetchParkedSubjectSlab(deviceId, trackId),
113299
114120
  mediaStore: stores.mediaStore,
113300
114121
  logger: logger.child("EventMediaDispatcher"),
@@ -113622,8 +114443,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113622
114443
  NotificationCenter.declare(api.settingsStore),
113623
114444
  ViewerSettingsSnapshotStore.declare(api.settingsStore),
113624
114445
  declareMigrationMarkers(api.settingsStore),
113625
- SweepCursorStore.declare(api.settingsStore)
114446
+ SweepCursorStore.declare(api.settingsStore),
114447
+ FaceRescoreStateStore.declare(api.settingsStore)
113626
114448
  ]);
114449
+ this.faceRescoreState = new FaceRescoreStateStore({
114450
+ store: api.settingsStore,
114451
+ logger: this.ctx.logger.child("FaceRescoreState")
114452
+ });
113627
114453
  this.sweepCursors = new SweepCursorStore({
113628
114454
  store: api.settingsStore,
113629
114455
  logger: this.ctx.logger.child("SweepCursors")
@@ -113856,8 +114682,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113856
114682
  writeGate: this.mediaWriteGate,
113857
114683
  measureChroma: (bytes) => chromaSpread(bytes),
113858
114684
  readSource: async (mediaKey) => {
113859
- const file = await mediaStore.getById(mediaKey);
113860
- return file === null ? null : Buffer.from(file.base64, "base64");
114685
+ const file = await mediaStore.getBytesById(mediaKey);
114686
+ return file === null ? null : file.bytes;
113861
114687
  }
113862
114688
  });
113863
114689
  const retrainAnnotations = new RetrainAnnotationStore({
@@ -113886,8 +114712,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113886
114712
  media: {
113887
114713
  listTrackMedia: (trackId) => mediaStore.listInfoByOwner("track", trackId),
113888
114714
  readBlob: async (mediaKey) => {
113889
- const file = await mediaStore.getById(mediaKey);
113890
- return file === null ? null : Buffer.from(file.base64, "base64");
114715
+ const file = await mediaStore.getBytesById(mediaKey);
114716
+ return file === null ? null : file.bytes;
113891
114717
  }
113892
114718
  },
113893
114719
  frames: retrainFrames,
@@ -114226,13 +115052,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
114226
115052
  const plateMediaStore = stores.mediaStore;
114227
115053
  const deriveCropFromKeyFrame = createKeyFrameCrop({
114228
115054
  getMedia: async (mediaKey) => {
114229
- const m = await plateMediaStore.getById(mediaKey);
115055
+ const m = await plateMediaStore.getBytesById(mediaKey);
114230
115056
  return m ? {
114231
- base64: m.base64,
115057
+ bytes: m.bytes,
114232
115058
  timestamp: m.timestamp
114233
115059
  } : null;
114234
115060
  },
114235
- decodeJpegToRgb: (base64) => decodeJpegToRgb(base64),
115061
+ decodeJpegToRgb: (jpeg) => decodeJpegToRgb(jpeg),
114236
115062
  cropRegionToJpeg: async (bytes, w, h, norm) => (await extractCrop(bytes, w, h, norm)).crop,
114237
115063
  maxSkewMs: KEYFRAME_CROP_MAX_SKEW_MS,
114238
115064
  minSubjectIou: KEYFRAME_CROP_MIN_SUBJECT_IOU,
@@ -114775,9 +115601,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
114775
115601
  }));
114776
115602
  },
114777
115603
  readMedia: async (mediaId) => {
114778
- const file = await stores.mediaStore.getById(mediaId);
115604
+ const file = await stores.mediaStore.getBytesById(mediaId);
114779
115605
  if (file === null) return null;
114780
- return Buffer.from(file.base64, "base64");
115606
+ return file.bytes;
114781
115607
  },
114782
115608
  locationDescriptions: async () => {
114783
115609
  const rows = await api.deviceManager.listLocationDescriptions.query();
@@ -115030,8 +115856,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115030
115856
  const handler = createTrainingExportHandler({
115031
115857
  buildPlan: (deviceIds) => this.buildTrainingExportPlan(deviceIds),
115032
115858
  readBlob: async (mediaId) => {
115033
- const file = await (this.mediaStore?.getById(mediaId) ?? Promise.resolve(null));
115034
- return file === null ? null : Buffer.from(file.base64, "base64");
115859
+ const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
115860
+ return file === null ? null : file.bytes;
115035
115861
  },
115036
115862
  logger: this.ctx.logger,
115037
115863
  now: () => Date.now()
@@ -115501,8 +116327,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115501
116327
  const handler = createDebugExportHandler({
115502
116328
  buildPlan: (deviceIds) => buildDebugBundlePlan(this.buildDebugServiceDeps(), deviceIds),
115503
116329
  readMedia: async (key) => {
115504
- const file = await this.mediaStore?.getById(key);
115505
- return file === null || file === void 0 ? null : Buffer.from(file.base64, "base64");
116330
+ const file = await this.mediaStore?.getBytesById(key);
116331
+ return file === null || file === void 0 ? null : file.bytes;
115506
116332
  },
115507
116333
  logger: this.ctx.logger.child("debug-export"),
115508
116334
  now: () => Date.now()
@@ -115897,7 +116723,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115897
116723
  trackId: o.trackId,
115898
116724
  deviceId: o.deviceId
115899
116725
  })),
115900
- externalNonBufferMb: Math.max(0, Math.round((mem.external - mem.arrayBuffers) / 1048576))
116726
+ externalNonBufferMb: Math.max(0, Math.round((mem.external - mem.arrayBuffers) / 1048576)),
116727
+ mapSizes: {
116728
+ processors: this.processors.size,
116729
+ processorInputs: this.processorInputs.size,
116730
+ lastActiveTrackIds: this.lastActiveTrackIds.size,
116731
+ shortMotionlessCells: this.shortMotionlessCells.size,
116732
+ proxies: this.proxies.size,
116733
+ lastRunnerNodeByDevice: this.lastRunnerNodeByDevice.size,
116734
+ pendingConfirmationLooks: this.pendingConfirmationLooks.size,
116735
+ audioEpisodeTracks: this.audioEpisodeTracks.size,
116736
+ detailStepsByDevice: this.detailStepsByDevice.size
116737
+ }
115901
116738
  } });
115902
116739
  }
115903
116740
  /** Residents neither active in the TrackStore nor carried by the tracker
@@ -115920,6 +116757,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115920
116757
  this.faceReembedAutoTimer = setInterval(() => {
115921
116758
  this.maybeAutoReembedFaces().then(() => this.faceAlignmentJobs?.maybeAutoRunPhotoPass());
115922
116759
  }, FACE_REEMBED_AUTO_CHECK_MS);
116760
+ this.faceRescoreTimer = setInterval(() => {
116761
+ this.faceRescoreRuntimeOrNull()?.scheduler.tick().catch((err) => {
116762
+ this.ctx.logger.warn("face rescore tick failed", { meta: { error: String(err) } });
116763
+ });
116764
+ }, FACE_RESCORE_TICK_MS);
115923
116765
  this.faceGalleryPruneTimer = setInterval(() => {
115924
116766
  this.faceAlignmentJobs?.prune();
115925
116767
  }, FACE_GALLERY_PRUNE_INTERVAL_MS);
@@ -116332,9 +117174,35 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116332
117174
  ...this.sceneProvider !== null ? [{
116333
117175
  capability: require_clip_model_registry.sceneMonitorCapability,
116334
117176
  provider: this.sceneProvider
116335
- }] : []
117177
+ }] : [],
117178
+ {
117179
+ capability: require_clip_model_registry.deviceRemovalHookCapability,
117180
+ provider: this.removalHook
117181
+ }
116336
117182
  ];
116337
117183
  }
117184
+ /** The `device-removal-hook` provider (D865): told by device-manager, caught up from its tombstones. */
117185
+ get removalHook() {
117186
+ this.removalHookImpl ??= createAnalyticsRemovalHook({
117187
+ logger: this.ctx.logger,
117188
+ deleteDeviceEvents: (input) => this.deleteDeviceEvents(input),
117189
+ wipeAllAnalytics: (input) => this.wipeAllAnalytics(input),
117190
+ evictSensorEvents: async (deviceId, cutoffMs) => await this.sensorEventStore?.evictBefore(deviceId, cutoffMs) ?? 0,
117191
+ eventCountsByDevice: async () => await this.eventStore?.countByDevice() ?? /* @__PURE__ */ new Map(),
117192
+ trackDeviceIds: async () => await this.trackStore?.listDeviceIds() ?? []
117193
+ });
117194
+ return this.removalHookImpl;
117195
+ }
117196
+ removalHookImpl = null;
117197
+ /** Catch up on deletions missed while this addon was down (D865) — tombstones only. */
117198
+ async onHubReachable() {
117199
+ await super.onHubReachable();
117200
+ this.ctx.addDisposer(require_clip_model_registry.armRemovalReconcile({
117201
+ api: this.ctx.api,
117202
+ logger: this.ctx.logger,
117203
+ remove: (input) => this.removalHook.onDeviceRemoved(input)
117204
+ }));
117205
+ }
116338
117206
  /**
116339
117207
  * `BaseAddon.updateGlobalSettings` calls this after the write lands —
116340
117208
  * gated to the LOCAL node's own write (a sibling-node write returns before
@@ -116396,6 +117264,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116396
117264
  clearInterval(this.faceGalleryPruneTimer);
116397
117265
  this.faceGalleryPruneTimer = null;
116398
117266
  }
117267
+ if (this.faceRescoreTimer) {
117268
+ clearInterval(this.faceRescoreTimer);
117269
+ this.faceRescoreTimer = null;
117270
+ }
117271
+ this.faceRescoreRuntime?.job.stop();
116399
117272
  if (this.orphanAuditTimer) {
116400
117273
  clearInterval(this.orphanAuditTimer);
116401
117274
  this.orphanAuditTimer = null;
@@ -122341,8 +123214,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122341
123214
  loadAlignedCrop: async (sample) => {
122342
123215
  if (sample.mediaId === void 0) return null;
122343
123216
  try {
122344
- const file = await mediaStore.getById(sample.mediaId);
122345
- return file === null ? null : Buffer.from(file.base64, "base64");
123217
+ const file = await mediaStore.getBytesById(sample.mediaId);
123218
+ return file === null ? null : file.bytes;
122346
123219
  } catch (err) {
122347
123220
  this.ctx.logger.warn("face re-embed: aligned crop read threw", { meta: {
122348
123221
  sampleId: sample.sampleId,
@@ -122406,8 +123279,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122406
123279
  loadAlignedCrop: async (sample) => {
122407
123280
  if (sample.mediaId === void 0) return null;
122408
123281
  try {
122409
- const file = await mediaStore.getById(sample.mediaId);
122410
- return file === null ? null : Buffer.from(file.base64, "base64");
123282
+ const file = await mediaStore.getBytesById(sample.mediaId);
123283
+ return file === null ? null : file.bytes;
122411
123284
  } catch (err) {
122412
123285
  this.ctx.logger.warn("face sample re-embed: aligned crop read threw", { meta: {
122413
123286
  sampleId: sample.sampleId,
@@ -122467,8 +123340,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122467
123340
  loadAlignedCrop: async (sample) => {
122468
123341
  if (sample.mediaId === void 0) return null;
122469
123342
  try {
122470
- const file = await mediaStore.getById(sample.mediaId);
122471
- return file === null ? null : Buffer.from(file.base64, "base64");
123343
+ const file = await mediaStore.getBytesById(sample.mediaId);
123344
+ return file === null ? null : file.bytes;
122472
123345
  } catch (err) {
122473
123346
  this.ctx.logger.warn("face embedding audit: aligned crop read threw", { meta: {
122474
123347
  sampleId: sample.sampleId,
@@ -122614,9 +123487,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122614
123487
  };
122615
123488
  if (!api || !faceStore || !mediaStore) return refuse("failed", "the addon API or the face/media stores are not ready");
122616
123489
  if (face.mediaId === void 0) return refuse("no-pixels", "this face kept no crop — there is nothing to re-embed from");
122617
- const file = await mediaStore.getById(face.mediaId);
123490
+ const file = await mediaStore.getBytesById(face.mediaId);
122618
123491
  if (file === null) return refuse("no-pixels", `the crop blob ${face.mediaId} is gone`);
122619
- const raster = await decodeJpegToRgb(file.base64);
123492
+ const raster = await decodeJpegToRgb(file.bytes);
122620
123493
  const templatePx = PipelineAnalyticsAddon.ARCFACE_TEMPLATE_PX;
122621
123494
  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`);
122622
123495
  const outcome = await new FaceReembedEmbedder({
@@ -122629,7 +123502,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122629
123502
  modelId: face.embeddingModelId ?? LEGACY_MODEL_ID,
122630
123503
  dim: face.embedding.length,
122631
123504
  mediaId: face.mediaId
122632
- }, Buffer.from(file.base64, "base64"));
123505
+ }, file.bytes);
122633
123506
  if (outcome.kind === "no-capable-node") return refuse("no-capable-node", outcome.reason);
122634
123507
  if (outcome.kind === "failed") return refuse("failed", outcome.reason);
122635
123508
  if (!await faceStore.restampEmbedding({
@@ -122771,7 +123644,34 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122771
123644
  };
122772
123645
  }
122773
123646
  buildFaceRescoreActionHandlers() {
122774
- return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
123647
+ return {
123648
+ "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input),
123649
+ "face.rescoreAll": () => Promise.resolve(this.requireFaceRescoreRuntime("face.rescoreAll").job.start()),
123650
+ "face.rescoreStatus": () => Promise.resolve(this.requireFaceRescoreRuntime("face.rescoreStatus").job.status())
123651
+ };
123652
+ }
123653
+ /** The whole-archive rescore runtime, built once the stores exist; else null. */
123654
+ faceRescoreRuntimeOrNull() {
123655
+ if (this.faceRescoreRuntime) return this.faceRescoreRuntime;
123656
+ const api = this.ctx.api;
123657
+ const faceStore = this.faceStore;
123658
+ const identityStore = this.identityStore;
123659
+ const state = this.faceRescoreState;
123660
+ if (!api || !faceStore || !identityStore || !state) return null;
123661
+ this.faceRescoreRuntime = buildFaceRescoreRuntime({
123662
+ api,
123663
+ faceStore,
123664
+ identityStore,
123665
+ state,
123666
+ logger: this.ctx.logger.child("FaceRescore"),
123667
+ paramsForDevice: async (deviceId) => this.faceRescoreParams(await this.resolveDeviceFaceSettings(deviceId))
123668
+ }, Date.now());
123669
+ return this.faceRescoreRuntime;
123670
+ }
123671
+ requireFaceRescoreRuntime(action) {
123672
+ const runtime = this.faceRescoreRuntimeOrNull();
123673
+ if (!runtime) throw new Error(`${action}: the face stores are not ready`);
123674
+ return runtime;
122775
123675
  }
122776
123676
  buildStillnessCalibrationActionHandlers() {
122777
123677
  return { "stillness.calibrate": (input) => this.calibrateStillnessForDevice(input) };
@@ -124390,10 +125290,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
124390
125290
  return this.applyThumbVariant(base, variant);
124391
125291
  }
124392
125292
  async readMediaById(mediaId) {
124393
- const file = await (this.mediaStore?.getById(mediaId) ?? Promise.resolve(null));
125293
+ const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
124394
125294
  if (!file) return null;
124395
125295
  return {
124396
- bytes: Buffer.from(file.base64, "base64"),
125296
+ bytes: file.bytes,
124397
125297
  mediaId: file.mediaId,
124398
125298
  kind: file.kind,
124399
125299
  deviceId: file.deviceId
@@ -124448,9 +125348,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
124448
125348
  const listInfo = (ownerKind, id, deviceId) => this.mediaStore?.listInfoByOwner(ownerKind, id, deviceId) ?? Promise.resolve([]);
124449
125349
  const getTrackIdForEvent = (eventId) => require_clip_model_registry.isEventOwnerType(ownerType) ? this.eventStore?.getTrackIdForEvent(eventId, ownerType) ?? Promise.resolve(null) : Promise.resolve(null);
124450
125350
  const readBlob = async (mediaId) => {
124451
- const file = await (this.mediaStore?.getById(mediaId) ?? Promise.resolve(null));
125351
+ const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
124452
125352
  return file === null ? null : {
124453
- bytes: Buffer.from(file.base64, "base64"),
125353
+ bytes: file.bytes,
124454
125354
  mediaId: file.mediaId,
124455
125355
  kind: file.kind,
124456
125356
  deviceId: file.deviceId