@camstack/addon-post-analysis 1.2.100 → 1.2.102
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/{dist-BQsMSX2A.js → dist-BwxXdV9d.js} +31 -51
- package/dist/{dist-ecY_-nDx.mjs → dist-CZm8slPB.mjs} +31 -51
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Caz0R4UZ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-MP5xue-m.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-By6c47y7.mjs → hostInit-CkptLue6.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +823 -189
- package/dist/pipeline-analytics/index.mjs +823 -189
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BwxXdV9d.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -8304,23 +8304,23 @@ var en_default = {
|
|
|
8304
8304
|
},
|
|
8305
8305
|
"class.other": "{{name}}",
|
|
8306
8306
|
"group.others.person": {
|
|
8307
|
-
"one": "
|
|
8307
|
+
"one": "{{count}} other person",
|
|
8308
8308
|
"other": "{{count}} other people"
|
|
8309
8309
|
},
|
|
8310
8310
|
"group.others.vehicle": {
|
|
8311
|
-
"one": "
|
|
8311
|
+
"one": "{{count}} other vehicle",
|
|
8312
8312
|
"other": "{{count}} other vehicles"
|
|
8313
8313
|
},
|
|
8314
8314
|
"group.others.animal": {
|
|
8315
|
-
"one": "
|
|
8315
|
+
"one": "{{count}} other animal",
|
|
8316
8316
|
"other": "{{count}} other animals"
|
|
8317
8317
|
},
|
|
8318
8318
|
"group.others.package": {
|
|
8319
|
-
"one": "
|
|
8319
|
+
"one": "{{count}} other package",
|
|
8320
8320
|
"other": "{{count}} other packages"
|
|
8321
8321
|
},
|
|
8322
8322
|
"group.others.other": {
|
|
8323
|
-
"one": "
|
|
8323
|
+
"one": "{{count}} other {{classNoun}}",
|
|
8324
8324
|
"other": "{{count}} other {{classNoun}}"
|
|
8325
8325
|
},
|
|
8326
8326
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
@@ -8359,8 +8359,11 @@ var en_default = {
|
|
|
8359
8359
|
"one": "{{count}} addon update available",
|
|
8360
8360
|
"other": "{{count}} addon updates available"
|
|
8361
8361
|
},
|
|
8362
|
+
"system.addon-update-available.title.onNode": {
|
|
8363
|
+
"one": "{{count}} addon update available on {{nodeId}}",
|
|
8364
|
+
"other": "{{count}} addon updates available on {{nodeId}}"
|
|
8365
|
+
},
|
|
8362
8366
|
"system.addon-update-available.body": "{{list}}",
|
|
8363
|
-
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8364
8367
|
"system.server-update-available.title": {
|
|
8365
8368
|
"one": "Server update available on {{count}} node",
|
|
8366
8369
|
"other": "Server update available on {{count}} nodes"
|
|
@@ -8375,6 +8378,7 @@ var en_default = {
|
|
|
8375
8378
|
"system.addon-updated.body": "{{list}}",
|
|
8376
8379
|
"system.server-updated.title": "Server updated",
|
|
8377
8380
|
"system.server-updated.body": "{{packageName}} on {{nodeId}} is now {{toVersion}} (was {{fromVersion}})",
|
|
8381
|
+
"system.server-updated.body.toOnly": "{{packageName}} on {{nodeId}} is now {{toVersion}}",
|
|
8378
8382
|
"system.server-updated.body.plain": "{{packageName}} on {{nodeId}} was updated",
|
|
8379
8383
|
"system.export-completed.title": "Export ready",
|
|
8380
8384
|
"system.export-completed.body": "The footage export is ready",
|
|
@@ -8518,23 +8522,23 @@ var it_default = {
|
|
|
8518
8522
|
},
|
|
8519
8523
|
"class.other": "{{name}}",
|
|
8520
8524
|
"group.others.person": {
|
|
8521
|
-
"one": "
|
|
8525
|
+
"one": "{{count}} altra persona",
|
|
8522
8526
|
"other": "{{count}} altre persone"
|
|
8523
8527
|
},
|
|
8524
8528
|
"group.others.vehicle": {
|
|
8525
|
-
"one": "
|
|
8529
|
+
"one": "{{count}} altro veicolo",
|
|
8526
8530
|
"other": "{{count}} altri veicoli"
|
|
8527
8531
|
},
|
|
8528
8532
|
"group.others.animal": {
|
|
8529
|
-
"one": "
|
|
8533
|
+
"one": "{{count}} altro animale",
|
|
8530
8534
|
"other": "{{count}} altri animali"
|
|
8531
8535
|
},
|
|
8532
8536
|
"group.others.package": {
|
|
8533
|
-
"one": "
|
|
8537
|
+
"one": "{{count}} altro pacco",
|
|
8534
8538
|
"other": "{{count}} altri pacchi"
|
|
8535
8539
|
},
|
|
8536
8540
|
"group.others.other": {
|
|
8537
|
-
"one": "
|
|
8541
|
+
"one": "{{count}} altro {{classNoun}}",
|
|
8538
8542
|
"other": "{{count}} altri {{classNoun}}"
|
|
8539
8543
|
},
|
|
8540
8544
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
@@ -8573,8 +8577,11 @@ var it_default = {
|
|
|
8573
8577
|
"one": "{{count}} aggiornamento addon disponibile",
|
|
8574
8578
|
"other": "{{count}} aggiornamenti addon disponibili"
|
|
8575
8579
|
},
|
|
8580
|
+
"system.addon-update-available.title.onNode": {
|
|
8581
|
+
"one": "{{count}} aggiornamento addon disponibile su {{nodeId}}",
|
|
8582
|
+
"other": "{{count}} aggiornamenti addon disponibili su {{nodeId}}"
|
|
8583
|
+
},
|
|
8576
8584
|
"system.addon-update-available.body": "{{list}}",
|
|
8577
|
-
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8578
8585
|
"system.server-update-available.title": {
|
|
8579
8586
|
"one": "Aggiornamento server disponibile su {{count}} nodo",
|
|
8580
8587
|
"other": "Aggiornamento server disponibile su {{count}} nodi"
|
|
@@ -8589,6 +8596,7 @@ var it_default = {
|
|
|
8589
8596
|
"system.addon-updated.body": "{{list}}",
|
|
8590
8597
|
"system.server-updated.title": "Server aggiornato",
|
|
8591
8598
|
"system.server-updated.body": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}} (era {{fromVersion}})",
|
|
8599
|
+
"system.server-updated.body.toOnly": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}}",
|
|
8592
8600
|
"system.server-updated.body.plain": "{{packageName}} sul nodo {{nodeId}} è stato aggiornato",
|
|
8593
8601
|
"system.export-completed.title": "Export pronto",
|
|
8594
8602
|
"system.export-completed.body": "L'export del filmato è pronto",
|
|
@@ -10411,15 +10419,15 @@ function incomingFromUpdateAvailable(facts, data) {
|
|
|
10411
10419
|
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
|
|
10412
10420
|
const packageNames = unique(packages.map((pkg) => pkg.packageName));
|
|
10413
10421
|
const signature = packages.map((pkg) => `${pkg.nodeId ?? ""}:${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",");
|
|
10414
|
-
const list = packages.map(formatAvailableLine).join("\n");
|
|
10415
|
-
const nodes = nodeIds.join(", ");
|
|
10416
10422
|
const head = packages[0];
|
|
10417
10423
|
const oneNode = nodeIds.length === 1 ? nodeIds[0] : void 0;
|
|
10424
|
+
const list = packages.map((pkg) => formatAvailableLine(pkg, oneNode !== void 0)).join("\n");
|
|
10425
|
+
const nodes = nodeIds.join(", ");
|
|
10418
10426
|
return systemSubject({
|
|
10419
10427
|
facts,
|
|
10420
10428
|
kind,
|
|
10421
10429
|
subject: addon ? `packages:addon:${signature}` : `packages:server:${signature}`,
|
|
10422
|
-
titleKey: `system.${kind}.title`,
|
|
10430
|
+
titleKey: addon && oneNode !== void 0 ? `system.${kind}.title.onNode` : `system.${kind}.title`,
|
|
10423
10431
|
bodyKey: !addon && oneNode !== void 0 ? `system.${kind}.body.onNode` : !addon && nodeIds.length > 1 ? `system.${kind}.body.onNodes` : `system.${kind}.body`,
|
|
10424
10432
|
textParams: {
|
|
10425
10433
|
list,
|
|
@@ -10478,12 +10486,12 @@ function incomingFromAddonUpdates(facts, updates) {
|
|
|
10478
10486
|
function incomingFromServerUpdated(facts, data) {
|
|
10479
10487
|
const kind = stringField(data, "kind");
|
|
10480
10488
|
const requestedBy = stringField(data, "requestedBy");
|
|
10481
|
-
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:"))) return null;
|
|
10482
10489
|
const packageName = stringField(data, "packageName") ?? "@camstack/server";
|
|
10483
10490
|
const fromVersion = stringField(data, "fromVersion");
|
|
10484
10491
|
const toVersion = stringField(data, "toVersion") ?? versionFromRequestedBy(requestedBy);
|
|
10492
|
+
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:") && toVersion !== void 0)) return null;
|
|
10485
10493
|
const nodeId = stringField(data, "nodeId") ?? "hub";
|
|
10486
|
-
const bodyKey =
|
|
10494
|
+
const bodyKey = toVersion === void 0 ? "system.server-updated.body.plain" : fromVersion === void 0 ? "system.server-updated.body.toOnly" : "system.server-updated.body";
|
|
10487
10495
|
return systemSubject({
|
|
10488
10496
|
facts,
|
|
10489
10497
|
kind: "server-updated",
|
|
@@ -10564,9 +10572,15 @@ function updatePackages(data) {
|
|
|
10564
10572
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10565
10573
|
}];
|
|
10566
10574
|
}
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10575
|
+
/**
|
|
10576
|
+
* One line of an availability list — the SAME shape the completed-update list
|
|
10577
|
+
* uses (`incomingFromAddonUpdates`), and deliberately word-free: the list is a
|
|
10578
|
+
* frozen param, so any English in it ("current") is delivered verbatim to an
|
|
10579
|
+
* Italian household. An arrow is not a language.
|
|
10580
|
+
*/
|
|
10581
|
+
function formatAvailableLine(pkg, nodeNamedInTitle) {
|
|
10582
|
+
const line = `${pkg.packageName} ${pkg.currentVersion} → ${pkg.latestVersion}`;
|
|
10583
|
+
return pkg.nodeId !== void 0 && !nodeNamedInTitle ? `${line} (${pkg.nodeId})` : line;
|
|
10570
10584
|
}
|
|
10571
10585
|
function versionFromRequestedBy(requestedBy) {
|
|
10572
10586
|
if (requestedBy === void 0) return void 0;
|
|
@@ -28811,19 +28825,29 @@ var BEST_FRAME_POLICY = {
|
|
|
28811
28825
|
sizeAware: true
|
|
28812
28826
|
};
|
|
28813
28827
|
/**
|
|
28814
|
-
* Best FACE (face-recognizer hold buffer) —
|
|
28815
|
-
*
|
|
28816
|
-
*
|
|
28817
|
-
*
|
|
28818
|
-
*
|
|
28819
|
-
*
|
|
28820
|
-
*
|
|
28828
|
+
* Best FACE (face-recognizer hold buffer) — SIZE-AWARE, no hysteresis, no rate
|
|
28829
|
+
* limit.
|
|
28830
|
+
*
|
|
28831
|
+
* No hysteresis / no gap, unchanged: accepting a new best only swaps an
|
|
28832
|
+
* already-decoded crop buffer in memory (cheap; no capture is triggered), and a
|
|
28833
|
+
* track may live well under the best-frame rate-limit window, so a gap would
|
|
28834
|
+
* silently discard the real peak for nothing.
|
|
28835
|
+
*
|
|
28836
|
+
* `sizeAware` since 2026-08-19. It used to be confidence-ONLY, and that was the
|
|
28837
|
+
* wrong ranking: SCRFD saturates its confidence on surveillance footage, so a
|
|
28838
|
+
* 12 px face and a 40 px face both answer ~0.9 and the track simply kept
|
|
28839
|
+
* whichever arrived first. The size that matters is the face's DETECTION-FRAME
|
|
28840
|
+
* short side, because that bounds the landmark precision the alignment warp
|
|
28841
|
+
* consumes — `face-size-score.ts` carries the measurement that set the
|
|
28842
|
+
* reference. Because `minGapMs` is 0 here, enabling this does NOT switch on the
|
|
28843
|
+
* decisive-improvement rate-limit bypass (there is no rate limit to bypass); it
|
|
28844
|
+
* only adds the size factor to the ranking.
|
|
28821
28845
|
*/
|
|
28822
28846
|
var BEST_FACE_POLICY = {
|
|
28823
28847
|
hysteresis: 0,
|
|
28824
28848
|
minGapMs: 0,
|
|
28825
28849
|
edgeAware: false,
|
|
28826
|
-
sizeAware:
|
|
28850
|
+
sizeAware: true
|
|
28827
28851
|
};
|
|
28828
28852
|
/**
|
|
28829
28853
|
* Best OBJECT (CLIP) embedding + its tight crop — confidence-only in-memory
|
|
@@ -31058,8 +31082,14 @@ function reportUnvouchedClips(deps, deviceId, clips) {
|
|
|
31058
31082
|
* @durable class=config owner=pipeline-analytics
|
|
31059
31083
|
* write="a signed-in viewer user saves a named snapshot of their local stores"
|
|
31060
31084
|
* retention="none — a row goes only when its owner deletes it"
|
|
31085
|
+
*
|
|
31086
|
+
* Renamed from the bare `viewer-settings-snapshots` on 2026-08-19: an addon
|
|
31087
|
+
* collection carries its owner's prefix, and the bare name sat in the same flat
|
|
31088
|
+
* namespace as the kernel's `users` / `system-settings`. Rows already written
|
|
31089
|
+
* under the old name are adopted at boot by
|
|
31090
|
+
* `viewer-settings-legacy-adoption.ts`.
|
|
31061
31091
|
*/
|
|
31062
|
-
var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
|
|
31092
|
+
var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
|
|
31063
31093
|
var COLUMNS = [
|
|
31064
31094
|
{
|
|
31065
31095
|
name: "id",
|
|
@@ -31159,7 +31189,7 @@ var ViewerSettingsSnapshotStore = class {
|
|
|
31159
31189
|
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31160
31190
|
columns: [...COLUMNS],
|
|
31161
31191
|
indexes: [{
|
|
31162
|
-
name: "
|
|
31192
|
+
name: "idx_pa_viewer_settings_owner",
|
|
31163
31193
|
columns: ["ownerUserId"]
|
|
31164
31194
|
}]
|
|
31165
31195
|
});
|
|
@@ -31258,6 +31288,183 @@ function makeViewerSettingsActionHandlers(store) {
|
|
|
31258
31288
|
}
|
|
31259
31289
|
};
|
|
31260
31290
|
}
|
|
31291
|
+
//#endregion
|
|
31292
|
+
//#region src/pipeline-analytics/viewer-settings-legacy-adoption.ts
|
|
31293
|
+
/**
|
|
31294
|
+
* @durable class=config owner=pipeline-analytics
|
|
31295
|
+
* write="NOTHING writes it any more — it is the pre-2026-08-19 bare name of
|
|
31296
|
+
* `pipeline-analytics:viewer-settings-snapshots`, declared here only so its
|
|
31297
|
+
* surviving rows can be READ once and moved"
|
|
31298
|
+
* retention="drained, not pruned: a row goes as soon as the successor is
|
|
31299
|
+
* confirmed to hold the same id. The table is empty and inert once every
|
|
31300
|
+
* hub has booted this addon; it is then dropped through the kernel's
|
|
31301
|
+
* `RETIRED_COLLECTIONS` and this module deleted."
|
|
31302
|
+
*
|
|
31303
|
+
* Bare, and therefore a namespacing violation — hence the single
|
|
31304
|
+
* `ALLOWED_BARE` entry in `scripts/check-collection-namespacing.ts`, which
|
|
31305
|
+
* names this file and the condition for removing both.
|
|
31306
|
+
*/
|
|
31307
|
+
var LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
|
|
31308
|
+
/**
|
|
31309
|
+
* The ORIGINAL column list, byte-for-byte.
|
|
31310
|
+
*
|
|
31311
|
+
* Do not "tidy" this to reference the successor's constant: they are allowed to
|
|
31312
|
+
* diverge, and the day they do, a shared constant would silently rebuild the
|
|
31313
|
+
* legacy table and destroy the rows.
|
|
31314
|
+
*/
|
|
31315
|
+
var LEGACY_COLUMNS = [
|
|
31316
|
+
{
|
|
31317
|
+
name: "id",
|
|
31318
|
+
type: "TEXT",
|
|
31319
|
+
primaryKey: true,
|
|
31320
|
+
notNull: true
|
|
31321
|
+
},
|
|
31322
|
+
{
|
|
31323
|
+
name: "name",
|
|
31324
|
+
type: "TEXT",
|
|
31325
|
+
notNull: true
|
|
31326
|
+
},
|
|
31327
|
+
{
|
|
31328
|
+
name: "ownerUserId",
|
|
31329
|
+
type: "TEXT",
|
|
31330
|
+
notNull: true
|
|
31331
|
+
},
|
|
31332
|
+
{
|
|
31333
|
+
name: "ownerUsername",
|
|
31334
|
+
type: "TEXT",
|
|
31335
|
+
notNull: true
|
|
31336
|
+
},
|
|
31337
|
+
{
|
|
31338
|
+
name: "createdAt",
|
|
31339
|
+
type: "INTEGER",
|
|
31340
|
+
notNull: true
|
|
31341
|
+
},
|
|
31342
|
+
{
|
|
31343
|
+
name: "updatedAt",
|
|
31344
|
+
type: "INTEGER",
|
|
31345
|
+
notNull: true
|
|
31346
|
+
},
|
|
31347
|
+
{
|
|
31348
|
+
name: "blob",
|
|
31349
|
+
type: "JSON",
|
|
31350
|
+
notNull: true
|
|
31351
|
+
}
|
|
31352
|
+
];
|
|
31353
|
+
/** Bounded read: this table held 1 row on the reference hub. */
|
|
31354
|
+
var LEGACY_READ_LIMIT = 500;
|
|
31355
|
+
function errText(err) {
|
|
31356
|
+
return err instanceof Error ? err.message : String(err);
|
|
31357
|
+
}
|
|
31358
|
+
function asRows(raw) {
|
|
31359
|
+
if (!Array.isArray(raw)) return [];
|
|
31360
|
+
const out = [];
|
|
31361
|
+
for (const item of raw) {
|
|
31362
|
+
if (typeof item !== "object" || item === null) continue;
|
|
31363
|
+
const id = Reflect.get(item, "id");
|
|
31364
|
+
const data = Reflect.get(item, "data");
|
|
31365
|
+
if (typeof id !== "string" || typeof data !== "object" || data === null) continue;
|
|
31366
|
+
out.push({
|
|
31367
|
+
id,
|
|
31368
|
+
data
|
|
31369
|
+
});
|
|
31370
|
+
}
|
|
31371
|
+
return out;
|
|
31372
|
+
}
|
|
31373
|
+
/**
|
|
31374
|
+
* Move every legacy row into the successor and clear the legacy table.
|
|
31375
|
+
*
|
|
31376
|
+
* Returns the number of legacy rows RETIRED — the completion metric, not the
|
|
31377
|
+
* number written. A row copied but not yet verified counts as zero and is
|
|
31378
|
+
* retried on the next boot, which is the safe direction.
|
|
31379
|
+
*/
|
|
31380
|
+
async function adoptLegacyViewerSettingsSnapshots(deps) {
|
|
31381
|
+
await deps.store.declareCollection.mutate({
|
|
31382
|
+
collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31383
|
+
columns: LEGACY_COLUMNS
|
|
31384
|
+
});
|
|
31385
|
+
let legacy;
|
|
31386
|
+
try {
|
|
31387
|
+
legacy = asRows(await deps.store.query.query({
|
|
31388
|
+
collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31389
|
+
filter: { limit: LEGACY_READ_LIMIT }
|
|
31390
|
+
}));
|
|
31391
|
+
} catch (err) {
|
|
31392
|
+
deps.logger.warn("viewer-settings legacy adoption skipped — legacy read failed", { meta: {
|
|
31393
|
+
collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31394
|
+
error: errText(err)
|
|
31395
|
+
} });
|
|
31396
|
+
return 0;
|
|
31397
|
+
}
|
|
31398
|
+
if (legacy.length === 0) return 0;
|
|
31399
|
+
let existing;
|
|
31400
|
+
try {
|
|
31401
|
+
existing = new Set(asRows(await deps.store.query.query({
|
|
31402
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31403
|
+
filter: { limit: LEGACY_READ_LIMIT }
|
|
31404
|
+
})).map((r) => r.id));
|
|
31405
|
+
} catch (err) {
|
|
31406
|
+
deps.logger.warn("viewer-settings legacy adoption skipped — successor read failed", { meta: {
|
|
31407
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31408
|
+
error: errText(err)
|
|
31409
|
+
} });
|
|
31410
|
+
return 0;
|
|
31411
|
+
}
|
|
31412
|
+
let written = 0;
|
|
31413
|
+
for (const row of legacy) {
|
|
31414
|
+
if (existing.has(row.id)) continue;
|
|
31415
|
+
try {
|
|
31416
|
+
await deps.store.set.mutate({
|
|
31417
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31418
|
+
key: row.id,
|
|
31419
|
+
value: row.data
|
|
31420
|
+
});
|
|
31421
|
+
written += 1;
|
|
31422
|
+
} catch (err) {
|
|
31423
|
+
deps.logger.warn("viewer-settings legacy adoption — one row failed to write", { meta: {
|
|
31424
|
+
snapshotId: row.id,
|
|
31425
|
+
error: errText(err)
|
|
31426
|
+
} });
|
|
31427
|
+
}
|
|
31428
|
+
}
|
|
31429
|
+
let landed;
|
|
31430
|
+
try {
|
|
31431
|
+
landed = new Set(asRows(await deps.store.query.query({
|
|
31432
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31433
|
+
filter: { limit: LEGACY_READ_LIMIT }
|
|
31434
|
+
})).map((r) => r.id));
|
|
31435
|
+
} catch (err) {
|
|
31436
|
+
deps.logger.warn("viewer-settings legacy adoption — verification read failed, legacy rows kept", { meta: {
|
|
31437
|
+
written,
|
|
31438
|
+
error: errText(err)
|
|
31439
|
+
} });
|
|
31440
|
+
return 0;
|
|
31441
|
+
}
|
|
31442
|
+
let retired = 0;
|
|
31443
|
+
for (const row of legacy) {
|
|
31444
|
+
if (!landed.has(row.id)) continue;
|
|
31445
|
+
try {
|
|
31446
|
+
await deps.store.delete.mutate({
|
|
31447
|
+
collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31448
|
+
key: row.id
|
|
31449
|
+
});
|
|
31450
|
+
retired += 1;
|
|
31451
|
+
} catch (err) {
|
|
31452
|
+
deps.logger.warn("viewer-settings legacy adoption — one legacy row failed to delete", { meta: {
|
|
31453
|
+
snapshotId: row.id,
|
|
31454
|
+
error: errText(err)
|
|
31455
|
+
} });
|
|
31456
|
+
}
|
|
31457
|
+
}
|
|
31458
|
+
deps.logger.warn("viewer-settings snapshots adopted from the legacy bare collection", { meta: {
|
|
31459
|
+
from: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31460
|
+
to: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
31461
|
+
found: legacy.length,
|
|
31462
|
+
written,
|
|
31463
|
+
retired,
|
|
31464
|
+
kept: legacy.length - retired
|
|
31465
|
+
} });
|
|
31466
|
+
return retired;
|
|
31467
|
+
}
|
|
31261
31468
|
/**
|
|
31262
31469
|
* The VIDEO's playback rate — 4×, the same as {@link NC_GIF_SPEED}.
|
|
31263
31470
|
*
|
|
@@ -32065,7 +32272,11 @@ var AudioDetectionSettingsSchema = require_dist.object({
|
|
|
32065
32272
|
* thresholds, is what bounds the worst case — a mis-tuned threshold can
|
|
32066
32273
|
* only ever cost the operator this many markers a day. */
|
|
32067
32274
|
audioMarkerMaxPerDay: require_dist.number().int().positive().default(12),
|
|
32068
|
-
/** Materialize one event + one synthetic track per classified episode.
|
|
32275
|
+
/** Materialize one event + one synthetic track per classified episode.
|
|
32276
|
+
* OFF switches off the GROUPING only — classified audio still persists an
|
|
32277
|
+
* event (class change, then one per 5 s heartbeat) and still reaches the
|
|
32278
|
+
* Notification Center; it just opens no synthetic track. A knob that names
|
|
32279
|
+
* the grouping must never silence the path (D62). */
|
|
32069
32280
|
audioEpisodeEnabled: require_dist.boolean().default(true),
|
|
32070
32281
|
/** Class must be heard continuously this long before the episode opens. */
|
|
32071
32282
|
audioEpisodeOpenHoldSec: require_dist.number().nonnegative().default(1.5),
|
|
@@ -34285,11 +34496,44 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
34285
34496
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
34286
34497
|
}
|
|
34287
34498
|
//#endregion
|
|
34499
|
+
//#region src/pipeline-analytics/pipeline/embedding-magnitude-gate.ts
|
|
34500
|
+
/**
|
|
34501
|
+
* `null` = accept. A verdict = refuse, carrying both numbers so the caller can
|
|
34502
|
+
* name them in a log.
|
|
34503
|
+
*
|
|
34504
|
+
* Accepts unconditionally when the bound is 0 (disabled) or when the magnitude
|
|
34505
|
+
* is absent / non-finite — an older runner that does not report it must keep
|
|
34506
|
+
* working exactly as before, never be silently gated to nothing. Pure.
|
|
34507
|
+
*/
|
|
34508
|
+
function embeddingMagnitudeVerdict(magnitude, bound) {
|
|
34509
|
+
if (!(bound > 0)) return null;
|
|
34510
|
+
if (magnitude === void 0 || !Number.isFinite(magnitude)) return null;
|
|
34511
|
+
if (magnitude <= bound) return null;
|
|
34512
|
+
return {
|
|
34513
|
+
magnitude,
|
|
34514
|
+
bound
|
|
34515
|
+
};
|
|
34516
|
+
}
|
|
34517
|
+
/**
|
|
34518
|
+
* Map a detection-frame face short side to the 0..1 `sizeScore` the
|
|
34519
|
+
* {@link ./best-detection-tracker.BestDetectionTracker} multiplies into its
|
|
34520
|
+
* effective score. Linear up to {@link FACE_SIZE_SCORE_REFERENCE_PX}, saturated
|
|
34521
|
+
* above it, 0 for a missing/degenerate measurement (so an unmeasurable face can
|
|
34522
|
+
* never outrank a measured one). Pure.
|
|
34523
|
+
*/
|
|
34524
|
+
function faceSizeScore(detectionFrameShortSidePx) {
|
|
34525
|
+
if (!Number.isFinite(detectionFrameShortSidePx) || detectionFrameShortSidePx <= 0) return 0;
|
|
34526
|
+
if (detectionFrameShortSidePx >= 32) return 1;
|
|
34527
|
+
return detectionFrameShortSidePx / 32;
|
|
34528
|
+
}
|
|
34529
|
+
//#endregion
|
|
34288
34530
|
//#region src/pipeline-analytics/face-recognizer.ts
|
|
34289
34531
|
/** Attribution of an identity resolved by the in-process ArcFace matcher. */
|
|
34290
34532
|
var FACE_RECOGNIZER_STEP_ID = "face-recognizer";
|
|
34291
34533
|
/** At most one "dropping imageless track" log per this interval, per recognizer. */
|
|
34292
34534
|
var FACE_IMAGELESS_LOG_THROTTLE_MS = 6e4;
|
|
34535
|
+
/** At most one 'embedding magnitude' sample line per device per this interval. */
|
|
34536
|
+
var FACE_MAGNITUDE_LOG_THROTTLE_MS = 6e4;
|
|
34293
34537
|
/**
|
|
34294
34538
|
* arcface model id stamped on a detail-plane face candidate when the gallery is
|
|
34295
34539
|
* empty (collect-only). The detail-subtree result carries no `embeddingModelId`
|
|
@@ -34304,6 +34548,8 @@ var FaceRecognizer = class {
|
|
|
34304
34548
|
names = /* @__PURE__ */ new Map();
|
|
34305
34549
|
aggregates = /* @__PURE__ */ new Map();
|
|
34306
34550
|
bestFace = /* @__PURE__ */ new Map();
|
|
34551
|
+
/** Per-device throttle clock for the magnitude sample line. */
|
|
34552
|
+
lastMagnitudeLogAt = /* @__PURE__ */ new Map();
|
|
34307
34553
|
/**
|
|
34308
34554
|
* Peak identity-MATCH cosine per track for the currently-assigned identity
|
|
34309
34555
|
* (NOT the detection confidence held in {@link bestFace}). Reset on an
|
|
@@ -34353,6 +34599,21 @@ var FaceRecognizer = class {
|
|
|
34353
34599
|
* input source moves from the per-frame plane to this per-track call.
|
|
34354
34600
|
*/
|
|
34355
34601
|
async ingestFaceDetail(input) {
|
|
34602
|
+
const verdict = embeddingMagnitudeVerdict(input.embeddingMagnitude, input.settings.maxEmbeddingMagnitude);
|
|
34603
|
+
if (verdict !== null) {
|
|
34604
|
+
this.deps.logger.info("face: dropping detail — raw embedding magnitude over bound", {
|
|
34605
|
+
tags: { deviceId: input.deviceId },
|
|
34606
|
+
meta: {
|
|
34607
|
+
trackId: input.trackId,
|
|
34608
|
+
magnitude: Number(verdict.magnitude.toFixed(3)),
|
|
34609
|
+
bound: verdict.bound,
|
|
34610
|
+
score: input.score,
|
|
34611
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: input.nativeFaceShortSidePx } : {}
|
|
34612
|
+
}
|
|
34613
|
+
});
|
|
34614
|
+
return;
|
|
34615
|
+
}
|
|
34616
|
+
this.logEmbeddingMagnitude(input);
|
|
34356
34617
|
const modelId = this.gallery[0]?.modelId ?? FALLBACK_FACE_MODEL_ID;
|
|
34357
34618
|
const candidate = {
|
|
34358
34619
|
trackId: input.trackId,
|
|
@@ -34379,6 +34640,30 @@ var FaceRecognizer = class {
|
|
|
34379
34640
|
...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
|
|
34380
34641
|
});
|
|
34381
34642
|
}
|
|
34643
|
+
/**
|
|
34644
|
+
* At most one magnitude line per device per
|
|
34645
|
+
* {@link FACE_MAGNITUDE_LOG_THROTTLE_MS}. This is the ONLY way the operator
|
|
34646
|
+
* can see where this cluster's usable and degenerate face populations sit, so
|
|
34647
|
+
* it must exist before `maxEmbeddingMagnitude` can honestly be turned on.
|
|
34648
|
+
*/
|
|
34649
|
+
logEmbeddingMagnitude(input) {
|
|
34650
|
+
const magnitude = input.embeddingMagnitude;
|
|
34651
|
+
if (magnitude === void 0 || !Number.isFinite(magnitude)) return;
|
|
34652
|
+
const now = Date.now();
|
|
34653
|
+
if (now - (this.lastMagnitudeLogAt.get(input.deviceId) ?? 0) < FACE_MAGNITUDE_LOG_THROTTLE_MS) return;
|
|
34654
|
+
this.lastMagnitudeLogAt.set(input.deviceId, now);
|
|
34655
|
+
this.deps.logger.info("face: embedding magnitude", {
|
|
34656
|
+
tags: { deviceId: input.deviceId },
|
|
34657
|
+
meta: {
|
|
34658
|
+
trackId: input.trackId,
|
|
34659
|
+
magnitude: Number(magnitude.toFixed(3)),
|
|
34660
|
+
bound: input.settings.maxEmbeddingMagnitude,
|
|
34661
|
+
score: input.score,
|
|
34662
|
+
detectionFrameShortSidePx: input.faceBbox !== void 0 ? Math.round(Math.min(input.faceBbox.w, input.faceBbox.h)) : 0,
|
|
34663
|
+
...input.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: Math.round(input.nativeFaceShortSidePx) } : {}
|
|
34664
|
+
}
|
|
34665
|
+
});
|
|
34666
|
+
}
|
|
34382
34667
|
async processFrame(input) {
|
|
34383
34668
|
const { settings } = input;
|
|
34384
34669
|
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
|
|
@@ -34440,7 +34725,8 @@ var FaceRecognizer = class {
|
|
|
34440
34725
|
}
|
|
34441
34726
|
const recognizedIdentityId = state.assignedIdentityId ?? void 0;
|
|
34442
34727
|
const held = this.bestFace.get(c.trackId);
|
|
34443
|
-
const
|
|
34728
|
+
const detectionFrameShortSidePx = c.faceBbox !== void 0 ? Math.min(c.faceBbox.w, c.faceBbox.h) : 0;
|
|
34729
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp, void 0, void 0, void 0, faceSizeScore(detectionFrameShortSidePx));
|
|
34444
34730
|
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
34445
34731
|
if (isNewBest || needsCrop) {
|
|
34446
34732
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
@@ -34700,14 +34986,38 @@ var FaceSettingsSchema = require_dist.object({
|
|
|
34700
34986
|
* stripped from the per-device schema).
|
|
34701
34987
|
*/
|
|
34702
34988
|
enabled: require_dist.boolean().default(true),
|
|
34703
|
-
/**
|
|
34704
|
-
*
|
|
34705
|
-
*
|
|
34706
|
-
*
|
|
34707
|
-
|
|
34708
|
-
|
|
34709
|
-
*
|
|
34710
|
-
|
|
34989
|
+
/**
|
|
34990
|
+
* Cosine similarity (on L2-normalized arcface vectors) required to match.
|
|
34991
|
+
*
|
|
34992
|
+
* History: 0.55→0.62 on 2026-07-23, then back to 0.55 on 2026-08-18 once the
|
|
34993
|
+
* dead `assignUniquePerFrame` margin and the poisoned gallery were fixed —
|
|
34994
|
+
* with both repaired, genuine probes scored median 0.38 / p90 0.51, so 0.62
|
|
34995
|
+
* left almost nothing recognisable. That retune lived only as a RUNTIME
|
|
34996
|
+
* override until 2026-08-19; it is the schema default now, because a default
|
|
34997
|
+
* nobody runs is a decision nobody made.
|
|
34998
|
+
*
|
|
34999
|
+
* ⚠ NOT the end state. 0.55 is the least-bad value for the CURRENT crop
|
|
35000
|
+
* quality, where most of what reaches ArcFace is not a correctly aligned
|
|
35001
|
+
* face (measured 2026-08-19 over 60 live crops pulled from the hub: 20%
|
|
35002
|
+
* aligned, 18% marginal, 62% misaligned or containing no detectable face).
|
|
35003
|
+
* On correctly aligned crops the SAME models separate at a far lower
|
|
35004
|
+
* operating point — 300 LFW pairs gave genuine median 0.605 / impostor p90
|
|
35005
|
+
* 0.175, best accuracy 98.3% at threshold 0.28.
|
|
35006
|
+
*
|
|
35007
|
+
* Recalibration plan, in this order:
|
|
35008
|
+
* 1. land the landmark-precision gate (the alignment fix),
|
|
35009
|
+
* 2. re-enrol the gallery from crops produced AFTER it,
|
|
35010
|
+
* 3. re-measure genuine/impostor over a week of post-fix traffic,
|
|
35011
|
+
* 4. expect to lower this toward ~0.30, and `margin` with it.
|
|
35012
|
+
* Lowering it BEFORE step 1 would be actively harmful: a misaligned crop
|
|
35013
|
+
* scores ~0.9 against everything, so the degenerate population sits ABOVE
|
|
35014
|
+
* any threshold you could pick — the knob cannot reach it.
|
|
35015
|
+
*/
|
|
35016
|
+
similarityThreshold: require_dist.number().min(0).max(1).default(.55),
|
|
35017
|
+
/** Reject ambiguous matches: require best − secondBest ≥ margin. 0.10→0.15
|
|
35018
|
+
* (2026-07-23), then →0.12 (2026-08-18) alongside the threshold retune —
|
|
35019
|
+
* same story and the same recalibration plan as {@link similarityThreshold}. */
|
|
35020
|
+
margin: require_dist.number().min(0).max(1).default(.12),
|
|
34711
35021
|
/** Minimum face-detection confidence for a face to be considered. */
|
|
34712
35022
|
minFaceConfidence: require_dist.number().min(0).max(1).default(.5),
|
|
34713
35023
|
/**
|
|
@@ -34751,6 +35061,33 @@ var FaceSettingsSchema = require_dist.object({
|
|
|
34751
35061
|
* 1 (0 confirmations would assign on a single noisy frame — nonsensical;
|
|
34752
35062
|
* such a value falls back to the default). */
|
|
34753
35063
|
confirmFrames: require_dist.number().int().min(1).default(3),
|
|
35064
|
+
/**
|
|
35065
|
+
* Upper bound on the RAW ArcFace feature magnitude a face may have and still
|
|
35066
|
+
* be embedded into the buffer / matched / enrolled. `0` (the default) DISABLES
|
|
35067
|
+
* the gate.
|
|
35068
|
+
*
|
|
35069
|
+
* This exists because a degenerate crop does not score LOW, it scores HIGH:
|
|
35070
|
+
* measured on this cluster 2026-08-18/19, 28.8% of all pairs of DIFFERENT
|
|
35071
|
+
* faces exceeded cosine 0.90 and the worst 40% of the face population had
|
|
35072
|
+
* collapsed onto effectively one vector (mean pairwise cosine 0.976). No
|
|
35073
|
+
* similarity threshold can reach that — it sits above every threshold — so the
|
|
35074
|
+
* only defence is to refuse the embedding before it is stored or compared.
|
|
35075
|
+
*
|
|
35076
|
+
* Direction: for this model the DEGENERATE crops carry the LARGER norm (the
|
|
35077
|
+
* inverse of the MagFace convention), hence a maximum. Over 140 LFW faces
|
|
35078
|
+
* through this exact model and preprocessing, usable crops sat at ≈4.4 (p95
|
|
35079
|
+
* 5.25) regardless of size, grey level or blur, while information-free crops
|
|
35080
|
+
* jumped to 10–16; a bound of 6.0 kept 97.6% of usable crops and rejected
|
|
35081
|
+
* 93.2% of degenerate ones.
|
|
35082
|
+
*
|
|
35083
|
+
* Default 0 ON PURPOSE. Those numbers are LFW's, not this cluster's, and the
|
|
35084
|
+
* magnitude has never been observable here — `result-assembler` discarded it
|
|
35085
|
+
* one line after computing it. The field and its logging ship first; set a
|
|
35086
|
+
* real bound once a week of live `face: embedding magnitude` lines says where
|
|
35087
|
+
* this cluster's populations actually sit. Enabling a gate against a number
|
|
35088
|
+
* nobody has measured in production is how recognition silently stops.
|
|
35089
|
+
*/
|
|
35090
|
+
maxEmbeddingMagnitude: require_dist.number().min(0).default(0),
|
|
34754
35091
|
/** Max buffered faces kept per device. */
|
|
34755
35092
|
bufferMaxPerDevice: require_dist.number().int().min(0).default(50)
|
|
34756
35093
|
});
|
|
@@ -34769,6 +35106,7 @@ function resolveFaceSettings(raw) {
|
|
|
34769
35106
|
recognitionMinFacePx: pick("recognitionMinFacePx"),
|
|
34770
35107
|
suggestionMinCosine: pick("suggestionMinCosine"),
|
|
34771
35108
|
minIdentitySamples: pick("minIdentitySamples"),
|
|
35109
|
+
maxEmbeddingMagnitude: pick("maxEmbeddingMagnitude"),
|
|
34772
35110
|
confirmFrames: pick("confirmFrames"),
|
|
34773
35111
|
bufferMaxPerDevice: pick("bufferMaxPerDevice")
|
|
34774
35112
|
};
|
|
@@ -39240,6 +39578,147 @@ async function applyTrackFlags(deps, input) {
|
|
|
39240
39578
|
};
|
|
39241
39579
|
}
|
|
39242
39580
|
//#endregion
|
|
39581
|
+
//#region src/pipeline-analytics/pipeline/plate-vote.ts
|
|
39582
|
+
/**
|
|
39583
|
+
* Per-character VOTE over a track's plate reads.
|
|
39584
|
+
*
|
|
39585
|
+
* A track produces up to `plate-ocr`'s `cadence.maxPerTrack` reads of the SAME
|
|
39586
|
+
* physical plate, from different frames. The recognizer used to keep the single
|
|
39587
|
+
* read with the highest OCR score (`holdBest`, argmax) — and OCR score does not
|
|
39588
|
+
* correlate with correctness. In the 2026-08 audit the top-scoring read of a
|
|
39589
|
+
* track was routinely the wrong one while two lower-scoring reads agreed on the
|
|
39590
|
+
* right glyph: score ranks how confident the recognizer was about the pixels it
|
|
39591
|
+
* saw, not how much of the plate those pixels were.
|
|
39592
|
+
*
|
|
39593
|
+
* So the reads vote. Each read is normalized (upper, alnum only), aligned to a
|
|
39594
|
+
* reference read, and contributes its OCR score to whichever glyph it saw at
|
|
39595
|
+
* each reference position; the winner per position is the output. Two reads
|
|
39596
|
+
* that agree beat one confident read that disagrees, which is the whole point.
|
|
39597
|
+
*
|
|
39598
|
+
* ALIGNMENT — reads differ in length (4–9 glyphs live: a dropped leading
|
|
39599
|
+
* district letter, a hallucinated trailing glyph). A positional vote on raw
|
|
39600
|
+
* indices would let one missing character shift every later column and destroy
|
|
39601
|
+
* the agreement it was supposed to measure. Each read is therefore aligned to
|
|
39602
|
+
* the reference by Levenshtein traceback: a match or substitution votes at the
|
|
39603
|
+
* reference position it aligned to, an insertion votes nowhere, and a deletion
|
|
39604
|
+
* simply abstains at that position.
|
|
39605
|
+
*
|
|
39606
|
+
* REFERENCE — the length carrying the most SCORE MASS wins (a hallucinated
|
|
39607
|
+
* 9-glyph read does not get to set the output length just by being longest);
|
|
39608
|
+
* ties inside that length go to the highest-scoring read. With ≤5 reads this is
|
|
39609
|
+
* the honest cheap heuristic: there is no third source of truth to consult.
|
|
39610
|
+
*/
|
|
39611
|
+
/**
|
|
39612
|
+
* Align `candidate` to `reference` by Levenshtein traceback. Returns one entry
|
|
39613
|
+
* per REFERENCE position: the candidate glyph that aligned there, or `null`
|
|
39614
|
+
* when the candidate had nothing at that position (a deletion). Candidate
|
|
39615
|
+
* insertions are dropped — they have no reference column to vote in.
|
|
39616
|
+
*/
|
|
39617
|
+
function alignToReference(reference, candidate) {
|
|
39618
|
+
const n = reference.length;
|
|
39619
|
+
const m = candidate.length;
|
|
39620
|
+
const dist = [];
|
|
39621
|
+
for (let i = 0; i <= n; i++) {
|
|
39622
|
+
const row = new Array(m + 1).fill(0);
|
|
39623
|
+
row[0] = i;
|
|
39624
|
+
dist.push(row);
|
|
39625
|
+
}
|
|
39626
|
+
const first = dist[0];
|
|
39627
|
+
if (first === void 0) return [];
|
|
39628
|
+
for (let j = 0; j <= m; j++) first[j] = j;
|
|
39629
|
+
for (let i = 1; i <= n; i++) {
|
|
39630
|
+
const prev = dist[i - 1];
|
|
39631
|
+
const cur = dist[i];
|
|
39632
|
+
if (prev === void 0 || cur === void 0) continue;
|
|
39633
|
+
for (let j = 1; j <= m; j++) {
|
|
39634
|
+
const cost = reference[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
39635
|
+
cur[j] = Math.min((prev[j] ?? 0) + 1, (cur[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
39636
|
+
}
|
|
39637
|
+
}
|
|
39638
|
+
const aligned = new Array(n).fill(null);
|
|
39639
|
+
let i = n;
|
|
39640
|
+
let j = m;
|
|
39641
|
+
while (i > 0 && j > 0) {
|
|
39642
|
+
const cur = dist[i];
|
|
39643
|
+
const prev = dist[i - 1];
|
|
39644
|
+
if (cur === void 0 || prev === void 0) break;
|
|
39645
|
+
const cost = reference[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
39646
|
+
if (cur[j] === (prev[j - 1] ?? 0) + cost) {
|
|
39647
|
+
aligned[i - 1] = candidate[j - 1] ?? null;
|
|
39648
|
+
i--;
|
|
39649
|
+
j--;
|
|
39650
|
+
} else if (cur[j] === (prev[j] ?? 0) + 1) i--;
|
|
39651
|
+
else j--;
|
|
39652
|
+
}
|
|
39653
|
+
return aligned;
|
|
39654
|
+
}
|
|
39655
|
+
/** The read whose LENGTH carries the most score mass; ties → highest score. */
|
|
39656
|
+
function pickReference(reads) {
|
|
39657
|
+
const massByLength = /* @__PURE__ */ new Map();
|
|
39658
|
+
for (const r of reads) massByLength.set(r.text.length, (massByLength.get(r.text.length) ?? 0) + r.score);
|
|
39659
|
+
let bestLength = 0;
|
|
39660
|
+
let bestMass = -1;
|
|
39661
|
+
for (const [length, mass] of massByLength) if (mass > bestMass || mass === bestMass && length < bestLength) {
|
|
39662
|
+
bestMass = mass;
|
|
39663
|
+
bestLength = length;
|
|
39664
|
+
}
|
|
39665
|
+
let reference = "";
|
|
39666
|
+
let referenceScore = -1;
|
|
39667
|
+
for (const r of reads) if (r.text.length === bestLength && r.score > referenceScore) {
|
|
39668
|
+
referenceScore = r.score;
|
|
39669
|
+
reference = r.text;
|
|
39670
|
+
}
|
|
39671
|
+
return reference;
|
|
39672
|
+
}
|
|
39673
|
+
/**
|
|
39674
|
+
* Confidence-weighted per-character vote over a track's plate reads. Returns
|
|
39675
|
+
* the normalized winning text (`''` when there is nothing to vote on).
|
|
39676
|
+
*/
|
|
39677
|
+
function votePlateText(reads) {
|
|
39678
|
+
const normalized = [];
|
|
39679
|
+
for (const r of reads) {
|
|
39680
|
+
const text = normalizePlate(r.text);
|
|
39681
|
+
if (text.length > 0) normalized.push({
|
|
39682
|
+
text,
|
|
39683
|
+
score: r.score
|
|
39684
|
+
});
|
|
39685
|
+
}
|
|
39686
|
+
if (normalized.length === 0) return "";
|
|
39687
|
+
const only = normalized[0];
|
|
39688
|
+
if (normalized.length === 1 && only !== void 0) return only.text;
|
|
39689
|
+
const reference = pickReference(normalized);
|
|
39690
|
+
if (reference.length === 0) return "";
|
|
39691
|
+
const columns = [];
|
|
39692
|
+
for (let i = 0; i < reference.length; i++) columns.push(/* @__PURE__ */ new Map());
|
|
39693
|
+
for (const r of normalized) {
|
|
39694
|
+
const aligned = r.text === reference ? [...r.text] : alignToReference(reference, r.text);
|
|
39695
|
+
for (let i = 0; i < reference.length; i++) {
|
|
39696
|
+
const glyph = aligned[i];
|
|
39697
|
+
if (glyph === null || glyph === void 0) continue;
|
|
39698
|
+
const column = columns[i];
|
|
39699
|
+
if (column === void 0) continue;
|
|
39700
|
+
column.set(glyph, (column.get(glyph) ?? 0) + r.score);
|
|
39701
|
+
}
|
|
39702
|
+
}
|
|
39703
|
+
let out = "";
|
|
39704
|
+
for (let i = 0; i < reference.length; i++) {
|
|
39705
|
+
const column = columns[i];
|
|
39706
|
+
const referenceGlyph = reference[i] ?? "";
|
|
39707
|
+
if (column === void 0 || column.size === 0) {
|
|
39708
|
+
out += referenceGlyph;
|
|
39709
|
+
continue;
|
|
39710
|
+
}
|
|
39711
|
+
let bestGlyph = referenceGlyph;
|
|
39712
|
+
let bestMass = column.get(referenceGlyph) ?? -1;
|
|
39713
|
+
for (const [glyph, mass] of column) if (mass > bestMass) {
|
|
39714
|
+
bestMass = mass;
|
|
39715
|
+
bestGlyph = glyph;
|
|
39716
|
+
}
|
|
39717
|
+
out += bestGlyph;
|
|
39718
|
+
}
|
|
39719
|
+
return out;
|
|
39720
|
+
}
|
|
39721
|
+
//#endregion
|
|
39243
39722
|
//#region src/pipeline-analytics/pipeline/plate-matcher.ts
|
|
39244
39723
|
/**
|
|
39245
39724
|
* plate-matcher — name a plate read against the vehicle gallery by text vicinity.
|
|
@@ -39288,11 +39767,11 @@ function matchPlateText(probe, gallery, opts) {
|
|
|
39288
39767
|
//#endregion
|
|
39289
39768
|
//#region src/pipeline-analytics/plate-recognizer.ts
|
|
39290
39769
|
/**
|
|
39291
|
-
* PlateRecognizer —
|
|
39292
|
-
*
|
|
39293
|
-
*
|
|
39294
|
-
*
|
|
39295
|
-
*
|
|
39770
|
+
* PlateRecognizer — collects EVERY license-plate OCR read a vehicle track
|
|
39771
|
+
* produces and stores their per-character VOTE (`plate-vote.ts`) at close, plus
|
|
39772
|
+
* the max-score read's crop for the gallery tile. Unlike faces there is NO
|
|
39773
|
+
* gallery matching: the OCR text IS the label and already rides on the event via
|
|
39774
|
+
* the detection pipeline. Mirrors FaceRecognizer's hold→onTrackEnd lifecycle.
|
|
39296
39775
|
*/
|
|
39297
39776
|
var PlateRecognizer = class {
|
|
39298
39777
|
deps;
|
|
@@ -39355,7 +39834,7 @@ var PlateRecognizer = class {
|
|
|
39355
39834
|
const minConfidence = input.minConfidence ?? 0;
|
|
39356
39835
|
const candidates = input.tracked.filter((t) => typeof t.plateText === "string" && t.plateText.length > 0 && typeof t.plateScore === "number" && t.plateScore >= minConfidence && t.plateBbox !== void 0);
|
|
39357
39836
|
if (candidates.length === 0) return;
|
|
39358
|
-
for (const c of candidates) await this.
|
|
39837
|
+
for (const c of candidates) await this.observeRead({
|
|
39359
39838
|
deviceId: input.deviceId,
|
|
39360
39839
|
trackId: c.trackId,
|
|
39361
39840
|
text: c.plateText,
|
|
@@ -39378,15 +39857,50 @@ var PlateRecognizer = class {
|
|
|
39378
39857
|
* gallery stays empty (0 plateCrop) — the observed live gap.
|
|
39379
39858
|
*/
|
|
39380
39859
|
async observePlateRead(input) {
|
|
39381
|
-
|
|
39382
|
-
|
|
39383
|
-
|
|
39860
|
+
const minConfidence = input.minConfidence ?? 0;
|
|
39861
|
+
const reason = !isPlausiblePlateRead(input.text, input.score) ? "implausible" : input.score < minConfidence ? "below-min-confidence" : null;
|
|
39862
|
+
if (reason !== null) {
|
|
39863
|
+
this.deps.logger.info("plate: read discarded", {
|
|
39864
|
+
tags: {
|
|
39865
|
+
deviceId: input.deviceId,
|
|
39866
|
+
trackId: input.trackId
|
|
39867
|
+
},
|
|
39868
|
+
meta: {
|
|
39869
|
+
text: input.text,
|
|
39870
|
+
score: input.score,
|
|
39871
|
+
minConfidence,
|
|
39872
|
+
reason
|
|
39873
|
+
}
|
|
39874
|
+
});
|
|
39875
|
+
return;
|
|
39876
|
+
}
|
|
39877
|
+
await this.observeRead(input);
|
|
39384
39878
|
}
|
|
39385
|
-
/**
|
|
39386
|
-
*
|
|
39387
|
-
|
|
39879
|
+
/**
|
|
39880
|
+
* Record one plate read for a track (shared by the per-frame + detail-plane
|
|
39881
|
+
* paths).
|
|
39882
|
+
*
|
|
39883
|
+
* EVERY read is kept for the close-time vote — a track's reads disagree, and
|
|
39884
|
+
* the disagreement is the signal. The crop + geometry still track the
|
|
39885
|
+
* MAX-SCORE read: the vote decides the text, but the picture the operator
|
|
39886
|
+
* reviews should be the sharpest frame the track produced.
|
|
39887
|
+
*
|
|
39888
|
+
* The read list is capped at {@link MAX_VOTING_READS} in ARRIVAL order, not
|
|
39889
|
+
* by score: evicting the lowest-scoring read would re-introduce the very
|
|
39890
|
+
* ranking this vote exists to stop trusting.
|
|
39891
|
+
*/
|
|
39892
|
+
async observeRead(input) {
|
|
39388
39893
|
const held = this.bestPlate.get(input.trackId);
|
|
39389
|
-
|
|
39894
|
+
const vote = {
|
|
39895
|
+
text: input.text,
|
|
39896
|
+
score: input.score
|
|
39897
|
+
};
|
|
39898
|
+
if (held !== void 0 && input.score <= held.score) {
|
|
39899
|
+
if (held.reads.length < 5) held.reads.push(vote);
|
|
39900
|
+
return;
|
|
39901
|
+
}
|
|
39902
|
+
const reads = held?.reads ?? [];
|
|
39903
|
+
if (reads.length < 5) reads.push(vote);
|
|
39390
39904
|
let crop;
|
|
39391
39905
|
if (input.frameHandle !== void 0) try {
|
|
39392
39906
|
crop = await this.deps.captureCrop(input.frameHandle, input.bbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId, input.deviceId) ?? void 0;
|
|
@@ -39402,6 +39916,7 @@ var PlateRecognizer = class {
|
|
|
39402
39916
|
this.bestPlate.set(input.trackId, {
|
|
39403
39917
|
text: input.text,
|
|
39404
39918
|
score: input.score,
|
|
39919
|
+
reads,
|
|
39405
39920
|
bbox: input.bbox,
|
|
39406
39921
|
timestamp: input.timestamp,
|
|
39407
39922
|
frameWidth: input.frameWidth,
|
|
@@ -39478,14 +39993,16 @@ var PlateRecognizer = class {
|
|
|
39478
39993
|
}
|
|
39479
39994
|
});
|
|
39480
39995
|
}
|
|
39481
|
-
const
|
|
39996
|
+
const voted = votePlateText(held.reads);
|
|
39997
|
+
const text = voted.length > 0 && isPlausiblePlateRead(voted, held.score) ? voted : normalizePlate(held.text);
|
|
39998
|
+
const match = this.matchVehicle(text, held.score);
|
|
39482
39999
|
try {
|
|
39483
40000
|
await this.deps.plateStore.insert({
|
|
39484
40001
|
id: plateId,
|
|
39485
40002
|
deviceId,
|
|
39486
40003
|
trackId,
|
|
39487
40004
|
timestamp: held.timestamp,
|
|
39488
|
-
text
|
|
40005
|
+
text,
|
|
39489
40006
|
score: held.score,
|
|
39490
40007
|
...mediaKey !== void 0 ? { mediaKey } : {},
|
|
39491
40008
|
corrected: false,
|
|
@@ -39506,7 +40023,10 @@ var PlateRecognizer = class {
|
|
|
39506
40023
|
},
|
|
39507
40024
|
meta: {
|
|
39508
40025
|
plateId,
|
|
39509
|
-
text
|
|
40026
|
+
text,
|
|
40027
|
+
bestRead: normalizePlate(held.text),
|
|
40028
|
+
reads: held.reads.map((r) => `${normalizePlate(r.text)}@${r.score.toFixed(2)}`),
|
|
40029
|
+
votedFromReads: held.reads.length,
|
|
39510
40030
|
score: held.score,
|
|
39511
40031
|
hasCrop: mediaKey !== void 0,
|
|
39512
40032
|
recognizedVehicleId: match?.vehicleId ?? null
|
|
@@ -40020,6 +40540,129 @@ function buildRecordedStillPorts(input) {
|
|
|
40020
40540
|
})
|
|
40021
40541
|
};
|
|
40022
40542
|
}
|
|
40543
|
+
/** Where the completion marker lives. */
|
|
40544
|
+
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
40545
|
+
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
40546
|
+
/**
|
|
40547
|
+
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
40548
|
+
*
|
|
40549
|
+
* `null` = leave the row alone.
|
|
40550
|
+
*/
|
|
40551
|
+
function decideRowMigration(data, now) {
|
|
40552
|
+
const label = data["label"];
|
|
40553
|
+
if (typeof label !== "string" || label.length === 0) return null;
|
|
40554
|
+
if (typeof data["subLabel"] === "string" && data["subLabel"].length > 0) return null;
|
|
40555
|
+
if (data["labelMeta"] !== null && data["labelMeta"] !== void 0) return null;
|
|
40556
|
+
if (isMacroClass(label)) return {
|
|
40557
|
+
label: null,
|
|
40558
|
+
labelScore: null,
|
|
40559
|
+
labelMeta: null
|
|
40560
|
+
};
|
|
40561
|
+
return {
|
|
40562
|
+
label: null,
|
|
40563
|
+
labelScore: null,
|
|
40564
|
+
labelMeta: null,
|
|
40565
|
+
subLabel: label,
|
|
40566
|
+
subLabelScore: null,
|
|
40567
|
+
subLabelMeta: {
|
|
40568
|
+
stepId: MIGRATION_STEP_ID,
|
|
40569
|
+
decidedAt: now
|
|
40570
|
+
}
|
|
40571
|
+
};
|
|
40572
|
+
}
|
|
40573
|
+
/** Migrate one collection, paged and bounded. */
|
|
40574
|
+
async function migrateCollectionLabelTiers(deps, collection) {
|
|
40575
|
+
let scanned = 0;
|
|
40576
|
+
let demoted = 0;
|
|
40577
|
+
let cleared = 0;
|
|
40578
|
+
let skipped = 0;
|
|
40579
|
+
let offset = 0;
|
|
40580
|
+
let truncated = false;
|
|
40581
|
+
for (;;) {
|
|
40582
|
+
if (scanned >= 2e5) {
|
|
40583
|
+
truncated = true;
|
|
40584
|
+
break;
|
|
40585
|
+
}
|
|
40586
|
+
const rows = await deps.store.query.query({
|
|
40587
|
+
collection,
|
|
40588
|
+
filter: {
|
|
40589
|
+
limit: 500,
|
|
40590
|
+
offset
|
|
40591
|
+
}
|
|
40592
|
+
});
|
|
40593
|
+
if (rows.length === 0) break;
|
|
40594
|
+
for (const row of rows) {
|
|
40595
|
+
scanned++;
|
|
40596
|
+
const patch = decideRowMigration(row.data, deps.now());
|
|
40597
|
+
if (patch === null) {
|
|
40598
|
+
skipped++;
|
|
40599
|
+
continue;
|
|
40600
|
+
}
|
|
40601
|
+
try {
|
|
40602
|
+
await deps.store.update.mutate({
|
|
40603
|
+
collection,
|
|
40604
|
+
id: row.id,
|
|
40605
|
+
data: patch
|
|
40606
|
+
});
|
|
40607
|
+
if (patch["subLabel"] === void 0) cleared++;
|
|
40608
|
+
else demoted++;
|
|
40609
|
+
} catch (err) {
|
|
40610
|
+
deps.logger.warn("label tier migration row failed", { meta: {
|
|
40611
|
+
collection,
|
|
40612
|
+
id: row.id,
|
|
40613
|
+
error: String(err)
|
|
40614
|
+
} });
|
|
40615
|
+
skipped++;
|
|
40616
|
+
}
|
|
40617
|
+
}
|
|
40618
|
+
if (rows.length < 500) break;
|
|
40619
|
+
offset += rows.length;
|
|
40620
|
+
}
|
|
40621
|
+
return {
|
|
40622
|
+
collection,
|
|
40623
|
+
scanned,
|
|
40624
|
+
demoted,
|
|
40625
|
+
cleared,
|
|
40626
|
+
skipped,
|
|
40627
|
+
truncated
|
|
40628
|
+
};
|
|
40629
|
+
}
|
|
40630
|
+
/**
|
|
40631
|
+
* Run the migration across the collections that carry the tiered columns, once.
|
|
40632
|
+
*
|
|
40633
|
+
* The marker short-circuits a completed migration; it is written only when
|
|
40634
|
+
* NEITHER collection was truncated, so an interrupted pass resumes on the next
|
|
40635
|
+
* boot instead of declaring itself done.
|
|
40636
|
+
*/
|
|
40637
|
+
async function runLabelTierMigration(deps, collections) {
|
|
40638
|
+
try {
|
|
40639
|
+
const marker = await deps.store.get.query({
|
|
40640
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40641
|
+
key: MIGRATION_MARKER_KEY
|
|
40642
|
+
});
|
|
40643
|
+
if (marker !== null && marker !== void 0) return [];
|
|
40644
|
+
} catch {}
|
|
40645
|
+
const stats = [];
|
|
40646
|
+
for (const collection of collections) stats.push(await migrateCollectionLabelTiers(deps, collection));
|
|
40647
|
+
if (stats.every((s) => !s.truncated)) try {
|
|
40648
|
+
await deps.store.set.mutate({
|
|
40649
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40650
|
+
key: MIGRATION_MARKER_KEY,
|
|
40651
|
+
value: { completedAt: deps.now() }
|
|
40652
|
+
});
|
|
40653
|
+
} catch (err) {
|
|
40654
|
+
deps.logger.warn("label tier migration marker failed", { meta: { error: String(err) } });
|
|
40655
|
+
}
|
|
40656
|
+
for (const s of stats) deps.logger.info("label tier migration", { meta: {
|
|
40657
|
+
collection: s.collection,
|
|
40658
|
+
scanned: s.scanned,
|
|
40659
|
+
demoted: s.demoted,
|
|
40660
|
+
cleared: s.cleared,
|
|
40661
|
+
skipped: s.skipped,
|
|
40662
|
+
truncated: s.truncated
|
|
40663
|
+
} });
|
|
40664
|
+
return stats;
|
|
40665
|
+
}
|
|
40023
40666
|
//#endregion
|
|
40024
40667
|
//#region src/durable/lts-aggregator.ts
|
|
40025
40668
|
/** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
|
|
@@ -45211,6 +45854,55 @@ var ANALYTICS_COLLECTIONS = [
|
|
|
45211
45854
|
why: "a five-minute statistics row summarises an INTERVAL, not an entity — there is no owner it can be orphaned from. Its bound is a ROW CAP per (subject, series, scope) enforced by the aggregator itself (105,120 rows ≈ one year), deliberately not an age sweep: D61 collapsed ten disagreeing age clocks into one, and an LTS series has no clock but its own."
|
|
45212
45855
|
}
|
|
45213
45856
|
},
|
|
45857
|
+
{
|
|
45858
|
+
collection: RETRAIN_ANNOTATIONS_COLLECTION,
|
|
45859
|
+
classification: {
|
|
45860
|
+
kind: "never-orphaned",
|
|
45861
|
+
why: "the ground truth an operator DREW. It declares `trackId` and must be classified by hand for that reason: the id is provenance, not ownership. The dataset addresses COPIES (D81), which is precisely what makes a 'trained' track evictable again — so a track ageing out leaves the annotations standing. Rows go only through `deleteForFrame`, when the operator deselects the frame. Deleting one because its track expired would destroy labelling work no sweep can recreate."
|
|
45862
|
+
}
|
|
45863
|
+
},
|
|
45864
|
+
{
|
|
45865
|
+
collection: RETRAIN_FRAMES_COLLECTION,
|
|
45866
|
+
classification: {
|
|
45867
|
+
kind: "never-orphaned",
|
|
45868
|
+
why: "the frame COPIES the dataset owns outright, under a `retrain` prefix no owner cascade and no age sweep walks (D81). Also declares `trackId` as provenance. Rows go only through `deleteFrame`, after their annotations, on operator deselection. NOTE: nothing caps this table or the disk it consumes — that is a deliberate decision, not an oversight, and it is the one collection here whose bound is an operator's habit."
|
|
45869
|
+
}
|
|
45870
|
+
},
|
|
45871
|
+
{
|
|
45872
|
+
collection: SCENE_COLLECTION,
|
|
45873
|
+
classification: {
|
|
45874
|
+
kind: "never-orphaned",
|
|
45875
|
+
why: "operator intent — one row per (deviceId, monitorId), created and destroyed by scene CRUD or by a device-removal sweep. Nothing retention does can orphan it, and no age clock applies: a scene the operator configured and never revisits is not stale, it is configured."
|
|
45876
|
+
}
|
|
45877
|
+
},
|
|
45878
|
+
{
|
|
45879
|
+
collection: SCENE_LATCH_COLLECTION,
|
|
45880
|
+
classification: {
|
|
45881
|
+
kind: "never-orphaned",
|
|
45882
|
+
why: "it IS owned — by the scene set — but the owner-cleanup is self-contained: `pruneExcept(the scene set)` runs on every scene reload, so a latch whose scene is gone is collected there and never by retention. Deliberately NOT `owner-indirection`: that kind puts a collection into `auditableCollections()`, and the orphan audit resolves exactly one indirection, that of `pipeline-analytics:media`."
|
|
45883
|
+
}
|
|
45884
|
+
},
|
|
45885
|
+
{
|
|
45886
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
45887
|
+
classification: {
|
|
45888
|
+
kind: "never-orphaned",
|
|
45889
|
+
why: "a one-row completion marker for the label-tier 4g pass. It summarises a one-time event, so there is no entity it can be orphaned from — and its PRESENCE is what stops the migration running again, so deleting it is not cleanup, it is a re-run over every track, face and object-event row."
|
|
45890
|
+
}
|
|
45891
|
+
},
|
|
45892
|
+
{
|
|
45893
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
45894
|
+
classification: {
|
|
45895
|
+
kind: "never-orphaned",
|
|
45896
|
+
why: "a hub user's named snapshot of their own viewer layout. It is not analytics data — it happens to live in this addon because the actions ride `addons.custom`. Owned by the user who saved it and deleted only by them; no track, device or event can orphan it, and no age applies."
|
|
45897
|
+
}
|
|
45898
|
+
},
|
|
45899
|
+
{
|
|
45900
|
+
collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
45901
|
+
classification: {
|
|
45902
|
+
kind: "never-orphaned",
|
|
45903
|
+
why: "the drained predecessor of the row above, declared only so its rows can be read once and moved (`viewer-settings-legacy-adoption.ts`). Retention must never touch it: its rows go one way only, to the successor, gated on a re-read that confirms they landed. Delete this entry together with that module."
|
|
45904
|
+
}
|
|
45905
|
+
},
|
|
45214
45906
|
{
|
|
45215
45907
|
collection: STATIONARY_COLLECTION,
|
|
45216
45908
|
classification: {
|
|
@@ -49687,7 +50379,7 @@ function buildGlobalSettingsSchema() {
|
|
|
49687
50379
|
type: "boolean",
|
|
49688
50380
|
key: "audioEpisodeEnabled",
|
|
49689
50381
|
label: "Create audio episodes",
|
|
49690
|
-
description: "
|
|
50382
|
+
description: "Off switches off the GROUPING only: classified audio still persists an event and still notifies, but no timeline track is opened and a long sound is no longer collapsed into one episode (one event per class change, then one every 5 s while it lasts).",
|
|
49691
50383
|
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeEnabled
|
|
49692
50384
|
},
|
|
49693
50385
|
{
|
|
@@ -49813,6 +50505,15 @@ function buildGlobalSettingsSchema() {
|
|
|
49813
50505
|
step: 1,
|
|
49814
50506
|
default: FACE_DEFAULTS.confirmFrames
|
|
49815
50507
|
},
|
|
50508
|
+
{
|
|
50509
|
+
type: "number",
|
|
50510
|
+
key: "maxEmbeddingMagnitude",
|
|
50511
|
+
label: "Max embedding magnitude (0 = off)",
|
|
50512
|
+
description: "Refuse a face whose RAW ArcFace feature magnitude exceeds this, before it is buffered, matched or enrolled. A degenerate crop does not score LOW against the gallery — it scores HIGH against everything — so no similarity threshold can catch it; the raw magnitude can. On this model a usable face sits around 4.4 whatever its size, grey level or blur, while an information-free crop jumps to 10–16. Leave at 0 until the \"face: embedding magnitude\" log lines show where YOUR cameras sit; a bound guessed from someone else's data silently stops recognising everybody.",
|
|
50513
|
+
min: 0,
|
|
50514
|
+
step: .5,
|
|
50515
|
+
default: FACE_DEFAULTS.maxEmbeddingMagnitude
|
|
50516
|
+
},
|
|
49816
50517
|
{
|
|
49817
50518
|
type: "number",
|
|
49818
50519
|
key: "bufferMaxPerDevice",
|
|
@@ -49890,129 +50591,6 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
49890
50591
|
movedFraction
|
|
49891
50592
|
};
|
|
49892
50593
|
}
|
|
49893
|
-
/** Where the completion marker lives. */
|
|
49894
|
-
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
49895
|
-
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
49896
|
-
/**
|
|
49897
|
-
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
49898
|
-
*
|
|
49899
|
-
* `null` = leave the row alone.
|
|
49900
|
-
*/
|
|
49901
|
-
function decideRowMigration(data, now) {
|
|
49902
|
-
const label = data["label"];
|
|
49903
|
-
if (typeof label !== "string" || label.length === 0) return null;
|
|
49904
|
-
if (typeof data["subLabel"] === "string" && data["subLabel"].length > 0) return null;
|
|
49905
|
-
if (data["labelMeta"] !== null && data["labelMeta"] !== void 0) return null;
|
|
49906
|
-
if (isMacroClass(label)) return {
|
|
49907
|
-
label: null,
|
|
49908
|
-
labelScore: null,
|
|
49909
|
-
labelMeta: null
|
|
49910
|
-
};
|
|
49911
|
-
return {
|
|
49912
|
-
label: null,
|
|
49913
|
-
labelScore: null,
|
|
49914
|
-
labelMeta: null,
|
|
49915
|
-
subLabel: label,
|
|
49916
|
-
subLabelScore: null,
|
|
49917
|
-
subLabelMeta: {
|
|
49918
|
-
stepId: MIGRATION_STEP_ID,
|
|
49919
|
-
decidedAt: now
|
|
49920
|
-
}
|
|
49921
|
-
};
|
|
49922
|
-
}
|
|
49923
|
-
/** Migrate one collection, paged and bounded. */
|
|
49924
|
-
async function migrateCollectionLabelTiers(deps, collection) {
|
|
49925
|
-
let scanned = 0;
|
|
49926
|
-
let demoted = 0;
|
|
49927
|
-
let cleared = 0;
|
|
49928
|
-
let skipped = 0;
|
|
49929
|
-
let offset = 0;
|
|
49930
|
-
let truncated = false;
|
|
49931
|
-
for (;;) {
|
|
49932
|
-
if (scanned >= 2e5) {
|
|
49933
|
-
truncated = true;
|
|
49934
|
-
break;
|
|
49935
|
-
}
|
|
49936
|
-
const rows = await deps.store.query.query({
|
|
49937
|
-
collection,
|
|
49938
|
-
filter: {
|
|
49939
|
-
limit: 500,
|
|
49940
|
-
offset
|
|
49941
|
-
}
|
|
49942
|
-
});
|
|
49943
|
-
if (rows.length === 0) break;
|
|
49944
|
-
for (const row of rows) {
|
|
49945
|
-
scanned++;
|
|
49946
|
-
const patch = decideRowMigration(row.data, deps.now());
|
|
49947
|
-
if (patch === null) {
|
|
49948
|
-
skipped++;
|
|
49949
|
-
continue;
|
|
49950
|
-
}
|
|
49951
|
-
try {
|
|
49952
|
-
await deps.store.update.mutate({
|
|
49953
|
-
collection,
|
|
49954
|
-
id: row.id,
|
|
49955
|
-
data: patch
|
|
49956
|
-
});
|
|
49957
|
-
if (patch["subLabel"] === void 0) cleared++;
|
|
49958
|
-
else demoted++;
|
|
49959
|
-
} catch (err) {
|
|
49960
|
-
deps.logger.warn("label tier migration row failed", { meta: {
|
|
49961
|
-
collection,
|
|
49962
|
-
id: row.id,
|
|
49963
|
-
error: String(err)
|
|
49964
|
-
} });
|
|
49965
|
-
skipped++;
|
|
49966
|
-
}
|
|
49967
|
-
}
|
|
49968
|
-
if (rows.length < 500) break;
|
|
49969
|
-
offset += rows.length;
|
|
49970
|
-
}
|
|
49971
|
-
return {
|
|
49972
|
-
collection,
|
|
49973
|
-
scanned,
|
|
49974
|
-
demoted,
|
|
49975
|
-
cleared,
|
|
49976
|
-
skipped,
|
|
49977
|
-
truncated
|
|
49978
|
-
};
|
|
49979
|
-
}
|
|
49980
|
-
/**
|
|
49981
|
-
* Run the migration across the collections that carry the tiered columns, once.
|
|
49982
|
-
*
|
|
49983
|
-
* The marker short-circuits a completed migration; it is written only when
|
|
49984
|
-
* NEITHER collection was truncated, so an interrupted pass resumes on the next
|
|
49985
|
-
* boot instead of declaring itself done.
|
|
49986
|
-
*/
|
|
49987
|
-
async function runLabelTierMigration(deps, collections) {
|
|
49988
|
-
try {
|
|
49989
|
-
const marker = await deps.store.get.query({
|
|
49990
|
-
collection: MIGRATION_MARKER_COLLECTION,
|
|
49991
|
-
key: MIGRATION_MARKER_KEY
|
|
49992
|
-
});
|
|
49993
|
-
if (marker !== null && marker !== void 0) return [];
|
|
49994
|
-
} catch {}
|
|
49995
|
-
const stats = [];
|
|
49996
|
-
for (const collection of collections) stats.push(await migrateCollectionLabelTiers(deps, collection));
|
|
49997
|
-
if (stats.every((s) => !s.truncated)) try {
|
|
49998
|
-
await deps.store.set.mutate({
|
|
49999
|
-
collection: MIGRATION_MARKER_COLLECTION,
|
|
50000
|
-
key: MIGRATION_MARKER_KEY,
|
|
50001
|
-
value: { completedAt: deps.now() }
|
|
50002
|
-
});
|
|
50003
|
-
} catch (err) {
|
|
50004
|
-
deps.logger.warn("label tier migration marker failed", { meta: { error: String(err) } });
|
|
50005
|
-
}
|
|
50006
|
-
for (const s of stats) deps.logger.info("label tier migration", { meta: {
|
|
50007
|
-
collection: s.collection,
|
|
50008
|
-
scanned: s.scanned,
|
|
50009
|
-
demoted: s.demoted,
|
|
50010
|
-
cleared: s.cleared,
|
|
50011
|
-
skipped: s.skipped,
|
|
50012
|
-
truncated: s.truncated
|
|
50013
|
-
} });
|
|
50014
|
-
return stats;
|
|
50015
|
-
}
|
|
50016
50594
|
//#endregion
|
|
50017
50595
|
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
50018
50596
|
/**
|
|
@@ -51316,6 +51894,16 @@ var OVERLAY_SYNTHESIS_WARN_THROTTLE_MS = 6e4;
|
|
|
51316
51894
|
var AUDIO_EPISODE_VISUAL_QUIET_MS = 2e3;
|
|
51317
51895
|
/** Persist lastSeen on an open synthetic audio track at most this often. */
|
|
51318
51896
|
var AUDIO_EPISODE_PERSIST_MS = 2e3;
|
|
51897
|
+
/**
|
|
51898
|
+
* Coalescing window for classified audio inserts when episodes are OFF.
|
|
51899
|
+
*
|
|
51900
|
+
* With `audioEpisodeEnabled: false` there is no hysteresis to collapse a sound
|
|
51901
|
+
* into one row, but the classifier still fires at ~30 Hz — so the pre-episode
|
|
51902
|
+
* rule applies again: while the SAME class persists, one row per heartbeat;
|
|
51903
|
+
* a class change inserts immediately. The knob turns off the grouping, never
|
|
51904
|
+
* the persistence (D62).
|
|
51905
|
+
*/
|
|
51906
|
+
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
51319
51907
|
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
51320
51908
|
/**
|
|
51321
51909
|
* Stored media kinds that carry NO drawn bounding box, in fallback preference
|
|
@@ -52424,6 +53012,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52424
53012
|
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
52425
53013
|
]);
|
|
52426
53014
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
53015
|
+
try {
|
|
53016
|
+
await adoptLegacyViewerSettingsSnapshots({
|
|
53017
|
+
store: api.settingsStore,
|
|
53018
|
+
logger: this.ctx.logger.child("viewer-settings")
|
|
53019
|
+
});
|
|
53020
|
+
} catch (err) {
|
|
53021
|
+
this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: require_dist.errMsg(err) } });
|
|
53022
|
+
}
|
|
52427
53023
|
runLabelTierMigration({
|
|
52428
53024
|
store: api.settingsStore,
|
|
52429
53025
|
logger: this.ctx.logger,
|
|
@@ -55163,6 +55759,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55163
55759
|
frameHeight: frame.frameHeight,
|
|
55164
55760
|
score: detail.score,
|
|
55165
55761
|
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
55762
|
+
...detail.embeddingMagnitude !== void 0 ? { embeddingMagnitude: detail.embeddingMagnitude } : {},
|
|
55166
55763
|
parentBbox: { ...frame.bbox },
|
|
55167
55764
|
...detail.bbox !== void 0 ? { faceBbox: { ...detail.bbox } } : {},
|
|
55168
55765
|
...detail.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: detail.nativeFaceShortSidePx } : {},
|
|
@@ -55630,7 +56227,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55630
56227
|
return;
|
|
55631
56228
|
}
|
|
55632
56229
|
}
|
|
55633
|
-
|
|
56230
|
+
/**
|
|
56231
|
+
* The ONE writer for a classified audio observation: the durable row, the
|
|
56232
|
+
* Notification Center hook fired in the same persist moment (D-2), the
|
|
56233
|
+
* track-level `audioLabels` stamp, and the telemetry event.
|
|
56234
|
+
*
|
|
56235
|
+
* Shared by both routes on purpose. `audioEpisodeEnabled` selects WHEN this
|
|
56236
|
+
* runs (once per episode, or once per heartbeat), never WHETHER — a knob
|
|
56237
|
+
* that names the grouping must not be able to silence the classified path
|
|
56238
|
+
* (D62). This body was inlined in `openAudioEpisode` and the knob-off camera
|
|
56239
|
+
* therefore persisted nothing at all.
|
|
56240
|
+
*/
|
|
56241
|
+
async persistClassifiedAudio(input) {
|
|
55634
56242
|
const ev = {
|
|
55635
56243
|
id: (0, node_crypto.randomUUID)(),
|
|
55636
56244
|
deviceId: input.deviceId,
|
|
@@ -55639,18 +56247,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55639
56247
|
rms: input.rms,
|
|
55640
56248
|
dbfs: input.dbfs,
|
|
55641
56249
|
classification: {
|
|
55642
|
-
className:
|
|
56250
|
+
className: input.className,
|
|
55643
56251
|
...input.originalClass !== void 0 ? { originalClass: input.originalClass } : {},
|
|
55644
56252
|
score: input.score
|
|
55645
56253
|
}
|
|
55646
56254
|
};
|
|
55647
56255
|
this.lastAudioInsertByDevice.set(input.deviceId, {
|
|
55648
|
-
className:
|
|
56256
|
+
className: input.className,
|
|
55649
56257
|
atMs: input.timestamp
|
|
55650
56258
|
});
|
|
55651
56259
|
await this.eventStore.insertAudio(ev);
|
|
55652
56260
|
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
55653
|
-
this.trackStore?.addAudioLabelEpisode(input.deviceId,
|
|
56261
|
+
this.trackStore?.addAudioLabelEpisode(input.deviceId, input.className, input.score, input.timestamp);
|
|
55654
56262
|
this.ctx.eventBus.emit({
|
|
55655
56263
|
id: `pa-${ev.id}`,
|
|
55656
56264
|
timestamp: new Date(ev.timestamp),
|
|
@@ -55667,6 +56275,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55667
56275
|
timestamp: ev.timestamp
|
|
55668
56276
|
}
|
|
55669
56277
|
});
|
|
56278
|
+
}
|
|
56279
|
+
/**
|
|
56280
|
+
* Classified audio with episodes OFF: persist on class change or heartbeat,
|
|
56281
|
+
* and open NO synthetic track. This is the whole of what the knob switches —
|
|
56282
|
+
* the timeline track and the episode coalescing — and it is what the form
|
|
56283
|
+
* promises.
|
|
56284
|
+
*/
|
|
56285
|
+
async persistClassifiedAudioUngrouped(input) {
|
|
56286
|
+
const last = this.lastAudioInsertByDevice.get(input.deviceId);
|
|
56287
|
+
const sameClass = last?.className === input.className;
|
|
56288
|
+
const heartbeatDue = last === void 0 || input.timestamp - last.atMs >= AUDIO_EVENT_HEARTBEAT_MS;
|
|
56289
|
+
if (sameClass && !heartbeatDue) return;
|
|
56290
|
+
await this.persistClassifiedAudio(input);
|
|
56291
|
+
}
|
|
56292
|
+
async openAudioEpisode(input, ref) {
|
|
56293
|
+
await this.persistClassifiedAudio({
|
|
56294
|
+
...input,
|
|
56295
|
+
className: ref.className
|
|
56296
|
+
});
|
|
55670
56297
|
if (this.hasLiveVisualTrack(input.deviceId, input.timestamp)) return;
|
|
55671
56298
|
const materializer = this.syntheticTracks;
|
|
55672
56299
|
if (materializer === null) return;
|
|
@@ -55778,19 +56405,26 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55778
56405
|
});
|
|
55779
56406
|
return;
|
|
55780
56407
|
}
|
|
55781
|
-
|
|
56408
|
+
const classified = {
|
|
55782
56409
|
deviceId,
|
|
55783
56410
|
timestamp,
|
|
55784
56411
|
className: route.className,
|
|
55785
56412
|
score: topClassification.score,
|
|
55786
|
-
originalClass: topClassification.debug
|
|
56413
|
+
...topClassification.debug?.originalClass !== void 0 ? { originalClass: topClassification.debug.originalClass } : {},
|
|
55787
56414
|
rms: level?.rms ?? 0,
|
|
55788
|
-
dbfs: level?.dbfs ?? 0
|
|
56415
|
+
dbfs: level?.dbfs ?? 0
|
|
56416
|
+
};
|
|
56417
|
+
if (!settings.audioEpisodeEnabled) {
|
|
56418
|
+
await this.persistClassifiedAudioUngrouped(classified);
|
|
56419
|
+
return;
|
|
56420
|
+
}
|
|
56421
|
+
await this.driveAudioEpisode({
|
|
56422
|
+
...classified,
|
|
55789
56423
|
knobs: this.audioEpisodeKnobs(settings)
|
|
55790
56424
|
});
|
|
55791
56425
|
return;
|
|
55792
56426
|
}
|
|
55793
|
-
await this.driveAudioEpisode({
|
|
56427
|
+
if (settings.audioEpisodeEnabled) await this.driveAudioEpisode({
|
|
55794
56428
|
deviceId,
|
|
55795
56429
|
timestamp,
|
|
55796
56430
|
className: null,
|