@camstack/addon-post-analysis 1.2.102 → 1.2.104
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-CZm8slPB.mjs → dist-IZLiqXRa.mjs} +10 -1
- package/dist/{dist-BwxXdV9d.js → dist-KXhjlCmh.js} +10 -1
- 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-MP5xue-m.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D8fRo8aP.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-CkptLue6.mjs → hostInit-7agPxY0d.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +266 -16
- package/dist/pipeline-analytics/index.mjs +266 -16
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -20739,6 +20739,7 @@ DeviceType.Camera, method(object({
|
|
|
20739
20739
|
isBattery: boolean(),
|
|
20740
20740
|
reason: _enum([
|
|
20741
20741
|
"disabled",
|
|
20742
|
+
"offline",
|
|
20742
20743
|
"sleeping",
|
|
20743
20744
|
"unreachable",
|
|
20744
20745
|
"waking"
|
|
@@ -20784,9 +20785,17 @@ targets: array(object({
|
|
|
20784
20785
|
sleeping: boolean(),
|
|
20785
20786
|
/** Current device state rendered over the cached frame. State images
|
|
20786
20787
|
* remain authoritative even when their photographic background is
|
|
20787
|
-
* old; null means the link must carry a current camera frame.
|
|
20788
|
+
* old; null means the link must carry a current camera frame.
|
|
20789
|
+
*
|
|
20790
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
20791
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
20792
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
20793
|
+
* — the frame comes from here, and so must the sentence over it
|
|
20794
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
20795
|
+
* state. */
|
|
20788
20796
|
stateReason: _enum([
|
|
20789
20797
|
"disabled",
|
|
20798
|
+
"offline",
|
|
20790
20799
|
"sleeping",
|
|
20791
20800
|
"unreachable",
|
|
20792
20801
|
"waking"
|
|
@@ -20770,6 +20770,7 @@ DeviceType.Camera, method(object({
|
|
|
20770
20770
|
isBattery: boolean(),
|
|
20771
20771
|
reason: _enum([
|
|
20772
20772
|
"disabled",
|
|
20773
|
+
"offline",
|
|
20773
20774
|
"sleeping",
|
|
20774
20775
|
"unreachable",
|
|
20775
20776
|
"waking"
|
|
@@ -20815,9 +20816,17 @@ targets: array(object({
|
|
|
20815
20816
|
sleeping: boolean(),
|
|
20816
20817
|
/** Current device state rendered over the cached frame. State images
|
|
20817
20818
|
* remain authoritative even when their photographic background is
|
|
20818
|
-
* old; null means the link must carry a current camera frame.
|
|
20819
|
+
* old; null means the link must carry a current camera frame.
|
|
20820
|
+
*
|
|
20821
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
20822
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
20823
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
20824
|
+
* — the frame comes from here, and so must the sentence over it
|
|
20825
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
20826
|
+
* state. */
|
|
20819
20827
|
stateReason: _enum([
|
|
20820
20828
|
"disabled",
|
|
20829
|
+
"offline",
|
|
20821
20830
|
"sleeping",
|
|
20822
20831
|
"unreachable",
|
|
20823
20832
|
"waking"
|
|
@@ -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-KXhjlCmh.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as PoolMemoryWatchdog, Q as embeddingEncoderCapability, dt as resolvePoolMemoryPolicy, ot as parseProcStatus, tt as hfModelUrl, yt as BaseAddon } from "../dist-
|
|
1
|
+
import { O as PoolMemoryWatchdog, Q as embeddingEncoderCapability, dt as resolvePoolMemoryPolicy, ot as parseProcStatus, tt as hfModelUrl, yt as BaseAddon } from "../dist-IZLiqXRa.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import "node:fs";
|
|
@@ -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-KXhjlCmh.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");
|
|
@@ -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",
|
|
@@ -18163,7 +18171,7 @@ function applyTemplatePatch(merged, template) {
|
|
|
18163
18171
|
return withoutTemplate;
|
|
18164
18172
|
}
|
|
18165
18173
|
/** Narrowing guard — the persisted `rule` column is `unknown` until parsed. */
|
|
18166
|
-
function isRecord$
|
|
18174
|
+
function isRecord$2(value) {
|
|
18167
18175
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18168
18176
|
}
|
|
18169
18177
|
/**
|
|
@@ -18193,7 +18201,7 @@ function isRecord$1(value) {
|
|
|
18193
18201
|
* the same reason.
|
|
18194
18202
|
*/
|
|
18195
18203
|
function withCreatedAt(rowId, raw, synthesised, now) {
|
|
18196
|
-
if (!isRecord$
|
|
18204
|
+
if (!isRecord$2(raw)) return raw;
|
|
18197
18205
|
if (typeof raw["createdAt"] === "number") return raw;
|
|
18198
18206
|
const stamped = synthesised.get(rowId) ?? now();
|
|
18199
18207
|
synthesised.set(rowId, stamped);
|
|
@@ -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;
|
|
@@ -40540,10 +40557,38 @@ function buildRecordedStillPorts(input) {
|
|
|
40540
40557
|
})
|
|
40541
40558
|
};
|
|
40542
40559
|
}
|
|
40543
|
-
/**
|
|
40560
|
+
/**
|
|
40561
|
+
* Where the completion marker lives.
|
|
40562
|
+
*
|
|
40563
|
+
* @durable class=config owner=pipeline-analytics
|
|
40564
|
+
* write="one marker row when a boot migration completes; overwritten only by
|
|
40565
|
+
* a re-run of the same migration"
|
|
40566
|
+
* loss="a re-scan of the analytics store on the next boot — idempotent,
|
|
40567
|
+
* never destructive"
|
|
40568
|
+
* retention="never pruned: one row per completed migration, bounded by the
|
|
40569
|
+
* number of migrations ever written (two today)"
|
|
40570
|
+
*/
|
|
40544
40571
|
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
40545
40572
|
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
40546
40573
|
/**
|
|
40574
|
+
* The owner declares its marker collection — like every other store in this
|
|
40575
|
+
* addon. KV shape (a single `data` column: `get`/`set` route the value as a
|
|
40576
|
+
* JSON blob; the engine adds the implicit `id` primary key). This declaration
|
|
40577
|
+
* never existed, so the engine refused every marker write, the catch below
|
|
40578
|
+
* warned, and every boot re-scanned the full analytics store — ~36k rows per
|
|
40579
|
+
* boot from the day the migration shipped.
|
|
40580
|
+
*/
|
|
40581
|
+
async function declareMigrationMarkers(store) {
|
|
40582
|
+
await store.declareCollection.mutate({
|
|
40583
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40584
|
+
columns: [{
|
|
40585
|
+
name: "data",
|
|
40586
|
+
type: "TEXT",
|
|
40587
|
+
notNull: true
|
|
40588
|
+
}]
|
|
40589
|
+
});
|
|
40590
|
+
}
|
|
40591
|
+
/**
|
|
40547
40592
|
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
40548
40593
|
*
|
|
40549
40594
|
* `null` = leave the row alone.
|
|
@@ -43702,11 +43747,11 @@ function rowToSensorEvent(id, data) {
|
|
|
43702
43747
|
deviceId: Number(data["deviceId"]),
|
|
43703
43748
|
sourceDeviceId: Number(data["sourceDeviceId"]),
|
|
43704
43749
|
kind: String(data["kind"]),
|
|
43705
|
-
value: isRecord(value) ? value : null,
|
|
43750
|
+
value: isRecord$1(value) ? value : null,
|
|
43706
43751
|
timestamp: Number(data["timestamp"])
|
|
43707
43752
|
};
|
|
43708
43753
|
}
|
|
43709
|
-
function isRecord(x) {
|
|
43754
|
+
function isRecord$1(x) {
|
|
43710
43755
|
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
43711
43756
|
}
|
|
43712
43757
|
/**
|
|
@@ -43724,6 +43769,47 @@ function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
|
43724
43769
|
return mark.priorRollbacks < 3;
|
|
43725
43770
|
}
|
|
43726
43771
|
//#endregion
|
|
43772
|
+
//#region src/pipeline-analytics/store/audio-track-class.ts
|
|
43773
|
+
/** The taxonomy macro every audio-sourced track carries as its class. */
|
|
43774
|
+
var AUDIO_TRACK_CLASS = "audio";
|
|
43775
|
+
/**
|
|
43776
|
+
* True when this row predates the contract: audio-sourced, but carrying a
|
|
43777
|
+
* naked audio macro as its class.
|
|
43778
|
+
*
|
|
43779
|
+
* Deliberately NOT "className is in AUDIO_MACRO_LABELS" — an id added to the
|
|
43780
|
+
* vocabulary after a row was written would then read as modern forever. The
|
|
43781
|
+
* question is the simple one: is this an audio row that does not say `audio`?
|
|
43782
|
+
*/
|
|
43783
|
+
function isLegacyAudioTrackClass(track) {
|
|
43784
|
+
return track.source === "audio" && track.className !== "audio";
|
|
43785
|
+
}
|
|
43786
|
+
/**
|
|
43787
|
+
* Bring a hydrated row up to the contract. Returns the SAME reference when
|
|
43788
|
+
* there is nothing to do, so the hot path (every non-audio track, and every
|
|
43789
|
+
* audio track written after this shipped) allocates nothing.
|
|
43790
|
+
*
|
|
43791
|
+
* The naked class is preserved as an `audioLabels` entry when — and only when
|
|
43792
|
+
* — the row carries none. On the 576 legacy rows measured on the live hub that
|
|
43793
|
+
* branch is unreachable (all of them have labels); it exists so the rewrite
|
|
43794
|
+
* can never be the step that DESTROYS the only copy of what was heard.
|
|
43795
|
+
*/
|
|
43796
|
+
function normalizeAudioTrackClass(track) {
|
|
43797
|
+
if (!isLegacyAudioTrackClass(track)) return track;
|
|
43798
|
+
const heard = {
|
|
43799
|
+
label: track.className,
|
|
43800
|
+
peakScore: 0,
|
|
43801
|
+
count: 1,
|
|
43802
|
+
firstAt: track.firstSeen,
|
|
43803
|
+
lastAt: track.lastSeen
|
|
43804
|
+
};
|
|
43805
|
+
const audioLabels = track.audioLabels !== void 0 && track.audioLabels.length > 0 ? track.audioLabels : [heard];
|
|
43806
|
+
return {
|
|
43807
|
+
...track,
|
|
43808
|
+
className: AUDIO_TRACK_CLASS,
|
|
43809
|
+
audioLabels
|
|
43810
|
+
};
|
|
43811
|
+
}
|
|
43812
|
+
//#endregion
|
|
43727
43813
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
43728
43814
|
function encodeRecentCursor(cursor) {
|
|
43729
43815
|
return Buffer.from(JSON.stringify({
|
|
@@ -44023,6 +44109,24 @@ var TRACKS_INDEXES = [{
|
|
|
44023
44109
|
name: "idx_tracks_device_firstSeen",
|
|
44024
44110
|
columns: ["deviceId", "firstSeen"]
|
|
44025
44111
|
}];
|
|
44112
|
+
function isRecord(v) {
|
|
44113
|
+
return typeof v === "object" && v !== null;
|
|
44114
|
+
}
|
|
44115
|
+
/** Validate the JSON column back into `Track.audioLabels` — a malformed row
|
|
44116
|
+
* yields null rather than a lying shape (this column now carries the meaning
|
|
44117
|
+
* of an audio row, D189; it must not be trusted by assertion). */
|
|
44118
|
+
function readAudioLabels(v) {
|
|
44119
|
+
if (!Array.isArray(v)) return null;
|
|
44120
|
+
const out = [];
|
|
44121
|
+
for (const e of v) if (isRecord(e) && typeof e["label"] === "string" && typeof e["peakScore"] === "number" && typeof e["count"] === "number" && typeof e["firstAt"] === "number" && typeof e["lastAt"] === "number") out.push({
|
|
44122
|
+
label: e["label"],
|
|
44123
|
+
peakScore: e["peakScore"],
|
|
44124
|
+
count: e["count"],
|
|
44125
|
+
firstAt: e["firstAt"],
|
|
44126
|
+
lastAt: e["lastAt"]
|
|
44127
|
+
});
|
|
44128
|
+
return out.length > 0 ? out : null;
|
|
44129
|
+
}
|
|
44026
44130
|
/** Serialize the per-label aggregate map into the `Track.audioLabels`
|
|
44027
44131
|
* array shape, most-frequent label first. */
|
|
44028
44132
|
function audioLabelsToArray(agg) {
|
|
@@ -45223,8 +45327,9 @@ var TrackStore = class {
|
|
|
45223
45327
|
});
|
|
45224
45328
|
const names = /* @__PURE__ */ new Set();
|
|
45225
45329
|
for (const r of rows) {
|
|
45330
|
+
const isAudio = r.data["source"] === "audio";
|
|
45226
45331
|
const className = r.data["className"];
|
|
45227
|
-
if (typeof className === "string" && className.length > 0) names.add(className);
|
|
45332
|
+
if (typeof className === "string" && className.length > 0) names.add(isAudio ? AUDIO_TRACK_CLASS : className);
|
|
45228
45333
|
const classes = r.data["classes"];
|
|
45229
45334
|
if (Array.isArray(classes)) {
|
|
45230
45335
|
for (const c of classes) if (typeof c === "string" && c.length > 0) names.add(c);
|
|
@@ -45353,7 +45458,7 @@ var TrackStore = class {
|
|
|
45353
45458
|
const importance = data["importance"];
|
|
45354
45459
|
const bestEventId = data["bestEventId"];
|
|
45355
45460
|
const importanceReason = data["importanceReason"];
|
|
45356
|
-
const audioLabels = data["audioLabels"];
|
|
45461
|
+
const audioLabels = readAudioLabels(data["audioLabels"]);
|
|
45357
45462
|
const retrainParsed = require_dist.RetrainStatusSchema.safeParse(data["retrainStatus"]);
|
|
45358
45463
|
const debug = data["debug"];
|
|
45359
45464
|
const favourited = data["favourited"];
|
|
@@ -45371,7 +45476,7 @@ var TrackStore = class {
|
|
|
45371
45476
|
maxX: envMaxX,
|
|
45372
45477
|
maxY: envMaxY
|
|
45373
45478
|
} : null;
|
|
45374
|
-
return {
|
|
45479
|
+
return normalizeAudioTrackClass({
|
|
45375
45480
|
trackId: id,
|
|
45376
45481
|
deviceId: Number(data["deviceId"]),
|
|
45377
45482
|
className: String(data["className"]),
|
|
@@ -45402,7 +45507,7 @@ var TrackStore = class {
|
|
|
45402
45507
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
45403
45508
|
observations,
|
|
45404
45509
|
...envelope !== null ? { envelope } : {}
|
|
45405
|
-
};
|
|
45510
|
+
});
|
|
45406
45511
|
}
|
|
45407
45512
|
};
|
|
45408
45513
|
//#endregion
|
|
@@ -47735,7 +47840,7 @@ var AudioMarkerProjector = class {
|
|
|
47735
47840
|
cameraId: deviceId,
|
|
47736
47841
|
sourceDeviceId: deviceId,
|
|
47737
47842
|
source: "audio",
|
|
47738
|
-
kind:
|
|
47843
|
+
kind: AUDIO_TRACK_CLASS,
|
|
47739
47844
|
timestamp: timestampMs,
|
|
47740
47845
|
...audioLabels !== void 0 ? { audioLabels } : {}
|
|
47741
47846
|
});
|
|
@@ -48943,6 +49048,23 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
48943
49048
|
* {@link isSpatialTrack}.
|
|
48944
49049
|
*/
|
|
48945
49050
|
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
49051
|
+
/**
|
|
49052
|
+
* What the debounce treats as "the same event happening again".
|
|
49053
|
+
*
|
|
49054
|
+
* `(camera, kind)` — plus, for an audio row, the class HEARD. Since D189 every
|
|
49055
|
+
* audio row carries `kind: 'audio'` and puts the class on `audioLabels`, so
|
|
49056
|
+
* keying on `kind` alone would collapse this to one bucket per camera. The
|
|
49057
|
+
* episode hysteresis cools down PER CLASS, so a bark three seconds after speech
|
|
49058
|
+
* is a legitimate second episode — and a per-camera bucket would drop it.
|
|
49059
|
+
*
|
|
49060
|
+
* Only the FIRST label is used: a materialize carries the one class the episode
|
|
49061
|
+
* opened on. Joining all of them would make `[speech]` and `[speech, dog]`
|
|
49062
|
+
* different buckets, which is the opposite of what a debounce is for.
|
|
49063
|
+
*/
|
|
49064
|
+
function debounceKey(input) {
|
|
49065
|
+
const heard = input.audioLabels?.[0]?.label;
|
|
49066
|
+
return heard === void 0 ? `${input.cameraId}:${input.kind}` : `${input.cameraId}:${input.kind}:${heard}`;
|
|
49067
|
+
}
|
|
48946
49068
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
48947
49069
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
48948
49070
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
@@ -48985,7 +49107,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48985
49107
|
snapshotTimeoutMs;
|
|
48986
49108
|
makeId;
|
|
48987
49109
|
now;
|
|
48988
|
-
/** Last snapshot time per
|
|
49110
|
+
/** Last snapshot time per {@link debounceKey}. */
|
|
48989
49111
|
lastAt = /* @__PURE__ */ new Map();
|
|
48990
49112
|
constructor(deps) {
|
|
48991
49113
|
this.deps = deps;
|
|
@@ -49024,7 +49146,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
49024
49146
|
* still lands a track (with no media) — it never blocks the durable record.
|
|
49025
49147
|
*/
|
|
49026
49148
|
async materialize(input) {
|
|
49027
|
-
const key =
|
|
49149
|
+
const key = debounceKey(input);
|
|
49028
49150
|
const last = this.lastAt.get(key);
|
|
49029
49151
|
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
49030
49152
|
this.lastAt.set(key, this.now());
|
|
@@ -50591,6 +50713,126 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
50591
50713
|
movedFraction
|
|
50592
50714
|
};
|
|
50593
50715
|
}
|
|
50716
|
+
/** Where the completion marker lives — the SAME collection
|
|
50717
|
+
* `runLabelTierMigration` owns and classifies; referencing the constant
|
|
50718
|
+
* instead of repeating the literal keeps one declarer for the guards. */
|
|
50719
|
+
var AUDIO_CLASS_MIGRATION_MARKER_COLLECTION = MIGRATION_MARKER_COLLECTION;
|
|
50720
|
+
var AUDIO_CLASS_MIGRATION_MARKER_KEY = "audio-track-class-d189";
|
|
50721
|
+
/**
|
|
50722
|
+
* The decision for ONE stored row, exposed so the rule is testable without a
|
|
50723
|
+
* store. `null` = leave the row alone.
|
|
50724
|
+
*
|
|
50725
|
+
* Reads the RAW row rather than a hydrated `Track` on purpose: this is the
|
|
50726
|
+
* column the pass rewrites, and hydrating through `rowToTrack` would apply the
|
|
50727
|
+
* read-side normalization and hide the very rows it is looking for.
|
|
50728
|
+
*/
|
|
50729
|
+
function decideAudioClassRowMigration(data) {
|
|
50730
|
+
if (data["source"] !== "audio") return null;
|
|
50731
|
+
const className = data["className"];
|
|
50732
|
+
if (typeof className !== "string" || className.length === 0) return null;
|
|
50733
|
+
if (className === "audio") return null;
|
|
50734
|
+
const stored = data["audioLabels"];
|
|
50735
|
+
if (Array.isArray(stored) && stored.length > 0) return { className: AUDIO_TRACK_CLASS };
|
|
50736
|
+
const firstSeen = typeof data["firstSeen"] === "number" ? data["firstSeen"] : 0;
|
|
50737
|
+
return {
|
|
50738
|
+
className: AUDIO_TRACK_CLASS,
|
|
50739
|
+
audioLabels: [{
|
|
50740
|
+
label: className,
|
|
50741
|
+
peakScore: 0,
|
|
50742
|
+
count: 1,
|
|
50743
|
+
firstAt: firstSeen,
|
|
50744
|
+
lastAt: typeof data["lastSeen"] === "number" ? data["lastSeen"] : firstSeen
|
|
50745
|
+
}]
|
|
50746
|
+
};
|
|
50747
|
+
}
|
|
50748
|
+
/**
|
|
50749
|
+
* Run the pass over the tracks collection, once.
|
|
50750
|
+
*
|
|
50751
|
+
* The marker short-circuits a completed migration; it is written only when the
|
|
50752
|
+
* pass did NOT truncate, so an interrupted run resumes on the next boot instead
|
|
50753
|
+
* of declaring itself done.
|
|
50754
|
+
*/
|
|
50755
|
+
async function runAudioTrackClassMigration(deps, collection) {
|
|
50756
|
+
try {
|
|
50757
|
+
const marker = await deps.store.get.query({
|
|
50758
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50759
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY
|
|
50760
|
+
});
|
|
50761
|
+
if (marker !== null && marker !== void 0) return null;
|
|
50762
|
+
} catch {}
|
|
50763
|
+
let scanned = 0;
|
|
50764
|
+
let rewritten = 0;
|
|
50765
|
+
let labelsRecovered = 0;
|
|
50766
|
+
let skipped = 0;
|
|
50767
|
+
let offset = 0;
|
|
50768
|
+
let truncated = false;
|
|
50769
|
+
for (;;) {
|
|
50770
|
+
if (scanned >= 2e5) {
|
|
50771
|
+
truncated = true;
|
|
50772
|
+
break;
|
|
50773
|
+
}
|
|
50774
|
+
const rows = await deps.store.query.query({
|
|
50775
|
+
collection,
|
|
50776
|
+
filter: {
|
|
50777
|
+
limit: 500,
|
|
50778
|
+
offset
|
|
50779
|
+
}
|
|
50780
|
+
});
|
|
50781
|
+
if (rows.length === 0) break;
|
|
50782
|
+
for (const row of rows) {
|
|
50783
|
+
scanned++;
|
|
50784
|
+
const patch = decideAudioClassRowMigration(row.data);
|
|
50785
|
+
if (patch === null) {
|
|
50786
|
+
skipped++;
|
|
50787
|
+
continue;
|
|
50788
|
+
}
|
|
50789
|
+
const deviceId = typeof row.data["deviceId"] === "number" ? row.data["deviceId"] : void 0;
|
|
50790
|
+
try {
|
|
50791
|
+
await deps.store.update.mutate({
|
|
50792
|
+
collection,
|
|
50793
|
+
id: row.id,
|
|
50794
|
+
data: patch
|
|
50795
|
+
});
|
|
50796
|
+
rewritten++;
|
|
50797
|
+
if (patch["audioLabels"] !== void 0) labelsRecovered++;
|
|
50798
|
+
} catch (err) {
|
|
50799
|
+
deps.logger.warn("audio track class migration row failed", {
|
|
50800
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
50801
|
+
meta: {
|
|
50802
|
+
collection,
|
|
50803
|
+
id: row.id,
|
|
50804
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50805
|
+
}
|
|
50806
|
+
});
|
|
50807
|
+
skipped++;
|
|
50808
|
+
}
|
|
50809
|
+
}
|
|
50810
|
+
if (rows.length < 500) break;
|
|
50811
|
+
offset += rows.length;
|
|
50812
|
+
}
|
|
50813
|
+
if (!truncated) try {
|
|
50814
|
+
await deps.store.set.mutate({
|
|
50815
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50816
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY,
|
|
50817
|
+
value: {
|
|
50818
|
+
completedAt: deps.now(),
|
|
50819
|
+
rewritten
|
|
50820
|
+
}
|
|
50821
|
+
});
|
|
50822
|
+
} catch (err) {
|
|
50823
|
+
deps.logger.warn("audio track class migration marker failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50824
|
+
}
|
|
50825
|
+
const stats = {
|
|
50826
|
+
collection,
|
|
50827
|
+
scanned,
|
|
50828
|
+
rewritten,
|
|
50829
|
+
labelsRecovered,
|
|
50830
|
+
skipped,
|
|
50831
|
+
truncated
|
|
50832
|
+
};
|
|
50833
|
+
deps.logger.info("audio track class migration", { meta: { ...stats } });
|
|
50834
|
+
return stats;
|
|
50835
|
+
}
|
|
50594
50836
|
//#endregion
|
|
50595
50837
|
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
50596
50838
|
/**
|
|
@@ -53009,7 +53251,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53009
53251
|
AnalyticsLts.declare(api.settingsStore),
|
|
53010
53252
|
SceneStore.declare(api.settingsStore),
|
|
53011
53253
|
NotificationCenter.declare(api.settingsStore),
|
|
53012
|
-
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
53254
|
+
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
53255
|
+
declareMigrationMarkers(api.settingsStore)
|
|
53013
53256
|
]);
|
|
53014
53257
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
53015
53258
|
try {
|
|
@@ -53027,6 +53270,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53027
53270
|
}, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]).catch((err) => {
|
|
53028
53271
|
this.ctx.logger.warn("label tier migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53029
53272
|
});
|
|
53273
|
+
runAudioTrackClassMigration({
|
|
53274
|
+
store: api.settingsStore,
|
|
53275
|
+
logger: this.ctx.logger,
|
|
53276
|
+
now: () => Date.now()
|
|
53277
|
+
}, TRACKS_COLLECTION).catch((err) => {
|
|
53278
|
+
this.ctx.logger.warn("audio track class migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53279
|
+
});
|
|
53030
53280
|
}
|
|
53031
53281
|
/** Event-media storage. By default it uses `ctx.kernel.storage`, rooted at
|
|
53032
53282
|
* the node data dir (appData) → heavy thumbnail/track-frame churn hammers
|
|
@@ -56302,7 +56552,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
56302
56552
|
cameraId: input.deviceId,
|
|
56303
56553
|
sourceDeviceId: input.deviceId,
|
|
56304
56554
|
source: "audio",
|
|
56305
|
-
kind:
|
|
56555
|
+
kind: AUDIO_TRACK_CLASS,
|
|
56306
56556
|
timestamp: input.timestamp,
|
|
56307
56557
|
audioLabels: [{
|
|
56308
56558
|
label: ref.className,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as encodeVectorBase64, A as RetrainStatusSchema, At as literal, B as TrackSourceSchema, C as NcSnoozeSchema, Ct as isDeviceScopedCap, D as OpsLogEntrySchema, Dt as array, E as NcTaxonomySchema, Et as _enum, F as SceneMonitorSchema, Ft as string, G as audioMetricsCapability, H as addonWidgetsSourceCapability, I as TIMELAPSE_DENSE_FLOOR_SEC, It as unknown, J as cosineSimilarity$1, K as audioModeOf, L as TimelapseRuleInputSchema, Lt as EventCategory, M as SCENE_DEFAULT_ANCHOR_THRESHOLD, Mt as object, N as SCENE_DEFAULT_UNCOVERED_POLICY, Nt as partialRecord, Ot as boolean, P as SCENE_DIVERGED, Pt as record, R as TimelapseRulePatchSchema, S as NcSnoozeInputSchema, St as hydrateSchema, T as NcSystemEventKindSchema, Tt as sleep, U as alarmPanelCapability, V as VISIT_MERGE_GAP_MS, W as assertTimelapseCadences, X as defineCustomActions, Y as customAction, Z as deriveRecordingMode, _ as NcRuleInputSchema, _t as zoneAnalyticsCapability, at as notificationRulesCapability, b as NcRuleTargetSchema, bt as DeviceType, c as EVENT_PAD_MS, ct as plateGalleryCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, et as faceGalleryCapability, f as NC_CONDITION_CATALOG, ft as sceneMonitorCapability, g as NcConditionDescriptorSchema, gt as videoclipsCapability, h as NC_TAXONOMY, ht as vectorDimFromBase64, i as DETECTION_MACRO_CLASSES, it as kebabToCamel, jt as number, k as RECORDING_EXPORT_MAX_READ_BYTES, kt as discriminatedUnion, l as LabelAttributionSchema, lt as readDeviceStateFrom, mt as systemEventFilterApplies, n as DEFAULT_EVENT_COLOR, nt as isDetectionMacroClass, o as DeclaredDevices, p as NC_DEFAULT_SNOOZE_MINUTES, pt as subKindsOf, q as buildEventKindDescriptor, rt as isScheduleActive, s as EVENT_KIND_BY_CAP, st as pipelineAnalyticsCapability, t as BaseDevice, u as MACRO_LABELS, ut as readTimelapseGeneratedAt, v as NcRulePatchSchema, vt as errMsg, w as NcSnoozeSuppressedSchema, wt as nodePin, x as NcScheduleSchema, xt as createEvent, y as NcRuleSchema, yt as BaseAddon, z as TimelapseRuleSchema } from "../dist-
|
|
1
|
+
import { $ as encodeVectorBase64, A as RetrainStatusSchema, At as literal, B as TrackSourceSchema, C as NcSnoozeSchema, Ct as isDeviceScopedCap, D as OpsLogEntrySchema, Dt as array, E as NcTaxonomySchema, Et as _enum, F as SceneMonitorSchema, Ft as string, G as audioMetricsCapability, H as addonWidgetsSourceCapability, I as TIMELAPSE_DENSE_FLOOR_SEC, It as unknown, J as cosineSimilarity$1, K as audioModeOf, L as TimelapseRuleInputSchema, Lt as EventCategory, M as SCENE_DEFAULT_ANCHOR_THRESHOLD, Mt as object, N as SCENE_DEFAULT_UNCOVERED_POLICY, Nt as partialRecord, Ot as boolean, P as SCENE_DIVERGED, Pt as record, R as TimelapseRulePatchSchema, S as NcSnoozeInputSchema, St as hydrateSchema, T as NcSystemEventKindSchema, Tt as sleep, U as alarmPanelCapability, V as VISIT_MERGE_GAP_MS, W as assertTimelapseCadences, X as defineCustomActions, Y as customAction, Z as deriveRecordingMode, _ as NcRuleInputSchema, _t as zoneAnalyticsCapability, at as notificationRulesCapability, b as NcRuleTargetSchema, bt as DeviceType, c as EVENT_PAD_MS, ct as plateGalleryCapability, d as NC_ALARM_SYSTEM_EVENT_KINDS, et as faceGalleryCapability, f as NC_CONDITION_CATALOG, ft as sceneMonitorCapability, g as NcConditionDescriptorSchema, gt as videoclipsCapability, h as NC_TAXONOMY, ht as vectorDimFromBase64, i as DETECTION_MACRO_CLASSES, it as kebabToCamel, jt as number, k as RECORDING_EXPORT_MAX_READ_BYTES, kt as discriminatedUnion, l as LabelAttributionSchema, lt as readDeviceStateFrom, mt as systemEventFilterApplies, n as DEFAULT_EVENT_COLOR, nt as isDetectionMacroClass, o as DeclaredDevices, p as NC_DEFAULT_SNOOZE_MINUTES, pt as subKindsOf, q as buildEventKindDescriptor, rt as isScheduleActive, s as EVENT_KIND_BY_CAP, st as pipelineAnalyticsCapability, t as BaseDevice, u as MACRO_LABELS, ut as readTimelapseGeneratedAt, v as NcRulePatchSchema, vt as errMsg, w as NcSnoozeSuppressedSchema, wt as nodePin, x as NcScheduleSchema, xt as createEvent, y as NcRuleSchema, yt as BaseAddon, z as TimelapseRuleSchema } from "../dist-IZLiqXRa.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -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",
|
|
@@ -18141,7 +18149,7 @@ function applyTemplatePatch(merged, template) {
|
|
|
18141
18149
|
return withoutTemplate;
|
|
18142
18150
|
}
|
|
18143
18151
|
/** Narrowing guard — the persisted `rule` column is `unknown` until parsed. */
|
|
18144
|
-
function isRecord$
|
|
18152
|
+
function isRecord$2(value) {
|
|
18145
18153
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18146
18154
|
}
|
|
18147
18155
|
/**
|
|
@@ -18171,7 +18179,7 @@ function isRecord$1(value) {
|
|
|
18171
18179
|
* the same reason.
|
|
18172
18180
|
*/
|
|
18173
18181
|
function withCreatedAt(rowId, raw, synthesised, now) {
|
|
18174
|
-
if (!isRecord$
|
|
18182
|
+
if (!isRecord$2(raw)) return raw;
|
|
18175
18183
|
if (typeof raw["createdAt"] === "number") return raw;
|
|
18176
18184
|
const stamped = synthesised.get(rowId) ?? now();
|
|
18177
18185
|
synthesised.set(rowId, stamped);
|
|
@@ -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;
|
|
@@ -40480,10 +40497,38 @@ function buildRecordedStillPorts(input) {
|
|
|
40480
40497
|
})
|
|
40481
40498
|
};
|
|
40482
40499
|
}
|
|
40483
|
-
/**
|
|
40500
|
+
/**
|
|
40501
|
+
* Where the completion marker lives.
|
|
40502
|
+
*
|
|
40503
|
+
* @durable class=config owner=pipeline-analytics
|
|
40504
|
+
* write="one marker row when a boot migration completes; overwritten only by
|
|
40505
|
+
* a re-run of the same migration"
|
|
40506
|
+
* loss="a re-scan of the analytics store on the next boot — idempotent,
|
|
40507
|
+
* never destructive"
|
|
40508
|
+
* retention="never pruned: one row per completed migration, bounded by the
|
|
40509
|
+
* number of migrations ever written (two today)"
|
|
40510
|
+
*/
|
|
40484
40511
|
var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
|
|
40485
40512
|
var MIGRATION_MARKER_KEY = "label-tier-4g";
|
|
40486
40513
|
/**
|
|
40514
|
+
* The owner declares its marker collection — like every other store in this
|
|
40515
|
+
* addon. KV shape (a single `data` column: `get`/`set` route the value as a
|
|
40516
|
+
* JSON blob; the engine adds the implicit `id` primary key). This declaration
|
|
40517
|
+
* never existed, so the engine refused every marker write, the catch below
|
|
40518
|
+
* warned, and every boot re-scanned the full analytics store — ~36k rows per
|
|
40519
|
+
* boot from the day the migration shipped.
|
|
40520
|
+
*/
|
|
40521
|
+
async function declareMigrationMarkers(store) {
|
|
40522
|
+
await store.declareCollection.mutate({
|
|
40523
|
+
collection: MIGRATION_MARKER_COLLECTION,
|
|
40524
|
+
columns: [{
|
|
40525
|
+
name: "data",
|
|
40526
|
+
type: "TEXT",
|
|
40527
|
+
notNull: true
|
|
40528
|
+
}]
|
|
40529
|
+
});
|
|
40530
|
+
}
|
|
40531
|
+
/**
|
|
40487
40532
|
* The decision for ONE row, exposed so the rule is testable without a store.
|
|
40488
40533
|
*
|
|
40489
40534
|
* `null` = leave the row alone.
|
|
@@ -43642,11 +43687,11 @@ function rowToSensorEvent(id, data) {
|
|
|
43642
43687
|
deviceId: Number(data["deviceId"]),
|
|
43643
43688
|
sourceDeviceId: Number(data["sourceDeviceId"]),
|
|
43644
43689
|
kind: String(data["kind"]),
|
|
43645
|
-
value: isRecord(value) ? value : null,
|
|
43690
|
+
value: isRecord$1(value) ? value : null,
|
|
43646
43691
|
timestamp: Number(data["timestamp"])
|
|
43647
43692
|
};
|
|
43648
43693
|
}
|
|
43649
|
-
function isRecord(x) {
|
|
43694
|
+
function isRecord$1(x) {
|
|
43650
43695
|
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
43651
43696
|
}
|
|
43652
43697
|
/**
|
|
@@ -43664,6 +43709,47 @@ function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
|
43664
43709
|
return mark.priorRollbacks < 3;
|
|
43665
43710
|
}
|
|
43666
43711
|
//#endregion
|
|
43712
|
+
//#region src/pipeline-analytics/store/audio-track-class.ts
|
|
43713
|
+
/** The taxonomy macro every audio-sourced track carries as its class. */
|
|
43714
|
+
var AUDIO_TRACK_CLASS = "audio";
|
|
43715
|
+
/**
|
|
43716
|
+
* True when this row predates the contract: audio-sourced, but carrying a
|
|
43717
|
+
* naked audio macro as its class.
|
|
43718
|
+
*
|
|
43719
|
+
* Deliberately NOT "className is in AUDIO_MACRO_LABELS" — an id added to the
|
|
43720
|
+
* vocabulary after a row was written would then read as modern forever. The
|
|
43721
|
+
* question is the simple one: is this an audio row that does not say `audio`?
|
|
43722
|
+
*/
|
|
43723
|
+
function isLegacyAudioTrackClass(track) {
|
|
43724
|
+
return track.source === "audio" && track.className !== "audio";
|
|
43725
|
+
}
|
|
43726
|
+
/**
|
|
43727
|
+
* Bring a hydrated row up to the contract. Returns the SAME reference when
|
|
43728
|
+
* there is nothing to do, so the hot path (every non-audio track, and every
|
|
43729
|
+
* audio track written after this shipped) allocates nothing.
|
|
43730
|
+
*
|
|
43731
|
+
* The naked class is preserved as an `audioLabels` entry when — and only when
|
|
43732
|
+
* — the row carries none. On the 576 legacy rows measured on the live hub that
|
|
43733
|
+
* branch is unreachable (all of them have labels); it exists so the rewrite
|
|
43734
|
+
* can never be the step that DESTROYS the only copy of what was heard.
|
|
43735
|
+
*/
|
|
43736
|
+
function normalizeAudioTrackClass(track) {
|
|
43737
|
+
if (!isLegacyAudioTrackClass(track)) return track;
|
|
43738
|
+
const heard = {
|
|
43739
|
+
label: track.className,
|
|
43740
|
+
peakScore: 0,
|
|
43741
|
+
count: 1,
|
|
43742
|
+
firstAt: track.firstSeen,
|
|
43743
|
+
lastAt: track.lastSeen
|
|
43744
|
+
};
|
|
43745
|
+
const audioLabels = track.audioLabels !== void 0 && track.audioLabels.length > 0 ? track.audioLabels : [heard];
|
|
43746
|
+
return {
|
|
43747
|
+
...track,
|
|
43748
|
+
className: AUDIO_TRACK_CLASS,
|
|
43749
|
+
audioLabels
|
|
43750
|
+
};
|
|
43751
|
+
}
|
|
43752
|
+
//#endregion
|
|
43667
43753
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
43668
43754
|
function encodeRecentCursor(cursor) {
|
|
43669
43755
|
return Buffer.from(JSON.stringify({
|
|
@@ -43963,6 +44049,24 @@ var TRACKS_INDEXES = [{
|
|
|
43963
44049
|
name: "idx_tracks_device_firstSeen",
|
|
43964
44050
|
columns: ["deviceId", "firstSeen"]
|
|
43965
44051
|
}];
|
|
44052
|
+
function isRecord(v) {
|
|
44053
|
+
return typeof v === "object" && v !== null;
|
|
44054
|
+
}
|
|
44055
|
+
/** Validate the JSON column back into `Track.audioLabels` — a malformed row
|
|
44056
|
+
* yields null rather than a lying shape (this column now carries the meaning
|
|
44057
|
+
* of an audio row, D189; it must not be trusted by assertion). */
|
|
44058
|
+
function readAudioLabels(v) {
|
|
44059
|
+
if (!Array.isArray(v)) return null;
|
|
44060
|
+
const out = [];
|
|
44061
|
+
for (const e of v) if (isRecord(e) && typeof e["label"] === "string" && typeof e["peakScore"] === "number" && typeof e["count"] === "number" && typeof e["firstAt"] === "number" && typeof e["lastAt"] === "number") out.push({
|
|
44062
|
+
label: e["label"],
|
|
44063
|
+
peakScore: e["peakScore"],
|
|
44064
|
+
count: e["count"],
|
|
44065
|
+
firstAt: e["firstAt"],
|
|
44066
|
+
lastAt: e["lastAt"]
|
|
44067
|
+
});
|
|
44068
|
+
return out.length > 0 ? out : null;
|
|
44069
|
+
}
|
|
43966
44070
|
/** Serialize the per-label aggregate map into the `Track.audioLabels`
|
|
43967
44071
|
* array shape, most-frequent label first. */
|
|
43968
44072
|
function audioLabelsToArray(agg) {
|
|
@@ -45163,8 +45267,9 @@ var TrackStore = class {
|
|
|
45163
45267
|
});
|
|
45164
45268
|
const names = /* @__PURE__ */ new Set();
|
|
45165
45269
|
for (const r of rows) {
|
|
45270
|
+
const isAudio = r.data["source"] === "audio";
|
|
45166
45271
|
const className = r.data["className"];
|
|
45167
|
-
if (typeof className === "string" && className.length > 0) names.add(className);
|
|
45272
|
+
if (typeof className === "string" && className.length > 0) names.add(isAudio ? AUDIO_TRACK_CLASS : className);
|
|
45168
45273
|
const classes = r.data["classes"];
|
|
45169
45274
|
if (Array.isArray(classes)) {
|
|
45170
45275
|
for (const c of classes) if (typeof c === "string" && c.length > 0) names.add(c);
|
|
@@ -45293,7 +45398,7 @@ var TrackStore = class {
|
|
|
45293
45398
|
const importance = data["importance"];
|
|
45294
45399
|
const bestEventId = data["bestEventId"];
|
|
45295
45400
|
const importanceReason = data["importanceReason"];
|
|
45296
|
-
const audioLabels = data["audioLabels"];
|
|
45401
|
+
const audioLabels = readAudioLabels(data["audioLabels"]);
|
|
45297
45402
|
const retrainParsed = RetrainStatusSchema.safeParse(data["retrainStatus"]);
|
|
45298
45403
|
const debug = data["debug"];
|
|
45299
45404
|
const favourited = data["favourited"];
|
|
@@ -45311,7 +45416,7 @@ var TrackStore = class {
|
|
|
45311
45416
|
maxX: envMaxX,
|
|
45312
45417
|
maxY: envMaxY
|
|
45313
45418
|
} : null;
|
|
45314
|
-
return {
|
|
45419
|
+
return normalizeAudioTrackClass({
|
|
45315
45420
|
trackId: id,
|
|
45316
45421
|
deviceId: Number(data["deviceId"]),
|
|
45317
45422
|
className: String(data["className"]),
|
|
@@ -45342,7 +45447,7 @@ var TrackStore = class {
|
|
|
45342
45447
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
45343
45448
|
observations,
|
|
45344
45449
|
...envelope !== null ? { envelope } : {}
|
|
45345
|
-
};
|
|
45450
|
+
});
|
|
45346
45451
|
}
|
|
45347
45452
|
};
|
|
45348
45453
|
//#endregion
|
|
@@ -47675,7 +47780,7 @@ var AudioMarkerProjector = class {
|
|
|
47675
47780
|
cameraId: deviceId,
|
|
47676
47781
|
sourceDeviceId: deviceId,
|
|
47677
47782
|
source: "audio",
|
|
47678
|
-
kind:
|
|
47783
|
+
kind: AUDIO_TRACK_CLASS,
|
|
47679
47784
|
timestamp: timestampMs,
|
|
47680
47785
|
...audioLabels !== void 0 ? { audioLabels } : {}
|
|
47681
47786
|
});
|
|
@@ -48883,6 +48988,23 @@ async function projectSensorMarkers(deps, data, timestamp) {
|
|
|
48883
48988
|
* {@link isSpatialTrack}.
|
|
48884
48989
|
*/
|
|
48885
48990
|
var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
48991
|
+
/**
|
|
48992
|
+
* What the debounce treats as "the same event happening again".
|
|
48993
|
+
*
|
|
48994
|
+
* `(camera, kind)` — plus, for an audio row, the class HEARD. Since D189 every
|
|
48995
|
+
* audio row carries `kind: 'audio'` and puts the class on `audioLabels`, so
|
|
48996
|
+
* keying on `kind` alone would collapse this to one bucket per camera. The
|
|
48997
|
+
* episode hysteresis cools down PER CLASS, so a bark three seconds after speech
|
|
48998
|
+
* is a legitimate second episode — and a per-camera bucket would drop it.
|
|
48999
|
+
*
|
|
49000
|
+
* Only the FIRST label is used: a materialize carries the one class the episode
|
|
49001
|
+
* opened on. Joining all of them would make `[speech]` and `[speech, dog]`
|
|
49002
|
+
* different buckets, which is the opposite of what a debounce is for.
|
|
49003
|
+
*/
|
|
49004
|
+
function debounceKey(input) {
|
|
49005
|
+
const heard = input.audioLabels?.[0]?.label;
|
|
49006
|
+
return heard === void 0 ? `${input.cameraId}:${input.kind}` : `${input.cameraId}:${input.kind}:${heard}`;
|
|
49007
|
+
}
|
|
48886
49008
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
48887
49009
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
48888
49010
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
@@ -48925,7 +49047,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48925
49047
|
snapshotTimeoutMs;
|
|
48926
49048
|
makeId;
|
|
48927
49049
|
now;
|
|
48928
|
-
/** Last snapshot time per
|
|
49050
|
+
/** Last snapshot time per {@link debounceKey}. */
|
|
48929
49051
|
lastAt = /* @__PURE__ */ new Map();
|
|
48930
49052
|
constructor(deps) {
|
|
48931
49053
|
this.deps = deps;
|
|
@@ -48964,7 +49086,7 @@ var SyntheticTrackMaterializer = class {
|
|
|
48964
49086
|
* still lands a track (with no media) — it never blocks the durable record.
|
|
48965
49087
|
*/
|
|
48966
49088
|
async materialize(input) {
|
|
48967
|
-
const key =
|
|
49089
|
+
const key = debounceKey(input);
|
|
48968
49090
|
const last = this.lastAt.get(key);
|
|
48969
49091
|
if (last !== void 0 && this.now() - last < this.debounceMs) return null;
|
|
48970
49092
|
this.lastAt.set(key, this.now());
|
|
@@ -50531,6 +50653,126 @@ function evaluatePeriodicSnapshot(input) {
|
|
|
50531
50653
|
movedFraction
|
|
50532
50654
|
};
|
|
50533
50655
|
}
|
|
50656
|
+
/** Where the completion marker lives — the SAME collection
|
|
50657
|
+
* `runLabelTierMigration` owns and classifies; referencing the constant
|
|
50658
|
+
* instead of repeating the literal keeps one declarer for the guards. */
|
|
50659
|
+
var AUDIO_CLASS_MIGRATION_MARKER_COLLECTION = MIGRATION_MARKER_COLLECTION;
|
|
50660
|
+
var AUDIO_CLASS_MIGRATION_MARKER_KEY = "audio-track-class-d189";
|
|
50661
|
+
/**
|
|
50662
|
+
* The decision for ONE stored row, exposed so the rule is testable without a
|
|
50663
|
+
* store. `null` = leave the row alone.
|
|
50664
|
+
*
|
|
50665
|
+
* Reads the RAW row rather than a hydrated `Track` on purpose: this is the
|
|
50666
|
+
* column the pass rewrites, and hydrating through `rowToTrack` would apply the
|
|
50667
|
+
* read-side normalization and hide the very rows it is looking for.
|
|
50668
|
+
*/
|
|
50669
|
+
function decideAudioClassRowMigration(data) {
|
|
50670
|
+
if (data["source"] !== "audio") return null;
|
|
50671
|
+
const className = data["className"];
|
|
50672
|
+
if (typeof className !== "string" || className.length === 0) return null;
|
|
50673
|
+
if (className === "audio") return null;
|
|
50674
|
+
const stored = data["audioLabels"];
|
|
50675
|
+
if (Array.isArray(stored) && stored.length > 0) return { className: AUDIO_TRACK_CLASS };
|
|
50676
|
+
const firstSeen = typeof data["firstSeen"] === "number" ? data["firstSeen"] : 0;
|
|
50677
|
+
return {
|
|
50678
|
+
className: AUDIO_TRACK_CLASS,
|
|
50679
|
+
audioLabels: [{
|
|
50680
|
+
label: className,
|
|
50681
|
+
peakScore: 0,
|
|
50682
|
+
count: 1,
|
|
50683
|
+
firstAt: firstSeen,
|
|
50684
|
+
lastAt: typeof data["lastSeen"] === "number" ? data["lastSeen"] : firstSeen
|
|
50685
|
+
}]
|
|
50686
|
+
};
|
|
50687
|
+
}
|
|
50688
|
+
/**
|
|
50689
|
+
* Run the pass over the tracks collection, once.
|
|
50690
|
+
*
|
|
50691
|
+
* The marker short-circuits a completed migration; it is written only when the
|
|
50692
|
+
* pass did NOT truncate, so an interrupted run resumes on the next boot instead
|
|
50693
|
+
* of declaring itself done.
|
|
50694
|
+
*/
|
|
50695
|
+
async function runAudioTrackClassMigration(deps, collection) {
|
|
50696
|
+
try {
|
|
50697
|
+
const marker = await deps.store.get.query({
|
|
50698
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50699
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY
|
|
50700
|
+
});
|
|
50701
|
+
if (marker !== null && marker !== void 0) return null;
|
|
50702
|
+
} catch {}
|
|
50703
|
+
let scanned = 0;
|
|
50704
|
+
let rewritten = 0;
|
|
50705
|
+
let labelsRecovered = 0;
|
|
50706
|
+
let skipped = 0;
|
|
50707
|
+
let offset = 0;
|
|
50708
|
+
let truncated = false;
|
|
50709
|
+
for (;;) {
|
|
50710
|
+
if (scanned >= 2e5) {
|
|
50711
|
+
truncated = true;
|
|
50712
|
+
break;
|
|
50713
|
+
}
|
|
50714
|
+
const rows = await deps.store.query.query({
|
|
50715
|
+
collection,
|
|
50716
|
+
filter: {
|
|
50717
|
+
limit: 500,
|
|
50718
|
+
offset
|
|
50719
|
+
}
|
|
50720
|
+
});
|
|
50721
|
+
if (rows.length === 0) break;
|
|
50722
|
+
for (const row of rows) {
|
|
50723
|
+
scanned++;
|
|
50724
|
+
const patch = decideAudioClassRowMigration(row.data);
|
|
50725
|
+
if (patch === null) {
|
|
50726
|
+
skipped++;
|
|
50727
|
+
continue;
|
|
50728
|
+
}
|
|
50729
|
+
const deviceId = typeof row.data["deviceId"] === "number" ? row.data["deviceId"] : void 0;
|
|
50730
|
+
try {
|
|
50731
|
+
await deps.store.update.mutate({
|
|
50732
|
+
collection,
|
|
50733
|
+
id: row.id,
|
|
50734
|
+
data: patch
|
|
50735
|
+
});
|
|
50736
|
+
rewritten++;
|
|
50737
|
+
if (patch["audioLabels"] !== void 0) labelsRecovered++;
|
|
50738
|
+
} catch (err) {
|
|
50739
|
+
deps.logger.warn("audio track class migration row failed", {
|
|
50740
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
50741
|
+
meta: {
|
|
50742
|
+
collection,
|
|
50743
|
+
id: row.id,
|
|
50744
|
+
error: err instanceof Error ? err.message : String(err)
|
|
50745
|
+
}
|
|
50746
|
+
});
|
|
50747
|
+
skipped++;
|
|
50748
|
+
}
|
|
50749
|
+
}
|
|
50750
|
+
if (rows.length < 500) break;
|
|
50751
|
+
offset += rows.length;
|
|
50752
|
+
}
|
|
50753
|
+
if (!truncated) try {
|
|
50754
|
+
await deps.store.set.mutate({
|
|
50755
|
+
collection: AUDIO_CLASS_MIGRATION_MARKER_COLLECTION,
|
|
50756
|
+
key: AUDIO_CLASS_MIGRATION_MARKER_KEY,
|
|
50757
|
+
value: {
|
|
50758
|
+
completedAt: deps.now(),
|
|
50759
|
+
rewritten
|
|
50760
|
+
}
|
|
50761
|
+
});
|
|
50762
|
+
} catch (err) {
|
|
50763
|
+
deps.logger.warn("audio track class migration marker failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
50764
|
+
}
|
|
50765
|
+
const stats = {
|
|
50766
|
+
collection,
|
|
50767
|
+
scanned,
|
|
50768
|
+
rewritten,
|
|
50769
|
+
labelsRecovered,
|
|
50770
|
+
skipped,
|
|
50771
|
+
truncated
|
|
50772
|
+
};
|
|
50773
|
+
deps.logger.info("audio track class migration", { meta: { ...stats } });
|
|
50774
|
+
return stats;
|
|
50775
|
+
}
|
|
50534
50776
|
//#endregion
|
|
50535
50777
|
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
50536
50778
|
/**
|
|
@@ -52949,7 +53191,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52949
53191
|
AnalyticsLts.declare(api.settingsStore),
|
|
52950
53192
|
SceneStore.declare(api.settingsStore),
|
|
52951
53193
|
NotificationCenter.declare(api.settingsStore),
|
|
52952
|
-
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
53194
|
+
ViewerSettingsSnapshotStore.declare(api.settingsStore),
|
|
53195
|
+
declareMigrationMarkers(api.settingsStore)
|
|
52953
53196
|
]);
|
|
52954
53197
|
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
52955
53198
|
try {
|
|
@@ -52967,6 +53210,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
52967
53210
|
}, [TRACKS_COLLECTION, OBJECT_EVENTS_COLLECTION]).catch((err) => {
|
|
52968
53211
|
this.ctx.logger.warn("label tier migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
52969
53212
|
});
|
|
53213
|
+
runAudioTrackClassMigration({
|
|
53214
|
+
store: api.settingsStore,
|
|
53215
|
+
logger: this.ctx.logger,
|
|
53216
|
+
now: () => Date.now()
|
|
53217
|
+
}, TRACKS_COLLECTION).catch((err) => {
|
|
53218
|
+
this.ctx.logger.warn("audio track class migration failed — will retry next boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
53219
|
+
});
|
|
52970
53220
|
}
|
|
52971
53221
|
/** Event-media storage. By default it uses `ctx.kernel.storage`, rooted at
|
|
52972
53222
|
* the node data dir (appData) → heavy thumbnail/track-frame churn hammers
|
|
@@ -56242,7 +56492,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
56242
56492
|
cameraId: input.deviceId,
|
|
56243
56493
|
sourceDeviceId: input.deviceId,
|
|
56244
56494
|
source: "audio",
|
|
56245
|
-
kind:
|
|
56495
|
+
kind: AUDIO_TRACK_CLASS,
|
|
56246
56496
|
timestamp: input.timestamp,
|
|
56247
56497
|
audioLabels: [{
|
|
56248
56498
|
label: ref.className,
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D8fRo8aP.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
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.104",
|
|
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",
|