@camstack/addon-post-analysis 1.2.167 → 1.2.169
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-DgOc1kH7.mjs → dist-Cubb8Avj.mjs} +48 -28
- package/dist/{dist-dGS8z89L.js → dist-DQVA7q-T.js} +59 -27
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CUasEI3s.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D74iweVp.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B3sU-393.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CD1z3U0p.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DnBMykUz.mjs → hostInit-BFi-TFDL.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +316 -28
- package/dist/pipeline-analytics/index.mjs +316 -28
- 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-DQVA7q-T.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");
|
|
@@ -3139,7 +3139,8 @@ var LlmVisionJudge = class {
|
|
|
3139
3139
|
prompt: call.prompt,
|
|
3140
3140
|
images,
|
|
3141
3141
|
jsonSchema: call.jsonSchema,
|
|
3142
|
-
temperature: 0
|
|
3142
|
+
temperature: 0,
|
|
3143
|
+
timeoutMs: call.timeoutMs
|
|
3143
3144
|
}).catch((err) => ({
|
|
3144
3145
|
ok: false,
|
|
3145
3146
|
message: String(err)
|
|
@@ -19705,8 +19706,37 @@ var TimelapseRenderError = class extends Error {
|
|
|
19705
19706
|
this.reason = reason;
|
|
19706
19707
|
}
|
|
19707
19708
|
};
|
|
19708
|
-
/**
|
|
19709
|
-
*
|
|
19709
|
+
/**
|
|
19710
|
+
* Profiles from cheapest to dearest, so the source is chosen by COST.
|
|
19711
|
+
*
|
|
19712
|
+
* `CamProfileSchema` is declared dearest-first; this is that order reversed.
|
|
19713
|
+
* A name the enum does not know sorts after all of them, alphabetically, so the
|
|
19714
|
+
* choice stays deterministic for a fleet with a bespoke profile.
|
|
19715
|
+
*/
|
|
19716
|
+
var PROFILE_BY_COST = [...require_dist.CamProfileSchema.options].reverse();
|
|
19717
|
+
function profileCost(profile) {
|
|
19718
|
+
const i = PROFILE_BY_COST.indexOf(profile);
|
|
19719
|
+
return i === -1 ? PROFILE_BY_COST.length : i;
|
|
19720
|
+
}
|
|
19721
|
+
/**
|
|
19722
|
+
* The CHEAPEST profile that has footage over the window.
|
|
19723
|
+
*
|
|
19724
|
+
* It used to be the profile with the MOST coverage, ties broken by name — and
|
|
19725
|
+
* that is a rule which picks `high` almost every time, because the high band is
|
|
19726
|
+
* the one that records continuously. On 2026-09-03 it did: a test render of a
|
|
19727
|
+
* ten-hour window chose `profile=high` at 97% coverage, which on this hub means
|
|
19728
|
+
* 4K read from a USB spinning disk. Decoding it made the machine stop answering
|
|
19729
|
+
* SSH and its own web UI for half an hour, and it had to be power-cycled.
|
|
19730
|
+
*
|
|
19731
|
+
* A timelapse is watched accelerated. Resolution buys nothing at 12 fps over a
|
|
19732
|
+
* night, and the cost difference between the bands is one to two orders of
|
|
19733
|
+
* magnitude in both bytes read and frames decoded. So there is no trade to
|
|
19734
|
+
* weigh: take the cheapest band that has anything, every time.
|
|
19735
|
+
*
|
|
19736
|
+
* Coverage no longer decides WHICH — only whether a band qualifies at all. A
|
|
19737
|
+
* band with less footage is still the right source; the gaps show as a shorter
|
|
19738
|
+
* video, not as a reason to read twenty times the bytes.
|
|
19739
|
+
*/
|
|
19710
19740
|
function pickProfile(ranges, window) {
|
|
19711
19741
|
const byProfile = /* @__PURE__ */ new Map();
|
|
19712
19742
|
for (const r of ranges) {
|
|
@@ -19714,15 +19744,15 @@ function pickProfile(ranges, window) {
|
|
|
19714
19744
|
if (list === void 0) byProfile.set(r.profile, [r]);
|
|
19715
19745
|
else list.push(r);
|
|
19716
19746
|
}
|
|
19717
|
-
|
|
19718
|
-
for (const [profile, rows] of
|
|
19747
|
+
const ordered = [...byProfile].sort((a, b) => profileCost(a[0]) - profileCost(b[0]) || a[0].localeCompare(b[0]));
|
|
19748
|
+
for (const [profile, rows] of ordered) {
|
|
19719
19749
|
const coverage = footageCoverage(rows, window.startMs, window.endMs);
|
|
19720
|
-
if (
|
|
19750
|
+
if (coverage.coveredMs > 0) return {
|
|
19721
19751
|
profile,
|
|
19722
19752
|
coverage
|
|
19723
19753
|
};
|
|
19724
19754
|
}
|
|
19725
|
-
return
|
|
19755
|
+
return null;
|
|
19726
19756
|
}
|
|
19727
19757
|
/**
|
|
19728
19758
|
* Is `record` the render this request is about to order?
|
|
@@ -27254,9 +27284,29 @@ async function renderTimelapseGif(ports, request) {
|
|
|
27254
27284
|
}
|
|
27255
27285
|
//#endregion
|
|
27256
27286
|
//#region src/shared/frame/square-subject-crop.ts
|
|
27257
|
-
/**
|
|
27258
|
-
*
|
|
27259
|
-
|
|
27287
|
+
/**
|
|
27288
|
+
* Expansion applied to the bbox long side to frame the subject with a little
|
|
27289
|
+
* breathing room.
|
|
27290
|
+
*
|
|
27291
|
+
* ×1.2 → ×1.05 (operator, 2026-09-03: "riduci e proviamo"). The complaint was
|
|
27292
|
+
* that a best shot carries too much surrounding scene, and the measurement says
|
|
27293
|
+
* where it goes. On 26 148 person samples from camera 617 the median bbox is
|
|
27294
|
+
* 1.44× taller than wide, so the square is driven by HEIGHT and half its width
|
|
27295
|
+
* is empty before the 16:9 extension adds any:
|
|
27296
|
+
*
|
|
27297
|
+
* subject inside the square 48% (waste 52%) ← this factor
|
|
27298
|
+
* square inside the 16:9 56% (waste 44%)
|
|
27299
|
+
* subject delivered 27%
|
|
27300
|
+
*
|
|
27301
|
+
* A prior analysis called the 16:9 extension the dominant waste. For a standing
|
|
27302
|
+
* person it is not: squaring costs more, because it pays for the height twice.
|
|
27303
|
+
* At ×1.05 the subject holds 63% of the square and ~35% of the canvas.
|
|
27304
|
+
*
|
|
27305
|
+
* The floor is 1.0, not lower: the square must CONTAIN the bbox, and detector
|
|
27306
|
+
* boxes hug the visible extent, so some margin is what keeps hair and feet in
|
|
27307
|
+
* shot when the box is a little tight.
|
|
27308
|
+
*/
|
|
27309
|
+
var SQUARE_SUBJECT_CROP_EXPANSION = 1.05;
|
|
27260
27310
|
/**
|
|
27261
27311
|
* Compute a SQUARE crop region in PIXEL space around the subject bbox.
|
|
27262
27312
|
*
|
|
@@ -33180,6 +33230,9 @@ function isDecisiveImprovement(current, candidate) {
|
|
|
33180
33230
|
if (!isSquareContained(candidate) && isSquareContained(current)) return false;
|
|
33181
33231
|
if (!isSemantic(candidate) && isSemantic(current)) return false;
|
|
33182
33232
|
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
33233
|
+
const currentSize = current.sizeScore;
|
|
33234
|
+
const candidateSize = candidate.sizeScore;
|
|
33235
|
+
if (currentSize !== void 0 && candidateSize !== void 0 && currentSize > 0 && candidateSize >= currentSize * 1.25) return true;
|
|
33183
33236
|
return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
|
|
33184
33237
|
}
|
|
33185
33238
|
/**
|
|
@@ -36626,19 +36679,19 @@ var BEST_FACE_POLICY = {
|
|
|
36626
36679
|
semanticAware: false
|
|
36627
36680
|
};
|
|
36628
36681
|
/**
|
|
36629
|
-
* Best OBJECT (CLIP) embedding + its tight crop —
|
|
36682
|
+
* Best OBJECT (CLIP) embedding + its tight crop — the in-memory
|
|
36630
36683
|
* DECISION (which live frame the embedding row + crop derive from). The
|
|
36631
36684
|
* durable, cross-restart "if better" gate stays DOWNSTREAM in
|
|
36632
36685
|
* `ObjectEmbeddingStore.upsertIfBetter` (the documented seam: this selector
|
|
36633
|
-
* resets on addon restart, the store row does not).
|
|
36634
|
-
*
|
|
36635
|
-
*
|
|
36686
|
+
* resets on addon restart, the store row does not). A SEPARATE named policy
|
|
36687
|
+
* from the face path: the two may drift apart deliberately without one
|
|
36688
|
+
* masquerading as the other.
|
|
36636
36689
|
*/
|
|
36637
36690
|
var BEST_OBJECT_EMBED_POLICY = {
|
|
36638
36691
|
hysteresis: 0,
|
|
36639
36692
|
minGapMs: 0,
|
|
36640
|
-
edgeAware:
|
|
36641
|
-
sizeAware:
|
|
36693
|
+
edgeAware: true,
|
|
36694
|
+
sizeAware: true,
|
|
36642
36695
|
semanticAware: false
|
|
36643
36696
|
};
|
|
36644
36697
|
/**
|
|
@@ -39437,6 +39490,12 @@ var TrackStore = class {
|
|
|
39437
39490
|
upsert(params) {
|
|
39438
39491
|
const existing = this.active.get(params.trackId);
|
|
39439
39492
|
if (existing) {
|
|
39493
|
+
if (params.matched === false) {
|
|
39494
|
+
existing.state = params.state;
|
|
39495
|
+
for (const z of params.zones) if (!existing.zonesVisited.includes(z)) existing.zonesVisited.push(z);
|
|
39496
|
+
if (!existing.classes.includes(params.className)) existing.classes.push(params.className);
|
|
39497
|
+
return existing;
|
|
39498
|
+
}
|
|
39440
39499
|
const last = existing.positions[existing.positions.length - 1];
|
|
39441
39500
|
const dist = last ? Math.sqrt((params.position.x - last.x) ** 2 + (params.position.y - last.y) ** 2) : 0;
|
|
39442
39501
|
existing.lastSeen = params.timestamp;
|
|
@@ -43985,6 +44044,36 @@ var AnalyticsQueryFacade = class {
|
|
|
43985
44044
|
//#region src/pipeline-analytics/audio-detection-settings.ts
|
|
43986
44045
|
var AudioDetectionSettingsSchema = require_dist.object({
|
|
43987
44046
|
classificationMinScore: require_dist.number().min(0).max(1).default(.6),
|
|
44047
|
+
/**
|
|
44048
|
+
* Macro classes allowed to produce an audio EVENT on this camera. Empty =
|
|
44049
|
+
* no policy, every class passes — so an upgrade changes nothing until an
|
|
44050
|
+
* operator states a policy.
|
|
44051
|
+
*
|
|
44052
|
+
* This belongs to the event generator, NOT to the classifier: the analyzer
|
|
44053
|
+
* keeps classifying everything it hears, `audioLabels` on the track keeps
|
|
44054
|
+
* carrying the full picture, and only the decision to MATERIALIZE an event
|
|
44055
|
+
* is narrowed. Raising a threshold inside the classifier would instead make
|
|
44056
|
+
* the sound invisible to every consumer at once.
|
|
44057
|
+
*/
|
|
44058
|
+
audioAllowedClasses: require_dist.array(require_dist.string()).default([]),
|
|
44059
|
+
/**
|
|
44060
|
+
* Corroboration window on the EVENT, per class: this many occurrences of
|
|
44061
|
+
* the same sound must land inside `audioConfirmWindowSec` before it becomes
|
|
44062
|
+
* an event. `1` = no window.
|
|
44063
|
+
*
|
|
44064
|
+
* Default 2-in-10s, set by the operator on 2026-09-03 after measuring 26
|
|
44065
|
+
* audio tracks in 24 h: the single-frame classification is the dominant
|
|
44066
|
+
* false positive, and a sound that is real is heard again within ten
|
|
44067
|
+
* seconds. A camera that wants every occurrence sets `1`.
|
|
44068
|
+
*
|
|
44069
|
+
* The Notification Center already has this exact brake for audio RULES
|
|
44070
|
+
* (`confirmHits` / `confirmWindowSec`). This is the same idea one level
|
|
44071
|
+
* earlier, so a camera can stop MAKING the single-frame events instead of
|
|
44072
|
+
* only declining to notify about them.
|
|
44073
|
+
*/
|
|
44074
|
+
audioConfirmHits: require_dist.number().int().min(1).default(2),
|
|
44075
|
+
/** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
|
|
44076
|
+
audioConfirmWindowSec: require_dist.number().positive().default(10),
|
|
43988
44077
|
levelDeviationDb: require_dist.number().positive().default(10),
|
|
43989
44078
|
levelWindowSec: require_dist.number().positive().default(10),
|
|
43990
44079
|
levelMaxWaitSec: require_dist.number().positive().default(20),
|
|
@@ -44025,8 +44114,17 @@ var AudioDetectionSettingsSchema = require_dist.object({
|
|
|
44025
44114
|
audioEpisodeOpenHoldSec: require_dist.number().nonnegative().default(1.5),
|
|
44026
44115
|
/** Class must be absent this long before the episode closes. */
|
|
44027
44116
|
audioEpisodeCloseGapSec: require_dist.number().positive().default(12),
|
|
44028
|
-
/** After close, the same class cannot reopen until this elapses.
|
|
44029
|
-
|
|
44117
|
+
/** After close, the same class cannot reopen until this elapses.
|
|
44118
|
+
*
|
|
44119
|
+
* This is the minimum spacing between two SYNTHETIC EVENTS (the episode's
|
|
44120
|
+
* event + timeline track) of the same sound on one camera — NOT a
|
|
44121
|
+
* notification floor. Notification spacing is the rule's own
|
|
44122
|
+
* `throttle.cooldownSec`, which already defaults to 60 s and is keyed
|
|
44123
|
+
* per-device; the two are independent brakes on different objects.
|
|
44124
|
+
*
|
|
44125
|
+
* Raised 30 → 60 s on 2026-09-03 by the operator, alongside the 2-in-10s
|
|
44126
|
+
* corroboration window above. */
|
|
44127
|
+
audioEpisodeCooldownSec: require_dist.number().nonnegative().default(60)
|
|
44030
44128
|
});
|
|
44031
44129
|
var AUDIO_DETECTION_DEFAULTS = AudioDetectionSettingsSchema.parse({});
|
|
44032
44130
|
/**
|
|
@@ -44203,14 +44301,45 @@ function observeAudioEpisode(state, sample, knobs) {
|
|
|
44203
44301
|
};
|
|
44204
44302
|
}
|
|
44205
44303
|
//#endregion
|
|
44304
|
+
//#region src/pipeline-analytics/audio-confirm-window.ts
|
|
44305
|
+
function emptyAudioConfirmState() {
|
|
44306
|
+
return { hitsByClass: /* @__PURE__ */ new Map() };
|
|
44307
|
+
}
|
|
44308
|
+
function observeAudioConfirm(state, input, cfg) {
|
|
44309
|
+
if (cfg.confirmHits <= 1) return {
|
|
44310
|
+
confirmed: true,
|
|
44311
|
+
nextState: state
|
|
44312
|
+
};
|
|
44313
|
+
const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
|
|
44314
|
+
const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
|
|
44315
|
+
const next = new Map(state.hitsByClass);
|
|
44316
|
+
if (kept.length < cfg.confirmHits) {
|
|
44317
|
+
next.set(input.className, kept);
|
|
44318
|
+
return {
|
|
44319
|
+
confirmed: false,
|
|
44320
|
+
nextState: { hitsByClass: next }
|
|
44321
|
+
};
|
|
44322
|
+
}
|
|
44323
|
+
next.delete(input.className);
|
|
44324
|
+
return {
|
|
44325
|
+
confirmed: true,
|
|
44326
|
+
nextState: { hitsByClass: next }
|
|
44327
|
+
};
|
|
44328
|
+
}
|
|
44329
|
+
//#endregion
|
|
44206
44330
|
//#region src/pipeline-analytics/audio-event-router.ts
|
|
44207
44331
|
/**
|
|
44208
44332
|
* Decide whether an audio frame is a (meaningful) classification event or a
|
|
44209
44333
|
* level event. `silence` is never a classification — it falls to the level
|
|
44210
44334
|
* path so only a genuine loudness peak/heartbeat can surface it.
|
|
44335
|
+
*
|
|
44336
|
+
* A class the operator did not allow takes the SAME route as a class that
|
|
44337
|
+
* failed the score: the level path. Both mean "not a classification event on
|
|
44338
|
+
* this camera", and giving the allowlist its own harder outcome (a silent
|
|
44339
|
+
* drop) would be a second arithmetic for one question.
|
|
44211
44340
|
*/
|
|
44212
44341
|
function classifyAudioFrame(top, cfg) {
|
|
44213
|
-
if (top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
|
|
44342
|
+
if ((cfg.allowedClasses.length === 0 || cfg.allowedClasses.includes(top?.className ?? "")) && top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
|
|
44214
44343
|
kind: "classification",
|
|
44215
44344
|
className: top.className,
|
|
44216
44345
|
score: top.score
|
|
@@ -59357,6 +59486,81 @@ function hasWrappedDetection(entries) {
|
|
|
59357
59486
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
59358
59487
|
}
|
|
59359
59488
|
//#endregion
|
|
59489
|
+
//#region src/pipeline-analytics/runtime/native-crop-dedupe.ts
|
|
59490
|
+
var NativeCropDedupe = class {
|
|
59491
|
+
config;
|
|
59492
|
+
entries = /* @__PURE__ */ new Map();
|
|
59493
|
+
fetched = 0;
|
|
59494
|
+
coalesced = 0;
|
|
59495
|
+
reused = 0;
|
|
59496
|
+
/**
|
|
59497
|
+
* What this actually saved, since the last read.
|
|
59498
|
+
*
|
|
59499
|
+
* Shipped WITH the optimisation, not after it: without a counter the only
|
|
59500
|
+
* evidence available is the process's RSS, which is allocator residue that
|
|
59501
|
+
* ratchets within a boot and cannot answer "did the dedupe fire". An
|
|
59502
|
+
* optimisation nobody can measure is a claim, not a fix.
|
|
59503
|
+
*/
|
|
59504
|
+
drainCounts() {
|
|
59505
|
+
const counts = {
|
|
59506
|
+
fetched: this.fetched,
|
|
59507
|
+
coalesced: this.coalesced,
|
|
59508
|
+
reused: this.reused
|
|
59509
|
+
};
|
|
59510
|
+
this.fetched = 0;
|
|
59511
|
+
this.coalesced = 0;
|
|
59512
|
+
this.reused = 0;
|
|
59513
|
+
return counts;
|
|
59514
|
+
}
|
|
59515
|
+
constructor(config) {
|
|
59516
|
+
this.config = config;
|
|
59517
|
+
}
|
|
59518
|
+
/** Retained entries — for the bound's own test, and for metrics. */
|
|
59519
|
+
size() {
|
|
59520
|
+
return this.entries.size;
|
|
59521
|
+
}
|
|
59522
|
+
async run(handle, region, maxWidth, fetch) {
|
|
59523
|
+
const key = `${handle.shmId}#${handle.slot}#${handle.seq}|${region.x},${region.y},${region.w},${region.h}|${maxWidth ?? "full"}`;
|
|
59524
|
+
const existing = this.entries.get(key);
|
|
59525
|
+
if (existing !== void 0) if (existing.value !== void 0 && existing.at !== void 0) {
|
|
59526
|
+
if (this.config.now() - existing.at < this.config.ttlMs) {
|
|
59527
|
+
this.reused += 1;
|
|
59528
|
+
return existing.value;
|
|
59529
|
+
}
|
|
59530
|
+
this.entries.delete(key);
|
|
59531
|
+
} else {
|
|
59532
|
+
this.coalesced += 1;
|
|
59533
|
+
return existing.pending;
|
|
59534
|
+
}
|
|
59535
|
+
this.fetched += 1;
|
|
59536
|
+
const pending = fetch();
|
|
59537
|
+
const entry = { pending };
|
|
59538
|
+
this.entries.set(key, entry);
|
|
59539
|
+
try {
|
|
59540
|
+
const result = await pending;
|
|
59541
|
+
if (result === null) {
|
|
59542
|
+
this.entries.delete(key);
|
|
59543
|
+
return null;
|
|
59544
|
+
}
|
|
59545
|
+
entry.value = result;
|
|
59546
|
+
entry.at = this.config.now();
|
|
59547
|
+
this.evictOverflow();
|
|
59548
|
+
return result;
|
|
59549
|
+
} catch (err) {
|
|
59550
|
+
this.entries.delete(key);
|
|
59551
|
+
throw err;
|
|
59552
|
+
}
|
|
59553
|
+
}
|
|
59554
|
+
/** Oldest-first, and only ever over the ceiling. */
|
|
59555
|
+
evictOverflow() {
|
|
59556
|
+
while (this.entries.size > this.config.maxEntries) {
|
|
59557
|
+
const oldest = this.entries.keys().next();
|
|
59558
|
+
if (oldest.done === true) return;
|
|
59559
|
+
this.entries.delete(oldest.value);
|
|
59560
|
+
}
|
|
59561
|
+
}
|
|
59562
|
+
};
|
|
59563
|
+
//#endregion
|
|
59360
59564
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
59361
59565
|
function createCaptureCrop(deps) {
|
|
59362
59566
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -59537,9 +59741,13 @@ function createNativeFrameTransport(deps) {
|
|
|
59537
59741
|
const now = Date.now();
|
|
59538
59742
|
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
59539
59743
|
lastCropMetricAt = now;
|
|
59744
|
+
const dedupe = cropDedupe.drainCounts();
|
|
59540
59745
|
cropMetricLogger.info("native-crop window", { meta: {
|
|
59541
59746
|
nativeHits,
|
|
59542
|
-
detectionFrameFallbacks: nativeFallbacks
|
|
59747
|
+
detectionFrameFallbacks: nativeFallbacks,
|
|
59748
|
+
wireFetches: dedupe.fetched,
|
|
59749
|
+
coalesced: dedupe.coalesced,
|
|
59750
|
+
reusedInWindow: dedupe.reused
|
|
59543
59751
|
} });
|
|
59544
59752
|
};
|
|
59545
59753
|
const requestNativeCrop = async (frameHandle, bbox, maxWidth, normalize) => {
|
|
@@ -59553,6 +59761,16 @@ function createNativeFrameTransport(deps) {
|
|
|
59553
59761
|
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
59554
59762
|
return await normalize(native);
|
|
59555
59763
|
};
|
|
59764
|
+
/**
|
|
59765
|
+
* Bounded and brief on purpose. Coalescing costs nothing; the 1.5 s window
|
|
59766
|
+
* holds at most 12 crops in THIS process to keep far more than that off
|
|
59767
|
+
* hub-main. It must never grow into the memory it was written to save.
|
|
59768
|
+
*/
|
|
59769
|
+
const cropDedupe = new NativeCropDedupe({
|
|
59770
|
+
now: () => Date.now(),
|
|
59771
|
+
ttlMs: 1500,
|
|
59772
|
+
maxEntries: 12
|
|
59773
|
+
});
|
|
59556
59774
|
const tryRequestNativeCrop = async (frameHandle, bbox, maxWidth, normalize) => {
|
|
59557
59775
|
try {
|
|
59558
59776
|
return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
|
|
@@ -59561,7 +59779,7 @@ function createNativeFrameTransport(deps) {
|
|
|
59561
59779
|
return null;
|
|
59562
59780
|
}
|
|
59563
59781
|
};
|
|
59564
|
-
const fetchNativeCropJpeg = async (frameHandle, paddedNorm, maxWidth) => tryRequestNativeCrop(frameHandle, paddedNorm, maxWidth, cropReplyToJpeg);
|
|
59782
|
+
const fetchNativeCropJpeg = async (frameHandle, paddedNorm, maxWidth) => cropDedupe.run(frameHandle, paddedNorm, maxWidth, () => tryRequestNativeCrop(frameHandle, paddedNorm, maxWidth, cropReplyToJpeg));
|
|
59565
59783
|
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
59566
59784
|
return (await fetchNativeCropJpeg(frameHandle, paddedNorm, maxWidth))?.jpeg ?? null;
|
|
59567
59785
|
};
|
|
@@ -61241,8 +61459,15 @@ var EventMediaDispatcher = class {
|
|
|
61241
61459
|
* OWN box burned onto the NATIVE full frame downscaled to
|
|
61242
61460
|
* {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
|
|
61243
61461
|
* detection frame (`frameData`) downscaled to the same cap with a `logger.warn`
|
|
61244
|
-
* — an honest LOWER-res frame, NEVER an upscale.
|
|
61245
|
-
*
|
|
61462
|
+
* — an honest LOWER-res frame, NEVER an upscale.
|
|
61463
|
+
*
|
|
61464
|
+
* Returns BOTH renderings of the same surface: the boxed tile the timeline
|
|
61465
|
+
* wants, and its CLEAN twin (`drawBoxedFrame(…, [])` — encode only, the same
|
|
61466
|
+
* call `keyFrame` uses). One extra encode, no second fetch and no second
|
|
61467
|
+
* decode, because the notification ladder needs a densely-written whole-scene
|
|
61468
|
+
* frame WITHOUT the rectangle and there was none: 19 of 38 measured
|
|
61469
|
+
* `frame:'full'` deliveries carried the box. `null` only if a sharp encode
|
|
61470
|
+
* throws.
|
|
61246
61471
|
*/
|
|
61247
61472
|
async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
|
|
61248
61473
|
let nf = null;
|
|
@@ -63076,6 +63301,36 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
63076
63301
|
step: .05,
|
|
63077
63302
|
default: AUDIO_DETECTION_DEFAULTS.classificationMinScore
|
|
63078
63303
|
},
|
|
63304
|
+
{
|
|
63305
|
+
type: "multiselect",
|
|
63306
|
+
key: "audioAllowedClasses",
|
|
63307
|
+
label: "Event classes",
|
|
63308
|
+
description: "Which sounds may become an event on this camera. Leave empty to allow every class. The classifier is untouched — it keeps hearing and labelling everything; this only narrows what is worth an event and a notification here.",
|
|
63309
|
+
options: require_dist.AUDIO_MACRO_LABELS.filter((l) => l.id !== "silence").map((l) => ({
|
|
63310
|
+
value: l.id,
|
|
63311
|
+
label: l.name
|
|
63312
|
+
})),
|
|
63313
|
+
default: AUDIO_DETECTION_DEFAULTS.audioAllowedClasses
|
|
63314
|
+
},
|
|
63315
|
+
{
|
|
63316
|
+
type: "number",
|
|
63317
|
+
key: "audioConfirmHits",
|
|
63318
|
+
label: "Occurrences to confirm",
|
|
63319
|
+
description: "How many times the same sound must be heard inside the window below before it becomes an event. 1 disables the window — every occurrence is an event.",
|
|
63320
|
+
min: 1,
|
|
63321
|
+
step: 1,
|
|
63322
|
+
default: AUDIO_DETECTION_DEFAULTS.audioConfirmHits
|
|
63323
|
+
},
|
|
63324
|
+
{
|
|
63325
|
+
type: "number",
|
|
63326
|
+
key: "audioConfirmWindowSec",
|
|
63327
|
+
label: "Confirmation window",
|
|
63328
|
+
description: "Width of the corroboration window. Occurrences are counted per class, so two different sounds never confirm each other. Ignored when occurrences is 1.",
|
|
63329
|
+
min: 1,
|
|
63330
|
+
step: 1,
|
|
63331
|
+
default: AUDIO_DETECTION_DEFAULTS.audioConfirmWindowSec,
|
|
63332
|
+
unit: "s"
|
|
63333
|
+
},
|
|
63079
63334
|
{
|
|
63080
63335
|
type: "number",
|
|
63081
63336
|
key: "levelDeviationDb",
|
|
@@ -65922,6 +66177,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65922
66177
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
65923
66178
|
lastMotionInsertByDevice = /* @__PURE__ */ new Map();
|
|
65924
66179
|
levelStateByDevice = /* @__PURE__ */ new Map();
|
|
66180
|
+
/** Per-device corroboration window for classified audio (see
|
|
66181
|
+
* `audio-confirm-window.ts`). Torn down wherever `levelStateByDevice` is —
|
|
66182
|
+
* a stale window on a removed device would confirm on its first frame back. */
|
|
66183
|
+
audioConfirmByDevice = /* @__PURE__ */ new Map();
|
|
65925
66184
|
/**
|
|
65926
66185
|
* Last ms a spatial track was alive on a device, any plane. Written on the
|
|
65927
66186
|
* frame path (one `Map.set` per processed frame), read O(1) by the audio-
|
|
@@ -67436,7 +67695,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
67436
67695
|
mimeType: i.mimeType
|
|
67437
67696
|
})),
|
|
67438
67697
|
jsonSchema: request.jsonSchema,
|
|
67439
|
-
temperature: request.temperature
|
|
67698
|
+
temperature: request.temperature,
|
|
67699
|
+
...request.timeoutMs !== void 0 ? { timeoutMs: request.timeoutMs } : {}
|
|
67440
67700
|
});
|
|
67441
67701
|
return result.ok ? {
|
|
67442
67702
|
ok: true,
|
|
@@ -68342,6 +68602,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68342
68602
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
68343
68603
|
this.forgetDeviceProcessors(data.deviceId);
|
|
68344
68604
|
this.levelStateByDevice.delete(data.deviceId);
|
|
68605
|
+
this.audioConfirmByDevice.delete(data.deviceId);
|
|
68345
68606
|
this.settingsCaches.clearDevice(data.deviceId);
|
|
68346
68607
|
this.rawDeviceBlobMirror.invalidate(data.deviceId);
|
|
68347
68608
|
}
|
|
@@ -68360,6 +68621,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68360
68621
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
68361
68622
|
this.forgetDeviceProcessors(deviceId);
|
|
68362
68623
|
this.levelStateByDevice.delete(deviceId);
|
|
68624
|
+
this.audioConfirmByDevice.delete(deviceId);
|
|
68363
68625
|
this.lastTrackActivityMs.delete(deviceId);
|
|
68364
68626
|
this.motionEventSnapshots?.forgetDevice(deviceId);
|
|
68365
68627
|
this.audioEpisodeByDevice.delete(deviceId);
|
|
@@ -68576,7 +68838,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68576
68838
|
mimeType: image.mimeType
|
|
68577
68839
|
})),
|
|
68578
68840
|
jsonSchema: i.jsonSchema,
|
|
68579
|
-
temperature: i.temperature
|
|
68841
|
+
temperature: i.temperature,
|
|
68842
|
+
...i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : {}
|
|
68580
68843
|
}),
|
|
68581
68844
|
cancelVision: async (requestId) => {
|
|
68582
68845
|
await api.llm.cancel.mutate({ requestId });
|
|
@@ -68871,6 +69134,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
68871
69134
|
this.nodeLiveness.clear();
|
|
68872
69135
|
this.gatedNodeByDevice.clear();
|
|
68873
69136
|
this.levelStateByDevice.clear();
|
|
69137
|
+
this.audioConfirmByDevice.clear();
|
|
68874
69138
|
this.settingsCaches.clearAll();
|
|
68875
69139
|
this.rawDeviceBlobMirror.invalidateAll();
|
|
68876
69140
|
this.rawGlobalStoreMirror.invalidate();
|
|
@@ -69038,6 +69302,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
69038
69302
|
trackId: t.trackId,
|
|
69039
69303
|
deviceId,
|
|
69040
69304
|
className: t.className,
|
|
69305
|
+
matched: t.matchedThisFrame !== false,
|
|
69041
69306
|
...t.labelPatch !== void 0 ? { labelPatch: t.labelPatch } : {},
|
|
69042
69307
|
timestamp: result.timestamp,
|
|
69043
69308
|
position: {
|
|
@@ -70885,12 +71150,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70885
71150
|
this.notificationCenter?.observeAudio(deviceId, {
|
|
70886
71151
|
timestampMs: timestamp,
|
|
70887
71152
|
...level?.dbfs !== void 0 ? { dbfs: level.dbfs } : {},
|
|
70888
|
-
labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass)
|
|
71153
|
+
labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass).filter((macroClass) => settings.audioAllowedClasses.length === 0 || settings.audioAllowedClasses.includes(macroClass))
|
|
70889
71154
|
});
|
|
70890
71155
|
const route = classifyAudioFrame(topClassification ? {
|
|
70891
71156
|
className: topClassification.macroClass,
|
|
70892
71157
|
score: topClassification.score
|
|
70893
|
-
} : void 0, {
|
|
71158
|
+
} : void 0, {
|
|
71159
|
+
classificationMinScore: settings.classificationMinScore,
|
|
71160
|
+
allowedClasses: settings.audioAllowedClasses
|
|
71161
|
+
});
|
|
70894
71162
|
if (route.kind === "classification") {
|
|
70895
71163
|
if (!topClassification) {
|
|
70896
71164
|
this.ctx.logger.warn("classifyAudioFrame returned classification but topClassification is null", {
|
|
@@ -70899,6 +71167,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
70899
71167
|
});
|
|
70900
71168
|
return;
|
|
70901
71169
|
}
|
|
71170
|
+
const confirm = observeAudioConfirm(this.audioConfirmByDevice.get(deviceId) ?? emptyAudioConfirmState(), {
|
|
71171
|
+
className: route.className,
|
|
71172
|
+
timestampMs: timestamp
|
|
71173
|
+
}, {
|
|
71174
|
+
confirmHits: settings.audioConfirmHits,
|
|
71175
|
+
confirmWindowSec: settings.audioConfirmWindowSec
|
|
71176
|
+
});
|
|
71177
|
+
this.audioConfirmByDevice.set(deviceId, confirm.nextState);
|
|
71178
|
+
if (!confirm.confirmed) {
|
|
71179
|
+
this.ctx.logger.debug("audio event held for corroboration", {
|
|
71180
|
+
tags: { deviceId },
|
|
71181
|
+
meta: {
|
|
71182
|
+
deviceId,
|
|
71183
|
+
className: route.className,
|
|
71184
|
+
confirmHits: settings.audioConfirmHits,
|
|
71185
|
+
confirmWindowSec: settings.audioConfirmWindowSec
|
|
71186
|
+
}
|
|
71187
|
+
});
|
|
71188
|
+
return;
|
|
71189
|
+
}
|
|
70902
71190
|
const classified = {
|
|
70903
71191
|
deviceId,
|
|
70904
71192
|
timestamp,
|