@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-Cubb8Avj.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -3132,7 +3132,8 @@ var LlmVisionJudge = class {
|
|
|
3132
3132
|
prompt: call.prompt,
|
|
3133
3133
|
images,
|
|
3134
3134
|
jsonSchema: call.jsonSchema,
|
|
3135
|
-
temperature: 0
|
|
3135
|
+
temperature: 0,
|
|
3136
|
+
timeoutMs: call.timeoutMs
|
|
3136
3137
|
}).catch((err) => ({
|
|
3137
3138
|
ok: false,
|
|
3138
3139
|
message: String(err)
|
|
@@ -19683,8 +19684,37 @@ var TimelapseRenderError = class extends Error {
|
|
|
19683
19684
|
this.reason = reason;
|
|
19684
19685
|
}
|
|
19685
19686
|
};
|
|
19686
|
-
/**
|
|
19687
|
-
*
|
|
19687
|
+
/**
|
|
19688
|
+
* Profiles from cheapest to dearest, so the source is chosen by COST.
|
|
19689
|
+
*
|
|
19690
|
+
* `CamProfileSchema` is declared dearest-first; this is that order reversed.
|
|
19691
|
+
* A name the enum does not know sorts after all of them, alphabetically, so the
|
|
19692
|
+
* choice stays deterministic for a fleet with a bespoke profile.
|
|
19693
|
+
*/
|
|
19694
|
+
var PROFILE_BY_COST = [...CamProfileSchema.options].reverse();
|
|
19695
|
+
function profileCost(profile) {
|
|
19696
|
+
const i = PROFILE_BY_COST.indexOf(profile);
|
|
19697
|
+
return i === -1 ? PROFILE_BY_COST.length : i;
|
|
19698
|
+
}
|
|
19699
|
+
/**
|
|
19700
|
+
* The CHEAPEST profile that has footage over the window.
|
|
19701
|
+
*
|
|
19702
|
+
* It used to be the profile with the MOST coverage, ties broken by name — and
|
|
19703
|
+
* that is a rule which picks `high` almost every time, because the high band is
|
|
19704
|
+
* the one that records continuously. On 2026-09-03 it did: a test render of a
|
|
19705
|
+
* ten-hour window chose `profile=high` at 97% coverage, which on this hub means
|
|
19706
|
+
* 4K read from a USB spinning disk. Decoding it made the machine stop answering
|
|
19707
|
+
* SSH and its own web UI for half an hour, and it had to be power-cycled.
|
|
19708
|
+
*
|
|
19709
|
+
* A timelapse is watched accelerated. Resolution buys nothing at 12 fps over a
|
|
19710
|
+
* night, and the cost difference between the bands is one to two orders of
|
|
19711
|
+
* magnitude in both bytes read and frames decoded. So there is no trade to
|
|
19712
|
+
* weigh: take the cheapest band that has anything, every time.
|
|
19713
|
+
*
|
|
19714
|
+
* Coverage no longer decides WHICH — only whether a band qualifies at all. A
|
|
19715
|
+
* band with less footage is still the right source; the gaps show as a shorter
|
|
19716
|
+
* video, not as a reason to read twenty times the bytes.
|
|
19717
|
+
*/
|
|
19688
19718
|
function pickProfile(ranges, window) {
|
|
19689
19719
|
const byProfile = /* @__PURE__ */ new Map();
|
|
19690
19720
|
for (const r of ranges) {
|
|
@@ -19692,15 +19722,15 @@ function pickProfile(ranges, window) {
|
|
|
19692
19722
|
if (list === void 0) byProfile.set(r.profile, [r]);
|
|
19693
19723
|
else list.push(r);
|
|
19694
19724
|
}
|
|
19695
|
-
|
|
19696
|
-
for (const [profile, rows] of
|
|
19725
|
+
const ordered = [...byProfile].sort((a, b) => profileCost(a[0]) - profileCost(b[0]) || a[0].localeCompare(b[0]));
|
|
19726
|
+
for (const [profile, rows] of ordered) {
|
|
19697
19727
|
const coverage = footageCoverage(rows, window.startMs, window.endMs);
|
|
19698
|
-
if (
|
|
19728
|
+
if (coverage.coveredMs > 0) return {
|
|
19699
19729
|
profile,
|
|
19700
19730
|
coverage
|
|
19701
19731
|
};
|
|
19702
19732
|
}
|
|
19703
|
-
return
|
|
19733
|
+
return null;
|
|
19704
19734
|
}
|
|
19705
19735
|
/**
|
|
19706
19736
|
* Is `record` the render this request is about to order?
|
|
@@ -27232,9 +27262,29 @@ async function renderTimelapseGif(ports, request) {
|
|
|
27232
27262
|
}
|
|
27233
27263
|
//#endregion
|
|
27234
27264
|
//#region src/shared/frame/square-subject-crop.ts
|
|
27235
|
-
/**
|
|
27236
|
-
*
|
|
27237
|
-
|
|
27265
|
+
/**
|
|
27266
|
+
* Expansion applied to the bbox long side to frame the subject with a little
|
|
27267
|
+
* breathing room.
|
|
27268
|
+
*
|
|
27269
|
+
* ×1.2 → ×1.05 (operator, 2026-09-03: "riduci e proviamo"). The complaint was
|
|
27270
|
+
* that a best shot carries too much surrounding scene, and the measurement says
|
|
27271
|
+
* where it goes. On 26 148 person samples from camera 617 the median bbox is
|
|
27272
|
+
* 1.44× taller than wide, so the square is driven by HEIGHT and half its width
|
|
27273
|
+
* is empty before the 16:9 extension adds any:
|
|
27274
|
+
*
|
|
27275
|
+
* subject inside the square 48% (waste 52%) ← this factor
|
|
27276
|
+
* square inside the 16:9 56% (waste 44%)
|
|
27277
|
+
* subject delivered 27%
|
|
27278
|
+
*
|
|
27279
|
+
* A prior analysis called the 16:9 extension the dominant waste. For a standing
|
|
27280
|
+
* person it is not: squaring costs more, because it pays for the height twice.
|
|
27281
|
+
* At ×1.05 the subject holds 63% of the square and ~35% of the canvas.
|
|
27282
|
+
*
|
|
27283
|
+
* The floor is 1.0, not lower: the square must CONTAIN the bbox, and detector
|
|
27284
|
+
* boxes hug the visible extent, so some margin is what keeps hair and feet in
|
|
27285
|
+
* shot when the box is a little tight.
|
|
27286
|
+
*/
|
|
27287
|
+
var SQUARE_SUBJECT_CROP_EXPANSION = 1.05;
|
|
27238
27288
|
/**
|
|
27239
27289
|
* Compute a SQUARE crop region in PIXEL space around the subject bbox.
|
|
27240
27290
|
*
|
|
@@ -33155,6 +33205,9 @@ function isDecisiveImprovement(current, candidate) {
|
|
|
33155
33205
|
if (!isSquareContained(candidate) && isSquareContained(current)) return false;
|
|
33156
33206
|
if (!isSemantic(candidate) && isSemantic(current)) return false;
|
|
33157
33207
|
if (!candidate.edgeClear && current.edgeClear) return false;
|
|
33208
|
+
const currentSize = current.sizeScore;
|
|
33209
|
+
const candidateSize = candidate.sizeScore;
|
|
33210
|
+
if (currentSize !== void 0 && candidateSize !== void 0 && currentSize > 0 && candidateSize >= currentSize * 1.25) return true;
|
|
33158
33211
|
return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
|
|
33159
33212
|
}
|
|
33160
33213
|
/**
|
|
@@ -36601,19 +36654,19 @@ var BEST_FACE_POLICY = {
|
|
|
36601
36654
|
semanticAware: false
|
|
36602
36655
|
};
|
|
36603
36656
|
/**
|
|
36604
|
-
* Best OBJECT (CLIP) embedding + its tight crop —
|
|
36657
|
+
* Best OBJECT (CLIP) embedding + its tight crop — the in-memory
|
|
36605
36658
|
* DECISION (which live frame the embedding row + crop derive from). The
|
|
36606
36659
|
* durable, cross-restart "if better" gate stays DOWNSTREAM in
|
|
36607
36660
|
* `ObjectEmbeddingStore.upsertIfBetter` (the documented seam: this selector
|
|
36608
|
-
* resets on addon restart, the store row does not).
|
|
36609
|
-
*
|
|
36610
|
-
*
|
|
36661
|
+
* resets on addon restart, the store row does not). A SEPARATE named policy
|
|
36662
|
+
* from the face path: the two may drift apart deliberately without one
|
|
36663
|
+
* masquerading as the other.
|
|
36611
36664
|
*/
|
|
36612
36665
|
var BEST_OBJECT_EMBED_POLICY = {
|
|
36613
36666
|
hysteresis: 0,
|
|
36614
36667
|
minGapMs: 0,
|
|
36615
|
-
edgeAware:
|
|
36616
|
-
sizeAware:
|
|
36668
|
+
edgeAware: true,
|
|
36669
|
+
sizeAware: true,
|
|
36617
36670
|
semanticAware: false
|
|
36618
36671
|
};
|
|
36619
36672
|
/**
|
|
@@ -39412,6 +39465,12 @@ var TrackStore = class {
|
|
|
39412
39465
|
upsert(params) {
|
|
39413
39466
|
const existing = this.active.get(params.trackId);
|
|
39414
39467
|
if (existing) {
|
|
39468
|
+
if (params.matched === false) {
|
|
39469
|
+
existing.state = params.state;
|
|
39470
|
+
for (const z of params.zones) if (!existing.zonesVisited.includes(z)) existing.zonesVisited.push(z);
|
|
39471
|
+
if (!existing.classes.includes(params.className)) existing.classes.push(params.className);
|
|
39472
|
+
return existing;
|
|
39473
|
+
}
|
|
39415
39474
|
const last = existing.positions[existing.positions.length - 1];
|
|
39416
39475
|
const dist = last ? Math.sqrt((params.position.x - last.x) ** 2 + (params.position.y - last.y) ** 2) : 0;
|
|
39417
39476
|
existing.lastSeen = params.timestamp;
|
|
@@ -43911,6 +43970,36 @@ var AnalyticsQueryFacade = class {
|
|
|
43911
43970
|
//#region src/pipeline-analytics/audio-detection-settings.ts
|
|
43912
43971
|
var AudioDetectionSettingsSchema = object({
|
|
43913
43972
|
classificationMinScore: number().min(0).max(1).default(.6),
|
|
43973
|
+
/**
|
|
43974
|
+
* Macro classes allowed to produce an audio EVENT on this camera. Empty =
|
|
43975
|
+
* no policy, every class passes — so an upgrade changes nothing until an
|
|
43976
|
+
* operator states a policy.
|
|
43977
|
+
*
|
|
43978
|
+
* This belongs to the event generator, NOT to the classifier: the analyzer
|
|
43979
|
+
* keeps classifying everything it hears, `audioLabels` on the track keeps
|
|
43980
|
+
* carrying the full picture, and only the decision to MATERIALIZE an event
|
|
43981
|
+
* is narrowed. Raising a threshold inside the classifier would instead make
|
|
43982
|
+
* the sound invisible to every consumer at once.
|
|
43983
|
+
*/
|
|
43984
|
+
audioAllowedClasses: array(string()).default([]),
|
|
43985
|
+
/**
|
|
43986
|
+
* Corroboration window on the EVENT, per class: this many occurrences of
|
|
43987
|
+
* the same sound must land inside `audioConfirmWindowSec` before it becomes
|
|
43988
|
+
* an event. `1` = no window.
|
|
43989
|
+
*
|
|
43990
|
+
* Default 2-in-10s, set by the operator on 2026-09-03 after measuring 26
|
|
43991
|
+
* audio tracks in 24 h: the single-frame classification is the dominant
|
|
43992
|
+
* false positive, and a sound that is real is heard again within ten
|
|
43993
|
+
* seconds. A camera that wants every occurrence sets `1`.
|
|
43994
|
+
*
|
|
43995
|
+
* The Notification Center already has this exact brake for audio RULES
|
|
43996
|
+
* (`confirmHits` / `confirmWindowSec`). This is the same idea one level
|
|
43997
|
+
* earlier, so a camera can stop MAKING the single-frame events instead of
|
|
43998
|
+
* only declining to notify about them.
|
|
43999
|
+
*/
|
|
44000
|
+
audioConfirmHits: number().int().min(1).default(2),
|
|
44001
|
+
/** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
|
|
44002
|
+
audioConfirmWindowSec: number().positive().default(10),
|
|
43914
44003
|
levelDeviationDb: number().positive().default(10),
|
|
43915
44004
|
levelWindowSec: number().positive().default(10),
|
|
43916
44005
|
levelMaxWaitSec: number().positive().default(20),
|
|
@@ -43951,8 +44040,17 @@ var AudioDetectionSettingsSchema = object({
|
|
|
43951
44040
|
audioEpisodeOpenHoldSec: number().nonnegative().default(1.5),
|
|
43952
44041
|
/** Class must be absent this long before the episode closes. */
|
|
43953
44042
|
audioEpisodeCloseGapSec: number().positive().default(12),
|
|
43954
|
-
/** After close, the same class cannot reopen until this elapses.
|
|
43955
|
-
|
|
44043
|
+
/** After close, the same class cannot reopen until this elapses.
|
|
44044
|
+
*
|
|
44045
|
+
* This is the minimum spacing between two SYNTHETIC EVENTS (the episode's
|
|
44046
|
+
* event + timeline track) of the same sound on one camera — NOT a
|
|
44047
|
+
* notification floor. Notification spacing is the rule's own
|
|
44048
|
+
* `throttle.cooldownSec`, which already defaults to 60 s and is keyed
|
|
44049
|
+
* per-device; the two are independent brakes on different objects.
|
|
44050
|
+
*
|
|
44051
|
+
* Raised 30 → 60 s on 2026-09-03 by the operator, alongside the 2-in-10s
|
|
44052
|
+
* corroboration window above. */
|
|
44053
|
+
audioEpisodeCooldownSec: number().nonnegative().default(60)
|
|
43956
44054
|
});
|
|
43957
44055
|
var AUDIO_DETECTION_DEFAULTS = AudioDetectionSettingsSchema.parse({});
|
|
43958
44056
|
/**
|
|
@@ -44129,14 +44227,45 @@ function observeAudioEpisode(state, sample, knobs) {
|
|
|
44129
44227
|
};
|
|
44130
44228
|
}
|
|
44131
44229
|
//#endregion
|
|
44230
|
+
//#region src/pipeline-analytics/audio-confirm-window.ts
|
|
44231
|
+
function emptyAudioConfirmState() {
|
|
44232
|
+
return { hitsByClass: /* @__PURE__ */ new Map() };
|
|
44233
|
+
}
|
|
44234
|
+
function observeAudioConfirm(state, input, cfg) {
|
|
44235
|
+
if (cfg.confirmHits <= 1) return {
|
|
44236
|
+
confirmed: true,
|
|
44237
|
+
nextState: state
|
|
44238
|
+
};
|
|
44239
|
+
const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
|
|
44240
|
+
const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
|
|
44241
|
+
const next = new Map(state.hitsByClass);
|
|
44242
|
+
if (kept.length < cfg.confirmHits) {
|
|
44243
|
+
next.set(input.className, kept);
|
|
44244
|
+
return {
|
|
44245
|
+
confirmed: false,
|
|
44246
|
+
nextState: { hitsByClass: next }
|
|
44247
|
+
};
|
|
44248
|
+
}
|
|
44249
|
+
next.delete(input.className);
|
|
44250
|
+
return {
|
|
44251
|
+
confirmed: true,
|
|
44252
|
+
nextState: { hitsByClass: next }
|
|
44253
|
+
};
|
|
44254
|
+
}
|
|
44255
|
+
//#endregion
|
|
44132
44256
|
//#region src/pipeline-analytics/audio-event-router.ts
|
|
44133
44257
|
/**
|
|
44134
44258
|
* Decide whether an audio frame is a (meaningful) classification event or a
|
|
44135
44259
|
* level event. `silence` is never a classification — it falls to the level
|
|
44136
44260
|
* path so only a genuine loudness peak/heartbeat can surface it.
|
|
44261
|
+
*
|
|
44262
|
+
* A class the operator did not allow takes the SAME route as a class that
|
|
44263
|
+
* failed the score: the level path. Both mean "not a classification event on
|
|
44264
|
+
* this camera", and giving the allowlist its own harder outcome (a silent
|
|
44265
|
+
* drop) would be a second arithmetic for one question.
|
|
44137
44266
|
*/
|
|
44138
44267
|
function classifyAudioFrame(top, cfg) {
|
|
44139
|
-
if (top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
|
|
44268
|
+
if ((cfg.allowedClasses.length === 0 || cfg.allowedClasses.includes(top?.className ?? "")) && top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
|
|
44140
44269
|
kind: "classification",
|
|
44141
44270
|
className: top.className,
|
|
44142
44271
|
score: top.score
|
|
@@ -59283,6 +59412,81 @@ function hasWrappedDetection(entries) {
|
|
|
59283
59412
|
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
59284
59413
|
}
|
|
59285
59414
|
//#endregion
|
|
59415
|
+
//#region src/pipeline-analytics/runtime/native-crop-dedupe.ts
|
|
59416
|
+
var NativeCropDedupe = class {
|
|
59417
|
+
config;
|
|
59418
|
+
entries = /* @__PURE__ */ new Map();
|
|
59419
|
+
fetched = 0;
|
|
59420
|
+
coalesced = 0;
|
|
59421
|
+
reused = 0;
|
|
59422
|
+
/**
|
|
59423
|
+
* What this actually saved, since the last read.
|
|
59424
|
+
*
|
|
59425
|
+
* Shipped WITH the optimisation, not after it: without a counter the only
|
|
59426
|
+
* evidence available is the process's RSS, which is allocator residue that
|
|
59427
|
+
* ratchets within a boot and cannot answer "did the dedupe fire". An
|
|
59428
|
+
* optimisation nobody can measure is a claim, not a fix.
|
|
59429
|
+
*/
|
|
59430
|
+
drainCounts() {
|
|
59431
|
+
const counts = {
|
|
59432
|
+
fetched: this.fetched,
|
|
59433
|
+
coalesced: this.coalesced,
|
|
59434
|
+
reused: this.reused
|
|
59435
|
+
};
|
|
59436
|
+
this.fetched = 0;
|
|
59437
|
+
this.coalesced = 0;
|
|
59438
|
+
this.reused = 0;
|
|
59439
|
+
return counts;
|
|
59440
|
+
}
|
|
59441
|
+
constructor(config) {
|
|
59442
|
+
this.config = config;
|
|
59443
|
+
}
|
|
59444
|
+
/** Retained entries — for the bound's own test, and for metrics. */
|
|
59445
|
+
size() {
|
|
59446
|
+
return this.entries.size;
|
|
59447
|
+
}
|
|
59448
|
+
async run(handle, region, maxWidth, fetch) {
|
|
59449
|
+
const key = `${handle.shmId}#${handle.slot}#${handle.seq}|${region.x},${region.y},${region.w},${region.h}|${maxWidth ?? "full"}`;
|
|
59450
|
+
const existing = this.entries.get(key);
|
|
59451
|
+
if (existing !== void 0) if (existing.value !== void 0 && existing.at !== void 0) {
|
|
59452
|
+
if (this.config.now() - existing.at < this.config.ttlMs) {
|
|
59453
|
+
this.reused += 1;
|
|
59454
|
+
return existing.value;
|
|
59455
|
+
}
|
|
59456
|
+
this.entries.delete(key);
|
|
59457
|
+
} else {
|
|
59458
|
+
this.coalesced += 1;
|
|
59459
|
+
return existing.pending;
|
|
59460
|
+
}
|
|
59461
|
+
this.fetched += 1;
|
|
59462
|
+
const pending = fetch();
|
|
59463
|
+
const entry = { pending };
|
|
59464
|
+
this.entries.set(key, entry);
|
|
59465
|
+
try {
|
|
59466
|
+
const result = await pending;
|
|
59467
|
+
if (result === null) {
|
|
59468
|
+
this.entries.delete(key);
|
|
59469
|
+
return null;
|
|
59470
|
+
}
|
|
59471
|
+
entry.value = result;
|
|
59472
|
+
entry.at = this.config.now();
|
|
59473
|
+
this.evictOverflow();
|
|
59474
|
+
return result;
|
|
59475
|
+
} catch (err) {
|
|
59476
|
+
this.entries.delete(key);
|
|
59477
|
+
throw err;
|
|
59478
|
+
}
|
|
59479
|
+
}
|
|
59480
|
+
/** Oldest-first, and only ever over the ceiling. */
|
|
59481
|
+
evictOverflow() {
|
|
59482
|
+
while (this.entries.size > this.config.maxEntries) {
|
|
59483
|
+
const oldest = this.entries.keys().next();
|
|
59484
|
+
if (oldest.done === true) return;
|
|
59485
|
+
this.entries.delete(oldest.value);
|
|
59486
|
+
}
|
|
59487
|
+
}
|
|
59488
|
+
};
|
|
59489
|
+
//#endregion
|
|
59286
59490
|
//#region src/pipeline-analytics/pipeline/capture-crop.ts
|
|
59287
59491
|
function createCaptureCrop(deps) {
|
|
59288
59492
|
return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
|
|
@@ -59463,9 +59667,13 @@ function createNativeFrameTransport(deps) {
|
|
|
59463
59667
|
const now = Date.now();
|
|
59464
59668
|
if (now - lastCropMetricAt < NATIVE_CROP_METRIC_INTERVAL_MS) return;
|
|
59465
59669
|
lastCropMetricAt = now;
|
|
59670
|
+
const dedupe = cropDedupe.drainCounts();
|
|
59466
59671
|
cropMetricLogger.info("native-crop window", { meta: {
|
|
59467
59672
|
nativeHits,
|
|
59468
|
-
detectionFrameFallbacks: nativeFallbacks
|
|
59673
|
+
detectionFrameFallbacks: nativeFallbacks,
|
|
59674
|
+
wireFetches: dedupe.fetched,
|
|
59675
|
+
coalesced: dedupe.coalesced,
|
|
59676
|
+
reusedInWindow: dedupe.reused
|
|
59469
59677
|
} });
|
|
59470
59678
|
};
|
|
59471
59679
|
const requestNativeCrop = async (frameHandle, bbox, maxWidth, normalize) => {
|
|
@@ -59479,6 +59687,16 @@ function createNativeFrameTransport(deps) {
|
|
|
59479
59687
|
if (!native || native.width <= 0 || native.height <= 0) return null;
|
|
59480
59688
|
return await normalize(native);
|
|
59481
59689
|
};
|
|
59690
|
+
/**
|
|
59691
|
+
* Bounded and brief on purpose. Coalescing costs nothing; the 1.5 s window
|
|
59692
|
+
* holds at most 12 crops in THIS process to keep far more than that off
|
|
59693
|
+
* hub-main. It must never grow into the memory it was written to save.
|
|
59694
|
+
*/
|
|
59695
|
+
const cropDedupe = new NativeCropDedupe({
|
|
59696
|
+
now: () => Date.now(),
|
|
59697
|
+
ttlMs: 1500,
|
|
59698
|
+
maxEntries: 12
|
|
59699
|
+
});
|
|
59482
59700
|
const tryRequestNativeCrop = async (frameHandle, bbox, maxWidth, normalize) => {
|
|
59483
59701
|
try {
|
|
59484
59702
|
return await requestNativeCrop(frameHandle, bbox, maxWidth, normalize);
|
|
@@ -59487,7 +59705,7 @@ function createNativeFrameTransport(deps) {
|
|
|
59487
59705
|
return null;
|
|
59488
59706
|
}
|
|
59489
59707
|
};
|
|
59490
|
-
const fetchNativeCropJpeg = async (frameHandle, paddedNorm, maxWidth) => tryRequestNativeCrop(frameHandle, paddedNorm, maxWidth, cropReplyToJpeg);
|
|
59708
|
+
const fetchNativeCropJpeg = async (frameHandle, paddedNorm, maxWidth) => cropDedupe.run(frameHandle, paddedNorm, maxWidth, () => tryRequestNativeCrop(frameHandle, paddedNorm, maxWidth, cropReplyToJpeg));
|
|
59491
59709
|
const tryNativeCrop = async (frameHandle, paddedNorm, maxWidth) => {
|
|
59492
59710
|
return (await fetchNativeCropJpeg(frameHandle, paddedNorm, maxWidth))?.jpeg ?? null;
|
|
59493
59711
|
};
|
|
@@ -61167,8 +61385,15 @@ var EventMediaDispatcher = class {
|
|
|
61167
61385
|
* OWN box burned onto the NATIVE full frame downscaled to
|
|
61168
61386
|
* {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
|
|
61169
61387
|
* detection frame (`frameData`) downscaled to the same cap with a `logger.warn`
|
|
61170
|
-
* — an honest LOWER-res frame, NEVER an upscale.
|
|
61171
|
-
*
|
|
61388
|
+
* — an honest LOWER-res frame, NEVER an upscale.
|
|
61389
|
+
*
|
|
61390
|
+
* Returns BOTH renderings of the same surface: the boxed tile the timeline
|
|
61391
|
+
* wants, and its CLEAN twin (`drawBoxedFrame(…, [])` — encode only, the same
|
|
61392
|
+
* call `keyFrame` uses). One extra encode, no second fetch and no second
|
|
61393
|
+
* decode, because the notification ladder needs a densely-written whole-scene
|
|
61394
|
+
* frame WITHOUT the rectangle and there was none: 19 of 38 measured
|
|
61395
|
+
* `frame:'full'` deliveries carried the box. `null` only if a sharp encode
|
|
61396
|
+
* throws.
|
|
61172
61397
|
*/
|
|
61173
61398
|
async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
|
|
61174
61399
|
let nf = null;
|
|
@@ -63002,6 +63227,36 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
63002
63227
|
step: .05,
|
|
63003
63228
|
default: AUDIO_DETECTION_DEFAULTS.classificationMinScore
|
|
63004
63229
|
},
|
|
63230
|
+
{
|
|
63231
|
+
type: "multiselect",
|
|
63232
|
+
key: "audioAllowedClasses",
|
|
63233
|
+
label: "Event classes",
|
|
63234
|
+
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.",
|
|
63235
|
+
options: AUDIO_MACRO_LABELS.filter((l) => l.id !== "silence").map((l) => ({
|
|
63236
|
+
value: l.id,
|
|
63237
|
+
label: l.name
|
|
63238
|
+
})),
|
|
63239
|
+
default: AUDIO_DETECTION_DEFAULTS.audioAllowedClasses
|
|
63240
|
+
},
|
|
63241
|
+
{
|
|
63242
|
+
type: "number",
|
|
63243
|
+
key: "audioConfirmHits",
|
|
63244
|
+
label: "Occurrences to confirm",
|
|
63245
|
+
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.",
|
|
63246
|
+
min: 1,
|
|
63247
|
+
step: 1,
|
|
63248
|
+
default: AUDIO_DETECTION_DEFAULTS.audioConfirmHits
|
|
63249
|
+
},
|
|
63250
|
+
{
|
|
63251
|
+
type: "number",
|
|
63252
|
+
key: "audioConfirmWindowSec",
|
|
63253
|
+
label: "Confirmation window",
|
|
63254
|
+
description: "Width of the corroboration window. Occurrences are counted per class, so two different sounds never confirm each other. Ignored when occurrences is 1.",
|
|
63255
|
+
min: 1,
|
|
63256
|
+
step: 1,
|
|
63257
|
+
default: AUDIO_DETECTION_DEFAULTS.audioConfirmWindowSec,
|
|
63258
|
+
unit: "s"
|
|
63259
|
+
},
|
|
63005
63260
|
{
|
|
63006
63261
|
type: "number",
|
|
63007
63262
|
key: "levelDeviationDb",
|
|
@@ -65848,6 +66103,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
65848
66103
|
lastAudioInsertByDevice = /* @__PURE__ */ new Map();
|
|
65849
66104
|
lastMotionInsertByDevice = /* @__PURE__ */ new Map();
|
|
65850
66105
|
levelStateByDevice = /* @__PURE__ */ new Map();
|
|
66106
|
+
/** Per-device corroboration window for classified audio (see
|
|
66107
|
+
* `audio-confirm-window.ts`). Torn down wherever `levelStateByDevice` is —
|
|
66108
|
+
* a stale window on a removed device would confirm on its first frame back. */
|
|
66109
|
+
audioConfirmByDevice = /* @__PURE__ */ new Map();
|
|
65851
66110
|
/**
|
|
65852
66111
|
* Last ms a spatial track was alive on a device, any plane. Written on the
|
|
65853
66112
|
* frame path (one `Map.set` per processed frame), read O(1) by the audio-
|
|
@@ -67362,7 +67621,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
67362
67621
|
mimeType: i.mimeType
|
|
67363
67622
|
})),
|
|
67364
67623
|
jsonSchema: request.jsonSchema,
|
|
67365
|
-
temperature: request.temperature
|
|
67624
|
+
temperature: request.temperature,
|
|
67625
|
+
...request.timeoutMs !== void 0 ? { timeoutMs: request.timeoutMs } : {}
|
|
67366
67626
|
});
|
|
67367
67627
|
return result.ok ? {
|
|
67368
67628
|
ok: true,
|
|
@@ -68268,6 +68528,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
68268
68528
|
this.overlaySynthesisWarnAt.delete(data.deviceId);
|
|
68269
68529
|
this.forgetDeviceProcessors(data.deviceId);
|
|
68270
68530
|
this.levelStateByDevice.delete(data.deviceId);
|
|
68531
|
+
this.audioConfirmByDevice.delete(data.deviceId);
|
|
68271
68532
|
this.settingsCaches.clearDevice(data.deviceId);
|
|
68272
68533
|
this.rawDeviceBlobMirror.invalidate(data.deviceId);
|
|
68273
68534
|
}
|
|
@@ -68286,6 +68547,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
68286
68547
|
this.overlaySynthesisWarnAt.delete(deviceId);
|
|
68287
68548
|
this.forgetDeviceProcessors(deviceId);
|
|
68288
68549
|
this.levelStateByDevice.delete(deviceId);
|
|
68550
|
+
this.audioConfirmByDevice.delete(deviceId);
|
|
68289
68551
|
this.lastTrackActivityMs.delete(deviceId);
|
|
68290
68552
|
this.motionEventSnapshots?.forgetDevice(deviceId);
|
|
68291
68553
|
this.audioEpisodeByDevice.delete(deviceId);
|
|
@@ -68502,7 +68764,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
68502
68764
|
mimeType: image.mimeType
|
|
68503
68765
|
})),
|
|
68504
68766
|
jsonSchema: i.jsonSchema,
|
|
68505
|
-
temperature: i.temperature
|
|
68767
|
+
temperature: i.temperature,
|
|
68768
|
+
...i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : {}
|
|
68506
68769
|
}),
|
|
68507
68770
|
cancelVision: async (requestId) => {
|
|
68508
68771
|
await api.llm.cancel.mutate({ requestId });
|
|
@@ -68797,6 +69060,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
68797
69060
|
this.nodeLiveness.clear();
|
|
68798
69061
|
this.gatedNodeByDevice.clear();
|
|
68799
69062
|
this.levelStateByDevice.clear();
|
|
69063
|
+
this.audioConfirmByDevice.clear();
|
|
68800
69064
|
this.settingsCaches.clearAll();
|
|
68801
69065
|
this.rawDeviceBlobMirror.invalidateAll();
|
|
68802
69066
|
this.rawGlobalStoreMirror.invalidate();
|
|
@@ -68964,6 +69228,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
68964
69228
|
trackId: t.trackId,
|
|
68965
69229
|
deviceId,
|
|
68966
69230
|
className: t.className,
|
|
69231
|
+
matched: t.matchedThisFrame !== false,
|
|
68967
69232
|
...t.labelPatch !== void 0 ? { labelPatch: t.labelPatch } : {},
|
|
68968
69233
|
timestamp: result.timestamp,
|
|
68969
69234
|
position: {
|
|
@@ -70811,12 +71076,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
70811
71076
|
this.notificationCenter?.observeAudio(deviceId, {
|
|
70812
71077
|
timestampMs: timestamp,
|
|
70813
71078
|
...level?.dbfs !== void 0 ? { dbfs: level.dbfs } : {},
|
|
70814
|
-
labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass)
|
|
71079
|
+
labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass).filter((macroClass) => settings.audioAllowedClasses.length === 0 || settings.audioAllowedClasses.includes(macroClass))
|
|
70815
71080
|
});
|
|
70816
71081
|
const route = classifyAudioFrame(topClassification ? {
|
|
70817
71082
|
className: topClassification.macroClass,
|
|
70818
71083
|
score: topClassification.score
|
|
70819
|
-
} : void 0, {
|
|
71084
|
+
} : void 0, {
|
|
71085
|
+
classificationMinScore: settings.classificationMinScore,
|
|
71086
|
+
allowedClasses: settings.audioAllowedClasses
|
|
71087
|
+
});
|
|
70820
71088
|
if (route.kind === "classification") {
|
|
70821
71089
|
if (!topClassification) {
|
|
70822
71090
|
this.ctx.logger.warn("classifyAudioFrame returned classification but topClassification is null", {
|
|
@@ -70825,6 +71093,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
70825
71093
|
});
|
|
70826
71094
|
return;
|
|
70827
71095
|
}
|
|
71096
|
+
const confirm = observeAudioConfirm(this.audioConfirmByDevice.get(deviceId) ?? emptyAudioConfirmState(), {
|
|
71097
|
+
className: route.className,
|
|
71098
|
+
timestampMs: timestamp
|
|
71099
|
+
}, {
|
|
71100
|
+
confirmHits: settings.audioConfirmHits,
|
|
71101
|
+
confirmWindowSec: settings.audioConfirmWindowSec
|
|
71102
|
+
});
|
|
71103
|
+
this.audioConfirmByDevice.set(deviceId, confirm.nextState);
|
|
71104
|
+
if (!confirm.confirmed) {
|
|
71105
|
+
this.ctx.logger.debug("audio event held for corroboration", {
|
|
71106
|
+
tags: { deviceId },
|
|
71107
|
+
meta: {
|
|
71108
|
+
deviceId,
|
|
71109
|
+
className: route.className,
|
|
71110
|
+
confirmHits: settings.audioConfirmHits,
|
|
71111
|
+
confirmWindowSec: settings.audioConfirmWindowSec
|
|
71112
|
+
}
|
|
71113
|
+
});
|
|
71114
|
+
return;
|
|
71115
|
+
}
|
|
70828
71116
|
const classified = {
|
|
70829
71117
|
deviceId,
|
|
70830
71118
|
timestamp,
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D74iweVp.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.169",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|