@camstack/addon-post-analysis 1.2.102 → 1.2.103
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.
|
@@ -8302,6 +8302,10 @@ var en_default = {
|
|
|
8302
8302
|
"one": "plate",
|
|
8303
8303
|
"other": "plates"
|
|
8304
8304
|
},
|
|
8305
|
+
"class.audio": {
|
|
8306
|
+
"one": "sound",
|
|
8307
|
+
"other": "sounds"
|
|
8308
|
+
},
|
|
8305
8309
|
"class.other": "{{name}}",
|
|
8306
8310
|
"group.others.person": {
|
|
8307
8311
|
"one": "{{count}} other person",
|
|
@@ -8520,6 +8524,10 @@ var it_default = {
|
|
|
8520
8524
|
"one": "targa",
|
|
8521
8525
|
"other": "targhe"
|
|
8522
8526
|
},
|
|
8527
|
+
"class.audio": {
|
|
8528
|
+
"one": "suono",
|
|
8529
|
+
"other": "suoni"
|
|
8530
|
+
},
|
|
8523
8531
|
"class.other": "{{name}}",
|
|
8524
8532
|
"group.others.person": {
|
|
8525
8533
|
"one": "{{count}} altra persona",
|
|
@@ -25198,7 +25206,16 @@ function clamp01$1(x) {
|
|
|
25198
25206
|
if (x > 1) return 1;
|
|
25199
25207
|
return x;
|
|
25200
25208
|
}
|
|
25201
|
-
/**
|
|
25209
|
+
/**
|
|
25210
|
+
* Rank a class name into [0,1] by how event-worthy it is. Case-insensitive.
|
|
25211
|
+
*
|
|
25212
|
+
* `audio` falls through to {@link CLASS_RANK_DEFAULT} on purpose — a sound is
|
|
25213
|
+
* not a subject in frame. Before D189 an audio episode carried the HEARD class
|
|
25214
|
+
* here, so `dog`/`cat`/`animal` landed in {@link ANIMAL_CLASSES} and a bark
|
|
25215
|
+
* ranked as if an animal had walked past, while `speech` (500 of the 576 audio
|
|
25216
|
+
* rows measured live) already ranked default. That boost was an accident of the
|
|
25217
|
+
* class collision, not a decision. Pinned by `key-event-scoring.spec.ts`.
|
|
25218
|
+
*/
|
|
25202
25219
|
function classRank(className) {
|
|
25203
25220
|
const c = className.toLowerCase();
|
|
25204
25221
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
@@ -40544,6 +40561,24 @@ function buildRecordedStillPorts(input) {
|
|
|
40544
40561
|
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
40545
40562
|
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
40546
40563
|
/**
|
|
40564
|
+
* The owner declares its marker collection — like every other store in this
|
|
40565
|
+
* addon. KV shape (a single `data` column: `get`/`set` route the value as a
|
|
40566
|
+
* JSON blob; the engine adds the implicit `id` primary key). This declaration
|
|
40567
|
+
* never existed, so the engine refused every marker write, the catch below
|
|
40568
|
+
* warned, and every boot re-scanned the full analytics store — ~36k rows per
|
|
40569
|
+
* boot from the day the migration shipped.
|
|
40570
|
+
*/
|
|
40571
|
+
async function declareMigrationMarkers(store) {
|
|
40572
|
+
await store.declareCollection.mutate({
|
|
40573
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40574
|
+
columns: [{
|
|
40575
|
+
name: "data",
|
|
40576
|
+
type: "TEXT",
|
|
40577
|
+
notNull: true
|
|
40578
|
+
}]
|
|
40579
|
+
});
|
|
40580
|
+
}
|
|
40581
|
+
/**
|
|
40547
40582
|
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
40548
40583
|
*
|
|
40549
40584
|
* `null` = leave the row alone.
|
|
@@ -43724,6 +43759,47 @@ function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
|
43724
43759
|
return mark.priorRollbacks < 3;
|
|
43725
43760
|
}
|
|
43726
43761
|
//#endregion
|
|
43762
|
+
//#region src/pipeline-analytics/store/audio-track-class.ts
|
|
43763
|
+
/** The taxonomy macro every audio-sourced track carries as its class. */
|
|
43764
|
+
var AUDIO_TRACK_CLASS = "audio";
|
|
43765
|
+
/**
|
|
43766
|
+
* True when this row predates the contract: audio-sourced, but carrying a
|
|
43767
|
+
* naked audio macro as its class.
|
|
43768
|
+
*
|
|
43769
|
+
* Deliberately NOT "className is in AUDIO_MACRO_LABELS" — an id added to the
|
|
43770
|
+
* vocabulary after a row was written would then read as modern forever. The
|
|
43771
|
+
* question is the simple one: is this an audio row that does not say `audio`?
|
|
43772
|
+
*/
|
|
43773
|
+
function isLegacyAudioTrackClass(track) {
|
|
43774
|
+
return track.source === "audio" && track.className !== "audio";
|
|
43775
|
+
}
|
|
43776
|
+
/**
|
|
43777
|
+
* Bring a hydrated row up to the contract. Returns the SAME reference when
|
|
43778
|
+
* there is nothing to do, so the hot path (every non-audio track, and every
|
|
43779
|
+
* audio track written after this shipped) allocates nothing.
|
|
43780
|
+
*
|
|
43781
|
+
* The naked class is preserved as an `audioLabels` entry when — and only when
|
|
43782
|
+
* — the row carries none. On the 576 legacy rows measured on the live hub that
|
|
43783
|
+
* branch is unreachable (all of them have labels); it exists so the rewrite
|
|
43784
|
+
* can never be the step that DESTROYS the only copy of what was heard.
|
|
43785
|
+
*/
|
|
43786
|
+
function normalizeAudioTrackClass(track) {
|
|
43787
|
+
if (!isLegacyAudioTrackClass(track)) return track;
|
|
43788
|
+
const heard = {
|
|
43789
|
+
label: track.className,
|
|
43790
|
+
peakScore: 0,
|
|
43791
|
+
count: 1,
|
|
43792
|
+
firstAt: track.firstSeen,
|
|
43793
|
+
lastAt: track.lastSeen
|
|
43794
|
+
};
|
|
43795
|
+
const audioLabels = track.audioLabels !== void 0 && track.audioLabels.length > 0 ? track.audioLabels : [heard];
|
|
43796
|
+
return {
|
|
43797
|
+
...track,
|
|
43798
|
+
className: AUDIO_TRACK_CLASS,
|
|
43799
|
+
audioLabels
|
|
43800
|
+
};
|
|
43801
|
+
}
|
|
43802
|
+
//#endregion
|
|
43727
43803
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
43728
43804
|
function encodeRecentCursor(cursor) {
|
|
43729
43805
|
return Buffer.from(JSON.stringify({
|
|
@@ -45371,7 +45447,7 @@ var TrackStore = class {
|
|
|
45371
45447
|
maxX: envMaxX,
|
|
45372
45448
|
maxY: envMaxY
|
|
45373
45449
|
} : null;
|
|
45374
|
-
return {
|
|
45450
|
+
return normalizeAudioTrackClass({
|
|
45375
45451
|
trackId: id,
|
|
45376
45452
|
deviceId: Number(data["deviceId"]),
|
|
45377
45453
|
className: String(data["className"]),
|
|
@@ -45402,7 +45478,7 @@ var TrackStore = class {
|
|
|
45402
45478
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
45403
45479
|
observations,
|
|
45404
45480
|
...envelope !== null ? { envelope } : {}
|
|
45405
|
-
};
|
|
45481
|
+
});
|
|
45406
45482
|
}
|
|
45407
45483
|
};
|
|
45408
45484
|
//#endregion
|
|
@@ -47735,7 +47811,7 @@ var AudioMarkerProjector = class {
|
|
|
47735
47811
|
cameraId: deviceId,
|
|
47736
47812
|
sourceDeviceId: deviceId,
|
|
47737
47813
|
source: "audio",
|
|
47738
|
-
kind:
|
|
47814
|
+
kind: AUDIO_TRACK_CLASS,
|
|
47739
47815
|
timestamp: timestampMs,
|
|
47740
47816
|
...audioLabels !== void 0 ? { audioLabels } : {}
|
|
47741
47817
|
});
|
|
@@ -48943,6 +49019,23 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
48943
49019
|
* {@link isSpatialTrack}.
|
|
48944
49020
|
*/
|
|
48945
49021
|
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
49022
|
+
/**
|
|
49023
|
+
* What the debounce treats as "the same event happening again".
|
|
49024
|
+
*
|
|
49025
|
+
* `(camera, kind)` — plus, for an audio row, the class HEARD. Since D189 every
|
|
49026
|
+
* audio row carries `kind: 'audio'` and puts the class on `audioLabels`, so
|
|
49027
|
+
* keying on `kind` alone would collapse this to one bucket per camera. The
|
|
49028
|
+
* episode hysteresis cools down PER CLASS, so a bark three seconds after speech
|
|
49029
|
+
* is a legitimate second episode — and a per-camera bucket would drop it.
|
|
49030
|
+
*
|
|
49031
|
+
* Only the FIRST label is used: a materialize carries the one class the episode
|
|
49032
|
+
* opened on. Joining all of them would make `[speech]` and `[speech, dog]`
|
|
49033
|
+
* different buckets, which is the opposite of what a debounce is for.
|
|
49034
|
+
*/
|
|
49035
|
+
function debounceKey(input) {
|
|
49036
|
+
const heard = input.audioLabels?.[0]?.label;
|
|
49037
|
+
return heard === void 0 ? `${input.cameraId}:${input.kind}` : `${input.cameraId}:${input.kind}:${heard}`;
|
|
49038
|
+
}
|
|
48946
49039
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
48947
49040
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
48948
49041
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
@@ -48985,7 +49078,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48985
49078
|
snapshotTimeoutMs;
|
|
48986
49079
|
makeId;
|
|
48987
49080
|
now;
|
|
48988
|
-
/** Last snapshot time per
|
|
49081
|
+
/** Last snapshot time per {@link debounceKey}. */
|
|
48989
49082
|
lastAt = /* @__PURE__ */ new Map();
|
|
48990
49083
|
constructor(deps) {
|
|
48991
49084
|
this.deps = deps;
|
|
@@ -49024,7 +49117,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
49024
49117
|
* still lands a track (with no media) — it never blocks the durable record.
|
|
49025
49118
|
*/
|
|
49026
49119
|
async materialize(input) {
|
|
49027
|
-
const key =
|
|
49120
|
+
const key = debounceKey(input);
|
|
49028
49121
|
const last = this.lastAt.get(key);
|
|
49029
49122
|
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
49030
49123
|
this.lastAt.set(key, this.now());
|
|
@@ -50591,6 +50684,125 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
50591
50684
|
movedFraction
|
|
50592
50685
|
};
|
|
50593
50686
|
}
|
|
50687
|
+
/** Where the completion marker lives — the collection `runLabelTierMigration`
|
|
50688
|
+
* already established for exactly this. */
|
|
50689
|
+
var AUDIO_CLASS_MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
50690
|
+
var AUDIO_CLASS_MIGRATION_MARKER_KEY = "audio-track-class-d189";
|
|
50691
|
+
/**
|
|
50692
|
+
* The decision for ONE stored row, exposed so the rule is testable without a
|
|
50693
|
+
* store. `null` = leave the row alone.
|
|
50694
|
+
*
|
|
50695
|
+
* Reads the RAW row rather than a hydrated `Track` on purpose: this is the
|
|
50696
|
+
* column the pass rewrites, and hydrating through `rowToTrack` would apply the
|
|
50697
|
+
* read-side normalization and hide the very rows it is looking for.
|
|
50698
|
+
*/
|
|
50699
|
+
function decideAudioClassRowMigration(data) {
|
|
50700
|
+
if (data["source"] !== "audio") return null;
|
|
50701
|
+
const className = data["className"];
|
|
50702
|
+
if (typeof className !== "string" || className.length === 0) return null;
|
|
50703
|
+
if (className === "audio") return null;
|
|
50704
|
+
const stored = data["audioLabels"];
|
|
50705
|
+
if (Array.isArray(stored) && stored.length > 0) return { className: AUDIO_TRACK_CLASS };
|
|
50706
|
+
const firstSeen = typeof data["firstSeen"] === "number" ? data["firstSeen"] : 0;
|
|
50707
|
+
return {
|
|
50708
|
+
className: AUDIO_TRACK_CLASS,
|
|
50709
|
+
audioLabels: [{
|
|
50710
|
+
label: className,
|
|
50711
|
+
peakScore: 0,
|
|
50712
|
+
count: 1,
|
|
50713
|
+
firstAt: firstSeen,
|
|
50714
|
+
lastAt: typeof data["lastSeen"] === "number" ? data["lastSeen"] : firstSeen
|
|
50715
|
+
}]
|
|
50716
|
+
};
|
|
50717
|
+
}
|
|
50718
|
+
/**
|
|
50719
|
+
* Run the pass over the tracks collection, once.
|
|
50720
|
+
*
|
|
50721
|
+
* The marker short-circuits a completed migration; it is written only when the
|
|
50722
|
+
* pass did NOT truncate, so an interrupted run resumes on the next boot instead
|
|
50723
|
+
* of declaring itself done.
|
|
50724
|
+
*/
|
|
50725
|
+
async function runAudioTrackClassMigration(deps, collection) {
|
|
50726
|
+
try {
|
|
50727
|
+
const marker = await deps.store.get.query({
|
|
50728
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50729
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY
|
|
50730
|
+
});
|
|
50731
|
+
if (marker !== null && marker !== void 0) return null;
|
|
50732
|
+
} catch {}
|
|
50733
|
+
let scanned = 0;
|
|
50734
|
+
let rewritten = 0;
|
|
50735
|
+
let labelsRecovered = 0;
|
|
50736
|
+
let skipped = 0;
|
|
50737
|
+
let offset = 0;
|
|
50738
|
+
let truncated = false;
|
|
50739
|
+
for (;;) {
|
|
50740
|
+
if (scanned >= 2e5) {
|
|
50741
|
+
truncated = true;
|
|
50742
|
+
break;
|
|
50743
|
+
}
|
|
50744
|
+
const rows = await deps.store.query.query({
|
|
50745
|
+
collection,
|
|
50746
|
+
filter: {
|
|
50747
|
+
limit: 500,
|
|
50748
|
+
offset
|
|
50749
|
+
}
|
|
50750
|
+
});
|
|
50751
|
+
if (rows.length === 0) break;
|
|
50752
|
+
for (const row of rows) {
|
|
50753
|
+
scanned++;
|
|
50754
|
+
const patch = decideAudioClassRowMigration(row.data);
|
|
50755
|
+
if (patch === null) {
|
|
50756
|
+
skipped++;
|
|
50757
|
+
continue;
|
|
50758
|
+
}
|
|
50759
|
+
const deviceId = typeof row.data["deviceId"] === "number" ? row.data["deviceId"] : void 0;
|
|
50760
|
+
try {
|
|
50761
|
+
await deps.store.update.mutate({
|
|
50762
|
+
collection,
|
|
50763
|
+
id: row.id,
|
|
50764
|
+
data: patch
|
|
50765
|
+
});
|
|
50766
|
+
rewritten++;
|
|
50767
|
+
if (patch["audioLabels"] !== void 0) labelsRecovered++;
|
|
50768
|
+
} catch (err) {
|
|
50769
|
+
deps.logger.warn("audio track class migration row failed", {
|
|
50770
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
50771
|
+
meta: {
|
|
50772
|
+
collection,
|
|
50773
|
+
id: row.id,
|
|
50774
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50775
|
+
}
|
|
50776
|
+
});
|
|
50777
|
+
skipped++;
|
|
50778
|
+
}
|
|
50779
|
+
}
|
|
50780
|
+
if (rows.length < 500) break;
|
|
50781
|
+
offset += rows.length;
|
|
50782
|
+
}
|
|
50783
|
+
if (!truncated) try {
|
|
50784
|
+
await deps.store.set.mutate({
|
|
50785
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50786
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY,
|
|
50787
|
+
value: {
|
|
50788
|
+
completedAt: deps.now(),
|
|
50789
|
+
rewritten
|
|
50790
|
+
}
|
|
50791
|
+
});
|
|
50792
|
+
} catch (err) {
|
|
50793
|
+
deps.logger.warn("audio track class migration marker failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50794
|
+
}
|
|
50795
|
+
const stats = {
|
|
50796
|
+
collection,
|
|
50797
|
+
scanned,
|
|
50798
|
+
rewritten,
|
|
50799
|
+
labelsRecovered,
|
|
50800
|
+
skipped,
|
|
50801
|
+
truncated
|
|
50802
|
+
};
|
|
50803
|
+
deps.logger.info("audio track class migration", { meta: { ...stats } });
|
|
50804
|
+
return stats;
|
|
50805
|
+
}
|
|
50594
50806
|
//#endregion
|
|
50595
50807
|
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
50596
50808
|
/**
|
|
@@ -53009,7 +53221,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53009
53221
|
AnalyticsLts.declare(api.settingsStore),
|
|
53010
53222
|
SceneStore.declare(api.settingsStore),
|
|
53011
53223
|
NotificationCenter.declare(api.settingsStore),
|
|
53012
|
-
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
53224
|
+
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
53225
|
+
declareMigrationMarkers(api.settingsStore)
|
|
53013
53226
|
]);
|
|
53014
53227
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
53015
53228
|
try {
|
|
@@ -53027,6 +53240,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53027
53240
|
}, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]).catch((err) => {
|
|
53028
53241
|
this.ctx.logger.warn("label tier migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53029
53242
|
});
|
|
53243
|
+
runAudioTrackClassMigration({
|
|
53244
|
+
store: api.settingsStore,
|
|
53245
|
+
logger: this.ctx.logger,
|
|
53246
|
+
now: () => Date.now()
|
|
53247
|
+
}, TRACKS_COLLECTION).catch((err) => {
|
|
53248
|
+
this.ctx.logger.warn("audio track class migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53249
|
+
});
|
|
53030
53250
|
}
|
|
53031
53251
|
/** Event-media storage. By default it uses `ctx.kernel.storage`, rooted at
|
|
53032
53252
|
* the node data dir (appData) → heavy thumbnail/track-frame churn hammers
|
|
@@ -56302,7 +56522,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
56302
56522
|
cameraId: input.deviceId,
|
|
56303
56523
|
sourceDeviceId: input.deviceId,
|
|
56304
56524
|
source: "audio",
|
|
56305
|
-
kind:
|
|
56525
|
+
kind: AUDIO_TRACK_CLASS,
|
|
56306
56526
|
timestamp: input.timestamp,
|
|
56307
56527
|
audioLabels: [{
|
|
56308
56528
|
label: ref.className,
|
|
@@ -8295,6 +8295,10 @@ var en_default = {
|
|
|
8295
8295
|
"one": "plate",
|
|
8296
8296
|
"other": "plates"
|
|
8297
8297
|
},
|
|
8298
|
+
"class.audio": {
|
|
8299
|
+
"one": "sound",
|
|
8300
|
+
"other": "sounds"
|
|
8301
|
+
},
|
|
8298
8302
|
"class.other": "{{name}}",
|
|
8299
8303
|
"group.others.person": {
|
|
8300
8304
|
"one": "{{count}} other person",
|
|
@@ -8513,6 +8517,10 @@ var it_default = {
|
|
|
8513
8517
|
"one": "targa",
|
|
8514
8518
|
"other": "targhe"
|
|
8515
8519
|
},
|
|
8520
|
+
"class.audio": {
|
|
8521
|
+
"one": "suono",
|
|
8522
|
+
"other": "suoni"
|
|
8523
|
+
},
|
|
8516
8524
|
"class.other": "{{name}}",
|
|
8517
8525
|
"group.others.person": {
|
|
8518
8526
|
"one": "{{count}} altra persona",
|
|
@@ -25176,7 +25184,16 @@ function clamp01$1(x) {
|
|
|
25176
25184
|
if (x > 1) return 1;
|
|
25177
25185
|
return x;
|
|
25178
25186
|
}
|
|
25179
|
-
/**
|
|
25187
|
+
/**
|
|
25188
|
+
* Rank a class name into [0,1] by how event-worthy it is. Case-insensitive.
|
|
25189
|
+
*
|
|
25190
|
+
* `audio` falls through to {@link CLASS_RANK_DEFAULT} on purpose — a sound is
|
|
25191
|
+
* not a subject in frame. Before D189 an audio episode carried the HEARD class
|
|
25192
|
+
* here, so `dog`/`cat`/`animal` landed in {@link ANIMAL_CLASSES} and a bark
|
|
25193
|
+
* ranked as if an animal had walked past, while `speech` (500 of the 576 audio
|
|
25194
|
+
* rows measured live) already ranked default. That boost was an accident of the
|
|
25195
|
+
* class collision, not a decision. Pinned by `key-event-scoring.spec.ts`.
|
|
25196
|
+
*/
|
|
25180
25197
|
function classRank(className) {
|
|
25181
25198
|
const c = className.toLowerCase();
|
|
25182
25199
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
@@ -40484,6 +40501,24 @@ function buildRecordedStillPorts(input) {
|
|
|
40484
40501
|
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
40485
40502
|
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
40486
40503
|
/**
|
|
40504
|
+
* The owner declares its marker collection — like every other store in this
|
|
40505
|
+
* addon. KV shape (a single `data` column: `get`/`set` route the value as a
|
|
40506
|
+
* JSON blob; the engine adds the implicit `id` primary key). This declaration
|
|
40507
|
+
* never existed, so the engine refused every marker write, the catch below
|
|
40508
|
+
* warned, and every boot re-scanned the full analytics store — ~36k rows per
|
|
40509
|
+
* boot from the day the migration shipped.
|
|
40510
|
+
*/
|
|
40511
|
+
async function declareMigrationMarkers(store) {
|
|
40512
|
+
await store.declareCollection.mutate({
|
|
40513
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40514
|
+
columns: [{
|
|
40515
|
+
name: "data",
|
|
40516
|
+
type: "TEXT",
|
|
40517
|
+
notNull: true
|
|
40518
|
+
}]
|
|
40519
|
+
});
|
|
40520
|
+
}
|
|
40521
|
+
/**
|
|
40487
40522
|
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
40488
40523
|
*
|
|
40489
40524
|
* `null` = leave the row alone.
|
|
@@ -43664,6 +43699,47 @@ function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
|
43664
43699
|
return mark.priorRollbacks < 3;
|
|
43665
43700
|
}
|
|
43666
43701
|
//#endregion
|
|
43702
|
+
//#region src/pipeline-analytics/store/audio-track-class.ts
|
|
43703
|
+
/** The taxonomy macro every audio-sourced track carries as its class. */
|
|
43704
|
+
var AUDIO_TRACK_CLASS = "audio";
|
|
43705
|
+
/**
|
|
43706
|
+
* True when this row predates the contract: audio-sourced, but carrying a
|
|
43707
|
+
* naked audio macro as its class.
|
|
43708
|
+
*
|
|
43709
|
+
* Deliberately NOT "className is in AUDIO_MACRO_LABELS" — an id added to the
|
|
43710
|
+
* vocabulary after a row was written would then read as modern forever. The
|
|
43711
|
+
* question is the simple one: is this an audio row that does not say `audio`?
|
|
43712
|
+
*/
|
|
43713
|
+
function isLegacyAudioTrackClass(track) {
|
|
43714
|
+
return track.source === "audio" && track.className !== "audio";
|
|
43715
|
+
}
|
|
43716
|
+
/**
|
|
43717
|
+
* Bring a hydrated row up to the contract. Returns the SAME reference when
|
|
43718
|
+
* there is nothing to do, so the hot path (every non-audio track, and every
|
|
43719
|
+
* audio track written after this shipped) allocates nothing.
|
|
43720
|
+
*
|
|
43721
|
+
* The naked class is preserved as an `audioLabels` entry when — and only when
|
|
43722
|
+
* — the row carries none. On the 576 legacy rows measured on the live hub that
|
|
43723
|
+
* branch is unreachable (all of them have labels); it exists so the rewrite
|
|
43724
|
+
* can never be the step that DESTROYS the only copy of what was heard.
|
|
43725
|
+
*/
|
|
43726
|
+
function normalizeAudioTrackClass(track) {
|
|
43727
|
+
if (!isLegacyAudioTrackClass(track)) return track;
|
|
43728
|
+
const heard = {
|
|
43729
|
+
label: track.className,
|
|
43730
|
+
peakScore: 0,
|
|
43731
|
+
count: 1,
|
|
43732
|
+
firstAt: track.firstSeen,
|
|
43733
|
+
lastAt: track.lastSeen
|
|
43734
|
+
};
|
|
43735
|
+
const audioLabels = track.audioLabels !== void 0 && track.audioLabels.length > 0 ? track.audioLabels : [heard];
|
|
43736
|
+
return {
|
|
43737
|
+
...track,
|
|
43738
|
+
className: AUDIO_TRACK_CLASS,
|
|
43739
|
+
audioLabels
|
|
43740
|
+
};
|
|
43741
|
+
}
|
|
43742
|
+
//#endregion
|
|
43667
43743
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
43668
43744
|
function encodeRecentCursor(cursor) {
|
|
43669
43745
|
return Buffer.from(JSON.stringify({
|
|
@@ -45311,7 +45387,7 @@ var TrackStore = class {
|
|
|
45311
45387
|
maxX: envMaxX,
|
|
45312
45388
|
maxY: envMaxY
|
|
45313
45389
|
} : null;
|
|
45314
|
-
return {
|
|
45390
|
+
return normalizeAudioTrackClass({
|
|
45315
45391
|
trackId: id,
|
|
45316
45392
|
deviceId: Number(data["deviceId"]),
|
|
45317
45393
|
className: String(data["className"]),
|
|
@@ -45342,7 +45418,7 @@ var TrackStore = class {
|
|
|
45342
45418
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
45343
45419
|
observations,
|
|
45344
45420
|
...envelope !== null ? { envelope } : {}
|
|
45345
|
-
};
|
|
45421
|
+
});
|
|
45346
45422
|
}
|
|
45347
45423
|
};
|
|
45348
45424
|
//#endregion
|
|
@@ -47675,7 +47751,7 @@ var AudioMarkerProjector = class {
|
|
|
47675
47751
|
cameraId: deviceId,
|
|
47676
47752
|
sourceDeviceId: deviceId,
|
|
47677
47753
|
source: "audio",
|
|
47678
|
-
kind:
|
|
47754
|
+
kind: AUDIO_TRACK_CLASS,
|
|
47679
47755
|
timestamp: timestampMs,
|
|
47680
47756
|
...audioLabels !== void 0 ? { audioLabels } : {}
|
|
47681
47757
|
});
|
|
@@ -48883,6 +48959,23 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
48883
48959
|
* {@link isSpatialTrack}.
|
|
48884
48960
|
*/
|
|
48885
48961
|
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
48962
|
+
/**
|
|
48963
|
+
* What the debounce treats as "the same event happening again".
|
|
48964
|
+
*
|
|
48965
|
+
* `(camera, kind)` — plus, for an audio row, the class HEARD. Since D189 every
|
|
48966
|
+
* audio row carries `kind: 'audio'` and puts the class on `audioLabels`, so
|
|
48967
|
+
* keying on `kind` alone would collapse this to one bucket per camera. The
|
|
48968
|
+
* episode hysteresis cools down PER CLASS, so a bark three seconds after speech
|
|
48969
|
+
* is a legitimate second episode — and a per-camera bucket would drop it.
|
|
48970
|
+
*
|
|
48971
|
+
* Only the FIRST label is used: a materialize carries the one class the episode
|
|
48972
|
+
* opened on. Joining all of them would make `[speech]` and `[speech, dog]`
|
|
48973
|
+
* different buckets, which is the opposite of what a debounce is for.
|
|
48974
|
+
*/
|
|
48975
|
+
function debounceKey(input) {
|
|
48976
|
+
const heard = input.audioLabels?.[0]?.label;
|
|
48977
|
+
return heard === void 0 ? `${input.cameraId}:${input.kind}` : `${input.cameraId}:${input.kind}:${heard}`;
|
|
48978
|
+
}
|
|
48886
48979
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
48887
48980
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
48888
48981
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
@@ -48925,7 +49018,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48925
49018
|
snapshotTimeoutMs;
|
|
48926
49019
|
makeId;
|
|
48927
49020
|
now;
|
|
48928
|
-
/** Last snapshot time per
|
|
49021
|
+
/** Last snapshot time per {@link debounceKey}. */
|
|
48929
49022
|
lastAt = /* @__PURE__ */ new Map();
|
|
48930
49023
|
constructor(deps) {
|
|
48931
49024
|
this.deps = deps;
|
|
@@ -48964,7 +49057,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48964
49057
|
* still lands a track (with no media) — it never blocks the durable record.
|
|
48965
49058
|
*/
|
|
48966
49059
|
async materialize(input) {
|
|
48967
|
-
const key =
|
|
49060
|
+
const key = debounceKey(input);
|
|
48968
49061
|
const last = this.lastAt.get(key);
|
|
48969
49062
|
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
48970
49063
|
this.lastAt.set(key, this.now());
|
|
@@ -50531,6 +50624,125 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
50531
50624
|
movedFraction
|
|
50532
50625
|
};
|
|
50533
50626
|
}
|
|
50627
|
+
/** Where the completion marker lives — the collection `runLabelTierMigration`
|
|
50628
|
+
* already established for exactly this. */
|
|
50629
|
+
var AUDIO_CLASS_MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
50630
|
+
var AUDIO_CLASS_MIGRATION_MARKER_KEY = "audio-track-class-d189";
|
|
50631
|
+
/**
|
|
50632
|
+
* The decision for ONE stored row, exposed so the rule is testable without a
|
|
50633
|
+
* store. `null` = leave the row alone.
|
|
50634
|
+
*
|
|
50635
|
+
* Reads the RAW row rather than a hydrated `Track` on purpose: this is the
|
|
50636
|
+
* column the pass rewrites, and hydrating through `rowToTrack` would apply the
|
|
50637
|
+
* read-side normalization and hide the very rows it is looking for.
|
|
50638
|
+
*/
|
|
50639
|
+
function decideAudioClassRowMigration(data) {
|
|
50640
|
+
if (data["source"] !== "audio") return null;
|
|
50641
|
+
const className = data["className"];
|
|
50642
|
+
if (typeof className !== "string" || className.length === 0) return null;
|
|
50643
|
+
if (className === "audio") return null;
|
|
50644
|
+
const stored = data["audioLabels"];
|
|
50645
|
+
if (Array.isArray(stored) && stored.length > 0) return { className: AUDIO_TRACK_CLASS };
|
|
50646
|
+
const firstSeen = typeof data["firstSeen"] === "number" ? data["firstSeen"] : 0;
|
|
50647
|
+
return {
|
|
50648
|
+
className: AUDIO_TRACK_CLASS,
|
|
50649
|
+
audioLabels: [{
|
|
50650
|
+
label: className,
|
|
50651
|
+
peakScore: 0,
|
|
50652
|
+
count: 1,
|
|
50653
|
+
firstAt: firstSeen,
|
|
50654
|
+
lastAt: typeof data["lastSeen"] === "number" ? data["lastSeen"] : firstSeen
|
|
50655
|
+
}]
|
|
50656
|
+
};
|
|
50657
|
+
}
|
|
50658
|
+
/**
|
|
50659
|
+
* Run the pass over the tracks collection, once.
|
|
50660
|
+
*
|
|
50661
|
+
* The marker short-circuits a completed migration; it is written only when the
|
|
50662
|
+
* pass did NOT truncate, so an interrupted run resumes on the next boot instead
|
|
50663
|
+
* of declaring itself done.
|
|
50664
|
+
*/
|
|
50665
|
+
async function runAudioTrackClassMigration(deps, collection) {
|
|
50666
|
+
try {
|
|
50667
|
+
const marker = await deps.store.get.query({
|
|
50668
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50669
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY
|
|
50670
|
+
});
|
|
50671
|
+
if (marker !== null && marker !== void 0) return null;
|
|
50672
|
+
} catch {}
|
|
50673
|
+
let scanned = 0;
|
|
50674
|
+
let rewritten = 0;
|
|
50675
|
+
let labelsRecovered = 0;
|
|
50676
|
+
let skipped = 0;
|
|
50677
|
+
let offset = 0;
|
|
50678
|
+
let truncated = false;
|
|
50679
|
+
for (;;) {
|
|
50680
|
+
if (scanned >= 2e5) {
|
|
50681
|
+
truncated = true;
|
|
50682
|
+
break;
|
|
50683
|
+
}
|
|
50684
|
+
const rows = await deps.store.query.query({
|
|
50685
|
+
collection,
|
|
50686
|
+
filter: {
|
|
50687
|
+
limit: 500,
|
|
50688
|
+
offset
|
|
50689
|
+
}
|
|
50690
|
+
});
|
|
50691
|
+
if (rows.length === 0) break;
|
|
50692
|
+
for (const row of rows) {
|
|
50693
|
+
scanned++;
|
|
50694
|
+
const patch = decideAudioClassRowMigration(row.data);
|
|
50695
|
+
if (patch === null) {
|
|
50696
|
+
skipped++;
|
|
50697
|
+
continue;
|
|
50698
|
+
}
|
|
50699
|
+
const deviceId = typeof row.data["deviceId"] === "number" ? row.data["deviceId"] : void 0;
|
|
50700
|
+
try {
|
|
50701
|
+
await deps.store.update.mutate({
|
|
50702
|
+
collection,
|
|
50703
|
+
id: row.id,
|
|
50704
|
+
data: patch
|
|
50705
|
+
});
|
|
50706
|
+
rewritten++;
|
|
50707
|
+
if (patch["audioLabels"] !== void 0) labelsRecovered++;
|
|
50708
|
+
} catch (err) {
|
|
50709
|
+
deps.logger.warn("audio track class migration row failed", {
|
|
50710
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
50711
|
+
meta: {
|
|
50712
|
+
collection,
|
|
50713
|
+
id: row.id,
|
|
50714
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50715
|
+
}
|
|
50716
|
+
});
|
|
50717
|
+
skipped++;
|
|
50718
|
+
}
|
|
50719
|
+
}
|
|
50720
|
+
if (rows.length < 500) break;
|
|
50721
|
+
offset += rows.length;
|
|
50722
|
+
}
|
|
50723
|
+
if (!truncated) try {
|
|
50724
|
+
await deps.store.set.mutate({
|
|
50725
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50726
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY,
|
|
50727
|
+
value: {
|
|
50728
|
+
completedAt: deps.now(),
|
|
50729
|
+
rewritten
|
|
50730
|
+
}
|
|
50731
|
+
});
|
|
50732
|
+
} catch (err) {
|
|
50733
|
+
deps.logger.warn("audio track class migration marker failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50734
|
+
}
|
|
50735
|
+
const stats = {
|
|
50736
|
+
collection,
|
|
50737
|
+
scanned,
|
|
50738
|
+
rewritten,
|
|
50739
|
+
labelsRecovered,
|
|
50740
|
+
skipped,
|
|
50741
|
+
truncated
|
|
50742
|
+
};
|
|
50743
|
+
deps.logger.info("audio track class migration", { meta: { ...stats } });
|
|
50744
|
+
return stats;
|
|
50745
|
+
}
|
|
50534
50746
|
//#endregion
|
|
50535
50747
|
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
50536
50748
|
/**
|
|
@@ -52949,7 +53161,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52949
53161
|
AnalyticsLts.declare(api.settingsStore),
|
|
52950
53162
|
SceneStore.declare(api.settingsStore),
|
|
52951
53163
|
NotificationCenter.declare(api.settingsStore),
|
|
52952
|
-
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
53164
|
+
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
53165
|
+
declareMigrationMarkers(api.settingsStore)
|
|
52953
53166
|
]);
|
|
52954
53167
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
52955
53168
|
try {
|
|
@@ -52967,6 +53180,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52967
53180
|
}, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]).catch((err) => {
|
|
52968
53181
|
this.ctx.logger.warn("label tier migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
52969
53182
|
});
|
|
53183
|
+
runAudioTrackClassMigration({
|
|
53184
|
+
store: api.settingsStore,
|
|
53185
|
+
logger: this.ctx.logger,
|
|
53186
|
+
now: () => Date.now()
|
|
53187
|
+
}, TRACKS_COLLECTION).catch((err) => {
|
|
53188
|
+
this.ctx.logger.warn("audio track class migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53189
|
+
});
|
|
52970
53190
|
}
|
|
52971
53191
|
/** Event-media storage. By default it uses `ctx.kernel.storage`, rooted at
|
|
52972
53192
|
* the node data dir (appData) → heavy thumbnail/track-frame churn hammers
|
|
@@ -56242,7 +56462,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
56242
56462
|
cameraId: input.deviceId,
|
|
56243
56463
|
sourceDeviceId: input.deviceId,
|
|
56244
56464
|
source: "audio",
|
|
56245
|
-
kind:
|
|
56465
|
+
kind: AUDIO_TRACK_CLASS,
|
|
56246
56466
|
timestamp: input.timestamp,
|
|
56247
56467
|
audioLabels: [{
|
|
56248
56468
|
label: ref.className,
|
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.103",
|
|
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",
|