@camstack/addon-post-analysis 1.2.219 → 1.2.221
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-Cnlk6Tyz.js → dist-B6jHfZ6s.js} +242 -30
- package/dist/{dist-D0l3xZqa.mjs → dist-Clp3AeIp.mjs} +237 -25
- 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-C8d8YCvw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BVv-ddKp.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-Dgmog5ky.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BCU_bvyw.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BsYVmiZ9.mjs → hostInit-EskKIFx1.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +259 -61
- package/dist/pipeline-analytics/index.mjs +259 -61
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as
|
|
1
|
+
import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as FailureCounters, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FULL_IMAGE_BBOX, h as FIRST_LEVEL_MACRO_CLASSES, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as LabelAttributionSchema, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-Clp3AeIp.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";
|
|
@@ -19980,6 +19980,14 @@ function checkTimelapsePacing(input) {
|
|
|
19980
19980
|
unservedWindows: []
|
|
19981
19981
|
};
|
|
19982
19982
|
}
|
|
19983
|
+
//#endregion
|
|
19984
|
+
//#region src/notification-center/timelapse/timelapse-renderer.ts
|
|
19985
|
+
/** `part / whole` to two decimals — the shape a log line is read in. An empty
|
|
19986
|
+
* whole is 0, never NaN: a ratio nobody can compare is not a measurement. */
|
|
19987
|
+
function ratio(part, whole) {
|
|
19988
|
+
if (whole <= 0) return 0;
|
|
19989
|
+
return Math.round(part / whole * 100) / 100;
|
|
19990
|
+
}
|
|
19983
19991
|
/** Bound on the post-render pacing check. The check is best-effort by charter:
|
|
19984
19992
|
* a byte read that hangs must delay delivery by at most this, never sink it. */
|
|
19985
19993
|
var PACING_CHECK_TIMEOUT_MS = 2e4;
|
|
@@ -20003,25 +20011,6 @@ function profileCost(profile) {
|
|
|
20003
20011
|
const i = PROFILE_BY_COST.indexOf(profile);
|
|
20004
20012
|
return i === -1 ? PROFILE_BY_COST.length : i;
|
|
20005
20013
|
}
|
|
20006
|
-
/**
|
|
20007
|
-
* The CHEAPEST profile that has footage over the window.
|
|
20008
|
-
*
|
|
20009
|
-
* It used to be the profile with the MOST coverage, ties broken by name — and
|
|
20010
|
-
* that is a rule which picks `high` almost every time, because the high band is
|
|
20011
|
-
* the one that records continuously. On 2026-09-03 it did: a test render of a
|
|
20012
|
-
* ten-hour window chose `profile=high` at 97% coverage, which on this hub means
|
|
20013
|
-
* 4K read from a USB spinning disk. Decoding it made the machine stop answering
|
|
20014
|
-
* SSH and its own web UI for half an hour, and it had to be power-cycled.
|
|
20015
|
-
*
|
|
20016
|
-
* A timelapse is watched accelerated. Resolution buys nothing at 12 fps over a
|
|
20017
|
-
* night, and the cost difference between the bands is one to two orders of
|
|
20018
|
-
* magnitude in both bytes read and frames decoded. So there is no trade to
|
|
20019
|
-
* weigh: take the cheapest band that has anything, every time.
|
|
20020
|
-
*
|
|
20021
|
-
* Coverage no longer decides WHICH — only whether a band qualifies at all. A
|
|
20022
|
-
* band with less footage is still the right source; the gaps show as a shorter
|
|
20023
|
-
* video, not as a reason to read twenty times the bytes.
|
|
20024
|
-
*/
|
|
20025
20014
|
function pickProfile(ranges, window) {
|
|
20026
20015
|
const byProfile = /* @__PURE__ */ new Map();
|
|
20027
20016
|
for (const r of ranges) {
|
|
@@ -20268,18 +20257,20 @@ var TimelapseRenderer = class {
|
|
|
20268
20257
|
const finished = await this.awaitReady(queued, tags);
|
|
20269
20258
|
const published = await this.publish(finished.id, request, tags);
|
|
20270
20259
|
const downloadUrl = await this.ports.resolveDownloadUrl?.({ exportId: finished.id }).catch(() => null);
|
|
20260
|
+
const archive = await this.measureArchive(finished.id, request, chosen, tags);
|
|
20261
|
+
const deliveredRows = archive?.rows ?? availability.filter((r) => r.profile === chosen.profile);
|
|
20271
20262
|
await this.verifyPacing({
|
|
20272
20263
|
exportId: finished.id,
|
|
20273
20264
|
request,
|
|
20274
20265
|
dense,
|
|
20275
20266
|
subjects,
|
|
20276
|
-
availability:
|
|
20267
|
+
availability: deliveredRows,
|
|
20277
20268
|
tags
|
|
20278
20269
|
});
|
|
20279
20270
|
return {
|
|
20280
20271
|
exportId: finished.id,
|
|
20281
20272
|
profile: chosen.profile,
|
|
20282
|
-
coverage: chosen.coverage,
|
|
20273
|
+
coverage: archive?.coverage ?? chosen.coverage,
|
|
20283
20274
|
plan,
|
|
20284
20275
|
detections,
|
|
20285
20276
|
artifact: published.video,
|
|
@@ -20387,6 +20378,96 @@ var TimelapseRenderer = class {
|
|
|
20387
20378
|
throw new TimelapseRenderError("export-timeout", `export ${queued.id} timed out after ${this.timeoutMs}ms` + (pollFailures > 0 ? ` (${pollFailures} poll failures, last: ${lastPollError})` : ""));
|
|
20388
20379
|
}
|
|
20389
20380
|
/**
|
|
20381
|
+
* The window against the archive, measured AFTER the export and always said.
|
|
20382
|
+
*
|
|
20383
|
+
* Three figures, one line: the window's length, what the PLAN was cut from
|
|
20384
|
+
* (the availability read before the export) and what the ARCHIVE holds (the
|
|
20385
|
+
* same read after it — the export's verified walk has repaired the index the
|
|
20386
|
+
* calendar serves from by then). `plannedOfArchive` is the ratio D445 found
|
|
20387
|
+
* at 0.20 on camera 4374 and nothing reported; a healthy night reads 1.00
|
|
20388
|
+
* because both reads return the same rows.
|
|
20389
|
+
*
|
|
20390
|
+
* WHY THE THRESHOLD IS A SLACK AND NOT A RATIO. The two reads are the SAME
|
|
20391
|
+
* query against the SAME index, so on a healthy night they return the same
|
|
20392
|
+
* rows and the shortfall is exactly 0 — there is no distribution to pick a
|
|
20393
|
+
* percentile out of. It is non-zero only when something CHANGED the index
|
|
20394
|
+
* between them, and the export's verified walk is the only thing that does.
|
|
20395
|
+
* So the bar is not "how short is too short" (the robot's night was 0.20 and
|
|
20396
|
+
* a milder one would be just as wrong); it is only "is this a real change or
|
|
20397
|
+
* a boundary artefact". One artefact exists: the window is closed, but a
|
|
20398
|
+
* segment straddling its end can finalize while the export runs and extend
|
|
20399
|
+
* the last range by its own length. {@link COVERAGE_COMPLETE_SLACK_MS} (30 s)
|
|
20400
|
+
* is the slack the coverage figure already allows at exactly those edges,
|
|
20401
|
+
* and it is several segments wide. No second constant, and nothing between
|
|
20402
|
+
* "unchanged" and "the planner rendered from a short answer".
|
|
20403
|
+
*
|
|
20404
|
+
* Never throws. A re-read that fails is SAID, the plan's figure stands, and
|
|
20405
|
+
* every figure that needed the archive is `null` rather than a stand-in.
|
|
20406
|
+
*/
|
|
20407
|
+
async measureArchive(exportId, request, chosen, tags) {
|
|
20408
|
+
const { window } = request;
|
|
20409
|
+
const windowSec = Math.round((window.endMs - window.startMs) / 1e3);
|
|
20410
|
+
const plannedSec = Math.round(chosen.coverage.coveredMs / 1e3);
|
|
20411
|
+
let rows;
|
|
20412
|
+
try {
|
|
20413
|
+
rows = (await this.ports.getAvailability({
|
|
20414
|
+
deviceId: request.deviceId,
|
|
20415
|
+
fromMs: window.startMs,
|
|
20416
|
+
toMs: window.endMs,
|
|
20417
|
+
profile: chosen.profile
|
|
20418
|
+
})).ranges.filter((r) => r.profile === chosen.profile);
|
|
20419
|
+
} catch (err) {
|
|
20420
|
+
this.ports.logger.warn("timelapse coverage: archive re-read failed — the window is reported against the plan only", {
|
|
20421
|
+
tags,
|
|
20422
|
+
meta: {
|
|
20423
|
+
exportId,
|
|
20424
|
+
ruleId: request.ruleId,
|
|
20425
|
+
profile: chosen.profile,
|
|
20426
|
+
windowSec,
|
|
20427
|
+
plannedSec,
|
|
20428
|
+
plannedOfWindow: ratio(chosen.coverage.coveredMs, window.endMs - window.startMs),
|
|
20429
|
+
archiveSec: null,
|
|
20430
|
+
plannedOfArchive: null,
|
|
20431
|
+
archiveOfWindow: null,
|
|
20432
|
+
error: String(err)
|
|
20433
|
+
}
|
|
20434
|
+
});
|
|
20435
|
+
return null;
|
|
20436
|
+
}
|
|
20437
|
+
const coverage = footageCoverage(rows, window.startMs, window.endMs);
|
|
20438
|
+
const shortfallMs = coverage.coveredMs - chosen.coverage.coveredMs;
|
|
20439
|
+
const meta = {
|
|
20440
|
+
exportId,
|
|
20441
|
+
ruleId: request.ruleId,
|
|
20442
|
+
profile: chosen.profile,
|
|
20443
|
+
windowSec,
|
|
20444
|
+
plannedSec,
|
|
20445
|
+
archiveSec: Math.round(coverage.coveredMs / 1e3),
|
|
20446
|
+
/** What the operator ASKED for against what the file HOLDS — the ratio
|
|
20447
|
+
* the robot's night made someone file a report over: ten hours in,
|
|
20448
|
+
* 6.4 s of video out. Low is not by itself a defect (a lazy source is
|
|
20449
|
+
* legitimately idle most of a night), which is why it never gates. */
|
|
20450
|
+
plannedOfWindow: ratio(chosen.coverage.coveredMs, window.endMs - window.startMs),
|
|
20451
|
+
archiveOfWindow: ratio(coverage.coveredMs, window.endMs - window.startMs),
|
|
20452
|
+
/** The one that IS a defect: what the planner saw against what the
|
|
20453
|
+
* export's verified walk found. 0.20 on camera 4374, 1.00 when healthy. */
|
|
20454
|
+
plannedOfArchive: ratio(chosen.coverage.coveredMs, coverage.coveredMs),
|
|
20455
|
+
shortfallSec: Math.round(shortfallMs / 1e3)
|
|
20456
|
+
};
|
|
20457
|
+
if (shortfallMs > 3e4) this.ports.logger.warn("timelapse coverage: the plan was cut from less footage than the archive holds", {
|
|
20458
|
+
tags,
|
|
20459
|
+
meta
|
|
20460
|
+
});
|
|
20461
|
+
else this.ports.logger.info("timelapse coverage: the window against the archive", {
|
|
20462
|
+
tags,
|
|
20463
|
+
meta
|
|
20464
|
+
});
|
|
20465
|
+
return {
|
|
20466
|
+
rows,
|
|
20467
|
+
coverage
|
|
20468
|
+
};
|
|
20469
|
+
}
|
|
20470
|
+
/**
|
|
20390
20471
|
* Run the pacing check against the finished file, and SAY what it found.
|
|
20391
20472
|
*
|
|
20392
20473
|
* Never throws and never blocks past its bound: the check is telemetry about
|
|
@@ -28409,6 +28490,34 @@ var MEDIA_INDEXES = [
|
|
|
28409
28490
|
"deviceId",
|
|
28410
28491
|
"sizeBytes"
|
|
28411
28492
|
]
|
|
28493
|
+
}),
|
|
28494
|
+
(
|
|
28495
|
+
/**
|
|
28496
|
+
* The per-LOCATION footprint — `COUNT(*)`/`SUM(sizeBytes) WHERE locationId
|
|
28497
|
+
* = ?`, and the legacy `locationId IS NULL` half of the same question. See
|
|
28498
|
+
* {@link MediaStore.footprintByLocation} (D388) and D447.
|
|
28499
|
+
*
|
|
28500
|
+
* **Why not `idx_media_location`.** It leads with `locationId`, so it finds
|
|
28501
|
+
* the range — and then, exactly as the `idx_media_device_size` note above
|
|
28502
|
+
* describes for `deviceId`, fetches the table row behind every entry to read
|
|
28503
|
+
* `sizeBytes`. Every row of the store is on SOME location, so the range is
|
|
28504
|
+
* the table: measured 2026-09-10/11 on `hub/sqlite-settings`, 4 128–6 962 ms
|
|
28505
|
+
* per statement, every five minutes (the occupancy refresh), all night —
|
|
28506
|
+
* `plan=["SEARCH … USING INDEX idx_media_location (locationId=?)"]` on every
|
|
28507
|
+
* WARN. That thread is synchronous, so every configuration read in the
|
|
28508
|
+
* cluster queued behind it: the orchestrator's `configMs=6640` at 23:26:11
|
|
28509
|
+
* is this statement's 6 002 ms at 23:26:10.
|
|
28510
|
+
*
|
|
28511
|
+
* With `sizeBytes` in the index the planner never touches the table
|
|
28512
|
+
* (`USING COVERING INDEX idx_media_location_size`), and the seal gate keeps
|
|
28513
|
+
* its own narrower index. Same shape, same one-off caveat as its two
|
|
28514
|
+
* siblings: ~30 bytes a row, one more b-tree per insert, and the first
|
|
28515
|
+
* `declareCollection` after this ships builds it over every existing row —
|
|
28516
|
+
* do not deploy it beside a running drain or seal.
|
|
28517
|
+
*/
|
|
28518
|
+
{
|
|
28519
|
+
name: "idx_media_location_size",
|
|
28520
|
+
columns: ["locationId", "sizeBytes"]
|
|
28412
28521
|
})
|
|
28413
28522
|
];
|
|
28414
28523
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
@@ -46396,6 +46505,17 @@ var AudioDetectionSettingsSchema = object({
|
|
|
46396
46505
|
audioConfirmHits: number().int().min(1).default(2),
|
|
46397
46506
|
/** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
|
|
46398
46507
|
audioConfirmWindowSec: number().positive().default(10),
|
|
46508
|
+
/**
|
|
46509
|
+
* How far above the camera's own 10 s mean a sample must sit to be a peak.
|
|
46510
|
+
*
|
|
46511
|
+
* A DIFFERENCE between two readings on the same camera, so a uniform scale
|
|
46512
|
+
* offset cancels exactly and the D459 correction did not touch it. Proved,
|
|
46513
|
+
* not assumed — D459 enumerates this alongside `audioMarkerMinDeviationDb`,
|
|
46514
|
+
* `audioMarkerMinDeviations` and the EWMA/MAD pair. "Correcting" it by
|
|
46515
|
+
* 12 dB would be introducing the very error the epoch removed.
|
|
46516
|
+
*
|
|
46517
|
+
* @dbfs-era relative
|
|
46518
|
+
*/
|
|
46399
46519
|
levelDeviationDb: number().positive().default(10),
|
|
46400
46520
|
levelWindowSec: number().positive().default(10),
|
|
46401
46521
|
levelMaxWaitSec: number().positive().default(20),
|
|
@@ -46405,14 +46525,44 @@ var AudioDetectionSettingsSchema = object({
|
|
|
46405
46525
|
/** EWMA time constant for the camera's "normal" loudness, in seconds. Also
|
|
46406
46526
|
* the warm-up: no marker until one time constant of audio has been heard. */
|
|
46407
46527
|
audioMarkerBaselineSec: number().positive().default(300),
|
|
46408
|
-
/**
|
|
46409
|
-
*
|
|
46528
|
+
/**
|
|
46529
|
+
* "High volume", absolute. A sample quieter than this is never a marker
|
|
46530
|
+
* however surprising it is for the camera.
|
|
46531
|
+
*
|
|
46532
|
+
* NOT shifted at the D459 epoch, for the same reason as `SILENCE_FLOOR_DBFS`
|
|
46533
|
+
* and with one extra argument of its own. `-25 dBFS is loud` is a statement
|
|
46534
|
+
* about the scale, not a measurement taken through the old decoder: nobody
|
|
46535
|
+
* read it off a meter, it is what "loud" means on a 16-bit full-scale range.
|
|
46536
|
+
*
|
|
46537
|
+
* The extra argument is that on the old scale this default was effectively
|
|
46538
|
+
* DEAD on the 19 mis-decoded cameras — a reading of -25 there required a
|
|
46539
|
+
* true level of -13 dBFS, which is a sound close to clipping. Bar 1 almost
|
|
46540
|
+
* never bound on them, so the feature has been running on two of its three
|
|
46541
|
+
* bars. At the epoch it starts working as designed rather than becoming too
|
|
46542
|
+
* permissive.
|
|
46543
|
+
*
|
|
46544
|
+
* That it also ships OFF by default (`audioMarkerEnabled`) is what makes
|
|
46545
|
+
* this safe to leave alone: the operator sizes the rate from the `wouldFire`
|
|
46546
|
+
* counter before paying for it, and that counter is now measuring the real
|
|
46547
|
+
* thing for the first time.
|
|
46548
|
+
*
|
|
46549
|
+
* @dbfs-era itu
|
|
46550
|
+
*/
|
|
46410
46551
|
audioMarkerMinDbfs: number().max(0).default(-25),
|
|
46411
46552
|
/** "Out of the ordinary": how many mean-absolute-deviations of the camera's
|
|
46412
46553
|
* OWN jitter the sample must sit above its own mean. */
|
|
46413
46554
|
audioMarkerMinDeviations: number().positive().default(4),
|
|
46414
|
-
/**
|
|
46415
|
-
*
|
|
46555
|
+
/**
|
|
46556
|
+
* Floor in dB on the distance above the camera's own mean — what holds when
|
|
46557
|
+
* a dead-steady camera's deviation unit collapses toward zero.
|
|
46558
|
+
*
|
|
46559
|
+
* A distance between two readings on the same camera: scale-invariant, and
|
|
46560
|
+
* untouched by D459. The numerical coincidence with the PCMU correction
|
|
46561
|
+
* (12 dB) is exactly that — a coincidence, and the reason this note exists
|
|
46562
|
+
* is so nobody reads it as one of the shifted numbers.
|
|
46563
|
+
*
|
|
46564
|
+
* @dbfs-era relative
|
|
46565
|
+
*/
|
|
46416
46566
|
audioMarkerMinDeviationDb: number().positive().default(12),
|
|
46417
46567
|
/** Audio matters most when nothing visual is happening. A marker is
|
|
46418
46568
|
* suppressed while a track is alive on the camera, or motion was reported,
|
|
@@ -64926,6 +65076,10 @@ var STALE_BASELINE_FACTOR = 5;
|
|
|
64926
65076
|
* has MAD 0 and would make every deviation infinite. 1 dB is below the
|
|
64927
65077
|
* quantisation of any real level meter, so it only ever binds on a synthetic
|
|
64928
65078
|
* or a genuinely dead-steady signal — where bar 2 is what should decide.
|
|
65079
|
+
*
|
|
65080
|
+
* A floor on a DEVIATION, not on a level: scale-invariant, untouched by D459.
|
|
65081
|
+
*
|
|
65082
|
+
* @dbfs-era relative
|
|
64929
65083
|
*/
|
|
64930
65084
|
var MIN_MAD_DB = 1;
|
|
64931
65085
|
function emptyAudioBaseline() {
|
|
@@ -65209,6 +65363,44 @@ function buildAudioLabels(input) {
|
|
|
65209
65363
|
}];
|
|
65210
65364
|
}
|
|
65211
65365
|
//#endregion
|
|
65366
|
+
//#region src/pipeline-analytics/services/event-kinds-config.ts
|
|
65367
|
+
/** The honest answer for a pipeline that could not be read: nothing advertised. */
|
|
65368
|
+
var NO_EMITTABLE = {
|
|
65369
|
+
macroClasses: [],
|
|
65370
|
+
audioEnabled: false
|
|
65371
|
+
};
|
|
65372
|
+
/** Depth-first walk of the step tree; the detector is a root today, but the
|
|
65373
|
+
* question is "is it enabled anywhere", not "is it a root". */
|
|
65374
|
+
function* walkSteps(steps) {
|
|
65375
|
+
for (const step of steps) {
|
|
65376
|
+
yield step;
|
|
65377
|
+
if (step.children !== void 0) yield* walkSteps(step.children);
|
|
65378
|
+
}
|
|
65379
|
+
}
|
|
65380
|
+
/**
|
|
65381
|
+
* The macro classes an enabled `object-detection` step emits. Order follows
|
|
65382
|
+
* the authority, not the stored array, so two cameras narrowed to the same
|
|
65383
|
+
* set render the same lanes in the same order.
|
|
65384
|
+
*/
|
|
65385
|
+
function macroClassesOf(step) {
|
|
65386
|
+
const stored = step.settings?.["enabledMacroClasses"];
|
|
65387
|
+
if (!Array.isArray(stored) || stored.length === 0) return [...FIRST_LEVEL_MACRO_CLASSES];
|
|
65388
|
+
const chosen = new Set(stored.filter((v) => typeof v === "string"));
|
|
65389
|
+
return FIRST_LEVEL_MACRO_CLASSES.filter((macro) => chosen.has(macro));
|
|
65390
|
+
}
|
|
65391
|
+
function emittableFromPipelineConfig(config) {
|
|
65392
|
+
let macroClasses = [];
|
|
65393
|
+
for (const step of walkSteps(config.steps)) {
|
|
65394
|
+
if (step.addonId !== "object-detection" || step.enabled === false) continue;
|
|
65395
|
+
macroClasses = macroClassesOf(step);
|
|
65396
|
+
break;
|
|
65397
|
+
}
|
|
65398
|
+
return {
|
|
65399
|
+
macroClasses,
|
|
65400
|
+
audioEnabled: config.audio?.enabled === true
|
|
65401
|
+
};
|
|
65402
|
+
}
|
|
65403
|
+
//#endregion
|
|
65212
65404
|
//#region src/pipeline-analytics/services/event-kinds.ts
|
|
65213
65405
|
/**
|
|
65214
65406
|
* Extensible per-device event kinds (Part B).
|
|
@@ -78116,28 +78308,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78116
78308
|
};
|
|
78117
78309
|
}
|
|
78118
78310
|
/**
|
|
78119
|
-
* Every event kind the device can produce: built-ins (motion + audio),
|
|
78120
|
-
* detection classes
|
|
78121
|
-
*
|
|
78122
|
-
*
|
|
78311
|
+
* Every event kind the device can produce: built-ins (motion + audio), the
|
|
78312
|
+
* detection classes its CONFIGURED pipeline can emit, and sensor kinds from
|
|
78313
|
+
* LINKED devices (device-manager `getLinkedDevices`, binding-driven per
|
|
78314
|
+
* linked device). Degrades to the built-ins on error.
|
|
78315
|
+
*
|
|
78316
|
+
* The detection and audio facts are read from `resolvePipeline` — the
|
|
78317
|
+
* configuration — and never from `getCameraStatus`, which is RUNTIME: an
|
|
78318
|
+
* on-motion camera holds no session between motion events
|
|
78319
|
+
* (`reasons.detection: 'armed:on-motion'`, `detection: null`) and was being
|
|
78320
|
+
* advertised as detecting nothing while its tracks said person / vehicle /
|
|
78321
|
+
* animal ([D449](../../../../docs/decisions/adr-0449-the-timeline-taxonomy-is-the-configured-one-raw-forward-has-no-switch-and-a-one-row-write-waits-on-the-disk.md)).
|
|
78123
78322
|
*/
|
|
78124
78323
|
async listEventKinds(input) {
|
|
78125
78324
|
const api = this.ctx.api;
|
|
78126
|
-
const
|
|
78127
|
-
this.ctx.logger.warn("listEventKinds: getCameraStatus failed", {
|
|
78128
|
-
tags: { deviceId: input.deviceId },
|
|
78129
|
-
meta: { error: errMsg$1(err) }
|
|
78130
|
-
});
|
|
78131
|
-
return null;
|
|
78132
|
-
});
|
|
78133
|
-
const detectionOn = status?.detection != null;
|
|
78134
|
-
const audioOn = status?.audio?.enabled === true;
|
|
78135
|
-
const detectionMacros = MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
|
|
78325
|
+
const emittable = await this.emittableFromConfig(input.deviceId, "listEventKinds");
|
|
78136
78326
|
return composeEventKinds({
|
|
78137
78327
|
linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
78138
78328
|
bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
78139
|
-
enabledMacroClasses: async () =>
|
|
78140
|
-
audioEnabled: async () =>
|
|
78329
|
+
enabledMacroClasses: async () => emittable.macroClasses,
|
|
78330
|
+
audioEnabled: async () => emittable.audioEnabled,
|
|
78141
78331
|
packageZonesEnabled: async (deviceId) => {
|
|
78142
78332
|
return (await this.resolveDevicePackageRules(deviceId)).some((r) => r.enabled !== false);
|
|
78143
78333
|
},
|
|
@@ -78151,6 +78341,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78151
78341
|
}, input.deviceId);
|
|
78152
78342
|
}
|
|
78153
78343
|
/**
|
|
78344
|
+
* What one camera's configured pipeline can emit (`resolvePipeline`), or
|
|
78345
|
+
* the honest fallback when the orchestrator could not answer: nothing
|
|
78346
|
+
* advertised, and a WARN saying so. The cap's answer is a plain list, so it
|
|
78347
|
+
* has no room for "unknown" — that residual of D315 is named in D449; the
|
|
78348
|
+
* steady state (an armed camera with no session) is no longer in it.
|
|
78349
|
+
*/
|
|
78350
|
+
async emittableFromConfig(deviceId, scope) {
|
|
78351
|
+
try {
|
|
78352
|
+
return emittableFromPipelineConfig(await this.ctx.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }));
|
|
78353
|
+
} catch (err) {
|
|
78354
|
+
this.ctx.logger.warn(`${scope}: resolvePipeline failed — no ML kinds advertised`, {
|
|
78355
|
+
tags: { deviceId },
|
|
78356
|
+
meta: { error: errMsg$1(err) }
|
|
78357
|
+
});
|
|
78358
|
+
return NO_EMITTABLE;
|
|
78359
|
+
}
|
|
78360
|
+
}
|
|
78361
|
+
/**
|
|
78154
78362
|
* `listEventKinds` for many cameras, in one call.
|
|
78155
78363
|
*
|
|
78156
78364
|
* The saving is not the round-trips the CALLER avoids — it is the fan-out
|
|
@@ -78162,11 +78370,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78162
78370
|
* `getLinkedDevices` alone took 4110 ms — each one runs a full-fleet
|
|
78163
78371
|
* `listAll` inside device-manager and thirty of them QUEUE.
|
|
78164
78372
|
*
|
|
78165
|
-
* Now
|
|
78166
|
-
* `
|
|
78167
|
-
* `
|
|
78168
|
-
*
|
|
78169
|
-
* maps instead of RPC.
|
|
78373
|
+
* Now two calls for the whole topology, whatever N is —
|
|
78374
|
+
* `prefetchDeviceTopology`'s `getLinkedDevicesBatch` + `getBindingsBatch` —
|
|
78375
|
+
* plus one `resolvePipeline` per camera, all in flight at once (D449; there
|
|
78376
|
+
* is no batch form of it). `composeEventKinds` is unchanged — it is handed
|
|
78377
|
+
* the same per-device client interfaces, backed by maps instead of RPC.
|
|
78170
78378
|
*
|
|
78171
78379
|
* A camera whose composition fails yields an EMPTY list rather than dropping
|
|
78172
78380
|
* out of the response: a caller that asked for twelve and got eleven cannot
|
|
@@ -78175,15 +78383,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78175
78383
|
async listEventKindsBatch(input) {
|
|
78176
78384
|
const api = this.ctx.api;
|
|
78177
78385
|
const deviceIds = [...new Set(input.deviceIds)];
|
|
78178
|
-
const
|
|
78179
|
-
this.ctx.logger.warn("listEventKindsBatch: getCameraStatuses failed", { meta: {
|
|
78180
|
-
deviceCount: deviceIds.length,
|
|
78181
|
-
error: errMsg$1(err)
|
|
78182
|
-
} });
|
|
78183
|
-
return [];
|
|
78184
|
-
});
|
|
78185
|
-
const statusByDevice = new Map(statuses.map((st) => [st.deviceId, st]));
|
|
78186
|
-
const detectionMacros = MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
|
|
78386
|
+
const emittableByDevice = new Map(await Promise.all(deviceIds.map(async (deviceId) => [deviceId, await this.emittableFromConfig(deviceId, "listEventKindsBatch")])));
|
|
78187
78387
|
const topology = await prefetchDeviceTopology({
|
|
78188
78388
|
getLinkedDevicesBatch: (i) => api.deviceManager.getLinkedDevicesBatch.query(i),
|
|
78189
78389
|
getBindingsBatch: (i) => api.deviceManager.getBindingsBatch.query(i)
|
|
@@ -78195,14 +78395,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
78195
78395
|
return null;
|
|
78196
78396
|
});
|
|
78197
78397
|
return composeEventKindsForDevices(deviceIds, async (deviceId) => {
|
|
78198
|
-
const
|
|
78199
|
-
const detectionOn = status?.detection != null;
|
|
78200
|
-
const audioOn = status?.audio?.enabled === true;
|
|
78398
|
+
const emittable = emittableByDevice.get(deviceId) ?? NO_EMITTABLE;
|
|
78201
78399
|
return composeEventKinds({
|
|
78202
78400
|
linkedDevices: topology?.linkedDevices ?? { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
78203
78401
|
bindings: topology?.bindings ?? { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
78204
|
-
enabledMacroClasses: async () =>
|
|
78205
|
-
audioEnabled: async () =>
|
|
78402
|
+
enabledMacroClasses: async () => emittable.macroClasses,
|
|
78403
|
+
audioEnabled: async () => emittable.audioEnabled,
|
|
78206
78404
|
packageZonesEnabled: async (id) => {
|
|
78207
78405
|
return (await this.resolveDevicePackageRules(id)).some((r) => r.enabled !== false);
|
|
78208
78406
|
},
|
|
@@ -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-BVv-ddKp.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.221",
|
|
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",
|