@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.
- package/dist/{clip-model-registry-E4CGZ8NR.js → clip-model-registry-C3AUTkBR.js} +1618 -45
- package/dist/{clip-model-registry-C_-tWzrX.mjs → clip-model-registry-CCwyUQ7k.mjs} +1595 -46
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +752 -631
- 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
- 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
- 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
- package/dist/pipeline-analytics/{hostInit-CMFNXHOl.mjs → hostInit-s-NoTpFK.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +982 -82
- package/dist/pipeline-analytics/index.mjs +1013 -113
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +5 -1
- 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
- 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-
|
|
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
|
-
|
|
45992
|
+
bytes: await this.storage.read({
|
|
45977
45993
|
location: mediaRowLocation(data),
|
|
45978
45994
|
relativePath: path
|
|
45979
|
-
})
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
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
|
|
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).
|
|
57264
|
-
*
|
|
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
|
-
*
|
|
57538
|
-
*
|
|
57539
|
-
*
|
|
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
|
-
|
|
57545
|
-
|
|
57546
|
-
|
|
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
|
|
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)(
|
|
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") :
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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?.
|
|
113419
|
+
const m = await this.mediaStore?.getBytesById(mediaKey);
|
|
112600
113420
|
return m ? {
|
|
112601
|
-
|
|
113421
|
+
bytes: m.bytes,
|
|
112602
113422
|
timestamp: m.timestamp
|
|
112603
113423
|
} : null;
|
|
112604
113424
|
},
|
|
112605
|
-
decodeJpegToRgb: (
|
|
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.
|
|
113860
|
-
return file === null ? null :
|
|
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.
|
|
113890
|
-
return file === null ? null :
|
|
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.
|
|
115055
|
+
const m = await plateMediaStore.getBytesById(mediaKey);
|
|
114230
115056
|
return m ? {
|
|
114231
|
-
|
|
115057
|
+
bytes: m.bytes,
|
|
114232
115058
|
timestamp: m.timestamp
|
|
114233
115059
|
} : null;
|
|
114234
115060
|
},
|
|
114235
|
-
decodeJpegToRgb: (
|
|
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.
|
|
115604
|
+
const file = await stores.mediaStore.getBytesById(mediaId);
|
|
114779
115605
|
if (file === null) return null;
|
|
114780
|
-
return
|
|
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?.
|
|
115034
|
-
return file === null ? null :
|
|
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?.
|
|
115505
|
-
return file === null || file === void 0 ? null :
|
|
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.
|
|
122345
|
-
return file === null ? null :
|
|
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.
|
|
122410
|
-
return file === null ? null :
|
|
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.
|
|
122471
|
-
return file === null ? null :
|
|
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.
|
|
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.
|
|
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
|
-
},
|
|
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 {
|
|
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?.
|
|
125293
|
+
const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
|
|
124394
125294
|
if (!file) return null;
|
|
124395
125295
|
return {
|
|
124396
|
-
bytes:
|
|
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?.
|
|
125351
|
+
const file = await (this.mediaStore?.getBytesById(mediaId) ?? Promise.resolve(null));
|
|
124452
125352
|
return file === null ? null : {
|
|
124453
|
-
bytes:
|
|
125353
|
+
bytes: file.bytes,
|
|
124454
125354
|
mediaId: file.mediaId,
|
|
124455
125355
|
kind: file.kind,
|
|
124456
125356
|
deviceId: file.deviceId
|