@camstack/addon-post-analysis 1.2.94 → 1.2.95
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/pipeline-analytics/index.js +215 -141
- package/dist/pipeline-analytics/index.mjs +215 -141
- package/package.json +1 -1
|
@@ -6405,6 +6405,16 @@ var NcDeviceStateCache = class {
|
|
|
6405
6405
|
}
|
|
6406
6406
|
};
|
|
6407
6407
|
//#endregion
|
|
6408
|
+
//#region src/notification-center/default-tap-url.ts
|
|
6409
|
+
/** The viewer's hosted base path under the hub. */
|
|
6410
|
+
var VIEWER_BASE_PATH = "/viewer/camstack";
|
|
6411
|
+
function defaultTapUrl(input) {
|
|
6412
|
+
if (input.systemEvent === true) return null;
|
|
6413
|
+
if (input.baseUrl === null || input.baseUrl.length === 0) return null;
|
|
6414
|
+
if (!Number.isInteger(input.deviceId) || input.deviceId <= 0) return null;
|
|
6415
|
+
return `${input.baseUrl.replace(/\/+$/, "")}${VIEWER_BASE_PATH}/camera/${String(input.deviceId)}?at=${String(input.timestampMs)}`;
|
|
6416
|
+
}
|
|
6417
|
+
//#endregion
|
|
6408
6418
|
//#region src/shared/frame/box-drawer.ts
|
|
6409
6419
|
var DEFAULT_COLOR = require_dist.DEFAULT_EVENT_COLOR;
|
|
6410
6420
|
var DEFAULT_QUALITY = 80;
|
|
@@ -9262,12 +9272,19 @@ var NcDispatcher = class {
|
|
|
9262
9272
|
}) : rendered;
|
|
9263
9273
|
const attachments = [...await this.withArtifactUrls(entry, await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
|
|
9264
9274
|
const params = pickParams(entry.payload.params);
|
|
9275
|
+
const tapUrl = params.clickUrl === void 0 && this.deps.viewerUrlBase !== void 0 ? defaultTapUrl({
|
|
9276
|
+
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
9277
|
+
timestampMs: subject.timestamp,
|
|
9278
|
+
baseUrl: await this.deps.viewerUrlBase().catch(() => null),
|
|
9279
|
+
systemEvent: systemEvent !== void 0
|
|
9280
|
+
}) : null;
|
|
9265
9281
|
const actions = await this.mintActions(entry, target);
|
|
9266
9282
|
return {
|
|
9267
9283
|
body,
|
|
9268
9284
|
title,
|
|
9269
9285
|
priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
|
|
9270
9286
|
...params,
|
|
9287
|
+
...tapUrl !== null ? { clickUrl: tapUrl } : {},
|
|
9271
9288
|
tag: entry.payload.group?.key ?? entry.ruleId,
|
|
9272
9289
|
...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
|
|
9273
9290
|
...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
|
|
@@ -19148,7 +19165,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19148
19165
|
confirmGate: (input) => confirmGate.judge(input),
|
|
19149
19166
|
confirmStats: () => confirmGate.stats()
|
|
19150
19167
|
} : {},
|
|
19151
|
-
releaseCooldown: (input) => this.releaseCooldown(input)
|
|
19168
|
+
releaseCooldown: (input) => this.releaseCooldown(input),
|
|
19169
|
+
...deps.dispatcher.viewerUrlBase !== void 0 ? { viewerUrlBase: deps.dispatcher.viewerUrlBase } : {}
|
|
19152
19170
|
});
|
|
19153
19171
|
this.outbox = new NcOutbox({
|
|
19154
19172
|
store: deps.store,
|
|
@@ -24856,6 +24874,188 @@ function clusterByEmbedding(items, threshold = .5) {
|
|
|
24856
24874
|
})).toSorted((a, b) => b.size - a.size || b.cohesion - a.cohesion);
|
|
24857
24875
|
}
|
|
24858
24876
|
//#endregion
|
|
24877
|
+
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
24878
|
+
/**
|
|
24879
|
+
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
24880
|
+
* Only identities meeting `minIdentitySamples` are eligible auto-match targets.
|
|
24881
|
+
*/
|
|
24882
|
+
function eligibleIdentities(gallery, probeModelId, probeDim, minIdentitySamples) {
|
|
24883
|
+
const counts = /* @__PURE__ */ new Map();
|
|
24884
|
+
for (const s of gallery) {
|
|
24885
|
+
if (s.modelId !== probeModelId) continue;
|
|
24886
|
+
if (s.embedding.length !== probeDim) continue;
|
|
24887
|
+
counts.set(s.identityId, (counts.get(s.identityId) ?? 0) + 1);
|
|
24888
|
+
}
|
|
24889
|
+
const eligible = /* @__PURE__ */ new Set();
|
|
24890
|
+
for (const [id, count] of counts) if (count >= minIdentitySamples) eligible.add(id);
|
|
24891
|
+
return eligible;
|
|
24892
|
+
}
|
|
24893
|
+
/**
|
|
24894
|
+
* Match a probe embedding against the gallery. Only samples with the same
|
|
24895
|
+
* `modelId` AND the same dimension are compared (model-version safety). Returns
|
|
24896
|
+
* the best identity when its score ≥ threshold and it beats the best OTHER
|
|
24897
|
+
* identity by ≥ margin; otherwise null.
|
|
24898
|
+
*/
|
|
24899
|
+
function matchEmbedding(probe, gallery, opts) {
|
|
24900
|
+
const probeVec = new Float32Array(probe.embedding);
|
|
24901
|
+
const eligible = eligibleIdentities(gallery, probe.modelId, probe.embedding.length, opts.minIdentitySamples ?? 1);
|
|
24902
|
+
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
24903
|
+
for (const s of gallery) {
|
|
24904
|
+
if (s.modelId !== probe.modelId) continue;
|
|
24905
|
+
if (s.embedding.length !== probe.embedding.length) continue;
|
|
24906
|
+
if (!eligible.has(s.identityId)) continue;
|
|
24907
|
+
const score = require_dist.cosineSimilarity(probeVec, new Float32Array(s.embedding));
|
|
24908
|
+
const prev = bestByIdentity.get(s.identityId);
|
|
24909
|
+
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
24910
|
+
}
|
|
24911
|
+
if (bestByIdentity.size === 0) return null;
|
|
24912
|
+
let bestId = null;
|
|
24913
|
+
let bestScore = -Infinity;
|
|
24914
|
+
let secondScore = -Infinity;
|
|
24915
|
+
for (const [id, score] of bestByIdentity) if (score > bestScore) {
|
|
24916
|
+
secondScore = bestScore;
|
|
24917
|
+
bestScore = score;
|
|
24918
|
+
bestId = id;
|
|
24919
|
+
} else if (score > secondScore) secondScore = score;
|
|
24920
|
+
if (bestId === null || bestScore < opts.threshold) return null;
|
|
24921
|
+
if (secondScore > -Infinity && bestScore - secondScore < opts.margin) return null;
|
|
24922
|
+
return {
|
|
24923
|
+
identityId: bestId,
|
|
24924
|
+
score: bestScore
|
|
24925
|
+
};
|
|
24926
|
+
}
|
|
24927
|
+
/**
|
|
24928
|
+
* Assign at most one identity per track AND at most one track per identity for
|
|
24929
|
+
* a single frame. Greedy by score: compute every candidate's full ranked match
|
|
24930
|
+
* list, then repeatedly take the globally-highest (track, identity) pair whose
|
|
24931
|
+
* track and identity are both still free, until none qualify. A losing track
|
|
24932
|
+
* falls back to its next-best free identity ≥ threshold. Ties resolve by input order.
|
|
24933
|
+
*/
|
|
24934
|
+
/**
|
|
24935
|
+
* Enrollment bound: the maximum cosine an embedding may score against a sample
|
|
24936
|
+
* of a DIFFERENT identity and still be enrolled. Genuine ArcFace cross-person
|
|
24937
|
+
* cosines sit under ~0.4; the low-information attractor (IR, blur, backs of
|
|
24938
|
+
* heads) sits at 0.95+. Measured 2026-08-18: 22 of 50 live samples were
|
|
24939
|
+
* enrolled from that attractor, with inter-identity cosines up to 0.988 — each
|
|
24940
|
+
* one a magnet that "recognises" every degraded probe as its owner.
|
|
24941
|
+
*/
|
|
24942
|
+
var ENROLLMENT_MAX_FOREIGN_COSINE = .9;
|
|
24943
|
+
/**
|
|
24944
|
+
* Gate an embedding BEFORE it becomes a gallery sample: condemned when its
|
|
24945
|
+
* best cosine against any sample of ANOTHER identity (same model + dimension)
|
|
24946
|
+
* reaches {@link ENROLLMENT_MAX_FOREIGN_COSINE}. Null = safe to enroll.
|
|
24947
|
+
* Closeness to the TARGET identity's own samples is expected and never
|
|
24948
|
+
* condemns.
|
|
24949
|
+
*/
|
|
24950
|
+
function enrollmentPromiscuity(embedding, modelId, targetIdentityId, gallery, bound = ENROLLMENT_MAX_FOREIGN_COSINE) {
|
|
24951
|
+
const probeVec = new Float32Array(embedding);
|
|
24952
|
+
let worst = null;
|
|
24953
|
+
for (const s of gallery) {
|
|
24954
|
+
if (s.identityId === targetIdentityId) continue;
|
|
24955
|
+
if (s.modelId !== modelId) continue;
|
|
24956
|
+
if (s.embedding.length !== embedding.length) continue;
|
|
24957
|
+
const cosine = require_dist.cosineSimilarity(probeVec, new Float32Array(s.embedding));
|
|
24958
|
+
if (cosine >= bound && (worst === null || cosine > worst.cosine)) worst = {
|
|
24959
|
+
identityId: s.identityId,
|
|
24960
|
+
cosine
|
|
24961
|
+
};
|
|
24962
|
+
}
|
|
24963
|
+
return worst;
|
|
24964
|
+
}
|
|
24965
|
+
function assignUniquePerFrame(candidates, gallery, opts) {
|
|
24966
|
+
const pairs = [];
|
|
24967
|
+
candidates.forEach((c, trackIdx) => {
|
|
24968
|
+
const probeVec = new Float32Array(c.embedding);
|
|
24969
|
+
const eligible = eligibleIdentities(gallery, c.modelId, c.embedding.length, opts.minIdentitySamples ?? 1);
|
|
24970
|
+
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
24971
|
+
for (const s of gallery) {
|
|
24972
|
+
if (s.modelId !== c.modelId) continue;
|
|
24973
|
+
if (s.embedding.length !== c.embedding.length) continue;
|
|
24974
|
+
if (!eligible.has(s.identityId)) continue;
|
|
24975
|
+
const score = require_dist.cosineSimilarity(probeVec, new Float32Array(s.embedding));
|
|
24976
|
+
const prev = bestByIdentity.get(s.identityId);
|
|
24977
|
+
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
24978
|
+
}
|
|
24979
|
+
let bestId = null;
|
|
24980
|
+
let bestScore = -1;
|
|
24981
|
+
let secondScore = -1;
|
|
24982
|
+
for (const [identityId, score] of bestByIdentity) if (score > bestScore) {
|
|
24983
|
+
if (bestId !== null) secondScore = bestScore;
|
|
24984
|
+
bestId = identityId;
|
|
24985
|
+
bestScore = score;
|
|
24986
|
+
} else if (score > secondScore) secondScore = score;
|
|
24987
|
+
const ambiguous = secondScore >= 0 && bestScore - secondScore < opts.margin;
|
|
24988
|
+
if (bestId !== null && !ambiguous) {
|
|
24989
|
+
for (const [identityId, score] of bestByIdentity) if (score >= opts.threshold) pairs.push({
|
|
24990
|
+
trackIdx,
|
|
24991
|
+
identityId,
|
|
24992
|
+
score
|
|
24993
|
+
});
|
|
24994
|
+
}
|
|
24995
|
+
});
|
|
24996
|
+
pairs.sort((a, b) => b.score - a.score);
|
|
24997
|
+
const takenTracks = /* @__PURE__ */ new Set();
|
|
24998
|
+
const takenIdentities = /* @__PURE__ */ new Set();
|
|
24999
|
+
const result = /* @__PURE__ */ new Map();
|
|
25000
|
+
for (const p of pairs) {
|
|
25001
|
+
if (takenTracks.has(p.trackIdx) || takenIdentities.has(p.identityId)) continue;
|
|
25002
|
+
takenTracks.add(p.trackIdx);
|
|
25003
|
+
takenIdentities.add(p.identityId);
|
|
25004
|
+
result.set(candidates[p.trackIdx].trackId, {
|
|
25005
|
+
identityId: p.identityId,
|
|
25006
|
+
score: p.score
|
|
25007
|
+
});
|
|
25008
|
+
}
|
|
25009
|
+
return result;
|
|
25010
|
+
}
|
|
25011
|
+
var EMPTY_AGGREGATE = Object.freeze({
|
|
25012
|
+
assignedIdentityId: null,
|
|
25013
|
+
confirmations: Object.freeze({})
|
|
25014
|
+
});
|
|
25015
|
+
/**
|
|
25016
|
+
* Fold one frame's match into a track's aggregate. A null match leaves state
|
|
25017
|
+
* untouched. An identity is assigned once its cumulative confirmation count
|
|
25018
|
+
* reaches `confirmFrames`; once assigned, a different identity must independently
|
|
25019
|
+
* reach `confirmFrames` to switch. `changed` is true only on a (re)assignment to
|
|
25020
|
+
* a NEW identity, so callers fire the label update exactly once per transition.
|
|
25021
|
+
*
|
|
25022
|
+
* On each (re)assignment the confirmations record is reset to contain only the
|
|
25023
|
+
* newly-assigned identity's count, so any challenger must independently re-reach
|
|
25024
|
+
* `confirmFrames` from scratch after any assignment.
|
|
25025
|
+
*/
|
|
25026
|
+
function updateTrackAggregate(prev, match, opts) {
|
|
25027
|
+
if (match === null) return {
|
|
25028
|
+
state: prev,
|
|
25029
|
+
changed: false
|
|
25030
|
+
};
|
|
25031
|
+
const count = (prev.confirmations[match.identityId] ?? 0) + 1;
|
|
25032
|
+
const confirmations = {
|
|
25033
|
+
...prev.confirmations,
|
|
25034
|
+
[match.identityId]: count
|
|
25035
|
+
};
|
|
25036
|
+
if (count < opts.confirmFrames) return {
|
|
25037
|
+
state: {
|
|
25038
|
+
...prev,
|
|
25039
|
+
confirmations
|
|
25040
|
+
},
|
|
25041
|
+
changed: false
|
|
25042
|
+
};
|
|
25043
|
+
if (prev.assignedIdentityId === match.identityId) return {
|
|
25044
|
+
state: {
|
|
25045
|
+
...prev,
|
|
25046
|
+
confirmations
|
|
25047
|
+
},
|
|
25048
|
+
changed: false
|
|
25049
|
+
};
|
|
25050
|
+
return {
|
|
25051
|
+
state: {
|
|
25052
|
+
assignedIdentityId: match.identityId,
|
|
25053
|
+
confirmations: { [match.identityId]: count }
|
|
25054
|
+
},
|
|
25055
|
+
changed: true
|
|
25056
|
+
};
|
|
25057
|
+
}
|
|
25058
|
+
//#endregion
|
|
24859
25059
|
//#region src/pipeline-analytics/face-gallery-provider.ts
|
|
24860
25060
|
/** Attribution of a face identity NAMED BY THE OPERATOR in the gallery. */
|
|
24861
25061
|
var OPERATOR_FACE_STEP_ID = "operator:face-gallery";
|
|
@@ -25102,6 +25302,19 @@ var FaceGalleryProvider = class {
|
|
|
25102
25302
|
}
|
|
25103
25303
|
const currentFace = await this.faceStore.get(faceId);
|
|
25104
25304
|
if (!currentFace) throw new Error(`FaceGalleryProvider.assignFace: face disappeared after pre-cleanup: ${faceId}`);
|
|
25305
|
+
const verdict = enrollmentPromiscuity(currentFace.embedding, MODEL_ID, identityId, await this.identityStore.loadGallery());
|
|
25306
|
+
if (verdict !== null) {
|
|
25307
|
+
this.logger.warn("assignFace refused: embedding too close to a FOREIGN identity sample", {
|
|
25308
|
+
tags: { deviceId: currentFace.deviceId },
|
|
25309
|
+
meta: {
|
|
25310
|
+
faceId,
|
|
25311
|
+
identityId,
|
|
25312
|
+
foreignIdentityId: verdict.identityId,
|
|
25313
|
+
cosine: verdict.cosine
|
|
25314
|
+
}
|
|
25315
|
+
});
|
|
25316
|
+
throw new Error(`FaceGalleryProvider.assignFace: embedding is ambiguous — cosine ${verdict.cosine.toFixed(3)} with a sample of another identity (${verdict.identityId}); pick a clearer crop`);
|
|
25317
|
+
}
|
|
25105
25318
|
let newKey;
|
|
25106
25319
|
if (currentFace.mediaKey) {
|
|
25107
25320
|
newKey = await this.mediaStore.reown({
|
|
@@ -33552,146 +33765,6 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
33552
33765
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
33553
33766
|
}
|
|
33554
33767
|
//#endregion
|
|
33555
|
-
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
33556
|
-
/**
|
|
33557
|
-
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
33558
|
-
* Only identities meeting `minIdentitySamples` are eligible auto-match targets.
|
|
33559
|
-
*/
|
|
33560
|
-
function eligibleIdentities(gallery, probeModelId, probeDim, minIdentitySamples) {
|
|
33561
|
-
const counts = /* @__PURE__ */ new Map();
|
|
33562
|
-
for (const s of gallery) {
|
|
33563
|
-
if (s.modelId !== probeModelId) continue;
|
|
33564
|
-
if (s.embedding.length !== probeDim) continue;
|
|
33565
|
-
counts.set(s.identityId, (counts.get(s.identityId) ?? 0) + 1);
|
|
33566
|
-
}
|
|
33567
|
-
const eligible = /* @__PURE__ */ new Set();
|
|
33568
|
-
for (const [id, count] of counts) if (count >= minIdentitySamples) eligible.add(id);
|
|
33569
|
-
return eligible;
|
|
33570
|
-
}
|
|
33571
|
-
/**
|
|
33572
|
-
* Match a probe embedding against the gallery. Only samples with the same
|
|
33573
|
-
* `modelId` AND the same dimension are compared (model-version safety). Returns
|
|
33574
|
-
* the best identity when its score ≥ threshold and it beats the best OTHER
|
|
33575
|
-
* identity by ≥ margin; otherwise null.
|
|
33576
|
-
*/
|
|
33577
|
-
function matchEmbedding(probe, gallery, opts) {
|
|
33578
|
-
const probeVec = new Float32Array(probe.embedding);
|
|
33579
|
-
const eligible = eligibleIdentities(gallery, probe.modelId, probe.embedding.length, opts.minIdentitySamples ?? 1);
|
|
33580
|
-
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
33581
|
-
for (const s of gallery) {
|
|
33582
|
-
if (s.modelId !== probe.modelId) continue;
|
|
33583
|
-
if (s.embedding.length !== probe.embedding.length) continue;
|
|
33584
|
-
if (!eligible.has(s.identityId)) continue;
|
|
33585
|
-
const score = require_dist.cosineSimilarity(probeVec, new Float32Array(s.embedding));
|
|
33586
|
-
const prev = bestByIdentity.get(s.identityId);
|
|
33587
|
-
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
33588
|
-
}
|
|
33589
|
-
if (bestByIdentity.size === 0) return null;
|
|
33590
|
-
let bestId = null;
|
|
33591
|
-
let bestScore = -Infinity;
|
|
33592
|
-
let secondScore = -Infinity;
|
|
33593
|
-
for (const [id, score] of bestByIdentity) if (score > bestScore) {
|
|
33594
|
-
secondScore = bestScore;
|
|
33595
|
-
bestScore = score;
|
|
33596
|
-
bestId = id;
|
|
33597
|
-
} else if (score > secondScore) secondScore = score;
|
|
33598
|
-
if (bestId === null || bestScore < opts.threshold) return null;
|
|
33599
|
-
if (secondScore > -Infinity && bestScore - secondScore < opts.margin) return null;
|
|
33600
|
-
return {
|
|
33601
|
-
identityId: bestId,
|
|
33602
|
-
score: bestScore
|
|
33603
|
-
};
|
|
33604
|
-
}
|
|
33605
|
-
/**
|
|
33606
|
-
* Assign at most one identity per track AND at most one track per identity for
|
|
33607
|
-
* a single frame. Greedy by score: compute every candidate's full ranked match
|
|
33608
|
-
* list, then repeatedly take the globally-highest (track, identity) pair whose
|
|
33609
|
-
* track and identity are both still free, until none qualify. A losing track
|
|
33610
|
-
* falls back to its next-best free identity ≥ threshold. Ties resolve by input order.
|
|
33611
|
-
*/
|
|
33612
|
-
function assignUniquePerFrame(candidates, gallery, opts) {
|
|
33613
|
-
const pairs = [];
|
|
33614
|
-
candidates.forEach((c, trackIdx) => {
|
|
33615
|
-
const probeVec = new Float32Array(c.embedding);
|
|
33616
|
-
const eligible = eligibleIdentities(gallery, c.modelId, c.embedding.length, opts.minIdentitySamples ?? 1);
|
|
33617
|
-
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
33618
|
-
for (const s of gallery) {
|
|
33619
|
-
if (s.modelId !== c.modelId) continue;
|
|
33620
|
-
if (s.embedding.length !== c.embedding.length) continue;
|
|
33621
|
-
if (!eligible.has(s.identityId)) continue;
|
|
33622
|
-
const score = require_dist.cosineSimilarity(probeVec, new Float32Array(s.embedding));
|
|
33623
|
-
const prev = bestByIdentity.get(s.identityId);
|
|
33624
|
-
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
33625
|
-
}
|
|
33626
|
-
for (const [identityId, score] of bestByIdentity) if (score >= opts.threshold) pairs.push({
|
|
33627
|
-
trackIdx,
|
|
33628
|
-
identityId,
|
|
33629
|
-
score
|
|
33630
|
-
});
|
|
33631
|
-
});
|
|
33632
|
-
pairs.sort((a, b) => b.score - a.score);
|
|
33633
|
-
const takenTracks = /* @__PURE__ */ new Set();
|
|
33634
|
-
const takenIdentities = /* @__PURE__ */ new Set();
|
|
33635
|
-
const result = /* @__PURE__ */ new Map();
|
|
33636
|
-
for (const p of pairs) {
|
|
33637
|
-
if (takenTracks.has(p.trackIdx) || takenIdentities.has(p.identityId)) continue;
|
|
33638
|
-
takenTracks.add(p.trackIdx);
|
|
33639
|
-
takenIdentities.add(p.identityId);
|
|
33640
|
-
result.set(candidates[p.trackIdx].trackId, {
|
|
33641
|
-
identityId: p.identityId,
|
|
33642
|
-
score: p.score
|
|
33643
|
-
});
|
|
33644
|
-
}
|
|
33645
|
-
return result;
|
|
33646
|
-
}
|
|
33647
|
-
var EMPTY_AGGREGATE = Object.freeze({
|
|
33648
|
-
assignedIdentityId: null,
|
|
33649
|
-
confirmations: Object.freeze({})
|
|
33650
|
-
});
|
|
33651
|
-
/**
|
|
33652
|
-
* Fold one frame's match into a track's aggregate. A null match leaves state
|
|
33653
|
-
* untouched. An identity is assigned once its cumulative confirmation count
|
|
33654
|
-
* reaches `confirmFrames`; once assigned, a different identity must independently
|
|
33655
|
-
* reach `confirmFrames` to switch. `changed` is true only on a (re)assignment to
|
|
33656
|
-
* a NEW identity, so callers fire the label update exactly once per transition.
|
|
33657
|
-
*
|
|
33658
|
-
* On each (re)assignment the confirmations record is reset to contain only the
|
|
33659
|
-
* newly-assigned identity's count, so any challenger must independently re-reach
|
|
33660
|
-
* `confirmFrames` from scratch after any assignment.
|
|
33661
|
-
*/
|
|
33662
|
-
function updateTrackAggregate(prev, match, opts) {
|
|
33663
|
-
if (match === null) return {
|
|
33664
|
-
state: prev,
|
|
33665
|
-
changed: false
|
|
33666
|
-
};
|
|
33667
|
-
const count = (prev.confirmations[match.identityId] ?? 0) + 1;
|
|
33668
|
-
const confirmations = {
|
|
33669
|
-
...prev.confirmations,
|
|
33670
|
-
[match.identityId]: count
|
|
33671
|
-
};
|
|
33672
|
-
if (count < opts.confirmFrames) return {
|
|
33673
|
-
state: {
|
|
33674
|
-
...prev,
|
|
33675
|
-
confirmations
|
|
33676
|
-
},
|
|
33677
|
-
changed: false
|
|
33678
|
-
};
|
|
33679
|
-
if (prev.assignedIdentityId === match.identityId) return {
|
|
33680
|
-
state: {
|
|
33681
|
-
...prev,
|
|
33682
|
-
confirmations
|
|
33683
|
-
},
|
|
33684
|
-
changed: false
|
|
33685
|
-
};
|
|
33686
|
-
return {
|
|
33687
|
-
state: {
|
|
33688
|
-
assignedIdentityId: match.identityId,
|
|
33689
|
-
confirmations: { [match.identityId]: count }
|
|
33690
|
-
},
|
|
33691
|
-
changed: true
|
|
33692
|
-
};
|
|
33693
|
-
}
|
|
33694
|
-
//#endregion
|
|
33695
33768
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
33696
33769
|
/** Attribution of an identity resolved by the in-process ArcFace matcher. */
|
|
33697
33770
|
var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
|
|
@@ -52396,6 +52469,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52396
52469
|
},
|
|
52397
52470
|
renderEventMedia: (req) => this.renderEventMedia(req),
|
|
52398
52471
|
publishArtifact: async (bytes, mime) => await this.ncArtifactPlane?.publish(bytes, mime) ?? null,
|
|
52472
|
+
viewerUrlBase: async () => pickArtifactBaseUrl(await this.ncEndpoints()),
|
|
52399
52473
|
indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
|
|
52400
52474
|
url: null,
|
|
52401
52475
|
artifactId: null
|
|
@@ -6398,6 +6398,16 @@ var NcDeviceStateCache = class {
|
|
|
6398
6398
|
}
|
|
6399
6399
|
};
|
|
6400
6400
|
//#endregion
|
|
6401
|
+
//#region src/notification-center/default-tap-url.ts
|
|
6402
|
+
/** The viewer's hosted base path under the hub. */
|
|
6403
|
+
var VIEWER_BASE_PATH = "/viewer/camstack";
|
|
6404
|
+
function defaultTapUrl(input) {
|
|
6405
|
+
if (input.systemEvent === true) return null;
|
|
6406
|
+
if (input.baseUrl === null || input.baseUrl.length === 0) return null;
|
|
6407
|
+
if (!Number.isInteger(input.deviceId) || input.deviceId <= 0) return null;
|
|
6408
|
+
return `${input.baseUrl.replace(/\/+$/, "")}${VIEWER_BASE_PATH}/camera/${String(input.deviceId)}?at=${String(input.timestampMs)}`;
|
|
6409
|
+
}
|
|
6410
|
+
//#endregion
|
|
6401
6411
|
//#region src/shared/frame/box-drawer.ts
|
|
6402
6412
|
var DEFAULT_COLOR = DEFAULT_EVENT_COLOR;
|
|
6403
6413
|
var DEFAULT_QUALITY = 80;
|
|
@@ -9255,12 +9265,19 @@ var NcDispatcher = class {
|
|
|
9255
9265
|
}) : rendered;
|
|
9256
9266
|
const attachments = [...await this.withArtifactUrls(entry, await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
|
|
9257
9267
|
const params = pickParams(entry.payload.params);
|
|
9268
|
+
const tapUrl = params.clickUrl === void 0 && this.deps.viewerUrlBase !== void 0 ? defaultTapUrl({
|
|
9269
|
+
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
9270
|
+
timestampMs: subject.timestamp,
|
|
9271
|
+
baseUrl: await this.deps.viewerUrlBase().catch(() => null),
|
|
9272
|
+
systemEvent: systemEvent !== void 0
|
|
9273
|
+
}) : null;
|
|
9258
9274
|
const actions = await this.mintActions(entry, target);
|
|
9259
9275
|
return {
|
|
9260
9276
|
body,
|
|
9261
9277
|
title,
|
|
9262
9278
|
priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
|
|
9263
9279
|
...params,
|
|
9280
|
+
...tapUrl !== null ? { clickUrl: tapUrl } : {},
|
|
9264
9281
|
tag: entry.payload.group?.key ?? entry.ruleId,
|
|
9265
9282
|
...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
|
|
9266
9283
|
...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
|
|
@@ -19126,7 +19143,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19126
19143
|
confirmGate: (input) => confirmGate.judge(input),
|
|
19127
19144
|
confirmStats: () => confirmGate.stats()
|
|
19128
19145
|
} : {},
|
|
19129
|
-
releaseCooldown: (input) => this.releaseCooldown(input)
|
|
19146
|
+
releaseCooldown: (input) => this.releaseCooldown(input),
|
|
19147
|
+
...deps.dispatcher.viewerUrlBase !== void 0 ? { viewerUrlBase: deps.dispatcher.viewerUrlBase } : {}
|
|
19130
19148
|
});
|
|
19131
19149
|
this.outbox = new NcOutbox({
|
|
19132
19150
|
store: deps.store,
|
|
@@ -24834,6 +24852,188 @@ function clusterByEmbedding(items, threshold = .5) {
|
|
|
24834
24852
|
})).toSorted((a, b) => b.size - a.size || b.cohesion - a.cohesion);
|
|
24835
24853
|
}
|
|
24836
24854
|
//#endregion
|
|
24855
|
+
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
24856
|
+
/**
|
|
24857
|
+
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
24858
|
+
* Only identities meeting `minIdentitySamples` are eligible auto-match targets.
|
|
24859
|
+
*/
|
|
24860
|
+
function eligibleIdentities(gallery, probeModelId, probeDim, minIdentitySamples) {
|
|
24861
|
+
const counts = /* @__PURE__ */ new Map();
|
|
24862
|
+
for (const s of gallery) {
|
|
24863
|
+
if (s.modelId !== probeModelId) continue;
|
|
24864
|
+
if (s.embedding.length !== probeDim) continue;
|
|
24865
|
+
counts.set(s.identityId, (counts.get(s.identityId) ?? 0) + 1);
|
|
24866
|
+
}
|
|
24867
|
+
const eligible = /* @__PURE__ */ new Set();
|
|
24868
|
+
for (const [id, count] of counts) if (count >= minIdentitySamples) eligible.add(id);
|
|
24869
|
+
return eligible;
|
|
24870
|
+
}
|
|
24871
|
+
/**
|
|
24872
|
+
* Match a probe embedding against the gallery. Only samples with the same
|
|
24873
|
+
* `modelId` AND the same dimension are compared (model-version safety). Returns
|
|
24874
|
+
* the best identity when its score ≥ threshold and it beats the best OTHER
|
|
24875
|
+
* identity by ≥ margin; otherwise null.
|
|
24876
|
+
*/
|
|
24877
|
+
function matchEmbedding(probe, gallery, opts) {
|
|
24878
|
+
const probeVec = new Float32Array(probe.embedding);
|
|
24879
|
+
const eligible = eligibleIdentities(gallery, probe.modelId, probe.embedding.length, opts.minIdentitySamples ?? 1);
|
|
24880
|
+
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
24881
|
+
for (const s of gallery) {
|
|
24882
|
+
if (s.modelId !== probe.modelId) continue;
|
|
24883
|
+
if (s.embedding.length !== probe.embedding.length) continue;
|
|
24884
|
+
if (!eligible.has(s.identityId)) continue;
|
|
24885
|
+
const score = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
|
|
24886
|
+
const prev = bestByIdentity.get(s.identityId);
|
|
24887
|
+
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
24888
|
+
}
|
|
24889
|
+
if (bestByIdentity.size === 0) return null;
|
|
24890
|
+
let bestId = null;
|
|
24891
|
+
let bestScore = -Infinity;
|
|
24892
|
+
let secondScore = -Infinity;
|
|
24893
|
+
for (const [id, score] of bestByIdentity) if (score > bestScore) {
|
|
24894
|
+
secondScore = bestScore;
|
|
24895
|
+
bestScore = score;
|
|
24896
|
+
bestId = id;
|
|
24897
|
+
} else if (score > secondScore) secondScore = score;
|
|
24898
|
+
if (bestId === null || bestScore < opts.threshold) return null;
|
|
24899
|
+
if (secondScore > -Infinity && bestScore - secondScore < opts.margin) return null;
|
|
24900
|
+
return {
|
|
24901
|
+
identityId: bestId,
|
|
24902
|
+
score: bestScore
|
|
24903
|
+
};
|
|
24904
|
+
}
|
|
24905
|
+
/**
|
|
24906
|
+
* Assign at most one identity per track AND at most one track per identity for
|
|
24907
|
+
* a single frame. Greedy by score: compute every candidate's full ranked match
|
|
24908
|
+
* list, then repeatedly take the globally-highest (track, identity) pair whose
|
|
24909
|
+
* track and identity are both still free, until none qualify. A losing track
|
|
24910
|
+
* falls back to its next-best free identity ≥ threshold. Ties resolve by input order.
|
|
24911
|
+
*/
|
|
24912
|
+
/**
|
|
24913
|
+
* Enrollment bound: the maximum cosine an embedding may score against a sample
|
|
24914
|
+
* of a DIFFERENT identity and still be enrolled. Genuine ArcFace cross-person
|
|
24915
|
+
* cosines sit under ~0.4; the low-information attractor (IR, blur, backs of
|
|
24916
|
+
* heads) sits at 0.95+. Measured 2026-08-18: 22 of 50 live samples were
|
|
24917
|
+
* enrolled from that attractor, with inter-identity cosines up to 0.988 — each
|
|
24918
|
+
* one a magnet that "recognises" every degraded probe as its owner.
|
|
24919
|
+
*/
|
|
24920
|
+
var ENROLLMENT_MAX_FOREIGN_COSINE = .9;
|
|
24921
|
+
/**
|
|
24922
|
+
* Gate an embedding BEFORE it becomes a gallery sample: condemned when its
|
|
24923
|
+
* best cosine against any sample of ANOTHER identity (same model + dimension)
|
|
24924
|
+
* reaches {@link ENROLLMENT_MAX_FOREIGN_COSINE}. Null = safe to enroll.
|
|
24925
|
+
* Closeness to the TARGET identity's own samples is expected and never
|
|
24926
|
+
* condemns.
|
|
24927
|
+
*/
|
|
24928
|
+
function enrollmentPromiscuity(embedding, modelId, targetIdentityId, gallery, bound = ENROLLMENT_MAX_FOREIGN_COSINE) {
|
|
24929
|
+
const probeVec = new Float32Array(embedding);
|
|
24930
|
+
let worst = null;
|
|
24931
|
+
for (const s of gallery) {
|
|
24932
|
+
if (s.identityId === targetIdentityId) continue;
|
|
24933
|
+
if (s.modelId !== modelId) continue;
|
|
24934
|
+
if (s.embedding.length !== embedding.length) continue;
|
|
24935
|
+
const cosine = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
|
|
24936
|
+
if (cosine >= bound && (worst === null || cosine > worst.cosine)) worst = {
|
|
24937
|
+
identityId: s.identityId,
|
|
24938
|
+
cosine
|
|
24939
|
+
};
|
|
24940
|
+
}
|
|
24941
|
+
return worst;
|
|
24942
|
+
}
|
|
24943
|
+
function assignUniquePerFrame(candidates, gallery, opts) {
|
|
24944
|
+
const pairs = [];
|
|
24945
|
+
candidates.forEach((c, trackIdx) => {
|
|
24946
|
+
const probeVec = new Float32Array(c.embedding);
|
|
24947
|
+
const eligible = eligibleIdentities(gallery, c.modelId, c.embedding.length, opts.minIdentitySamples ?? 1);
|
|
24948
|
+
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
24949
|
+
for (const s of gallery) {
|
|
24950
|
+
if (s.modelId !== c.modelId) continue;
|
|
24951
|
+
if (s.embedding.length !== c.embedding.length) continue;
|
|
24952
|
+
if (!eligible.has(s.identityId)) continue;
|
|
24953
|
+
const score = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
|
|
24954
|
+
const prev = bestByIdentity.get(s.identityId);
|
|
24955
|
+
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
24956
|
+
}
|
|
24957
|
+
let bestId = null;
|
|
24958
|
+
let bestScore = -1;
|
|
24959
|
+
let secondScore = -1;
|
|
24960
|
+
for (const [identityId, score] of bestByIdentity) if (score > bestScore) {
|
|
24961
|
+
if (bestId !== null) secondScore = bestScore;
|
|
24962
|
+
bestId = identityId;
|
|
24963
|
+
bestScore = score;
|
|
24964
|
+
} else if (score > secondScore) secondScore = score;
|
|
24965
|
+
const ambiguous = secondScore >= 0 && bestScore - secondScore < opts.margin;
|
|
24966
|
+
if (bestId !== null && !ambiguous) {
|
|
24967
|
+
for (const [identityId, score] of bestByIdentity) if (score >= opts.threshold) pairs.push({
|
|
24968
|
+
trackIdx,
|
|
24969
|
+
identityId,
|
|
24970
|
+
score
|
|
24971
|
+
});
|
|
24972
|
+
}
|
|
24973
|
+
});
|
|
24974
|
+
pairs.sort((a, b) => b.score - a.score);
|
|
24975
|
+
const takenTracks = /* @__PURE__ */ new Set();
|
|
24976
|
+
const takenIdentities = /* @__PURE__ */ new Set();
|
|
24977
|
+
const result = /* @__PURE__ */ new Map();
|
|
24978
|
+
for (const p of pairs) {
|
|
24979
|
+
if (takenTracks.has(p.trackIdx) || takenIdentities.has(p.identityId)) continue;
|
|
24980
|
+
takenTracks.add(p.trackIdx);
|
|
24981
|
+
takenIdentities.add(p.identityId);
|
|
24982
|
+
result.set(candidates[p.trackIdx].trackId, {
|
|
24983
|
+
identityId: p.identityId,
|
|
24984
|
+
score: p.score
|
|
24985
|
+
});
|
|
24986
|
+
}
|
|
24987
|
+
return result;
|
|
24988
|
+
}
|
|
24989
|
+
var EMPTY_AGGREGATE = Object.freeze({
|
|
24990
|
+
assignedIdentityId: null,
|
|
24991
|
+
confirmations: Object.freeze({})
|
|
24992
|
+
});
|
|
24993
|
+
/**
|
|
24994
|
+
* Fold one frame's match into a track's aggregate. A null match leaves state
|
|
24995
|
+
* untouched. An identity is assigned once its cumulative confirmation count
|
|
24996
|
+
* reaches `confirmFrames`; once assigned, a different identity must independently
|
|
24997
|
+
* reach `confirmFrames` to switch. `changed` is true only on a (re)assignment to
|
|
24998
|
+
* a NEW identity, so callers fire the label update exactly once per transition.
|
|
24999
|
+
*
|
|
25000
|
+
* On each (re)assignment the confirmations record is reset to contain only the
|
|
25001
|
+
* newly-assigned identity's count, so any challenger must independently re-reach
|
|
25002
|
+
* `confirmFrames` from scratch after any assignment.
|
|
25003
|
+
*/
|
|
25004
|
+
function updateTrackAggregate(prev, match, opts) {
|
|
25005
|
+
if (match === null) return {
|
|
25006
|
+
state: prev,
|
|
25007
|
+
changed: false
|
|
25008
|
+
};
|
|
25009
|
+
const count = (prev.confirmations[match.identityId] ?? 0) + 1;
|
|
25010
|
+
const confirmations = {
|
|
25011
|
+
...prev.confirmations,
|
|
25012
|
+
[match.identityId]: count
|
|
25013
|
+
};
|
|
25014
|
+
if (count < opts.confirmFrames) return {
|
|
25015
|
+
state: {
|
|
25016
|
+
...prev,
|
|
25017
|
+
confirmations
|
|
25018
|
+
},
|
|
25019
|
+
changed: false
|
|
25020
|
+
};
|
|
25021
|
+
if (prev.assignedIdentityId === match.identityId) return {
|
|
25022
|
+
state: {
|
|
25023
|
+
...prev,
|
|
25024
|
+
confirmations
|
|
25025
|
+
},
|
|
25026
|
+
changed: false
|
|
25027
|
+
};
|
|
25028
|
+
return {
|
|
25029
|
+
state: {
|
|
25030
|
+
assignedIdentityId: match.identityId,
|
|
25031
|
+
confirmations: { [match.identityId]: count }
|
|
25032
|
+
},
|
|
25033
|
+
changed: true
|
|
25034
|
+
};
|
|
25035
|
+
}
|
|
25036
|
+
//#endregion
|
|
24837
25037
|
//#region src/pipeline-analytics/face-gallery-provider.ts
|
|
24838
25038
|
/** Attribution of a face identity NAMED BY THE OPERATOR in the gallery. */
|
|
24839
25039
|
var OPERATOR_FACE_STEP_ID = "operator:face-gallery";
|
|
@@ -25080,6 +25280,19 @@ var FaceGalleryProvider = class {
|
|
|
25080
25280
|
}
|
|
25081
25281
|
const currentFace = await this.faceStore.get(faceId);
|
|
25082
25282
|
if (!currentFace) throw new Error(`FaceGalleryProvider.assignFace: face disappeared after pre-cleanup: ${faceId}`);
|
|
25283
|
+
const verdict = enrollmentPromiscuity(currentFace.embedding, MODEL_ID, identityId, await this.identityStore.loadGallery());
|
|
25284
|
+
if (verdict !== null) {
|
|
25285
|
+
this.logger.warn("assignFace refused: embedding too close to a FOREIGN identity sample", {
|
|
25286
|
+
tags: { deviceId: currentFace.deviceId },
|
|
25287
|
+
meta: {
|
|
25288
|
+
faceId,
|
|
25289
|
+
identityId,
|
|
25290
|
+
foreignIdentityId: verdict.identityId,
|
|
25291
|
+
cosine: verdict.cosine
|
|
25292
|
+
}
|
|
25293
|
+
});
|
|
25294
|
+
throw new Error(`FaceGalleryProvider.assignFace: embedding is ambiguous — cosine ${verdict.cosine.toFixed(3)} with a sample of another identity (${verdict.identityId}); pick a clearer crop`);
|
|
25295
|
+
}
|
|
25083
25296
|
let newKey;
|
|
25084
25297
|
if (currentFace.mediaKey) {
|
|
25085
25298
|
newKey = await this.mediaStore.reown({
|
|
@@ -33492,146 +33705,6 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
33492
33705
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
33493
33706
|
}
|
|
33494
33707
|
//#endregion
|
|
33495
|
-
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
33496
|
-
/**
|
|
33497
|
-
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
33498
|
-
* Only identities meeting `minIdentitySamples` are eligible auto-match targets.
|
|
33499
|
-
*/
|
|
33500
|
-
function eligibleIdentities(gallery, probeModelId, probeDim, minIdentitySamples) {
|
|
33501
|
-
const counts = /* @__PURE__ */ new Map();
|
|
33502
|
-
for (const s of gallery) {
|
|
33503
|
-
if (s.modelId !== probeModelId) continue;
|
|
33504
|
-
if (s.embedding.length !== probeDim) continue;
|
|
33505
|
-
counts.set(s.identityId, (counts.get(s.identityId) ?? 0) + 1);
|
|
33506
|
-
}
|
|
33507
|
-
const eligible = /* @__PURE__ */ new Set();
|
|
33508
|
-
for (const [id, count] of counts) if (count >= minIdentitySamples) eligible.add(id);
|
|
33509
|
-
return eligible;
|
|
33510
|
-
}
|
|
33511
|
-
/**
|
|
33512
|
-
* Match a probe embedding against the gallery. Only samples with the same
|
|
33513
|
-
* `modelId` AND the same dimension are compared (model-version safety). Returns
|
|
33514
|
-
* the best identity when its score ≥ threshold and it beats the best OTHER
|
|
33515
|
-
* identity by ≥ margin; otherwise null.
|
|
33516
|
-
*/
|
|
33517
|
-
function matchEmbedding(probe, gallery, opts) {
|
|
33518
|
-
const probeVec = new Float32Array(probe.embedding);
|
|
33519
|
-
const eligible = eligibleIdentities(gallery, probe.modelId, probe.embedding.length, opts.minIdentitySamples ?? 1);
|
|
33520
|
-
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
33521
|
-
for (const s of gallery) {
|
|
33522
|
-
if (s.modelId !== probe.modelId) continue;
|
|
33523
|
-
if (s.embedding.length !== probe.embedding.length) continue;
|
|
33524
|
-
if (!eligible.has(s.identityId)) continue;
|
|
33525
|
-
const score = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
|
|
33526
|
-
const prev = bestByIdentity.get(s.identityId);
|
|
33527
|
-
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
33528
|
-
}
|
|
33529
|
-
if (bestByIdentity.size === 0) return null;
|
|
33530
|
-
let bestId = null;
|
|
33531
|
-
let bestScore = -Infinity;
|
|
33532
|
-
let secondScore = -Infinity;
|
|
33533
|
-
for (const [id, score] of bestByIdentity) if (score > bestScore) {
|
|
33534
|
-
secondScore = bestScore;
|
|
33535
|
-
bestScore = score;
|
|
33536
|
-
bestId = id;
|
|
33537
|
-
} else if (score > secondScore) secondScore = score;
|
|
33538
|
-
if (bestId === null || bestScore < opts.threshold) return null;
|
|
33539
|
-
if (secondScore > -Infinity && bestScore - secondScore < opts.margin) return null;
|
|
33540
|
-
return {
|
|
33541
|
-
identityId: bestId,
|
|
33542
|
-
score: bestScore
|
|
33543
|
-
};
|
|
33544
|
-
}
|
|
33545
|
-
/**
|
|
33546
|
-
* Assign at most one identity per track AND at most one track per identity for
|
|
33547
|
-
* a single frame. Greedy by score: compute every candidate's full ranked match
|
|
33548
|
-
* list, then repeatedly take the globally-highest (track, identity) pair whose
|
|
33549
|
-
* track and identity are both still free, until none qualify. A losing track
|
|
33550
|
-
* falls back to its next-best free identity ≥ threshold. Ties resolve by input order.
|
|
33551
|
-
*/
|
|
33552
|
-
function assignUniquePerFrame(candidates, gallery, opts) {
|
|
33553
|
-
const pairs = [];
|
|
33554
|
-
candidates.forEach((c, trackIdx) => {
|
|
33555
|
-
const probeVec = new Float32Array(c.embedding);
|
|
33556
|
-
const eligible = eligibleIdentities(gallery, c.modelId, c.embedding.length, opts.minIdentitySamples ?? 1);
|
|
33557
|
-
const bestByIdentity = /* @__PURE__ */ new Map();
|
|
33558
|
-
for (const s of gallery) {
|
|
33559
|
-
if (s.modelId !== c.modelId) continue;
|
|
33560
|
-
if (s.embedding.length !== c.embedding.length) continue;
|
|
33561
|
-
if (!eligible.has(s.identityId)) continue;
|
|
33562
|
-
const score = cosineSimilarity$1(probeVec, new Float32Array(s.embedding));
|
|
33563
|
-
const prev = bestByIdentity.get(s.identityId);
|
|
33564
|
-
if (prev === void 0 || score > prev) bestByIdentity.set(s.identityId, score);
|
|
33565
|
-
}
|
|
33566
|
-
for (const [identityId, score] of bestByIdentity) if (score >= opts.threshold) pairs.push({
|
|
33567
|
-
trackIdx,
|
|
33568
|
-
identityId,
|
|
33569
|
-
score
|
|
33570
|
-
});
|
|
33571
|
-
});
|
|
33572
|
-
pairs.sort((a, b) => b.score - a.score);
|
|
33573
|
-
const takenTracks = /* @__PURE__ */ new Set();
|
|
33574
|
-
const takenIdentities = /* @__PURE__ */ new Set();
|
|
33575
|
-
const result = /* @__PURE__ */ new Map();
|
|
33576
|
-
for (const p of pairs) {
|
|
33577
|
-
if (takenTracks.has(p.trackIdx) || takenIdentities.has(p.identityId)) continue;
|
|
33578
|
-
takenTracks.add(p.trackIdx);
|
|
33579
|
-
takenIdentities.add(p.identityId);
|
|
33580
|
-
result.set(candidates[p.trackIdx].trackId, {
|
|
33581
|
-
identityId: p.identityId,
|
|
33582
|
-
score: p.score
|
|
33583
|
-
});
|
|
33584
|
-
}
|
|
33585
|
-
return result;
|
|
33586
|
-
}
|
|
33587
|
-
var EMPTY_AGGREGATE = Object.freeze({
|
|
33588
|
-
assignedIdentityId: null,
|
|
33589
|
-
confirmations: Object.freeze({})
|
|
33590
|
-
});
|
|
33591
|
-
/**
|
|
33592
|
-
* Fold one frame's match into a track's aggregate. A null match leaves state
|
|
33593
|
-
* untouched. An identity is assigned once its cumulative confirmation count
|
|
33594
|
-
* reaches `confirmFrames`; once assigned, a different identity must independently
|
|
33595
|
-
* reach `confirmFrames` to switch. `changed` is true only on a (re)assignment to
|
|
33596
|
-
* a NEW identity, so callers fire the label update exactly once per transition.
|
|
33597
|
-
*
|
|
33598
|
-
* On each (re)assignment the confirmations record is reset to contain only the
|
|
33599
|
-
* newly-assigned identity's count, so any challenger must independently re-reach
|
|
33600
|
-
* `confirmFrames` from scratch after any assignment.
|
|
33601
|
-
*/
|
|
33602
|
-
function updateTrackAggregate(prev, match, opts) {
|
|
33603
|
-
if (match === null) return {
|
|
33604
|
-
state: prev,
|
|
33605
|
-
changed: false
|
|
33606
|
-
};
|
|
33607
|
-
const count = (prev.confirmations[match.identityId] ?? 0) + 1;
|
|
33608
|
-
const confirmations = {
|
|
33609
|
-
...prev.confirmations,
|
|
33610
|
-
[match.identityId]: count
|
|
33611
|
-
};
|
|
33612
|
-
if (count < opts.confirmFrames) return {
|
|
33613
|
-
state: {
|
|
33614
|
-
...prev,
|
|
33615
|
-
confirmations
|
|
33616
|
-
},
|
|
33617
|
-
changed: false
|
|
33618
|
-
};
|
|
33619
|
-
if (prev.assignedIdentityId === match.identityId) return {
|
|
33620
|
-
state: {
|
|
33621
|
-
...prev,
|
|
33622
|
-
confirmations
|
|
33623
|
-
},
|
|
33624
|
-
changed: false
|
|
33625
|
-
};
|
|
33626
|
-
return {
|
|
33627
|
-
state: {
|
|
33628
|
-
assignedIdentityId: match.identityId,
|
|
33629
|
-
confirmations: { [match.identityId]: count }
|
|
33630
|
-
},
|
|
33631
|
-
changed: true
|
|
33632
|
-
};
|
|
33633
|
-
}
|
|
33634
|
-
//#endregion
|
|
33635
33708
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
33636
33709
|
/** Attribution of an identity resolved by the in-process ArcFace matcher. */
|
|
33637
33710
|
var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
|
|
@@ -52336,6 +52409,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52336
52409
|
},
|
|
52337
52410
|
renderEventMedia: (req) => this.renderEventMedia(req),
|
|
52338
52411
|
publishArtifact: async (bytes, mime) => await this.ncArtifactPlane?.publish(bytes, mime) ?? null,
|
|
52412
|
+
viewerUrlBase: async () => pickArtifactBaseUrl(await this.ncEndpoints()),
|
|
52339
52413
|
indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
|
|
52340
52414
|
url: null,
|
|
52341
52415
|
artifactId: null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.95",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|