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