@camstack/addon-post-analysis 1.2.90 → 1.2.92
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-BBIhkNDY.js → dist-BfrtqGyh.js} +63 -19
- package/dist/{dist-Bac7ww1C.mjs → dist-VnaTfwAL.mjs} +58 -20
- 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-DDC0Fqfb.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BIDGLXRs.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DhP09VMX.mjs → hostInit-CpWvyhen.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +1059 -237
- package/dist/pipeline-analytics/index.mjs +1021 -200
- 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-BfrtqGyh.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");
|
|
@@ -7786,17 +7786,40 @@ function referencesAudioClass(classes) {
|
|
|
7786
7786
|
if (classes === void 0) return false;
|
|
7787
7787
|
return classes.some((c) => c.trim().toLowerCase().startsWith("audio-"));
|
|
7788
7788
|
}
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7789
|
+
/**
|
|
7790
|
+
* Match one system subject against one rule's system-event condition.
|
|
7791
|
+
*
|
|
7792
|
+
* EVERY NARROWING FILTER IS SCOPED TO ITS KIND FAMILY, and the scoping is not
|
|
7793
|
+
* decided here: `systemEventFilterApplies` (in `@camstack/types`) is the one
|
|
7794
|
+
* statement of it, and both rule editors compute their filter visibility from
|
|
7795
|
+
* the same predicate. A filter outside a kind's family is IGNORED — never an
|
|
7796
|
+
* exclusion. It used to be an exclusion, and that made the operator's
|
|
7797
|
+
* eleven-kind rule with `deviceTypes: ['camera']` deliverable for exactly the
|
|
7798
|
+
* two kinds that carry a device type; the other nine failed closed and logged
|
|
7799
|
+
* nothing for four days.
|
|
7800
|
+
*
|
|
7801
|
+
* Inside its family a filter still narrows, and still fails CLOSED when the
|
|
7802
|
+
* subject cannot produce the identity it tests.
|
|
7803
|
+
*
|
|
7804
|
+
* Returns `undefined` on a match.
|
|
7805
|
+
*/
|
|
7806
|
+
function systemEventExclusion(condition, subject) {
|
|
7807
|
+
if (!condition.kinds.includes(subject.kind)) return "kinds";
|
|
7808
|
+
const applies = (filter) => require_dist.systemEventFilterApplies(subject.kind, filter);
|
|
7809
|
+
if (condition.deviceIds !== void 0 && applies("deviceIds")) {
|
|
7810
|
+
if (subject.deviceId === void 0 || !condition.deviceIds.includes(subject.deviceId)) return "deviceIds";
|
|
7795
7811
|
}
|
|
7796
|
-
if (condition.
|
|
7797
|
-
if (
|
|
7812
|
+
if (condition.deviceTypes !== void 0 && applies("deviceTypes")) {
|
|
7813
|
+
if (subject.deviceType === void 0 || !condition.deviceTypes.includes(subject.deviceType)) return "deviceTypes";
|
|
7814
|
+
}
|
|
7815
|
+
const wantedNodes = condition.nodeIds;
|
|
7816
|
+
if (wantedNodes !== void 0 && applies("nodeIds")) {
|
|
7817
|
+
if (!subjectNodes(subject).some((id) => wantedNodes.includes(id))) return "nodeIds";
|
|
7818
|
+
}
|
|
7819
|
+
const wantedPackages = condition.packageNames;
|
|
7820
|
+
if (wantedPackages !== void 0 && applies("packageNames")) {
|
|
7821
|
+
if (!subjectPackages(subject).some((name) => wantedPackages.includes(name))) return "packageNames";
|
|
7798
7822
|
}
|
|
7799
|
-
return true;
|
|
7800
7823
|
}
|
|
7801
7824
|
function subjectNodes(subject) {
|
|
7802
7825
|
const ids = [...subject.nodeIds ?? []];
|
|
@@ -7812,7 +7835,14 @@ function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
|
7812
7835
|
if (subject.kind === "system-event") {
|
|
7813
7836
|
if (rule.delivery !== "system-event") return fail("delivery");
|
|
7814
7837
|
const condition = rule.conditions.systemEvent;
|
|
7815
|
-
|
|
7838
|
+
const systemEvent = subject.systemEvent;
|
|
7839
|
+
if (condition === void 0 || systemEvent === void 0) return fail("systemEvent");
|
|
7840
|
+
const excluded = systemEventExclusion(condition, systemEvent);
|
|
7841
|
+
if (excluded !== void 0) return excluded === "kinds" ? fail("systemEvent") : {
|
|
7842
|
+
matched: false,
|
|
7843
|
+
failedCondition: "systemEvent",
|
|
7844
|
+
excludedBySystemEventFilter: excluded
|
|
7845
|
+
};
|
|
7816
7846
|
if (rule.conditions.occupancy !== void 0) return fail("occupancy");
|
|
7817
7847
|
if (rule.conditions.audio !== void 0) return fail("audio");
|
|
7818
7848
|
} else if (subject.kind === "occupancy-event") {
|
|
@@ -20060,6 +20090,39 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20060
20090
|
if (identity.isCamera && identity.detectionActive === false) return "detection-off";
|
|
20061
20091
|
}
|
|
20062
20092
|
/**
|
|
20093
|
+
* "Your rule covers this kind, and your own filter dropped it."
|
|
20094
|
+
*
|
|
20095
|
+
* INFO, not debug, and unconditional. The alternative is what shipped: a
|
|
20096
|
+
* rule that selected eleven system-event kinds delivered two of them for
|
|
20097
|
+
* four days, and the nine it silently refused produced no line anywhere —
|
|
20098
|
+
* indistinguishable from an intake that had stopped emitting. The filter is
|
|
20099
|
+
* NAMED, and so is the value that failed it, because the fix is always an
|
|
20100
|
+
* edit to that one list.
|
|
20101
|
+
*
|
|
20102
|
+
* `tags.deviceId` carries the SUBJECT's numeric device id (the system
|
|
20103
|
+
* subject's own `deviceId` is the sentinel {@link NC_SYSTEM_DEVICE_ID}), so
|
|
20104
|
+
* "why is 617 louder than 2018?" is answerable by grouping. A node- or
|
|
20105
|
+
* package-scoped exclusion is about no device and carries no tag.
|
|
20106
|
+
*/
|
|
20107
|
+
reportSystemFilterExclusion(rule, subject, filter) {
|
|
20108
|
+
const system = subject.systemEvent;
|
|
20109
|
+
if (system === void 0) return;
|
|
20110
|
+
this.logger.info("system event excluded by a rule filter", {
|
|
20111
|
+
...system.deviceId !== void 0 ? { tags: { deviceId: system.deviceId } } : {},
|
|
20112
|
+
meta: {
|
|
20113
|
+
ruleId: rule.id,
|
|
20114
|
+
rule: rule.name,
|
|
20115
|
+
kind: system.kind,
|
|
20116
|
+
filter,
|
|
20117
|
+
eventId: subject.recordId,
|
|
20118
|
+
subject: system.subject,
|
|
20119
|
+
...system.deviceType !== void 0 ? { deviceType: system.deviceType } : {},
|
|
20120
|
+
...system.nodeId !== void 0 ? { nodeId: system.nodeId } : {},
|
|
20121
|
+
...system.packageName !== void 0 ? { packageName: system.packageName } : {}
|
|
20122
|
+
}
|
|
20123
|
+
});
|
|
20124
|
+
}
|
|
20125
|
+
/**
|
|
20063
20126
|
* SAY that the scene gate suppressed this rule.
|
|
20064
20127
|
*
|
|
20065
20128
|
* A branch that drops work silently logs it — the three-hour media blackout
|
|
@@ -20479,6 +20542,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20479
20542
|
const evaluation = evaluateRule(rule, subject, (id) => this.deviceStates.get(id), (id) => this.zoneOwners.get(id), (sceneId) => this.sceneStates.get(sceneId));
|
|
20480
20543
|
if (!evaluation.matched) {
|
|
20481
20544
|
if (evaluation.failedCondition === "scene") this.reportSceneSuppression(rule, subject);
|
|
20545
|
+
if (evaluation.excludedBySystemEventFilter !== void 0) this.reportSystemFilterExclusion(rule, subject, evaluation.excludedBySystemEventFilter);
|
|
20482
20546
|
rejections.push(`${rule.name}:${evaluation.failedCondition ?? "unknown"}`);
|
|
20483
20547
|
outcomes.push({
|
|
20484
20548
|
ruleId: rule.id,
|
|
@@ -22379,7 +22443,7 @@ var ncActions = require_dist.defineCustomActions({
|
|
|
22379
22443
|
"nc.getArtifact": require_dist.customAction(require_dist.object({ artifactId: require_dist.string().min(1) }), require_dist.object({ artifact: NcArtifactRecordSchema.nullable() }), { caller: "required" })
|
|
22380
22444
|
});
|
|
22381
22445
|
/** Fail-closed caller resolution — an absent forwarded caller is NEVER admin. */
|
|
22382
|
-
function requireCaller(caller) {
|
|
22446
|
+
function requireCaller$1(caller) {
|
|
22383
22447
|
if (!caller || typeof caller.userId !== "string" || caller.userId.length === 0) throw new Error("forbidden: authenticated caller required");
|
|
22384
22448
|
return caller;
|
|
22385
22449
|
}
|
|
@@ -22477,14 +22541,14 @@ function makeNcActionHandlers(deps) {
|
|
|
22477
22541
|
};
|
|
22478
22542
|
return {
|
|
22479
22543
|
"nc.listRules": async (_input, caller) => {
|
|
22480
|
-
const c = requireCaller(caller);
|
|
22544
|
+
const c = requireCaller$1(caller);
|
|
22481
22545
|
return { rules: (c.isAdmin ? deps.ruleStore.list() : deps.ruleStore.listForOwner(c.userId)).map((r) => ({
|
|
22482
22546
|
...r,
|
|
22483
22547
|
readOnly: !c.isAdmin && r.ownerUserId !== c.userId
|
|
22484
22548
|
})) };
|
|
22485
22549
|
},
|
|
22486
22550
|
"nc.listTrackGroups": async (input, caller) => {
|
|
22487
|
-
requireCaller(caller);
|
|
22551
|
+
requireCaller$1(caller);
|
|
22488
22552
|
const list = deps.listTrackGroups;
|
|
22489
22553
|
if (list === void 0) return { groups: [] };
|
|
22490
22554
|
return { groups: [...await list(input)] };
|
|
@@ -22494,7 +22558,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22494
22558
|
taxonomy: require_dist.NC_TAXONOMY
|
|
22495
22559
|
}),
|
|
22496
22560
|
"nc.createRule": async (input, caller) => {
|
|
22497
|
-
const c = requireCaller(caller);
|
|
22561
|
+
const c = requireCaller$1(caller);
|
|
22498
22562
|
const parsed = require_dist.NcRuleInputSchema.parse(input.rule);
|
|
22499
22563
|
await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
|
|
22500
22564
|
const rule = await deps.ruleStore.create({
|
|
@@ -22508,7 +22572,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22508
22572
|
return { rule };
|
|
22509
22573
|
},
|
|
22510
22574
|
"nc.updateRule": async (input, caller) => {
|
|
22511
|
-
const c = requireCaller(caller);
|
|
22575
|
+
const c = requireCaller$1(caller);
|
|
22512
22576
|
assertRuleEditable(input.ruleId, c);
|
|
22513
22577
|
const patch = require_dist.NcRulePatchSchema.parse(input.patch);
|
|
22514
22578
|
if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
|
|
@@ -22521,7 +22585,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22521
22585
|
return { rule };
|
|
22522
22586
|
},
|
|
22523
22587
|
"nc.deleteRule": async (input, caller) => {
|
|
22524
|
-
const c = requireCaller(caller);
|
|
22588
|
+
const c = requireCaller$1(caller);
|
|
22525
22589
|
assertRuleEditable(input.ruleId, c);
|
|
22526
22590
|
await deps.ruleStore.delete(input.ruleId);
|
|
22527
22591
|
deps.logger.info("nc rule deleted", { meta: {
|
|
@@ -22531,11 +22595,11 @@ function makeNcActionHandlers(deps) {
|
|
|
22531
22595
|
return { success: true };
|
|
22532
22596
|
},
|
|
22533
22597
|
"nc.listSnoozes": async (_input, caller) => {
|
|
22534
|
-
const c = requireCaller(caller);
|
|
22598
|
+
const c = requireCaller$1(caller);
|
|
22535
22599
|
return { snoozes: [...deps.snooze.list(c.userId, c.isAdmin)] };
|
|
22536
22600
|
},
|
|
22537
22601
|
"nc.createSnooze": async (input, caller) => {
|
|
22538
|
-
const c = requireCaller(caller);
|
|
22602
|
+
const c = requireCaller$1(caller);
|
|
22539
22603
|
const parsed = require_dist.NcSnoozeInputSchema.parse(input.snooze);
|
|
22540
22604
|
if (parsed.ruleId !== void 0) assertRuleVisible(parsed.ruleId, c);
|
|
22541
22605
|
const snooze = await deps.snooze.create(parsed, c);
|
|
@@ -22548,12 +22612,12 @@ function makeNcActionHandlers(deps) {
|
|
|
22548
22612
|
return { snooze };
|
|
22549
22613
|
},
|
|
22550
22614
|
"nc.cancelSnooze": async (input, caller) => {
|
|
22551
|
-
const c = requireCaller(caller);
|
|
22615
|
+
const c = requireCaller$1(caller);
|
|
22552
22616
|
await deps.snooze.cancel(input.snoozeId, c);
|
|
22553
22617
|
return { success: true };
|
|
22554
22618
|
},
|
|
22555
22619
|
"nc.injectTestEvent": async (input, caller) => {
|
|
22556
|
-
const c = requireCaller(caller);
|
|
22620
|
+
const c = requireCaller$1(caller);
|
|
22557
22621
|
if (!c.isAdmin) throw new Error("forbidden: admin required to inject a test event");
|
|
22558
22622
|
const inject = deps.injectTestEvent;
|
|
22559
22623
|
if (inject === void 0) throw new Error("this node does not host the notification centre — inject on the hub");
|
|
@@ -22569,7 +22633,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22569
22633
|
return inject(parsed);
|
|
22570
22634
|
},
|
|
22571
22635
|
"nc.testRule": async (input, caller) => {
|
|
22572
|
-
const c = requireCaller(caller);
|
|
22636
|
+
const c = requireCaller$1(caller);
|
|
22573
22637
|
if (!c.isAdmin) throw new Error("forbidden: admin required to test a rule");
|
|
22574
22638
|
const inject = deps.injectTestEvent;
|
|
22575
22639
|
if (inject === void 0) throw new Error("this node does not host the notification centre — test on the hub");
|
|
@@ -22601,7 +22665,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22601
22665
|
};
|
|
22602
22666
|
},
|
|
22603
22667
|
"nc.getRuleStatus": async (input, caller) => {
|
|
22604
|
-
const c = requireCaller(caller);
|
|
22668
|
+
const c = requireCaller$1(caller);
|
|
22605
22669
|
const status = deps.ruleStatus;
|
|
22606
22670
|
if (status === void 0) throw new Error("this node does not host the notification centre — ask the hub");
|
|
22607
22671
|
const detection = deps.ruleStore.get(input.ruleId);
|
|
@@ -22617,7 +22681,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22617
22681
|
throw new Error(`rule not found: ${input.ruleId}`);
|
|
22618
22682
|
},
|
|
22619
22683
|
"nc.listTimelapseRules": async (_input, caller) => {
|
|
22620
|
-
const c = requireCaller(caller);
|
|
22684
|
+
const c = requireCaller$1(caller);
|
|
22621
22685
|
const tl = requireTimelapse();
|
|
22622
22686
|
return { rules: (c.isAdmin ? tl.store.list() : tl.store.listForOwner(c.userId)).map((r) => ({
|
|
22623
22687
|
...r,
|
|
@@ -22625,7 +22689,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22625
22689
|
})) };
|
|
22626
22690
|
},
|
|
22627
22691
|
"nc.upsertTimelapseRule": async (input, caller) => {
|
|
22628
|
-
const c = requireCaller(caller);
|
|
22692
|
+
const c = requireCaller$1(caller);
|
|
22629
22693
|
const tl = requireTimelapse();
|
|
22630
22694
|
if (input.ruleId === void 0) {
|
|
22631
22695
|
if (input.rule === void 0) throw new Error("upsertTimelapseRule: rule or ruleId required");
|
|
@@ -22661,7 +22725,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22661
22725
|
return { rule };
|
|
22662
22726
|
},
|
|
22663
22727
|
"nc.deleteTimelapseRule": async (input, caller) => {
|
|
22664
|
-
const c = requireCaller(caller);
|
|
22728
|
+
const c = requireCaller$1(caller);
|
|
22665
22729
|
const tl = requireTimelapse();
|
|
22666
22730
|
assertTimelapseEditable(input.ruleId, c, tl);
|
|
22667
22731
|
await tl.store.delete(input.ruleId);
|
|
@@ -22672,7 +22736,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22672
22736
|
return { success: true };
|
|
22673
22737
|
},
|
|
22674
22738
|
"nc.testTimelapseRule": async (input, caller) => {
|
|
22675
|
-
const c = requireCaller(caller);
|
|
22739
|
+
const c = requireCaller$1(caller);
|
|
22676
22740
|
if (!c.isAdmin) throw new Error("forbidden: admin required to produce a test timelapse");
|
|
22677
22741
|
const tl = requireTimelapse();
|
|
22678
22742
|
deps.logger.info("timelapse test requested", {
|
|
@@ -22699,14 +22763,14 @@ function makeNcActionHandlers(deps) {
|
|
|
22699
22763
|
};
|
|
22700
22764
|
},
|
|
22701
22765
|
"nc.setRuleTargetEnabled": async (input, caller) => {
|
|
22702
|
-
const c = requireCaller(caller);
|
|
22766
|
+
const c = requireCaller$1(caller);
|
|
22703
22767
|
assertRuleVisible(input.ruleId, c);
|
|
22704
22768
|
await assertTargetsOwned([input.targetId], c);
|
|
22705
22769
|
await deps.ruleStore.setRuleTargetEnabled(input.ruleId, input.targetId, input.enabled);
|
|
22706
22770
|
return { success: true };
|
|
22707
22771
|
},
|
|
22708
22772
|
"nc.listSummaryRules": async (_input, caller) => {
|
|
22709
|
-
const c = requireCaller(caller);
|
|
22773
|
+
const c = requireCaller$1(caller);
|
|
22710
22774
|
const sum = requireSummary();
|
|
22711
22775
|
return { rules: (c.isAdmin ? sum.store.list() : sum.store.listForOwner(c.userId)).map((r) => ({
|
|
22712
22776
|
...r,
|
|
@@ -22714,7 +22778,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22714
22778
|
})) };
|
|
22715
22779
|
},
|
|
22716
22780
|
"nc.upsertSummaryRule": async (input, caller) => {
|
|
22717
|
-
const c = requireCaller(caller);
|
|
22781
|
+
const c = requireCaller$1(caller);
|
|
22718
22782
|
if (!c.isAdmin) throw new Error("forbidden: admin required to author a summary rule");
|
|
22719
22783
|
const sum = requireSummary();
|
|
22720
22784
|
if (input.ruleId === void 0) {
|
|
@@ -22747,7 +22811,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22747
22811
|
return { rule };
|
|
22748
22812
|
},
|
|
22749
22813
|
"nc.deleteSummaryRule": async (input, caller) => {
|
|
22750
|
-
const c = requireCaller(caller);
|
|
22814
|
+
const c = requireCaller$1(caller);
|
|
22751
22815
|
if (!c.isAdmin) throw new Error("forbidden: admin required to delete a summary rule");
|
|
22752
22816
|
await requireSummary().store.delete(input.ruleId);
|
|
22753
22817
|
deps.logger.info("summary rule deleted", { meta: {
|
|
@@ -22757,7 +22821,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22757
22821
|
return { success: true };
|
|
22758
22822
|
},
|
|
22759
22823
|
"nc.testSummaryRule": async (input, caller) => {
|
|
22760
|
-
const c = requireCaller(caller);
|
|
22824
|
+
const c = requireCaller$1(caller);
|
|
22761
22825
|
if (!c.isAdmin) throw new Error("forbidden: admin required to produce a test summary");
|
|
22762
22826
|
const sum = requireSummary();
|
|
22763
22827
|
const startMs = input.windowStartMs;
|
|
@@ -22804,7 +22868,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22804
22868
|
"nc.setLanguage": async (input) => requireTexts().setLanguage(input.language),
|
|
22805
22869
|
"nc.setTextOverrides": async (input) => requireTexts().setOverrides(input.language, input.overrides),
|
|
22806
22870
|
"nc.listArtifacts": async (input, caller) => {
|
|
22807
|
-
const c = requireCaller(caller);
|
|
22871
|
+
const c = requireCaller$1(caller);
|
|
22808
22872
|
const page = await requireArtifacts().list(input, {
|
|
22809
22873
|
userId: c.userId,
|
|
22810
22874
|
isAdmin: c.isAdmin
|
|
@@ -22816,7 +22880,7 @@ function makeNcActionHandlers(deps) {
|
|
|
22816
22880
|
};
|
|
22817
22881
|
},
|
|
22818
22882
|
"nc.getArtifact": async (input, caller) => {
|
|
22819
|
-
const c = requireCaller(caller);
|
|
22883
|
+
const c = requireCaller$1(caller);
|
|
22820
22884
|
return { artifact: await requireArtifacts().get(input.artifactId, {
|
|
22821
22885
|
userId: c.userId,
|
|
22822
22886
|
isAdmin: c.isAdmin
|
|
@@ -24465,11 +24529,11 @@ var WipeResultSchema = require_dist.object({ deleted: require_dist.number() });
|
|
|
24465
24529
|
* and can never be called: the bridge insists on `{}` and the schema rejects
|
|
24466
24530
|
* it. Verified on the live hub, both directions.
|
|
24467
24531
|
*/
|
|
24468
|
-
var NoInputSchema = require_dist.object({});
|
|
24532
|
+
var NoInputSchema$1 = require_dist.object({});
|
|
24469
24533
|
var embeddingActions = require_dist.defineCustomActions({
|
|
24470
24534
|
"embedding.rebuild": require_dist.customAction(RebuildInputSchema, RebuildStartedSchema, { auth: "admin" }),
|
|
24471
|
-
"embedding.rebuildStatus": require_dist.customAction(NoInputSchema, RebuildStatusSchema),
|
|
24472
|
-
"embedding.wipe": require_dist.customAction(NoInputSchema, WipeResultSchema, { auth: "admin" })
|
|
24535
|
+
"embedding.rebuildStatus": require_dist.customAction(NoInputSchema$1, RebuildStatusSchema),
|
|
24536
|
+
"embedding.wipe": require_dist.customAction(NoInputSchema$1, WipeResultSchema, { auth: "admin" })
|
|
24473
24537
|
});
|
|
24474
24538
|
//#endregion
|
|
24475
24539
|
//#region src/pipeline-analytics/map-with-concurrency.ts
|
|
@@ -28792,88 +28856,6 @@ var PlateGalleryProvider = class {
|
|
|
28792
28856
|
}
|
|
28793
28857
|
};
|
|
28794
28858
|
//#endregion
|
|
28795
|
-
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
28796
|
-
/**
|
|
28797
|
-
* The operator button for the orphan audit.
|
|
28798
|
-
*
|
|
28799
|
-
* The audit engine, its five bounds and its resumable cursor already existed
|
|
28800
|
-
* and had run in `report` mode for weeks; what it had no way to be was
|
|
28801
|
-
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
28802
|
-
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
28803
|
-
* six hours, and remembering to flip it back — with nothing returned to look
|
|
28804
|
-
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
28805
|
-
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
28806
|
-
* drain by leaving a switch on.
|
|
28807
|
-
*
|
|
28808
|
-
* Three properties this action does NOT leave to the caller's care:
|
|
28809
|
-
*
|
|
28810
|
-
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
28811
|
-
* logs and returns, and deletes nothing — regardless of what the durable
|
|
28812
|
-
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
28813
|
-
* typed. The engine's own default is the same for the same reason.
|
|
28814
|
-
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
28815
|
-
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
28816
|
-
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
28817
|
-
* hub-main with every live frame.
|
|
28818
|
-
* 3. **It answers.** The report comes back per collection with rows AND
|
|
28819
|
-
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
28820
|
-
* from a log line.
|
|
28821
|
-
*/
|
|
28822
|
-
/**
|
|
28823
|
-
* `report` counts; `reclaim` deletes.
|
|
28824
|
-
*
|
|
28825
|
-
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
28826
|
-
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
28827
|
-
* unstated intent here is the one that destroys nothing.
|
|
28828
|
-
*/
|
|
28829
|
-
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
28830
|
-
var OrphanAuditActionInputSchema = require_dist.object({
|
|
28831
|
-
mode: OrphanAuditActionModeSchema,
|
|
28832
|
-
/**
|
|
28833
|
-
* Start every scope from the top instead of resuming its persisted cursor.
|
|
28834
|
-
*
|
|
28835
|
-
* What draining a backlog wants, and never the default — the cursor exists
|
|
28836
|
-
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
28837
|
-
*/
|
|
28838
|
-
restart: require_dist.boolean().optional(),
|
|
28839
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
28840
|
-
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
28841
|
-
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
28842
|
-
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
28843
|
-
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
28844
|
-
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
28845
|
-
/**
|
|
28846
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
28847
|
-
* exclusively save rows, never select them, which is what keeps this off
|
|
28848
|
-
* the one-age-keyed-sweep guard's books.
|
|
28849
|
-
*/
|
|
28850
|
-
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
28851
|
-
});
|
|
28852
|
-
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
28853
|
-
collection: require_dist.string(),
|
|
28854
|
-
examined: require_dist.number(),
|
|
28855
|
-
orphans: require_dist.number(),
|
|
28856
|
-
reclaimed: require_dist.number(),
|
|
28857
|
-
bytesReclaimed: require_dist.number(),
|
|
28858
|
-
exempt: require_dist.number(),
|
|
28859
|
-
withinGrace: require_dist.number(),
|
|
28860
|
-
unresolvable: require_dist.number(),
|
|
28861
|
-
stoppedBy: require_dist.string(),
|
|
28862
|
-
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
28863
|
-
nextCursor: require_dist.number()
|
|
28864
|
-
});
|
|
28865
|
-
var OrphanAuditActionResultSchema = require_dist.object({
|
|
28866
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
28867
|
-
ran: require_dist.boolean(),
|
|
28868
|
-
mode: require_dist._enum(["report", "reclaim"]),
|
|
28869
|
-
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
28870
|
-
totalOrphans: require_dist.number(),
|
|
28871
|
-
totalReclaimed: require_dist.number(),
|
|
28872
|
-
totalBytesReclaimed: require_dist.number(),
|
|
28873
|
-
durationMs: require_dist.number()
|
|
28874
|
-
});
|
|
28875
|
-
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
28876
|
-
//#endregion
|
|
28877
28859
|
//#region src/pipeline-analytics/store/storage-pressure.ts
|
|
28878
28860
|
/**
|
|
28879
28861
|
* storage-pressure — make a full disk something the system REACTS to, not just
|
|
@@ -29040,6 +29022,11 @@ var FACE_MEDIA_OWNER_PREFIX = "face-";
|
|
|
29040
29022
|
* `plate-<trackId>`. Used by `deleteByTracks` to derive the plate crop owners
|
|
29041
29023
|
* of a set of tracks without a `trackId` column on media rows. */
|
|
29042
29024
|
var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
29025
|
+
/** Index rows per page inside one presence chunk. Must stay under
|
|
29026
|
+
* `SETTINGS_QUERY_DEFAULT_ROW_CAP` (2 000) so the engine's backstop can never
|
|
29027
|
+
* silently truncate a page — an owner reported absent because a cap bit would
|
|
29028
|
+
* DELETE a live thumbnail from the surface. */
|
|
29029
|
+
var MEDIA_PRESENCE_PAGE_SIZE = 1e3;
|
|
29043
29030
|
/**
|
|
29044
29031
|
* Kinds that hold exactly ONE row per `(ownerKind, ownerId)` — the current
|
|
29045
29032
|
* "best"/rolling/first artefact, never a filmstrip. Their row id + blob path are
|
|
@@ -29467,6 +29454,59 @@ var MediaStore = class {
|
|
|
29467
29454
|
});
|
|
29468
29455
|
}
|
|
29469
29456
|
/**
|
|
29457
|
+
* Which of `ownerIds` hold at least ONE media row — index only, no blob is
|
|
29458
|
+
* opened, no base64 is allocated.
|
|
29459
|
+
*
|
|
29460
|
+
* This is the licence `videoclips` needs to publish a `thumbnail`. The
|
|
29461
|
+
* event-media URL is stamped from a template for every event in the system,
|
|
29462
|
+
* so "the plane is up" was being served as "there are bytes": 64 of one
|
|
29463
|
+
* camera's 179 clips pointed at a 404 on 2026-08-17.
|
|
29464
|
+
*
|
|
29465
|
+
* Batched by `whereIn` and paged by offset. NOT `listInfoByOwner` in a loop:
|
|
29466
|
+
* that is one query per owner, which is the N+1 this exists to prevent (and
|
|
29467
|
+
* `queryObjectByTrackIds` already records what believing `whereIn` was
|
|
29468
|
+
* unavailable cost the search path).
|
|
29469
|
+
*
|
|
29470
|
+
* `deviceId` is the AUTHORIZATION filter, exactly as in `listByOwner`.
|
|
29471
|
+
*
|
|
29472
|
+
* THROWS on a query failure rather than answering partially — a probe that
|
|
29473
|
+
* quietly under-reports reads as "these owners have nothing", and the caller
|
|
29474
|
+
* drops thumbnails that exist.
|
|
29475
|
+
*/
|
|
29476
|
+
async ownersWithMedia(ownerKind, ownerIds, deviceId) {
|
|
29477
|
+
const found = /* @__PURE__ */ new Set();
|
|
29478
|
+
const unique = [...new Set(ownerIds)];
|
|
29479
|
+
for (let i = 0; i < unique.length; i += 200) {
|
|
29480
|
+
const chunk = unique.slice(i, i + 200);
|
|
29481
|
+
const answered = /* @__PURE__ */ new Set();
|
|
29482
|
+
for (let offset = 0;; offset += MEDIA_PRESENCE_PAGE_SIZE) {
|
|
29483
|
+
const rows = await this.store.query.query({
|
|
29484
|
+
collection: MEDIA_COLLECTION,
|
|
29485
|
+
filter: {
|
|
29486
|
+
where: {
|
|
29487
|
+
ownerKind,
|
|
29488
|
+
deviceId
|
|
29489
|
+
},
|
|
29490
|
+
whereIn: { ownerId: chunk },
|
|
29491
|
+
orderBy: {
|
|
29492
|
+
field: "ownerId",
|
|
29493
|
+
direction: "asc"
|
|
29494
|
+
},
|
|
29495
|
+
limit: MEDIA_PRESENCE_PAGE_SIZE,
|
|
29496
|
+
offset
|
|
29497
|
+
}
|
|
29498
|
+
});
|
|
29499
|
+
for (const row of rows) {
|
|
29500
|
+
const ownerId = row.data["ownerId"];
|
|
29501
|
+
if (typeof ownerId === "string") answered.add(ownerId);
|
|
29502
|
+
}
|
|
29503
|
+
if (rows.length < 1e3 || answered.size >= chunk.length) break;
|
|
29504
|
+
}
|
|
29505
|
+
for (const ownerId of answered) found.add(ownerId);
|
|
29506
|
+
}
|
|
29507
|
+
return found;
|
|
29508
|
+
}
|
|
29509
|
+
/**
|
|
29470
29510
|
* Delete all media (blobs + index rows) for the given owners of one kind.
|
|
29471
29511
|
* Best-effort per row; returns the number of index rows removed.
|
|
29472
29512
|
*/
|
|
@@ -29814,6 +29854,88 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
29814
29854
|
};
|
|
29815
29855
|
}
|
|
29816
29856
|
//#endregion
|
|
29857
|
+
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
29858
|
+
/**
|
|
29859
|
+
* The operator button for the orphan audit.
|
|
29860
|
+
*
|
|
29861
|
+
* The audit engine, its five bounds and its resumable cursor already existed
|
|
29862
|
+
* and had run in `report` mode for weeks; what it had no way to be was
|
|
29863
|
+
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
29864
|
+
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
29865
|
+
* six hours, and remembering to flip it back — with nothing returned to look
|
|
29866
|
+
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
29867
|
+
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
29868
|
+
* drain by leaving a switch on.
|
|
29869
|
+
*
|
|
29870
|
+
* Three properties this action does NOT leave to the caller's care:
|
|
29871
|
+
*
|
|
29872
|
+
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
29873
|
+
* logs and returns, and deletes nothing — regardless of what the durable
|
|
29874
|
+
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
29875
|
+
* typed. The engine's own default is the same for the same reason.
|
|
29876
|
+
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
29877
|
+
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
29878
|
+
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
29879
|
+
* hub-main with every live frame.
|
|
29880
|
+
* 3. **It answers.** The report comes back per collection with rows AND
|
|
29881
|
+
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
29882
|
+
* from a log line.
|
|
29883
|
+
*/
|
|
29884
|
+
/**
|
|
29885
|
+
* `report` counts; `reclaim` deletes.
|
|
29886
|
+
*
|
|
29887
|
+
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
29888
|
+
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
29889
|
+
* unstated intent here is the one that destroys nothing.
|
|
29890
|
+
*/
|
|
29891
|
+
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
29892
|
+
var OrphanAuditActionInputSchema = require_dist.object({
|
|
29893
|
+
mode: OrphanAuditActionModeSchema,
|
|
29894
|
+
/**
|
|
29895
|
+
* Start every scope from the top instead of resuming its persisted cursor.
|
|
29896
|
+
*
|
|
29897
|
+
* What draining a backlog wants, and never the default — the cursor exists
|
|
29898
|
+
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
29899
|
+
*/
|
|
29900
|
+
restart: require_dist.boolean().optional(),
|
|
29901
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
29902
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
29903
|
+
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
29904
|
+
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
29905
|
+
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
29906
|
+
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
29907
|
+
/**
|
|
29908
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
29909
|
+
* exclusively save rows, never select them, which is what keeps this off
|
|
29910
|
+
* the one-age-keyed-sweep guard's books.
|
|
29911
|
+
*/
|
|
29912
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
29913
|
+
});
|
|
29914
|
+
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
29915
|
+
collection: require_dist.string(),
|
|
29916
|
+
examined: require_dist.number(),
|
|
29917
|
+
orphans: require_dist.number(),
|
|
29918
|
+
reclaimed: require_dist.number(),
|
|
29919
|
+
bytesReclaimed: require_dist.number(),
|
|
29920
|
+
exempt: require_dist.number(),
|
|
29921
|
+
withinGrace: require_dist.number(),
|
|
29922
|
+
unresolvable: require_dist.number(),
|
|
29923
|
+
stoppedBy: require_dist.string(),
|
|
29924
|
+
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
29925
|
+
nextCursor: require_dist.number()
|
|
29926
|
+
});
|
|
29927
|
+
var OrphanAuditActionResultSchema = require_dist.object({
|
|
29928
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
29929
|
+
ran: require_dist.boolean(),
|
|
29930
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
29931
|
+
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
29932
|
+
totalOrphans: require_dist.number(),
|
|
29933
|
+
totalReclaimed: require_dist.number(),
|
|
29934
|
+
totalBytesReclaimed: require_dist.number(),
|
|
29935
|
+
durationMs: require_dist.number()
|
|
29936
|
+
});
|
|
29937
|
+
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
29938
|
+
//#endregion
|
|
29817
29939
|
//#region src/pipeline-analytics/videoclip-visits.ts
|
|
29818
29940
|
/**
|
|
29819
29941
|
* Videoclip visits — the grain a clip should have on an events-mode camera.
|
|
@@ -29880,7 +30002,22 @@ function dominantKind(attached) {
|
|
|
29880
30002
|
for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
|
|
29881
30003
|
return "motion";
|
|
29882
30004
|
}
|
|
29883
|
-
|
|
30005
|
+
/**
|
|
30006
|
+
* The instant a recorded still can actually be decoded inside a visit: the
|
|
30007
|
+
* midpoint of its LONGEST contiguous range.
|
|
30008
|
+
*
|
|
30009
|
+
* NOT the midpoint of the visit. A visit spans its holes by construction (it is
|
|
30010
|
+
* ranges glued across gaps up to `VISIT_MERGE_GAP_MS`), so the naive midpoint
|
|
30011
|
+
* lands in dead air roughly as often as not and the recorder's still route
|
|
30012
|
+
* answers 204 for a visit with plenty of footage.
|
|
30013
|
+
*/
|
|
30014
|
+
function stillInstantMs(ranges) {
|
|
30015
|
+
let best;
|
|
30016
|
+
for (const range of ranges) if (best === void 0 || range.endMs - range.startMs > best.endMs - best.startMs) best = range;
|
|
30017
|
+
if (best === void 0) return void 0;
|
|
30018
|
+
return Math.floor((best.startMs + best.endMs) / 2);
|
|
30019
|
+
}
|
|
30020
|
+
function visitsToClips(visits, events, pad, vouched) {
|
|
29884
30021
|
const tagged = [
|
|
29885
30022
|
...events.object.map((event) => ({
|
|
29886
30023
|
kind: "object",
|
|
@@ -29898,7 +30035,8 @@ function visitsToClips(visits, events, pad) {
|
|
|
29898
30035
|
return visits.map((visit) => {
|
|
29899
30036
|
const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
|
|
29900
30037
|
const kind = dominantKind(attached);
|
|
29901
|
-
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl) ?? attached.find((a) => a.event.mediaUrl);
|
|
30038
|
+
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
|
|
30039
|
+
const stillAtMs = stillInstantMs(visit.ranges);
|
|
29902
30040
|
return {
|
|
29903
30041
|
id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
|
|
29904
30042
|
source: "analytics",
|
|
@@ -29907,8 +30045,10 @@ function visitsToClips(visits, events, pad) {
|
|
|
29907
30045
|
startMs: visit.startMs,
|
|
29908
30046
|
endMs: visit.endMs
|
|
29909
30047
|
},
|
|
29910
|
-
thumbnail: withThumb
|
|
29911
|
-
|
|
30048
|
+
...withThumb?.event.mediaUrl !== void 0 ? { thumbnail: withThumb.event.mediaUrl } : {},
|
|
30049
|
+
...stillAtMs !== void 0 ? { stillAtMs } : {},
|
|
30050
|
+
eventIds: attached.slice(0, 24).map((a) => a.event.id),
|
|
30051
|
+
eventCount: attached.length,
|
|
29912
30052
|
holes: [...visit.holes]
|
|
29913
30053
|
};
|
|
29914
30054
|
}).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
@@ -29932,6 +30072,17 @@ function visitsToClips(visits, events, pad) {
|
|
|
29932
30072
|
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
29933
30073
|
*/
|
|
29934
30074
|
var SOURCE = "analytics";
|
|
30075
|
+
/**
|
|
30076
|
+
* Backstop on the ANNOTATION read — never on the clip list.
|
|
30077
|
+
*
|
|
30078
|
+
* A visit is annotated by every event inside it, so the clip `limit` must not
|
|
30079
|
+
* reach this fetch (one 615 visit legitimately spans 2 345 motion rows). This
|
|
30080
|
+
* bound exists only so a pathological camera-day cannot pull an unbounded page:
|
|
30081
|
+
* the store orders `timestamp DESC`, so a truncation drops the OLDEST rows and
|
|
30082
|
+
* the EARLIEST visits silently lose their annotations — which is why hitting it
|
|
30083
|
+
* is logged, per camera, rather than absorbed.
|
|
30084
|
+
*/
|
|
30085
|
+
var VISIT_EVENT_FETCH_LIMIT = 5e3;
|
|
29935
30086
|
function clipIdFor(eventId, startMs, endMs) {
|
|
29936
30087
|
return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
|
|
29937
30088
|
}
|
|
@@ -29952,7 +30103,7 @@ function parseClipId(clipId) {
|
|
|
29952
30103
|
};
|
|
29953
30104
|
}
|
|
29954
30105
|
/** Map per-kind events into time-ranged clips, newest-first. Pure. */
|
|
29955
|
-
function eventsToClips(events, pad) {
|
|
30106
|
+
function eventsToClips(events, pad, vouched) {
|
|
29956
30107
|
const make = (e, kind) => {
|
|
29957
30108
|
const startMs = e.timestamp - pad.preMs;
|
|
29958
30109
|
const endMs = e.timestamp + pad.postMs;
|
|
@@ -29964,7 +30115,7 @@ function eventsToClips(events, pad) {
|
|
|
29964
30115
|
startMs,
|
|
29965
30116
|
endMs
|
|
29966
30117
|
},
|
|
29967
|
-
thumbnail: e.mediaUrl
|
|
30118
|
+
...e.mediaUrl !== void 0 && vouched.has(e.id) ? { thumbnail: e.mediaUrl } : {}
|
|
29968
30119
|
};
|
|
29969
30120
|
};
|
|
29970
30121
|
return [
|
|
@@ -29973,29 +30124,106 @@ function eventsToClips(events, pad) {
|
|
|
29973
30124
|
...events.audio.map((e) => make(e, "audio"))
|
|
29974
30125
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
29975
30126
|
}
|
|
30127
|
+
/** Every id in the three buckets, in the order the composers tag them. */
|
|
30128
|
+
function allEventIds(events) {
|
|
30129
|
+
return [
|
|
30130
|
+
...events.motion,
|
|
30131
|
+
...events.object,
|
|
30132
|
+
...events.audio
|
|
30133
|
+
].map((e) => e.id);
|
|
30134
|
+
}
|
|
30135
|
+
/**
|
|
30136
|
+
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
30137
|
+
* `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
|
|
30138
|
+
* owning TRACK's. Two batched index reads at most, and the second only over the
|
|
30139
|
+
* object events the first did not already answer for — motion and audio are
|
|
30140
|
+
* trackless roots, so they never need it.
|
|
30141
|
+
*
|
|
30142
|
+
* THROWS on a failed read. The caller fails closed on the CLAIM and open on the
|
|
30143
|
+
* LIST; swallowing here would collapse those two opposite directions into one.
|
|
30144
|
+
*/
|
|
30145
|
+
async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
|
|
30146
|
+
const eventIds = allEventIds(events);
|
|
30147
|
+
if (eventIds.length === 0) return /* @__PURE__ */ new Set();
|
|
30148
|
+
const ownMedia = await fetchMediaOwners({
|
|
30149
|
+
deviceId,
|
|
30150
|
+
ownerKind: "event",
|
|
30151
|
+
ownerIds: eventIds
|
|
30152
|
+
});
|
|
30153
|
+
const residualTracks = [];
|
|
30154
|
+
const seenTracks = /* @__PURE__ */ new Set();
|
|
30155
|
+
for (const event of events.object) {
|
|
30156
|
+
const trackId = event.trackId;
|
|
30157
|
+
if (trackId === void 0 || ownMedia.has(event.id) || seenTracks.has(trackId)) continue;
|
|
30158
|
+
seenTracks.add(trackId);
|
|
30159
|
+
residualTracks.push(trackId);
|
|
30160
|
+
}
|
|
30161
|
+
const trackMedia = residualTracks.length === 0 ? /* @__PURE__ */ new Set() : await fetchMediaOwners({
|
|
30162
|
+
deviceId,
|
|
30163
|
+
ownerKind: "track",
|
|
30164
|
+
ownerIds: residualTracks
|
|
30165
|
+
});
|
|
30166
|
+
const vouched = new Set(ownMedia);
|
|
30167
|
+
for (const event of events.object) {
|
|
30168
|
+
const trackId = event.trackId;
|
|
30169
|
+
if (trackId !== void 0 && trackMedia.has(trackId)) vouched.add(event.id);
|
|
30170
|
+
}
|
|
30171
|
+
return vouched;
|
|
30172
|
+
}
|
|
30173
|
+
/** Keep only clips that actually touch the caller's window. The reads are
|
|
30174
|
+
* widened by {@link leftOverhangMs}; the ANSWER is not. */
|
|
30175
|
+
function intersectsWindow(clip, since, until) {
|
|
30176
|
+
return clip.timeRange.startMs < until && clip.timeRange.endMs > since;
|
|
30177
|
+
}
|
|
30178
|
+
/**
|
|
30179
|
+
* How far BEFORE `since` the event + availability reads must reach.
|
|
30180
|
+
*
|
|
30181
|
+
* A visit straddling `since` keeps its heartbeat rows and loses the transition
|
|
30182
|
+
* row — the only one that ever carries a snapshot — so the same visit shows a
|
|
30183
|
+
* picture or doesn't depending on where the operator scrolled. One wider range
|
|
30184
|
+
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
30185
|
+
*/
|
|
30186
|
+
function leftOverhangMs(pad) {
|
|
30187
|
+
return require_dist.VISIT_MERGE_GAP_MS + pad.preMs;
|
|
30188
|
+
}
|
|
29976
30189
|
function createVideoclipsProvider(deps) {
|
|
29977
30190
|
return {
|
|
29978
30191
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
30192
|
+
const readSince = since - leftOverhangMs(deps.defaultPad);
|
|
29979
30193
|
const events = await deps.fetchEvents({
|
|
29980
30194
|
deviceId,
|
|
29981
|
-
since,
|
|
30195
|
+
since: readSince,
|
|
29982
30196
|
until,
|
|
29983
|
-
|
|
30197
|
+
limit: deps.fetchAvailability ? VISIT_EVENT_FETCH_LIMIT : limit
|
|
29984
30198
|
});
|
|
29985
|
-
|
|
30199
|
+
warnOnTruncatedFetch(deps, deviceId, events);
|
|
30200
|
+
let vouched = /* @__PURE__ */ new Set();
|
|
30201
|
+
try {
|
|
30202
|
+
vouched = await resolveVouchedEvents(deviceId, events, deps.fetchMediaOwners);
|
|
30203
|
+
} catch (err) {
|
|
30204
|
+
deps.logWarn?.("videoclips: media presence read failed — listing without thumbnails", {
|
|
30205
|
+
deviceId,
|
|
30206
|
+
events: allEventIds(events).length,
|
|
30207
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30208
|
+
});
|
|
30209
|
+
}
|
|
30210
|
+
let clips = eventsToClips(events, deps.defaultPad, vouched);
|
|
29986
30211
|
if (deps.fetchAvailability) try {
|
|
29987
30212
|
clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
|
|
29988
30213
|
deviceId,
|
|
29989
|
-
since,
|
|
30214
|
+
since: readSince,
|
|
29990
30215
|
until
|
|
29991
|
-
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad);
|
|
30216
|
+
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
|
|
29992
30217
|
} catch (err) {
|
|
29993
30218
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
29994
30219
|
deviceId,
|
|
29995
30220
|
error: err instanceof Error ? err.message : String(err)
|
|
29996
30221
|
});
|
|
29997
30222
|
}
|
|
29998
|
-
|
|
30223
|
+
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
30224
|
+
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
30225
|
+
reportUnvouchedClips(deps, deviceId, answer);
|
|
30226
|
+
return answer;
|
|
29999
30227
|
},
|
|
30000
30228
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
30001
30229
|
const parsed = parseClipId(clipId);
|
|
@@ -30008,6 +30236,247 @@ function createVideoclipsProvider(deps) {
|
|
|
30008
30236
|
}
|
|
30009
30237
|
};
|
|
30010
30238
|
}
|
|
30239
|
+
/** The annotation backstop biting is a SILENT loss of the earliest visits'
|
|
30240
|
+
* events — say so, per camera, instead of shipping a thinner list. */
|
|
30241
|
+
function warnOnTruncatedFetch(deps, deviceId, events) {
|
|
30242
|
+
if (!deps.fetchAvailability) return;
|
|
30243
|
+
const truncated = [
|
|
30244
|
+
"motion",
|
|
30245
|
+
"object",
|
|
30246
|
+
"audio"
|
|
30247
|
+
].filter((kind) => events[kind].length >= VISIT_EVENT_FETCH_LIMIT);
|
|
30248
|
+
if (truncated.length === 0) return;
|
|
30249
|
+
deps.logWarn?.("videoclips: annotation fetch truncated — earliest visits lose their events", {
|
|
30250
|
+
deviceId,
|
|
30251
|
+
kinds: truncated,
|
|
30252
|
+
limit: VISIT_EVENT_FETCH_LIMIT
|
|
30253
|
+
});
|
|
30254
|
+
}
|
|
30255
|
+
/** The operator's acceptance criterion as a number: a rising count on one
|
|
30256
|
+
* camera is its motion-snapshot capturer failing, not a quiet day. */
|
|
30257
|
+
function reportUnvouchedClips(deps, deviceId, clips) {
|
|
30258
|
+
const clipsWithoutStill = clips.filter((c) => c.thumbnail === void 0).length;
|
|
30259
|
+
if (clipsWithoutStill === 0) return;
|
|
30260
|
+
deps.logInfo?.("videoclips: visit has no vouched still", {
|
|
30261
|
+
deviceId,
|
|
30262
|
+
clipsWithoutStill,
|
|
30263
|
+
clips: clips.length
|
|
30264
|
+
});
|
|
30265
|
+
}
|
|
30266
|
+
//#endregion
|
|
30267
|
+
//#region src/pipeline-analytics/viewer-settings-actions.ts
|
|
30268
|
+
/**
|
|
30269
|
+
* Viewer settings snapshots — hub-shared named blobs of the viewer's stores.
|
|
30270
|
+
*
|
|
30271
|
+
* Rides `addons.custom` (`viewerSettings.*`) so it ships with an addon deploy
|
|
30272
|
+
* and does not wait on a cap/codegen/server image. Ownership is the
|
|
30273
|
+
* authenticated caller (`caller: 'required'`); snapshots are visible to every
|
|
30274
|
+
* hub user, overwrite/delete only of one's own.
|
|
30275
|
+
*/
|
|
30276
|
+
/**
|
|
30277
|
+
* @durable class=config owner=pipeline-analytics
|
|
30278
|
+
* write="a signed-in viewer user saves a named snapshot of their local stores"
|
|
30279
|
+
* retention="none — a row goes only when its owner deletes it"
|
|
30280
|
+
*/
|
|
30281
|
+
var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
|
|
30282
|
+
var COLUMNS = [
|
|
30283
|
+
{
|
|
30284
|
+
name: "id",
|
|
30285
|
+
type: "TEXT",
|
|
30286
|
+
primaryKey: true,
|
|
30287
|
+
notNull: true
|
|
30288
|
+
},
|
|
30289
|
+
{
|
|
30290
|
+
name: "name",
|
|
30291
|
+
type: "TEXT",
|
|
30292
|
+
notNull: true
|
|
30293
|
+
},
|
|
30294
|
+
{
|
|
30295
|
+
name: "ownerUserId",
|
|
30296
|
+
type: "TEXT",
|
|
30297
|
+
notNull: true
|
|
30298
|
+
},
|
|
30299
|
+
{
|
|
30300
|
+
name: "ownerUsername",
|
|
30301
|
+
type: "TEXT",
|
|
30302
|
+
notNull: true
|
|
30303
|
+
},
|
|
30304
|
+
{
|
|
30305
|
+
name: "createdAt",
|
|
30306
|
+
type: "INTEGER",
|
|
30307
|
+
notNull: true
|
|
30308
|
+
},
|
|
30309
|
+
{
|
|
30310
|
+
name: "updatedAt",
|
|
30311
|
+
type: "INTEGER",
|
|
30312
|
+
notNull: true
|
|
30313
|
+
},
|
|
30314
|
+
{
|
|
30315
|
+
name: "blob",
|
|
30316
|
+
type: "JSON",
|
|
30317
|
+
notNull: true
|
|
30318
|
+
}
|
|
30319
|
+
];
|
|
30320
|
+
var NameSchema = require_dist.string().trim().min(1).max(80);
|
|
30321
|
+
var BlobSchema = require_dist.record(require_dist.string(), require_dist.unknown());
|
|
30322
|
+
var ViewerSettingsSnapshotSummarySchema = require_dist.object({
|
|
30323
|
+
id: require_dist.string(),
|
|
30324
|
+
name: require_dist.string(),
|
|
30325
|
+
ownerUserId: require_dist.string(),
|
|
30326
|
+
ownerUsername: require_dist.string(),
|
|
30327
|
+
createdAt: require_dist.number().int(),
|
|
30328
|
+
updatedAt: require_dist.number().int(),
|
|
30329
|
+
mine: require_dist.boolean()
|
|
30330
|
+
});
|
|
30331
|
+
var ViewerSettingsSnapshotSchema = ViewerSettingsSnapshotSummarySchema.extend({ blob: BlobSchema });
|
|
30332
|
+
var NoInputSchema = require_dist.object({});
|
|
30333
|
+
var viewerSettingsActions = require_dist.defineCustomActions({
|
|
30334
|
+
"viewerSettings.list": require_dist.customAction(NoInputSchema, require_dist.object({ snapshots: require_dist.array(ViewerSettingsSnapshotSummarySchema) }), { caller: "required" }),
|
|
30335
|
+
"viewerSettings.save": require_dist.customAction(require_dist.object({
|
|
30336
|
+
name: NameSchema,
|
|
30337
|
+
blob: BlobSchema,
|
|
30338
|
+
ownerUsername: require_dist.string().min(1).max(80)
|
|
30339
|
+
}), require_dist.object({ snapshot: ViewerSettingsSnapshotSummarySchema }), {
|
|
30340
|
+
kind: "mutation",
|
|
30341
|
+
caller: "required"
|
|
30342
|
+
}),
|
|
30343
|
+
"viewerSettings.get": require_dist.customAction(require_dist.object({ id: require_dist.string().min(1) }), require_dist.object({ snapshot: ViewerSettingsSnapshotSchema.nullable() }), { caller: "required" }),
|
|
30344
|
+
"viewerSettings.delete": require_dist.customAction(require_dist.object({ id: require_dist.string().min(1) }), require_dist.object({ success: require_dist.literal(true) }), {
|
|
30345
|
+
kind: "mutation",
|
|
30346
|
+
caller: "required"
|
|
30347
|
+
})
|
|
30348
|
+
});
|
|
30349
|
+
function requireCaller(caller) {
|
|
30350
|
+
if (!caller) throw new Error("viewerSettings: authenticated caller required");
|
|
30351
|
+
return caller;
|
|
30352
|
+
}
|
|
30353
|
+
function asRecord(data) {
|
|
30354
|
+
return typeof data === "object" && data !== null ? data : {};
|
|
30355
|
+
}
|
|
30356
|
+
function toSummary(id, data, callerUserId) {
|
|
30357
|
+
const parsed = ViewerSettingsSnapshotSummarySchema.omit({ mine: true }).safeParse({
|
|
30358
|
+
id,
|
|
30359
|
+
name: data.name,
|
|
30360
|
+
ownerUserId: data.ownerUserId,
|
|
30361
|
+
ownerUsername: data.ownerUsername,
|
|
30362
|
+
createdAt: data.createdAt,
|
|
30363
|
+
updatedAt: data.updatedAt
|
|
30364
|
+
});
|
|
30365
|
+
if (!parsed.success) return null;
|
|
30366
|
+
return {
|
|
30367
|
+
...parsed.data,
|
|
30368
|
+
mine: parsed.data.ownerUserId === callerUserId
|
|
30369
|
+
};
|
|
30370
|
+
}
|
|
30371
|
+
var ViewerSettingsSnapshotStore = class {
|
|
30372
|
+
store;
|
|
30373
|
+
constructor(store) {
|
|
30374
|
+
this.store = store;
|
|
30375
|
+
}
|
|
30376
|
+
static async declare(store) {
|
|
30377
|
+
await store.declareCollection.mutate({
|
|
30378
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
30379
|
+
columns: [...COLUMNS],
|
|
30380
|
+
indexes: [{
|
|
30381
|
+
name: "idx_viewer_settings_owner",
|
|
30382
|
+
columns: ["ownerUserId"]
|
|
30383
|
+
}]
|
|
30384
|
+
});
|
|
30385
|
+
}
|
|
30386
|
+
async list(caller) {
|
|
30387
|
+
const rows = await this.store.query.query({
|
|
30388
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
30389
|
+
filter: {
|
|
30390
|
+
orderBy: {
|
|
30391
|
+
field: "updatedAt",
|
|
30392
|
+
direction: "desc"
|
|
30393
|
+
},
|
|
30394
|
+
limit: 200
|
|
30395
|
+
}
|
|
30396
|
+
});
|
|
30397
|
+
const out = [];
|
|
30398
|
+
for (const row of rows) {
|
|
30399
|
+
const summary = toSummary(row.id, asRecord(row.data), caller.userId);
|
|
30400
|
+
if (summary) out.push(summary);
|
|
30401
|
+
}
|
|
30402
|
+
return out;
|
|
30403
|
+
}
|
|
30404
|
+
async get(id, caller) {
|
|
30405
|
+
const row = (await this.store.query.query({
|
|
30406
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
30407
|
+
filter: {
|
|
30408
|
+
where: { id },
|
|
30409
|
+
limit: 1
|
|
30410
|
+
}
|
|
30411
|
+
}))[0];
|
|
30412
|
+
if (!row) return null;
|
|
30413
|
+
const data = asRecord(row.data);
|
|
30414
|
+
const summary = toSummary(row.id, data, caller.userId);
|
|
30415
|
+
if (!summary) return null;
|
|
30416
|
+
const blob = BlobSchema.safeParse(data.blob);
|
|
30417
|
+
if (!blob.success) return null;
|
|
30418
|
+
return {
|
|
30419
|
+
...summary,
|
|
30420
|
+
blob: blob.data
|
|
30421
|
+
};
|
|
30422
|
+
}
|
|
30423
|
+
async save(input, caller) {
|
|
30424
|
+
const now = Date.now();
|
|
30425
|
+
const prior = (await this.store.query.query({
|
|
30426
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
30427
|
+
filter: {
|
|
30428
|
+
where: {
|
|
30429
|
+
ownerUserId: caller.userId,
|
|
30430
|
+
name: input.name
|
|
30431
|
+
},
|
|
30432
|
+
limit: 1
|
|
30433
|
+
}
|
|
30434
|
+
}))[0];
|
|
30435
|
+
const id = prior?.id ?? (0, node_crypto.randomUUID)();
|
|
30436
|
+
const createdAt = typeof asRecord(prior?.data).createdAt === "number" ? asRecord(prior?.data).createdAt : now;
|
|
30437
|
+
await this.store.set.mutate({
|
|
30438
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
30439
|
+
key: id,
|
|
30440
|
+
value: {
|
|
30441
|
+
name: input.name,
|
|
30442
|
+
ownerUserId: caller.userId,
|
|
30443
|
+
ownerUsername: input.ownerUsername,
|
|
30444
|
+
createdAt,
|
|
30445
|
+
updatedAt: now,
|
|
30446
|
+
blob: input.blob
|
|
30447
|
+
}
|
|
30448
|
+
});
|
|
30449
|
+
return {
|
|
30450
|
+
id,
|
|
30451
|
+
name: input.name,
|
|
30452
|
+
ownerUserId: caller.userId,
|
|
30453
|
+
ownerUsername: input.ownerUsername,
|
|
30454
|
+
createdAt,
|
|
30455
|
+
updatedAt: now,
|
|
30456
|
+
mine: true
|
|
30457
|
+
};
|
|
30458
|
+
}
|
|
30459
|
+
async delete(id, caller) {
|
|
30460
|
+
const row = await this.get(id, caller);
|
|
30461
|
+
if (!row) throw new Error("viewerSettings: snapshot not found");
|
|
30462
|
+
if (row.ownerUserId !== caller.userId && !caller.isAdmin) throw new Error("viewerSettings: only the owner can delete this snapshot");
|
|
30463
|
+
await this.store.delete.mutate({
|
|
30464
|
+
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
30465
|
+
key: id
|
|
30466
|
+
});
|
|
30467
|
+
}
|
|
30468
|
+
};
|
|
30469
|
+
function makeViewerSettingsActionHandlers(store) {
|
|
30470
|
+
return {
|
|
30471
|
+
"viewerSettings.list": async (_input, caller) => ({ snapshots: await store.list(requireCaller(caller)) }),
|
|
30472
|
+
"viewerSettings.save": async (input, caller) => ({ snapshot: await store.save(input, requireCaller(caller)) }),
|
|
30473
|
+
"viewerSettings.get": async (input, caller) => ({ snapshot: await store.get(input.id, requireCaller(caller)) }),
|
|
30474
|
+
"viewerSettings.delete": async (input, caller) => {
|
|
30475
|
+
await store.delete(input.id, requireCaller(caller));
|
|
30476
|
+
return { success: true };
|
|
30477
|
+
}
|
|
30478
|
+
};
|
|
30479
|
+
}
|
|
30011
30480
|
/**
|
|
30012
30481
|
* The VIDEO's playback rate — 4×, the same as {@link NC_GIF_SPEED}.
|
|
30013
30482
|
*
|
|
@@ -30814,7 +31283,15 @@ var AudioDetectionSettingsSchema = require_dist.object({
|
|
|
30814
31283
|
/** Flood ceiling: markers per camera per rolling 24 h. This, not the
|
|
30815
31284
|
* thresholds, is what bounds the worst case — a mis-tuned threshold can
|
|
30816
31285
|
* only ever cost the operator this many markers a day. */
|
|
30817
|
-
audioMarkerMaxPerDay: require_dist.number().int().positive().default(12)
|
|
31286
|
+
audioMarkerMaxPerDay: require_dist.number().int().positive().default(12),
|
|
31287
|
+
/** Materialize one event + one synthetic track per classified episode. */
|
|
31288
|
+
audioEpisodeEnabled: require_dist.boolean().default(true),
|
|
31289
|
+
/** Class must be heard continuously this long before the episode opens. */
|
|
31290
|
+
audioEpisodeOpenHoldSec: require_dist.number().nonnegative().default(1.5),
|
|
31291
|
+
/** Class must be absent this long before the episode closes. */
|
|
31292
|
+
audioEpisodeCloseGapSec: require_dist.number().positive().default(12),
|
|
31293
|
+
/** After close, the same class cannot reopen until this elapses. */
|
|
31294
|
+
audioEpisodeCooldownSec: require_dist.number().nonnegative().default(30)
|
|
30818
31295
|
});
|
|
30819
31296
|
var AUDIO_DETECTION_DEFAULTS = AudioDetectionSettingsSchema.parse({});
|
|
30820
31297
|
/**
|
|
@@ -30838,6 +31315,159 @@ function resolveAudioDetectionSettings(raw) {
|
|
|
30838
31315
|
return resolved.success ? resolved.data : AUDIO_DETECTION_DEFAULTS;
|
|
30839
31316
|
}
|
|
30840
31317
|
//#endregion
|
|
31318
|
+
//#region src/pipeline-analytics/audio-episode-hysteresis.ts
|
|
31319
|
+
var EMPTY = {
|
|
31320
|
+
pendingClass: null,
|
|
31321
|
+
pendingSinceMs: 0,
|
|
31322
|
+
openClass: null,
|
|
31323
|
+
episodeId: null,
|
|
31324
|
+
lastHeardMs: 0,
|
|
31325
|
+
lastClosedClass: null,
|
|
31326
|
+
lastClosedMs: 0
|
|
31327
|
+
};
|
|
31328
|
+
function emptyAudioEpisodeState() {
|
|
31329
|
+
return EMPTY;
|
|
31330
|
+
}
|
|
31331
|
+
function episodeIdFor(className, timestampMs) {
|
|
31332
|
+
return `${className}:${timestampMs}`;
|
|
31333
|
+
}
|
|
31334
|
+
function inCooldown(state, className, timestampMs, knobs) {
|
|
31335
|
+
return state.lastClosedClass === className && timestampMs - state.lastClosedMs < knobs.cooldownMs;
|
|
31336
|
+
}
|
|
31337
|
+
function closedState(className, atMs) {
|
|
31338
|
+
return {
|
|
31339
|
+
...EMPTY,
|
|
31340
|
+
lastClosedClass: className,
|
|
31341
|
+
lastClosedMs: atMs
|
|
31342
|
+
};
|
|
31343
|
+
}
|
|
31344
|
+
function observeAudioEpisode(state, sample, knobs) {
|
|
31345
|
+
if (!knobs.enabled) return {
|
|
31346
|
+
nextState: EMPTY,
|
|
31347
|
+
action: { kind: "none" }
|
|
31348
|
+
};
|
|
31349
|
+
const { timestampMs, className } = sample;
|
|
31350
|
+
const openClass = state.openClass;
|
|
31351
|
+
const openId = state.episodeId;
|
|
31352
|
+
if (openClass !== null && openId !== null) {
|
|
31353
|
+
if (className === openClass) return {
|
|
31354
|
+
nextState: {
|
|
31355
|
+
...state,
|
|
31356
|
+
pendingClass: null,
|
|
31357
|
+
pendingSinceMs: 0,
|
|
31358
|
+
lastHeardMs: timestampMs
|
|
31359
|
+
},
|
|
31360
|
+
action: {
|
|
31361
|
+
kind: "hold",
|
|
31362
|
+
className: openClass,
|
|
31363
|
+
episodeId: openId
|
|
31364
|
+
}
|
|
31365
|
+
};
|
|
31366
|
+
if (className === null) {
|
|
31367
|
+
if (timestampMs - state.lastHeardMs < knobs.closeGapMs) return {
|
|
31368
|
+
nextState: state,
|
|
31369
|
+
action: { kind: "none" }
|
|
31370
|
+
};
|
|
31371
|
+
return {
|
|
31372
|
+
nextState: closedState(openClass, timestampMs),
|
|
31373
|
+
action: {
|
|
31374
|
+
kind: "close",
|
|
31375
|
+
className: openClass,
|
|
31376
|
+
episodeId: openId
|
|
31377
|
+
}
|
|
31378
|
+
};
|
|
31379
|
+
}
|
|
31380
|
+
if (inCooldown(state, className, timestampMs, knobs)) return {
|
|
31381
|
+
nextState: state,
|
|
31382
|
+
action: { kind: "none" }
|
|
31383
|
+
};
|
|
31384
|
+
const pendingSince = state.pendingClass === className ? state.pendingSinceMs : timestampMs;
|
|
31385
|
+
if (knobs.openHoldMs === 0 || state.pendingClass === className && timestampMs - pendingSince >= knobs.openHoldMs) {
|
|
31386
|
+
const openedId = episodeIdFor(className, timestampMs);
|
|
31387
|
+
return {
|
|
31388
|
+
nextState: {
|
|
31389
|
+
pendingClass: null,
|
|
31390
|
+
pendingSinceMs: 0,
|
|
31391
|
+
openClass: className,
|
|
31392
|
+
episodeId: openedId,
|
|
31393
|
+
lastHeardMs: timestampMs,
|
|
31394
|
+
lastClosedClass: openClass,
|
|
31395
|
+
lastClosedMs: timestampMs
|
|
31396
|
+
},
|
|
31397
|
+
action: {
|
|
31398
|
+
kind: "replace",
|
|
31399
|
+
closed: {
|
|
31400
|
+
className: openClass,
|
|
31401
|
+
episodeId: openId
|
|
31402
|
+
},
|
|
31403
|
+
opened: {
|
|
31404
|
+
className,
|
|
31405
|
+
episodeId: openedId
|
|
31406
|
+
}
|
|
31407
|
+
}
|
|
31408
|
+
};
|
|
31409
|
+
}
|
|
31410
|
+
return {
|
|
31411
|
+
nextState: {
|
|
31412
|
+
...state,
|
|
31413
|
+
pendingClass: className,
|
|
31414
|
+
pendingSinceMs: pendingSince
|
|
31415
|
+
},
|
|
31416
|
+
action: { kind: "none" }
|
|
31417
|
+
};
|
|
31418
|
+
}
|
|
31419
|
+
if (className === null) {
|
|
31420
|
+
if (state.pendingClass === null) return {
|
|
31421
|
+
nextState: state,
|
|
31422
|
+
action: { kind: "none" }
|
|
31423
|
+
};
|
|
31424
|
+
return {
|
|
31425
|
+
nextState: {
|
|
31426
|
+
...state,
|
|
31427
|
+
pendingClass: null,
|
|
31428
|
+
pendingSinceMs: 0
|
|
31429
|
+
},
|
|
31430
|
+
action: { kind: "none" }
|
|
31431
|
+
};
|
|
31432
|
+
}
|
|
31433
|
+
if (inCooldown(state, className, timestampMs, knobs)) return {
|
|
31434
|
+
nextState: state,
|
|
31435
|
+
action: { kind: "none" }
|
|
31436
|
+
};
|
|
31437
|
+
const pendingSince = state.pendingClass === className ? state.pendingSinceMs : timestampMs;
|
|
31438
|
+
if (knobs.openHoldMs === 0 || state.pendingClass === className && timestampMs - pendingSince >= knobs.openHoldMs) {
|
|
31439
|
+
const episodeId = episodeIdFor(className, timestampMs);
|
|
31440
|
+
return {
|
|
31441
|
+
nextState: {
|
|
31442
|
+
pendingClass: null,
|
|
31443
|
+
pendingSinceMs: 0,
|
|
31444
|
+
openClass: className,
|
|
31445
|
+
episodeId,
|
|
31446
|
+
lastHeardMs: timestampMs,
|
|
31447
|
+
lastClosedClass: state.lastClosedClass,
|
|
31448
|
+
lastClosedMs: state.lastClosedMs
|
|
31449
|
+
},
|
|
31450
|
+
action: {
|
|
31451
|
+
kind: "open",
|
|
31452
|
+
className,
|
|
31453
|
+
episodeId
|
|
31454
|
+
}
|
|
31455
|
+
};
|
|
31456
|
+
}
|
|
31457
|
+
if (state.pendingClass === className) return {
|
|
31458
|
+
nextState: state,
|
|
31459
|
+
action: { kind: "none" }
|
|
31460
|
+
};
|
|
31461
|
+
return {
|
|
31462
|
+
nextState: {
|
|
31463
|
+
...state,
|
|
31464
|
+
pendingClass: className,
|
|
31465
|
+
pendingSinceMs: timestampMs
|
|
31466
|
+
},
|
|
31467
|
+
action: { kind: "none" }
|
|
31468
|
+
};
|
|
31469
|
+
}
|
|
31470
|
+
//#endregion
|
|
30841
31471
|
//#region src/pipeline-analytics/audio-event-router.ts
|
|
30842
31472
|
/**
|
|
30843
31473
|
* Decide whether an audio frame is a (meaningful) classification event or a
|
|
@@ -46823,6 +47453,66 @@ var EventMediaDispatcher = class {
|
|
|
46823
47453
|
}
|
|
46824
47454
|
}
|
|
46825
47455
|
};
|
|
47456
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
47457
|
+
constructor(ms) {
|
|
47458
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
47459
|
+
this.name = "SnapshotTimeoutError";
|
|
47460
|
+
}
|
|
47461
|
+
};
|
|
47462
|
+
function withTimeout$1(promise, ms) {
|
|
47463
|
+
return new Promise((resolve, reject) => {
|
|
47464
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
47465
|
+
promise.then((value) => {
|
|
47466
|
+
clearTimeout(timer);
|
|
47467
|
+
resolve(value);
|
|
47468
|
+
}, (err) => {
|
|
47469
|
+
clearTimeout(timer);
|
|
47470
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47471
|
+
});
|
|
47472
|
+
});
|
|
47473
|
+
}
|
|
47474
|
+
var MotionEventSnapshotCapturer = class {
|
|
47475
|
+
deps;
|
|
47476
|
+
snapshotTimeoutMs;
|
|
47477
|
+
constructor(deps) {
|
|
47478
|
+
this.deps = deps;
|
|
47479
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47480
|
+
}
|
|
47481
|
+
/**
|
|
47482
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47483
|
+
* throws — the durable record is the motion row, already inserted by
|
|
47484
|
+
* the caller.
|
|
47485
|
+
*/
|
|
47486
|
+
async capture(input) {
|
|
47487
|
+
let snap;
|
|
47488
|
+
try {
|
|
47489
|
+
snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47490
|
+
} catch (err) {
|
|
47491
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47492
|
+
return;
|
|
47493
|
+
}
|
|
47494
|
+
if (snap === null) return;
|
|
47495
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
47496
|
+
let data = raw;
|
|
47497
|
+
try {
|
|
47498
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47499
|
+
} catch (err) {
|
|
47500
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47501
|
+
}
|
|
47502
|
+
try {
|
|
47503
|
+
await this.deps.media.put({
|
|
47504
|
+
deviceId: input.deviceId,
|
|
47505
|
+
ownerKind: "event",
|
|
47506
|
+
ownerId: input.eventId,
|
|
47507
|
+
kind: "snapshot",
|
|
47508
|
+
timestamp: input.timestamp,
|
|
47509
|
+
data
|
|
47510
|
+
});
|
|
47511
|
+
} catch (err) {
|
|
47512
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47513
|
+
}
|
|
47514
|
+
}
|
|
47515
|
+
};
|
|
46826
47516
|
/**
|
|
46827
47517
|
* Project one `DeviceStateChanged` of a mapped sensor/control cap into a
|
|
46828
47518
|
* synthetic track per camera the event belongs to. Returns the number of
|
|
@@ -46919,16 +47609,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
46919
47609
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
46920
47610
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
46921
47611
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
46922
|
-
var SnapshotTimeoutError
|
|
47612
|
+
var SnapshotTimeoutError = class extends Error {
|
|
46923
47613
|
constructor(ms) {
|
|
46924
47614
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
46925
47615
|
this.name = "SnapshotTimeoutError";
|
|
46926
47616
|
}
|
|
46927
47617
|
};
|
|
46928
47618
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
46929
|
-
function withTimeout
|
|
47619
|
+
function withTimeout(promise, ms) {
|
|
46930
47620
|
return new Promise((resolve, reject) => {
|
|
46931
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError
|
|
47621
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46932
47622
|
promise.then((value) => {
|
|
46933
47623
|
clearTimeout(timer);
|
|
46934
47624
|
resolve(value);
|
|
@@ -46983,11 +47673,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
46983
47673
|
*/
|
|
46984
47674
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
46985
47675
|
try {
|
|
46986
|
-
const snap = await withTimeout
|
|
47676
|
+
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46987
47677
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
46988
47678
|
return snap;
|
|
46989
47679
|
} catch (err) {
|
|
46990
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError
|
|
47680
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46991
47681
|
return null;
|
|
46992
47682
|
}
|
|
46993
47683
|
}
|
|
@@ -47053,66 +47743,6 @@ var SyntheticTrackMaterializer = class {
|
|
|
47053
47743
|
return track;
|
|
47054
47744
|
}
|
|
47055
47745
|
};
|
|
47056
|
-
var SnapshotTimeoutError = class extends Error {
|
|
47057
|
-
constructor(ms) {
|
|
47058
|
-
super(`snapshot cap timed out after ${ms}ms`);
|
|
47059
|
-
this.name = "SnapshotTimeoutError";
|
|
47060
|
-
}
|
|
47061
|
-
};
|
|
47062
|
-
function withTimeout(promise, ms) {
|
|
47063
|
-
return new Promise((resolve, reject) => {
|
|
47064
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47065
|
-
promise.then((value) => {
|
|
47066
|
-
clearTimeout(timer);
|
|
47067
|
-
resolve(value);
|
|
47068
|
-
}, (err) => {
|
|
47069
|
-
clearTimeout(timer);
|
|
47070
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47071
|
-
});
|
|
47072
|
-
});
|
|
47073
|
-
}
|
|
47074
|
-
var MotionEventSnapshotCapturer = class {
|
|
47075
|
-
deps;
|
|
47076
|
-
snapshotTimeoutMs;
|
|
47077
|
-
constructor(deps) {
|
|
47078
|
-
this.deps = deps;
|
|
47079
|
-
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47080
|
-
}
|
|
47081
|
-
/**
|
|
47082
|
-
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47083
|
-
* throws — the durable record is the motion row, already inserted by
|
|
47084
|
-
* the caller.
|
|
47085
|
-
*/
|
|
47086
|
-
async capture(input) {
|
|
47087
|
-
let snap;
|
|
47088
|
-
try {
|
|
47089
|
-
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47090
|
-
} catch (err) {
|
|
47091
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47092
|
-
return;
|
|
47093
|
-
}
|
|
47094
|
-
if (snap === null) return;
|
|
47095
|
-
const raw = Buffer.from(snap.base64, "base64");
|
|
47096
|
-
let data = raw;
|
|
47097
|
-
try {
|
|
47098
|
-
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47099
|
-
} catch (err) {
|
|
47100
|
-
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47101
|
-
}
|
|
47102
|
-
try {
|
|
47103
|
-
await this.deps.media.put({
|
|
47104
|
-
deviceId: input.deviceId,
|
|
47105
|
-
ownerKind: "event",
|
|
47106
|
-
ownerId: input.eventId,
|
|
47107
|
-
kind: "snapshot",
|
|
47108
|
-
timestamp: input.timestamp,
|
|
47109
|
-
data
|
|
47110
|
-
});
|
|
47111
|
-
} catch (err) {
|
|
47112
|
-
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47113
|
-
}
|
|
47114
|
-
}
|
|
47115
|
-
};
|
|
47116
47746
|
var TrackingSettingsSchema = require_dist.object({
|
|
47117
47747
|
/**
|
|
47118
47748
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -48402,6 +49032,51 @@ function buildGlobalSettingsSchema() {
|
|
|
48402
49032
|
}
|
|
48403
49033
|
]
|
|
48404
49034
|
},
|
|
49035
|
+
{
|
|
49036
|
+
id: "audio-episodes",
|
|
49037
|
+
title: "Audio episodes",
|
|
49038
|
+
description: "One timeline track and one event per classified sound, held open with hysteresis so a long cry is not four tracks. No class filter: speech, crying, a bark — the knobs below are what stop repeats. Leave on for a day or two and judge the spam, then tighten per camera.",
|
|
49039
|
+
columns: 2,
|
|
49040
|
+
fields: [
|
|
49041
|
+
{
|
|
49042
|
+
type: "boolean",
|
|
49043
|
+
key: "audioEpisodeEnabled",
|
|
49044
|
+
label: "Create audio episodes",
|
|
49045
|
+
description: "When off, classified audio still notifies from other paths but does not open a synthetic track or coalesce classification events.",
|
|
49046
|
+
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeEnabled
|
|
49047
|
+
},
|
|
49048
|
+
{
|
|
49049
|
+
type: "number",
|
|
49050
|
+
key: "audioEpisodeOpenHoldSec",
|
|
49051
|
+
label: "Open hold",
|
|
49052
|
+
description: "The class must be heard continuously this long before an episode opens. Filters 200 ms blips.",
|
|
49053
|
+
min: 0,
|
|
49054
|
+
step: .5,
|
|
49055
|
+
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeOpenHoldSec,
|
|
49056
|
+
unit: "s"
|
|
49057
|
+
},
|
|
49058
|
+
{
|
|
49059
|
+
type: "number",
|
|
49060
|
+
key: "audioEpisodeCloseGapSec",
|
|
49061
|
+
label: "Close gap",
|
|
49062
|
+
description: "The class must be absent this long before the episode closes. A pause inside a cry does not start a second track.",
|
|
49063
|
+
min: 1,
|
|
49064
|
+
step: 1,
|
|
49065
|
+
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeCloseGapSec,
|
|
49066
|
+
unit: "s"
|
|
49067
|
+
},
|
|
49068
|
+
{
|
|
49069
|
+
type: "number",
|
|
49070
|
+
key: "audioEpisodeCooldownSec",
|
|
49071
|
+
label: "Cooldown",
|
|
49072
|
+
description: "After an episode closes, the same class cannot reopen until this elapses.",
|
|
49073
|
+
min: 0,
|
|
49074
|
+
step: 5,
|
|
49075
|
+
default: AUDIO_DETECTION_DEFAULTS.audioEpisodeCooldownSec,
|
|
49076
|
+
unit: "s"
|
|
49077
|
+
}
|
|
49078
|
+
]
|
|
49079
|
+
},
|
|
48405
49080
|
{
|
|
48406
49081
|
id: "face-recognition",
|
|
48407
49082
|
title: "Face recognition",
|
|
@@ -49887,7 +50562,8 @@ var customActions = {
|
|
|
49887
50562
|
...ncActions,
|
|
49888
50563
|
...embeddingActions,
|
|
49889
50564
|
...debugActions,
|
|
49890
|
-
...orphanAuditActions
|
|
50565
|
+
...orphanAuditActions,
|
|
50566
|
+
...viewerSettingsActions
|
|
49891
50567
|
};
|
|
49892
50568
|
/**
|
|
49893
50569
|
* Assist threshold when the caller does not pin one. Packages have their own
|
|
@@ -49989,7 +50665,12 @@ var LTS_FLUSH_INTERVAL_MS = 6e4;
|
|
|
49989
50665
|
var OPS_LOG_RETENTION_MS = 720 * 60 * 6e4;
|
|
49990
50666
|
/** Throttle for the "overlay synthesis failed" warn — one line / minute / device. */
|
|
49991
50667
|
var OVERLAY_SYNTHESIS_WARN_THROTTLE_MS = 6e4;
|
|
49992
|
-
|
|
50668
|
+
/** How recently a spatial track must have been alive to count as "visual
|
|
50669
|
+
* happening" — stamp audio labels on that track instead of opening a
|
|
50670
|
+
* synthetic audio episode. */
|
|
50671
|
+
var AUDIO_EPISODE_VISUAL_QUIET_MS = 2e3;
|
|
50672
|
+
/** Persist lastSeen on an open synthetic audio track at most this often. */
|
|
50673
|
+
var AUDIO_EPISODE_PERSIST_MS = 2e3;
|
|
49993
50674
|
var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
49994
50675
|
/**
|
|
49995
50676
|
* Stored media kinds that carry NO drawn bounding box, in fallback preference
|
|
@@ -50461,6 +51142,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50461
51142
|
* fallible read on the decision path.
|
|
50462
51143
|
*/
|
|
50463
51144
|
lastTrackActivityMs = /* @__PURE__ */ new Map();
|
|
51145
|
+
/** Per-device classified-audio episode hysteresis. */
|
|
51146
|
+
audioEpisodeByDevice = /* @__PURE__ */ new Map();
|
|
51147
|
+
/** Open synthetic tracks keyed by hysteresis episodeId. */
|
|
51148
|
+
audioEpisodeTracks = /* @__PURE__ */ new Map();
|
|
51149
|
+
audioEpisodeLastPersistMs = /* @__PURE__ */ new Map();
|
|
50464
51150
|
settingsCaches = new DeviceSettingsCacheHub();
|
|
50465
51151
|
/** Avoids a store read every ~30 Hz audio frame. */
|
|
50466
51152
|
audioSettingsCache = this.settingsCaches.registerDeviceCache("audio-detection", SETTINGS_CACHE_TTL_MS);
|
|
@@ -50744,6 +51430,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50744
51430
|
* NAME — its scope list is derived from the classification registry, not
|
|
50745
51431
|
* from a set of store instances. */
|
|
50746
51432
|
settingsStore = null;
|
|
51433
|
+
viewerSettingsSnapshots = null;
|
|
50747
51434
|
/** Per-scope resume cursors, so a run interrupted by a restart costs one
|
|
50748
51435
|
* page rather than a full rescan. In RAM: losing them costs a rescan, and
|
|
50749
51436
|
* persisting them would be a durable record of a purely advisory position. */
|
|
@@ -50881,12 +51568,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50881
51568
|
...ncActions,
|
|
50882
51569
|
...embeddingActions,
|
|
50883
51570
|
...debugActions,
|
|
50884
|
-
...orphanAuditActions
|
|
51571
|
+
...orphanAuditActions,
|
|
51572
|
+
...viewerSettingsActions
|
|
50885
51573
|
},
|
|
50886
51574
|
actionHandlers: {
|
|
50887
51575
|
...ncHandlers,
|
|
50888
51576
|
...this.buildEmbeddingActionHandlers(),
|
|
50889
|
-
...this.buildDebugActionHandlers()
|
|
51577
|
+
...this.buildDebugActionHandlers(),
|
|
51578
|
+
...this.viewerSettingsSnapshots ? makeViewerSettingsActionHandlers(this.viewerSettingsSnapshots) : {}
|
|
50890
51579
|
}
|
|
50891
51580
|
} : {}
|
|
50892
51581
|
};
|
|
@@ -51061,8 +51750,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
51061
51750
|
OpsLogStore.declare(api.settingsStore),
|
|
51062
51751
|
AnalyticsLts.declare(api.settingsStore),
|
|
51063
51752
|
SceneStore.declare(api.settingsStore),
|
|
51064
|
-
NotificationCenter.declare(api.settingsStore)
|
|
51753
|
+
NotificationCenter.declare(api.settingsStore),
|
|
51754
|
+
ViewerSettingsSnapshotStore.declare(api.settingsStore)
|
|
51065
51755
|
]);
|
|
51756
|
+
this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
|
|
51066
51757
|
runLabelTierMigration({
|
|
51067
51758
|
store: api.settingsStore,
|
|
51068
51759
|
logger: this.ctx.logger,
|
|
@@ -52322,6 +53013,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52322
53013
|
this.forgetDeviceProcessors(deviceId);
|
|
52323
53014
|
this.levelStateByDevice.delete(deviceId);
|
|
52324
53015
|
this.lastTrackActivityMs.delete(deviceId);
|
|
53016
|
+
this.audioEpisodeByDevice.delete(deviceId);
|
|
53017
|
+
for (const [episodeId, track] of this.audioEpisodeTracks) if (track.deviceId === deviceId) {
|
|
53018
|
+
this.audioEpisodeTracks.delete(episodeId);
|
|
53019
|
+
this.audioEpisodeLastPersistMs.delete(episodeId);
|
|
53020
|
+
}
|
|
52325
53021
|
this.audioMarkers?.forgetDevice(deviceId);
|
|
52326
53022
|
this.settingsCaches.clearDevice(deviceId);
|
|
52327
53023
|
this.bindingCache?.invalidate(deviceId);
|
|
@@ -52586,6 +53282,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52586
53282
|
audio
|
|
52587
53283
|
};
|
|
52588
53284
|
},
|
|
53285
|
+
fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
|
|
53286
|
+
return stores.mediaStore.ownersWithMedia(ownerKind, ownerIds, deviceId);
|
|
53287
|
+
},
|
|
52589
53288
|
fetchAvailability: async ({ deviceId, since, until }) => {
|
|
52590
53289
|
return (await api.recording.getAvailability.query({
|
|
52591
53290
|
deviceId,
|
|
@@ -52597,6 +53296,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
52597
53296
|
tags: { deviceId: Number(meta["deviceId"]) },
|
|
52598
53297
|
meta
|
|
52599
53298
|
}),
|
|
53299
|
+
logInfo: (message, meta) => this.ctx.logger.info(message, {
|
|
53300
|
+
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53301
|
+
meta
|
|
53302
|
+
}),
|
|
52600
53303
|
resolvePlayback: async ({ deviceId, startMs, endMs }) => {
|
|
52601
53304
|
const manifest = await api.recording.getPlaybackManifest.query({
|
|
52602
53305
|
deviceId,
|
|
@@ -54218,6 +54921,146 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54218
54921
|
maxPerDay: settings.audioMarkerMaxPerDay
|
|
54219
54922
|
};
|
|
54220
54923
|
}
|
|
54924
|
+
audioEpisodeKnobs(settings) {
|
|
54925
|
+
return {
|
|
54926
|
+
enabled: settings.audioEpisodeEnabled,
|
|
54927
|
+
openHoldMs: settings.audioEpisodeOpenHoldSec * 1e3,
|
|
54928
|
+
closeGapMs: settings.audioEpisodeCloseGapSec * 1e3,
|
|
54929
|
+
cooldownMs: settings.audioEpisodeCooldownSec * 1e3
|
|
54930
|
+
};
|
|
54931
|
+
}
|
|
54932
|
+
hasLiveVisualTrack(deviceId, atMs) {
|
|
54933
|
+
const at = this.lastTrackActivityMs.get(deviceId);
|
|
54934
|
+
return at !== void 0 && atMs - at < AUDIO_EPISODE_VISUAL_QUIET_MS;
|
|
54935
|
+
}
|
|
54936
|
+
async driveAudioEpisode(input) {
|
|
54937
|
+
const { nextState, action } = observeAudioEpisode(this.audioEpisodeByDevice.get(input.deviceId) ?? emptyAudioEpisodeState(), {
|
|
54938
|
+
timestampMs: input.timestamp,
|
|
54939
|
+
className: input.className
|
|
54940
|
+
}, input.knobs);
|
|
54941
|
+
this.audioEpisodeByDevice.set(input.deviceId, nextState);
|
|
54942
|
+
switch (action.kind) {
|
|
54943
|
+
case "none": return;
|
|
54944
|
+
case "open":
|
|
54945
|
+
await this.openAudioEpisode(input, action);
|
|
54946
|
+
return;
|
|
54947
|
+
case "hold":
|
|
54948
|
+
await this.holdAudioEpisode(input, action);
|
|
54949
|
+
return;
|
|
54950
|
+
case "close":
|
|
54951
|
+
await this.persistAudioEpisodeLastSeen(action, input.timestamp, true);
|
|
54952
|
+
return;
|
|
54953
|
+
case "replace":
|
|
54954
|
+
await this.persistAudioEpisodeLastSeen(action.closed, input.timestamp, true);
|
|
54955
|
+
await this.openAudioEpisode(input, action.opened);
|
|
54956
|
+
return;
|
|
54957
|
+
}
|
|
54958
|
+
}
|
|
54959
|
+
async openAudioEpisode(input, ref) {
|
|
54960
|
+
const ev = {
|
|
54961
|
+
id: (0, node_crypto.randomUUID)(),
|
|
54962
|
+
deviceId: input.deviceId,
|
|
54963
|
+
timestamp: input.timestamp,
|
|
54964
|
+
kind: "audio",
|
|
54965
|
+
rms: input.rms,
|
|
54966
|
+
dbfs: input.dbfs,
|
|
54967
|
+
classification: {
|
|
54968
|
+
className: ref.className,
|
|
54969
|
+
...input.originalClass !== void 0 ? { originalClass: input.originalClass } : {},
|
|
54970
|
+
score: input.score
|
|
54971
|
+
}
|
|
54972
|
+
};
|
|
54973
|
+
this.lastAudioInsertByDevice.set(input.deviceId, {
|
|
54974
|
+
className: ref.className,
|
|
54975
|
+
atMs: input.timestamp
|
|
54976
|
+
});
|
|
54977
|
+
await this.eventStore.insertAudio(ev);
|
|
54978
|
+
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
54979
|
+
this.trackStore?.addAudioLabelEpisode(input.deviceId, ref.className, input.score, input.timestamp);
|
|
54980
|
+
this.ctx.eventBus.emit({
|
|
54981
|
+
id: `pa-${ev.id}`,
|
|
54982
|
+
timestamp: new Date(ev.timestamp),
|
|
54983
|
+
source: {
|
|
54984
|
+
type: "addon",
|
|
54985
|
+
id: "pipeline-analytics",
|
|
54986
|
+
addonId: "pipeline-analytics"
|
|
54987
|
+
},
|
|
54988
|
+
category: require_dist.EventCategory.PipelineAnalyticsDetectionEvent,
|
|
54989
|
+
data: {
|
|
54990
|
+
deviceId: input.deviceId,
|
|
54991
|
+
kind: "audio",
|
|
54992
|
+
eventId: ev.id,
|
|
54993
|
+
timestamp: ev.timestamp
|
|
54994
|
+
}
|
|
54995
|
+
});
|
|
54996
|
+
if (this.hasLiveVisualTrack(input.deviceId, input.timestamp)) return;
|
|
54997
|
+
const materializer = this.syntheticTracks;
|
|
54998
|
+
if (materializer === null) return;
|
|
54999
|
+
try {
|
|
55000
|
+
const track = await materializer.materialize({
|
|
55001
|
+
cameraId: input.deviceId,
|
|
55002
|
+
sourceDeviceId: input.deviceId,
|
|
55003
|
+
source: "audio",
|
|
55004
|
+
kind: ref.className,
|
|
55005
|
+
timestamp: input.timestamp,
|
|
55006
|
+
audioLabels: [{
|
|
55007
|
+
label: ref.className,
|
|
55008
|
+
peakScore: input.score,
|
|
55009
|
+
count: 1,
|
|
55010
|
+
firstAt: input.timestamp,
|
|
55011
|
+
lastAt: input.timestamp
|
|
55012
|
+
}]
|
|
55013
|
+
});
|
|
55014
|
+
if (track) {
|
|
55015
|
+
this.audioEpisodeTracks.set(ref.episodeId, track);
|
|
55016
|
+
this.audioEpisodeLastPersistMs.set(ref.episodeId, input.timestamp);
|
|
55017
|
+
}
|
|
55018
|
+
} catch (err) {
|
|
55019
|
+
this.ctx.logger.warn("audio-episode synthetic track failed", {
|
|
55020
|
+
tags: { deviceId: input.deviceId },
|
|
55021
|
+
meta: {
|
|
55022
|
+
className: ref.className,
|
|
55023
|
+
error: require_dist.errMsg(err)
|
|
55024
|
+
}
|
|
55025
|
+
});
|
|
55026
|
+
}
|
|
55027
|
+
}
|
|
55028
|
+
async holdAudioEpisode(input, ref) {
|
|
55029
|
+
await this.persistAudioEpisodeLastSeen(ref, input.timestamp, false);
|
|
55030
|
+
}
|
|
55031
|
+
async persistAudioEpisodeLastSeen(ref, timestamp, force) {
|
|
55032
|
+
const track = this.audioEpisodeTracks.get(ref.episodeId);
|
|
55033
|
+
if (!track) {
|
|
55034
|
+
if (force) {
|
|
55035
|
+
this.audioEpisodeTracks.delete(ref.episodeId);
|
|
55036
|
+
this.audioEpisodeLastPersistMs.delete(ref.episodeId);
|
|
55037
|
+
}
|
|
55038
|
+
return;
|
|
55039
|
+
}
|
|
55040
|
+
const last = this.audioEpisodeLastPersistMs.get(ref.episodeId) ?? 0;
|
|
55041
|
+
if (!force && timestamp - last < AUDIO_EPISODE_PERSIST_MS) return;
|
|
55042
|
+
const updated = {
|
|
55043
|
+
...track,
|
|
55044
|
+
lastSeen: timestamp
|
|
55045
|
+
};
|
|
55046
|
+
this.audioEpisodeTracks.set(ref.episodeId, updated);
|
|
55047
|
+
this.audioEpisodeLastPersistMs.set(ref.episodeId, timestamp);
|
|
55048
|
+
try {
|
|
55049
|
+
await this.trackStore?.persistSyntheticTrack(updated);
|
|
55050
|
+
} catch (err) {
|
|
55051
|
+
this.ctx.logger.warn("audio-episode lastSeen persist failed", {
|
|
55052
|
+
tags: { deviceId: track.deviceId },
|
|
55053
|
+
meta: {
|
|
55054
|
+
episodeId: ref.episodeId,
|
|
55055
|
+
error: require_dist.errMsg(err)
|
|
55056
|
+
}
|
|
55057
|
+
});
|
|
55058
|
+
}
|
|
55059
|
+
if (force) {
|
|
55060
|
+
this.audioEpisodeTracks.delete(ref.episodeId);
|
|
55061
|
+
this.audioEpisodeLastPersistMs.delete(ref.episodeId);
|
|
55062
|
+
}
|
|
55063
|
+
}
|
|
54221
55064
|
async handleAudioResult(data) {
|
|
54222
55065
|
if (this.shuttingDown) return;
|
|
54223
55066
|
const { deviceId, frame } = data;
|
|
@@ -54261,48 +55104,27 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
54261
55104
|
});
|
|
54262
55105
|
return;
|
|
54263
55106
|
}
|
|
54264
|
-
|
|
54265
|
-
const sameClass = last?.className === route.className;
|
|
54266
|
-
const heartbeatDue = !last || timestamp - last.atMs >= AUDIO_EVENT_HEARTBEAT_MS;
|
|
54267
|
-
if (sameClass && !heartbeatDue) return;
|
|
54268
|
-
const ev = {
|
|
54269
|
-
id: (0, node_crypto.randomUUID)(),
|
|
55107
|
+
await this.driveAudioEpisode({
|
|
54270
55108
|
deviceId,
|
|
54271
55109
|
timestamp,
|
|
54272
|
-
|
|
55110
|
+
className: route.className,
|
|
55111
|
+
score: topClassification.score,
|
|
55112
|
+
originalClass: topClassification.debug?.originalClass,
|
|
54273
55113
|
rms: level?.rms ?? 0,
|
|
54274
55114
|
dbfs: level?.dbfs ?? 0,
|
|
54275
|
-
|
|
54276
|
-
className: topClassification.macroClass,
|
|
54277
|
-
...topClassification.debug?.originalClass !== void 0 ? { originalClass: topClassification.debug.originalClass } : {},
|
|
54278
|
-
score: topClassification.score
|
|
54279
|
-
}
|
|
54280
|
-
};
|
|
54281
|
-
this.lastAudioInsertByDevice.set(deviceId, {
|
|
54282
|
-
className: route.className,
|
|
54283
|
-
atMs: timestamp
|
|
54284
|
-
});
|
|
54285
|
-
await this.eventStore.insertAudio(ev);
|
|
54286
|
-
this.notificationCenter?.onAudioEventPersisted(ev);
|
|
54287
|
-
this.trackStore?.addAudioLabelEpisode(deviceId, route.className, topClassification.score, timestamp);
|
|
54288
|
-
this.ctx.eventBus.emit({
|
|
54289
|
-
id: `pa-${ev.id}`,
|
|
54290
|
-
timestamp: new Date(ev.timestamp),
|
|
54291
|
-
source: {
|
|
54292
|
-
type: "addon",
|
|
54293
|
-
id: "pipeline-analytics",
|
|
54294
|
-
addonId: "pipeline-analytics"
|
|
54295
|
-
},
|
|
54296
|
-
category: require_dist.EventCategory.PipelineAnalyticsDetectionEvent,
|
|
54297
|
-
data: {
|
|
54298
|
-
deviceId,
|
|
54299
|
-
kind: "audio",
|
|
54300
|
-
eventId: ev.id,
|
|
54301
|
-
timestamp: ev.timestamp
|
|
54302
|
-
}
|
|
55115
|
+
knobs: this.audioEpisodeKnobs(settings)
|
|
54303
55116
|
});
|
|
54304
55117
|
return;
|
|
54305
55118
|
}
|
|
55119
|
+
await this.driveAudioEpisode({
|
|
55120
|
+
deviceId,
|
|
55121
|
+
timestamp,
|
|
55122
|
+
className: null,
|
|
55123
|
+
score: 0,
|
|
55124
|
+
rms: level?.rms ?? 0,
|
|
55125
|
+
dbfs: level?.dbfs ?? 0,
|
|
55126
|
+
knobs: this.audioEpisodeKnobs(settings)
|
|
55127
|
+
});
|
|
54306
55128
|
const res = observeLevel(this.levelStateByDevice.get(deviceId) ?? emptyLevelState(), {
|
|
54307
55129
|
timestampMs: timestamp,
|
|
54308
55130
|
dbfs: level?.dbfs ?? -Infinity
|