@camstack/addon-post-analysis 1.2.167 → 1.2.168

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.
@@ -13923,6 +13923,35 @@ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({
13923
13923
  auth: "admin"
13924
13924
  });
13925
13925
  /**
13926
+ * Transport bounds for AI capability methods.
13927
+ *
13928
+ * Its own module because `llm.cap.ts` and `llm-runtime.cap.ts` already import
13929
+ * each other: declaring the constant in either one hands the other `undefined`
13930
+ * at module-init time, and an undefined `timeoutMs` silently falls back to the
13931
+ * kernel's 60 s default — the exact failure this constant exists to prevent,
13932
+ * reintroduced invisibly. Found by the guard, not by review.
13933
+ */
13934
+ /**
13935
+ * Ceiling for a cap method that runs inference or loads a model.
13936
+ *
13937
+ * Deliberately far above anything the AI layer itself may wait for (a summary
13938
+ * asks 180 s by default, a rule may ask 600 s), because the TRANSPORT must
13939
+ * never be the layer that gives up first: when it does, the failure is
13940
+ * reported as a transport error for a model that was still thinking, naming
13941
+ * the wrong layer and hiding the real one.
13942
+ *
13943
+ * Measured 2026-09-03: a 6-image vision request needs 44.8 s warm and ~20 s
13944
+ * more on a cold model load. With no declaration every digest failed at
13945
+ * exactly 60 s and shipped without its text.
13946
+ *
13947
+ * A ceiling, not a budget. The AI layer's own timeouts (the rule's `timeoutMs`,
13948
+ * the profile's `timeoutMs` / `firstTokenTimeoutMs`) remain the things that
13949
+ * decide when to stop, because only they can say WHY.
13950
+ */
13951
+ var AI_CAP_TRANSPORT_TIMEOUT_MS = 6e5;
13952
+ /** A multi-gigabyte download. An hour is not generous, it is honest. */
13953
+ var AI_MODEL_INSTALL_TIMEOUT_MS = 60 * 6e4;
13954
+ /**
13926
13955
  * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
13927
13956
  * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
13928
13957
  * caps stay wire-compatible without a circular cap→cap import.
@@ -14194,16 +14223,19 @@ method(LlmGenerateBaseInputSchema.extend({
14194
14223
  timeoutMs: number().int().positive().optional()
14195
14224
  }), LlmGenerateResultSchema, {
14196
14225
  kind: "mutation",
14197
- auth: "admin"
14226
+ auth: "admin",
14227
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14198
14228
  }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
14199
14229
  kind: "mutation",
14200
- auth: "admin"
14230
+ auth: "admin",
14231
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14201
14232
  }), method(object({}), _void(), {
14202
14233
  kind: "mutation",
14203
14234
  auth: "admin"
14204
14235
  }), method(object({}), LlmRuntimeStatusSchema, { auth: "admin" }), method(object({ model: ManagedModelRefSchema }), _void(), {
14205
14236
  kind: "mutation",
14206
- auth: "admin"
14237
+ auth: "admin",
14238
+ timeoutMs: AI_MODEL_INSTALL_TIMEOUT_MS
14207
14239
  }), method(object({ file: string() }), _void(), {
14208
14240
  kind: "mutation",
14209
14241
  auth: "admin"
@@ -14371,7 +14403,13 @@ var ProfileRefInputSchema = object({
14371
14403
  addonId: string(),
14372
14404
  profileId: string()
14373
14405
  });
14374
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({
14406
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
14407
+ kind: "mutation",
14408
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14409
+ }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, {
14410
+ kind: "mutation",
14411
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14412
+ }), method(object({
14375
14413
  addonId: string().optional(),
14376
14414
  requestId: string()
14377
14415
  }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
@@ -14382,7 +14420,8 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14382
14420
  auth: "admin"
14383
14421
  }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
14384
14422
  kind: "mutation",
14385
- auth: "admin"
14423
+ auth: "admin",
14424
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14386
14425
  }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
14387
14426
  selector: LlmDefaultSelectorSchema,
14388
14427
  profileId: string().nullable()
@@ -43395,6 +43434,12 @@ function vectorDimFromBase64(encoded) {
43395
43434
  return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
43396
43435
  }
43397
43436
  //#endregion
43437
+ Object.defineProperty(exports, "AUDIO_MACRO_LABELS", {
43438
+ enumerable: true,
43439
+ get: function() {
43440
+ return AUDIO_MACRO_LABELS;
43441
+ }
43442
+ });
43398
43443
  Object.defineProperty(exports, "BaseAddon", {
43399
43444
  enumerable: true,
43400
43445
  get: function() {
@@ -43419,6 +43464,12 @@ Object.defineProperty(exports, "COCO_TO_MACRO", {
43419
43464
  return COCO_TO_MACRO;
43420
43465
  }
43421
43466
  });
43467
+ Object.defineProperty(exports, "CamProfileSchema", {
43468
+ enumerable: true,
43469
+ get: function() {
43470
+ return CamProfileSchema;
43471
+ }
43472
+ });
43422
43473
  Object.defineProperty(exports, "DEFAULT_EVENT_COLOR", {
43423
43474
  enumerable: true,
43424
43475
  get: function() {
@@ -13892,6 +13892,35 @@ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({
13892
13892
  auth: "admin"
13893
13893
  });
13894
13894
  /**
13895
+ * Transport bounds for AI capability methods.
13896
+ *
13897
+ * Its own module because `llm.cap.ts` and `llm-runtime.cap.ts` already import
13898
+ * each other: declaring the constant in either one hands the other `undefined`
13899
+ * at module-init time, and an undefined `timeoutMs` silently falls back to the
13900
+ * kernel's 60 s default — the exact failure this constant exists to prevent,
13901
+ * reintroduced invisibly. Found by the guard, not by review.
13902
+ */
13903
+ /**
13904
+ * Ceiling for a cap method that runs inference or loads a model.
13905
+ *
13906
+ * Deliberately far above anything the AI layer itself may wait for (a summary
13907
+ * asks 180 s by default, a rule may ask 600 s), because the TRANSPORT must
13908
+ * never be the layer that gives up first: when it does, the failure is
13909
+ * reported as a transport error for a model that was still thinking, naming
13910
+ * the wrong layer and hiding the real one.
13911
+ *
13912
+ * Measured 2026-09-03: a 6-image vision request needs 44.8 s warm and ~20 s
13913
+ * more on a cold model load. With no declaration every digest failed at
13914
+ * exactly 60 s and shipped without its text.
13915
+ *
13916
+ * A ceiling, not a budget. The AI layer's own timeouts (the rule's `timeoutMs`,
13917
+ * the profile's `timeoutMs` / `firstTokenTimeoutMs`) remain the things that
13918
+ * decide when to stop, because only they can say WHY.
13919
+ */
13920
+ var AI_CAP_TRANSPORT_TIMEOUT_MS = 6e5;
13921
+ /** A multi-gigabyte download. An hour is not generous, it is honest. */
13922
+ var AI_MODEL_INSTALL_TIMEOUT_MS = 60 * 6e4;
13923
+ /**
13895
13924
  * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
13896
13925
  * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
13897
13926
  * caps stay wire-compatible without a circular cap→cap import.
@@ -14163,16 +14192,19 @@ method(LlmGenerateBaseInputSchema.extend({
14163
14192
  timeoutMs: number().int().positive().optional()
14164
14193
  }), LlmGenerateResultSchema, {
14165
14194
  kind: "mutation",
14166
- auth: "admin"
14195
+ auth: "admin",
14196
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14167
14197
  }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
14168
14198
  kind: "mutation",
14169
- auth: "admin"
14199
+ auth: "admin",
14200
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14170
14201
  }), method(object({}), _void(), {
14171
14202
  kind: "mutation",
14172
14203
  auth: "admin"
14173
14204
  }), method(object({}), LlmRuntimeStatusSchema, { auth: "admin" }), method(object({ model: ManagedModelRefSchema }), _void(), {
14174
14205
  kind: "mutation",
14175
- auth: "admin"
14206
+ auth: "admin",
14207
+ timeoutMs: AI_MODEL_INSTALL_TIMEOUT_MS
14176
14208
  }), method(object({ file: string() }), _void(), {
14177
14209
  kind: "mutation",
14178
14210
  auth: "admin"
@@ -14340,7 +14372,13 @@ var ProfileRefInputSchema = object({
14340
14372
  addonId: string(),
14341
14373
  profileId: string()
14342
14374
  });
14343
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({
14375
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
14376
+ kind: "mutation",
14377
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14378
+ }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, {
14379
+ kind: "mutation",
14380
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14381
+ }), method(object({
14344
14382
  addonId: string().optional(),
14345
14383
  requestId: string()
14346
14384
  }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
@@ -14351,7 +14389,8 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14351
14389
  auth: "admin"
14352
14390
  }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
14353
14391
  kind: "mutation",
14354
- auth: "admin"
14392
+ auth: "admin",
14393
+ timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
14355
14394
  }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
14356
14395
  selector: LlmDefaultSelectorSchema,
14357
14396
  profileId: string().nullable()
@@ -43364,4 +43403,4 @@ function vectorDimFromBase64(encoded) {
43364
43403
  return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
43365
43404
  }
43366
43405
  //#endregion
43367
- export { buildEventKindDescriptor as $, NcSnoozeSuppressedSchema as A, DeviceType as At, SCENE_DIVERGED as B, literal as Bt, NcRuleInputSchema as C, subKindsOf as Ct, NcScheduleSchema as D, zoneAnalyticsCapability as Dt, NcRuleTargetSchema as E, videoclipsCapability as Et, RECORDING_EXPORT_MAX_READ_BYTES as F, sleep as Ft, TimelapseRuleSchema as G, string as Gt, TIMELAPSE_DENSE_FLOOR_SEC as H, object as Ht, RetrainStatusSchema as I, _enum as It, addonWidgetsSourceCapability as J, TrackSourceSchema as K, unknown as Kt, SCENE_CONFIRM_DEFAULT_TIMEOUT_MS as L, array as Lt, NcTaxonomySchema as M, hydrateSchema as Mt, OpsLogEntrySchema as N, isDeviceScopedCap as Nt, NcSnoozeInputSchema as O, errMsg as Ot, PoolMemoryWatchdog as P, nodePin as Pt, audioModeOf as Q, SCENE_DEFAULT_ANCHOR_THRESHOLD as R, boolean as Rt, NcConditionDescriptorSchema as S, sceneMonitorCapability as St, NcRuleSchema as T, vectorDimFromBase64 as Tt, TimelapseRuleInputSchema as U, partialRecord as Ut, SceneMonitorSchema as V, number as Vt, TimelapseRulePatchSchema as W, record as Wt, assertTimelapseCadences as X, alarmPanelCapability as Y, audioMetricsCapability as Z, NC_ALARM_SYSTEM_EVENT_KINDS as _, pipelineAnalyticsCapability as _t, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as a, encodeVectorBase64 as at, NC_SNOOZE_MAX_MINUTES as b, readTimelapseGeneratedAt as bt, DETECTION_PIPELINE_CAP_NAME as c, failureContributionCapability as ct, EVENT_PAD_MS as d, isScheduleActive as dt, cosineSimilarity as et, FULL_IMAGE_BBOX as f, isSourceCap as ft, MediaFileKindEnum as g, pickClusterStepModels as gt, MACRO_LABELS as h, parseProcStatus as ht, DEFAULT_EVENT_COLOR as i, embeddingEncoderCapability as it, NcSystemEventKindSchema as j, createEvent as jt, NcSnoozeSchema as k, BaseAddon as kt, DeclaredDevices as l, hfModelUrl as lt, LabelAttributionSchema as m, notificationRulesCapability as mt, CLUSTER_MODEL_SCOPED_STEPS as n, defineCustomActions as nt, DEFAULT_TIMELAPSE_PREVIEW_TEXT as o, evaluateSensorEdge as ot, FailureCounters as p, kebabToCamel as pt, VISIT_MERGE_GAP_MS as q, EventCategory as qt, COCO_TO_MACRO as r, deriveRecordingMode as rt, DETECTION_MACRO_CLASSES as s, faceGalleryCapability as st, BaseDevice as t, customAction as tt, EVENT_KIND_BY_CAP as u, isDetectionMacroClass as ut, NC_CONDITION_CATALOG as v, plateGalleryCapability as vt, NcRulePatchSchema as w, systemEventFilterApplies as wt, NC_TAXONOMY as x, resolvePoolMemoryPolicy as xt, NC_DEFAULT_SNOOZE_MINUTES as y, readDeviceStateFrom as yt, SCENE_DEFAULT_UNCOVERED_POLICY as z, discriminatedUnion as zt };
43406
+ export { audioModeOf as $, NcSnoozeSchema as A, BaseAddon as At, SCENE_DEFAULT_UNCOVERED_POLICY as B, boolean as Bt, NcConditionDescriptorSchema as C, sceneMonitorCapability as Ct, NcRuleTargetSchema as D, videoclipsCapability as Dt, NcRuleSchema as E, vectorDimFromBase64 as Et, PoolMemoryWatchdog as F, isDeviceScopedCap as Ft, TimelapseRulePatchSchema as G, partialRecord as Gt, SceneMonitorSchema as H, literal as Ht, RECORDING_EXPORT_MAX_READ_BYTES as I, nodePin as It, VISIT_MERGE_GAP_MS as J, unknown as Jt, TimelapseRuleSchema as K, record as Kt, RetrainStatusSchema as L, sleep as Lt, NcSystemEventKindSchema as M, DeviceType as Mt, NcTaxonomySchema as N, createEvent as Nt, NcScheduleSchema as O, zoneAnalyticsCapability as Ot, OpsLogEntrySchema as P, hydrateSchema as Pt, audioMetricsCapability as Q, SCENE_CONFIRM_DEFAULT_TIMEOUT_MS as R, _enum as Rt, NC_TAXONOMY as S, resolvePoolMemoryPolicy as St, NcRulePatchSchema as T, systemEventFilterApplies as Tt, TIMELAPSE_DENSE_FLOOR_SEC as U, number as Ut, SCENE_DIVERGED as V, discriminatedUnion as Vt, TimelapseRuleInputSchema as W, object as Wt, alarmPanelCapability as X, addonWidgetsSourceCapability as Y, EventCategory as Yt, assertTimelapseCadences as Z, MediaFileKindEnum as _, pickClusterStepModels as _t, DEFAULT_EVENT_COLOR as a, embeddingEncoderCapability as at, NC_DEFAULT_SNOOZE_MINUTES as b, readDeviceStateFrom as bt, DETECTION_MACRO_CLASSES as c, faceGalleryCapability as ct, EVENT_KIND_BY_CAP as d, isDetectionMacroClass as dt, buildEventKindDescriptor as et, EVENT_PAD_MS as f, isScheduleActive as ft, MACRO_LABELS as g, parseProcStatus as gt, LabelAttributionSchema as h, notificationRulesCapability as ht, COCO_TO_MACRO as i, deriveRecordingMode as it, NcSnoozeSuppressedSchema as j, CamProfileSchema as jt, NcSnoozeInputSchema as k, errMsg as kt, DETECTION_PIPELINE_CAP_NAME as l, failureContributionCapability as lt, FailureCounters as m, kebabToCamel as mt, BaseDevice as n, customAction as nt, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as o, encodeVectorBase64 as ot, FULL_IMAGE_BBOX as p, isSourceCap as pt, TrackSourceSchema as q, string as qt, CLUSTER_MODEL_SCOPED_STEPS as r, defineCustomActions as rt, DEFAULT_TIMELAPSE_PREVIEW_TEXT as s, evaluateSensorEdge as st, AUDIO_MACRO_LABELS as t, cosineSimilarity as tt, DeclaredDevices as u, hfModelUrl as ut, NC_ALARM_SYSTEM_EVENT_KINDS as v, pipelineAnalyticsCapability as vt, NcRuleInputSchema as w, subKindsOf as wt, NC_SNOOZE_MAX_MINUTES as x, readTimelapseGeneratedAt as xt, NC_CONDITION_CATALOG as y, plateGalleryCapability as yt, SCENE_DEFAULT_ANCHOR_THRESHOLD as z, array as zt };
@@ -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-dGS8z89L.js");
5
+ const require_dist = require("../dist-CftJzAHc.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs);
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { P as PoolMemoryWatchdog, ht as parseProcStatus, it as embeddingEncoderCapability, kt as BaseAddon, lt as hfModelUrl, xt as resolvePoolMemoryPolicy } from "../dist-DgOc1kH7.mjs";
1
+ import { At as BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-h9lRw5M-.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.57",
6
+ version: "1.2.58",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.137",
21
+ version: "1.2.138",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.103",
36
+ version: "1.2.104",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.137",
39
+ version: "1.2.138",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.57",
48
+ version: "1.2.58",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.103",
84
+ version: "1.2.104",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -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-dGS8z89L.js");
5
+ const require_dist = require("../dist-CftJzAHc.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
- /** The profile with the most footage over the window; ties broken by name so
19709
- * the same window always renders from the same source. */
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
- let best = null;
19718
- for (const [profile, rows] of [...byProfile].sort((a, b) => a[0].localeCompare(b[0]))) {
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 (best === null || coverage.coveredMs > best.coverage.coveredMs) best = {
19750
+ if (coverage.coveredMs > 0) return {
19721
19751
  profile,
19722
19752
  coverage
19723
19753
  };
19724
19754
  }
19725
- return best;
19755
+ return null;
19726
19756
  }
19727
19757
  /**
19728
19758
  * Is `record` the render this request is about to order?
@@ -43985,6 +44015,36 @@ var AnalyticsQueryFacade = class {
43985
44015
  //#region src/pipeline-analytics/audio-detection-settings.ts
43986
44016
  var AudioDetectionSettingsSchema = require_dist.object({
43987
44017
  classificationMinScore: require_dist.number().min(0).max(1).default(.6),
44018
+ /**
44019
+ * Macro classes allowed to produce an audio EVENT on this camera. Empty =
44020
+ * no policy, every class passes — so an upgrade changes nothing until an
44021
+ * operator states a policy.
44022
+ *
44023
+ * This belongs to the event generator, NOT to the classifier: the analyzer
44024
+ * keeps classifying everything it hears, `audioLabels` on the track keeps
44025
+ * carrying the full picture, and only the decision to MATERIALIZE an event
44026
+ * is narrowed. Raising a threshold inside the classifier would instead make
44027
+ * the sound invisible to every consumer at once.
44028
+ */
44029
+ audioAllowedClasses: require_dist.array(require_dist.string()).default([]),
44030
+ /**
44031
+ * Corroboration window on the EVENT, per class: this many occurrences of
44032
+ * the same sound must land inside `audioConfirmWindowSec` before it becomes
44033
+ * an event. `1` = no window.
44034
+ *
44035
+ * Default 2-in-10s, set by the operator on 2026-09-03 after measuring 26
44036
+ * audio tracks in 24 h: the single-frame classification is the dominant
44037
+ * false positive, and a sound that is real is heard again within ten
44038
+ * seconds. A camera that wants every occurrence sets `1`.
44039
+ *
44040
+ * The Notification Center already has this exact brake for audio RULES
44041
+ * (`confirmHits` / `confirmWindowSec`). This is the same idea one level
44042
+ * earlier, so a camera can stop MAKING the single-frame events instead of
44043
+ * only declining to notify about them.
44044
+ */
44045
+ audioConfirmHits: require_dist.number().int().min(1).default(2),
44046
+ /** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
44047
+ audioConfirmWindowSec: require_dist.number().positive().default(10),
43988
44048
  levelDeviationDb: require_dist.number().positive().default(10),
43989
44049
  levelWindowSec: require_dist.number().positive().default(10),
43990
44050
  levelMaxWaitSec: require_dist.number().positive().default(20),
@@ -44025,8 +44085,17 @@ var AudioDetectionSettingsSchema = require_dist.object({
44025
44085
  audioEpisodeOpenHoldSec: require_dist.number().nonnegative().default(1.5),
44026
44086
  /** Class must be absent this long before the episode closes. */
44027
44087
  audioEpisodeCloseGapSec: require_dist.number().positive().default(12),
44028
- /** After close, the same class cannot reopen until this elapses. */
44029
- audioEpisodeCooldownSec: require_dist.number().nonnegative().default(30)
44088
+ /** After close, the same class cannot reopen until this elapses.
44089
+ *
44090
+ * This is the minimum spacing between two SYNTHETIC EVENTS (the episode's
44091
+ * event + timeline track) of the same sound on one camera — NOT a
44092
+ * notification floor. Notification spacing is the rule's own
44093
+ * `throttle.cooldownSec`, which already defaults to 60 s and is keyed
44094
+ * per-device; the two are independent brakes on different objects.
44095
+ *
44096
+ * Raised 30 → 60 s on 2026-09-03 by the operator, alongside the 2-in-10s
44097
+ * corroboration window above. */
44098
+ audioEpisodeCooldownSec: require_dist.number().nonnegative().default(60)
44030
44099
  });
44031
44100
  var AUDIO_DETECTION_DEFAULTS = AudioDetectionSettingsSchema.parse({});
44032
44101
  /**
@@ -44203,14 +44272,45 @@ function observeAudioEpisode(state, sample, knobs) {
44203
44272
  };
44204
44273
  }
44205
44274
  //#endregion
44275
+ //#region src/pipeline-analytics/audio-confirm-window.ts
44276
+ function emptyAudioConfirmState() {
44277
+ return { hitsByClass: /* @__PURE__ */ new Map() };
44278
+ }
44279
+ function observeAudioConfirm(state, input, cfg) {
44280
+ if (cfg.confirmHits <= 1) return {
44281
+ confirmed: true,
44282
+ nextState: state
44283
+ };
44284
+ const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
44285
+ const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
44286
+ const next = new Map(state.hitsByClass);
44287
+ if (kept.length < cfg.confirmHits) {
44288
+ next.set(input.className, kept);
44289
+ return {
44290
+ confirmed: false,
44291
+ nextState: { hitsByClass: next }
44292
+ };
44293
+ }
44294
+ next.delete(input.className);
44295
+ return {
44296
+ confirmed: true,
44297
+ nextState: { hitsByClass: next }
44298
+ };
44299
+ }
44300
+ //#endregion
44206
44301
  //#region src/pipeline-analytics/audio-event-router.ts
44207
44302
  /**
44208
44303
  * Decide whether an audio frame is a (meaningful) classification event or a
44209
44304
  * level event. `silence` is never a classification — it falls to the level
44210
44305
  * path so only a genuine loudness peak/heartbeat can surface it.
44306
+ *
44307
+ * A class the operator did not allow takes the SAME route as a class that
44308
+ * failed the score: the level path. Both mean "not a classification event on
44309
+ * this camera", and giving the allowlist its own harder outcome (a silent
44310
+ * drop) would be a second arithmetic for one question.
44211
44311
  */
44212
44312
  function classifyAudioFrame(top, cfg) {
44213
- if (top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
44313
+ if ((cfg.allowedClasses.length === 0 || cfg.allowedClasses.includes(top?.className ?? "")) && top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
44214
44314
  kind: "classification",
44215
44315
  className: top.className,
44216
44316
  score: top.score
@@ -63076,6 +63176,36 @@ function buildGlobalSettingsSchema(cap) {
63076
63176
  step: .05,
63077
63177
  default: AUDIO_DETECTION_DEFAULTS.classificationMinScore
63078
63178
  },
63179
+ {
63180
+ type: "multiselect",
63181
+ key: "audioAllowedClasses",
63182
+ label: "Event classes",
63183
+ 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.",
63184
+ options: require_dist.AUDIO_MACRO_LABELS.filter((l) => l.id !== "silence").map((l) => ({
63185
+ value: l.id,
63186
+ label: l.name
63187
+ })),
63188
+ default: AUDIO_DETECTION_DEFAULTS.audioAllowedClasses
63189
+ },
63190
+ {
63191
+ type: "number",
63192
+ key: "audioConfirmHits",
63193
+ label: "Occurrences to confirm",
63194
+ 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.",
63195
+ min: 1,
63196
+ step: 1,
63197
+ default: AUDIO_DETECTION_DEFAULTS.audioConfirmHits
63198
+ },
63199
+ {
63200
+ type: "number",
63201
+ key: "audioConfirmWindowSec",
63202
+ label: "Confirmation window",
63203
+ description: "Width of the corroboration window. Occurrences are counted per class, so two different sounds never confirm each other. Ignored when occurrences is 1.",
63204
+ min: 1,
63205
+ step: 1,
63206
+ default: AUDIO_DETECTION_DEFAULTS.audioConfirmWindowSec,
63207
+ unit: "s"
63208
+ },
63079
63209
  {
63080
63210
  type: "number",
63081
63211
  key: "levelDeviationDb",
@@ -65922,6 +66052,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
65922
66052
  lastAudioInsertByDevice = /* @__PURE__ */ new Map();
65923
66053
  lastMotionInsertByDevice = /* @__PURE__ */ new Map();
65924
66054
  levelStateByDevice = /* @__PURE__ */ new Map();
66055
+ /** Per-device corroboration window for classified audio (see
66056
+ * `audio-confirm-window.ts`). Torn down wherever `levelStateByDevice` is —
66057
+ * a stale window on a removed device would confirm on its first frame back. */
66058
+ audioConfirmByDevice = /* @__PURE__ */ new Map();
65925
66059
  /**
65926
66060
  * Last ms a spatial track was alive on a device, any plane. Written on the
65927
66061
  * frame path (one `Map.set` per processed frame), read O(1) by the audio-
@@ -67436,7 +67570,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
67436
67570
  mimeType: i.mimeType
67437
67571
  })),
67438
67572
  jsonSchema: request.jsonSchema,
67439
- temperature: request.temperature
67573
+ temperature: request.temperature,
67574
+ ...request.timeoutMs !== void 0 ? { timeoutMs: request.timeoutMs } : {}
67440
67575
  });
67441
67576
  return result.ok ? {
67442
67577
  ok: true,
@@ -68342,6 +68477,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68342
68477
  this.overlaySynthesisWarnAt.delete(data.deviceId);
68343
68478
  this.forgetDeviceProcessors(data.deviceId);
68344
68479
  this.levelStateByDevice.delete(data.deviceId);
68480
+ this.audioConfirmByDevice.delete(data.deviceId);
68345
68481
  this.settingsCaches.clearDevice(data.deviceId);
68346
68482
  this.rawDeviceBlobMirror.invalidate(data.deviceId);
68347
68483
  }
@@ -68360,6 +68496,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68360
68496
  this.overlaySynthesisWarnAt.delete(deviceId);
68361
68497
  this.forgetDeviceProcessors(deviceId);
68362
68498
  this.levelStateByDevice.delete(deviceId);
68499
+ this.audioConfirmByDevice.delete(deviceId);
68363
68500
  this.lastTrackActivityMs.delete(deviceId);
68364
68501
  this.motionEventSnapshots?.forgetDevice(deviceId);
68365
68502
  this.audioEpisodeByDevice.delete(deviceId);
@@ -68576,7 +68713,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68576
68713
  mimeType: image.mimeType
68577
68714
  })),
68578
68715
  jsonSchema: i.jsonSchema,
68579
- temperature: i.temperature
68716
+ temperature: i.temperature,
68717
+ ...i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : {}
68580
68718
  }),
68581
68719
  cancelVision: async (requestId) => {
68582
68720
  await api.llm.cancel.mutate({ requestId });
@@ -68871,6 +69009,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68871
69009
  this.nodeLiveness.clear();
68872
69010
  this.gatedNodeByDevice.clear();
68873
69011
  this.levelStateByDevice.clear();
69012
+ this.audioConfirmByDevice.clear();
68874
69013
  this.settingsCaches.clearAll();
68875
69014
  this.rawDeviceBlobMirror.invalidateAll();
68876
69015
  this.rawGlobalStoreMirror.invalidate();
@@ -70885,12 +71024,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70885
71024
  this.notificationCenter?.observeAudio(deviceId, {
70886
71025
  timestampMs: timestamp,
70887
71026
  ...level?.dbfs !== void 0 ? { dbfs: level.dbfs } : {},
70888
- labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass)
71027
+ labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass).filter((macroClass) => settings.audioAllowedClasses.length === 0 || settings.audioAllowedClasses.includes(macroClass))
70889
71028
  });
70890
71029
  const route = classifyAudioFrame(topClassification ? {
70891
71030
  className: topClassification.macroClass,
70892
71031
  score: topClassification.score
70893
- } : void 0, { classificationMinScore: settings.classificationMinScore });
71032
+ } : void 0, {
71033
+ classificationMinScore: settings.classificationMinScore,
71034
+ allowedClasses: settings.audioAllowedClasses
71035
+ });
70894
71036
  if (route.kind === "classification") {
70895
71037
  if (!topClassification) {
70896
71038
  this.ctx.logger.warn("classifyAudioFrame returned classification but topClassification is null", {
@@ -70899,6 +71041,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70899
71041
  });
70900
71042
  return;
70901
71043
  }
71044
+ const confirm = observeAudioConfirm(this.audioConfirmByDevice.get(deviceId) ?? emptyAudioConfirmState(), {
71045
+ className: route.className,
71046
+ timestampMs: timestamp
71047
+ }, {
71048
+ confirmHits: settings.audioConfirmHits,
71049
+ confirmWindowSec: settings.audioConfirmWindowSec
71050
+ });
71051
+ this.audioConfirmByDevice.set(deviceId, confirm.nextState);
71052
+ if (!confirm.confirmed) {
71053
+ this.ctx.logger.debug("audio event held for corroboration", {
71054
+ tags: { deviceId },
71055
+ meta: {
71056
+ deviceId,
71057
+ className: route.className,
71058
+ confirmHits: settings.audioConfirmHits,
71059
+ confirmWindowSec: settings.audioConfirmWindowSec
71060
+ }
71061
+ });
71062
+ return;
71063
+ }
70902
71064
  const classified = {
70903
71065
  deviceId,
70904
71066
  timestamp,
@@ -1,4 +1,4 @@
1
- import { $ as buildEventKindDescriptor, A as NcSnoozeSuppressedSchema, At as DeviceType, B as SCENE_DIVERGED, Bt as literal, C as NcRuleInputSchema, Ct as subKindsOf, D as NcScheduleSchema, Dt as zoneAnalyticsCapability, E as NcRuleTargetSchema, Et as videoclipsCapability, F as RECORDING_EXPORT_MAX_READ_BYTES, Ft as sleep$1, G as TimelapseRuleSchema, Gt as string, H as TIMELAPSE_DENSE_FLOOR_SEC, Ht as object, I as RetrainStatusSchema, It as _enum, J as addonWidgetsSourceCapability, K as TrackSourceSchema, Kt as unknown, Lt as array, M as NcTaxonomySchema, Mt as hydrateSchema, N as OpsLogEntrySchema, Nt as isDeviceScopedCap, O as NcSnoozeInputSchema, Ot as errMsg, Pt as nodePin, Q as audioModeOf, R as SCENE_DEFAULT_ANCHOR_THRESHOLD, Rt as boolean, S as NcConditionDescriptorSchema, St as sceneMonitorCapability, T as NcRuleSchema, Tt as vectorDimFromBase64, U as TimelapseRuleInputSchema, Ut as partialRecord, V as SceneMonitorSchema, Vt as number, W as TimelapseRulePatchSchema, Wt as record, X as assertTimelapseCadences, Y as alarmPanelCapability, Z as audioMetricsCapability, _ as NC_ALARM_SYSTEM_EVENT_KINDS, _t as pipelineAnalyticsCapability, at as encodeVectorBase64, bt as readTimelapseGeneratedAt, ct as failureContributionCapability, d as EVENT_PAD_MS, dt as isScheduleActive, et as cosineSimilarity$1, f as FULL_IMAGE_BBOX, ft as isSourceCap, g as MediaFileKindEnum, gt as pickClusterStepModels, h as MACRO_LABELS, i as DEFAULT_EVENT_COLOR, j as NcSystemEventKindSchema, jt as createEvent, k as NcSnoozeSchema, kt as BaseAddon, l as DeclaredDevices, m as LabelAttributionSchema, mt as notificationRulesCapability, n as CLUSTER_MODEL_SCOPED_STEPS, nt as defineCustomActions, ot as evaluateSensorEdge, p as FailureCounters, pt as kebabToCamel, q as VISIT_MERGE_GAP_MS, qt as EventCategory, r as COCO_TO_MACRO, rt as deriveRecordingMode, s as DETECTION_MACRO_CLASSES, st as faceGalleryCapability, t as BaseDevice, tt as customAction, u as EVENT_KIND_BY_CAP, ut as isDetectionMacroClass, v as NC_CONDITION_CATALOG, vt as plateGalleryCapability, w as NcRulePatchSchema, wt as systemEventFilterApplies, x as NC_TAXONOMY, y as NC_DEFAULT_SNOOZE_MINUTES, yt as readDeviceStateFrom, z as SCENE_DEFAULT_UNCOVERED_POLICY, zt as discriminatedUnion } from "../dist-DgOc1kH7.mjs";
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-h9lRw5M-.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
- /** The profile with the most footage over the window; ties broken by name so
19687
- * the same window always renders from the same source. */
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
- let best = null;
19696
- for (const [profile, rows] of [...byProfile].sort((a, b) => a[0].localeCompare(b[0]))) {
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 (best === null || coverage.coveredMs > best.coverage.coveredMs) best = {
19728
+ if (coverage.coveredMs > 0) return {
19699
19729
  profile,
19700
19730
  coverage
19701
19731
  };
19702
19732
  }
19703
- return best;
19733
+ return null;
19704
19734
  }
19705
19735
  /**
19706
19736
  * Is `record` the render this request is about to order?
@@ -43911,6 +43941,36 @@ var AnalyticsQueryFacade = class {
43911
43941
  //#region src/pipeline-analytics/audio-detection-settings.ts
43912
43942
  var AudioDetectionSettingsSchema = object({
43913
43943
  classificationMinScore: number().min(0).max(1).default(.6),
43944
+ /**
43945
+ * Macro classes allowed to produce an audio EVENT on this camera. Empty =
43946
+ * no policy, every class passes — so an upgrade changes nothing until an
43947
+ * operator states a policy.
43948
+ *
43949
+ * This belongs to the event generator, NOT to the classifier: the analyzer
43950
+ * keeps classifying everything it hears, `audioLabels` on the track keeps
43951
+ * carrying the full picture, and only the decision to MATERIALIZE an event
43952
+ * is narrowed. Raising a threshold inside the classifier would instead make
43953
+ * the sound invisible to every consumer at once.
43954
+ */
43955
+ audioAllowedClasses: array(string()).default([]),
43956
+ /**
43957
+ * Corroboration window on the EVENT, per class: this many occurrences of
43958
+ * the same sound must land inside `audioConfirmWindowSec` before it becomes
43959
+ * an event. `1` = no window.
43960
+ *
43961
+ * Default 2-in-10s, set by the operator on 2026-09-03 after measuring 26
43962
+ * audio tracks in 24 h: the single-frame classification is the dominant
43963
+ * false positive, and a sound that is real is heard again within ten
43964
+ * seconds. A camera that wants every occurrence sets `1`.
43965
+ *
43966
+ * The Notification Center already has this exact brake for audio RULES
43967
+ * (`confirmHits` / `confirmWindowSec`). This is the same idea one level
43968
+ * earlier, so a camera can stop MAKING the single-frame events instead of
43969
+ * only declining to notify about them.
43970
+ */
43971
+ audioConfirmHits: number().int().min(1).default(2),
43972
+ /** Width of the corroboration window. Ignored when `audioConfirmHits` is 1. */
43973
+ audioConfirmWindowSec: number().positive().default(10),
43914
43974
  levelDeviationDb: number().positive().default(10),
43915
43975
  levelWindowSec: number().positive().default(10),
43916
43976
  levelMaxWaitSec: number().positive().default(20),
@@ -43951,8 +44011,17 @@ var AudioDetectionSettingsSchema = object({
43951
44011
  audioEpisodeOpenHoldSec: number().nonnegative().default(1.5),
43952
44012
  /** Class must be absent this long before the episode closes. */
43953
44013
  audioEpisodeCloseGapSec: number().positive().default(12),
43954
- /** After close, the same class cannot reopen until this elapses. */
43955
- audioEpisodeCooldownSec: number().nonnegative().default(30)
44014
+ /** After close, the same class cannot reopen until this elapses.
44015
+ *
44016
+ * This is the minimum spacing between two SYNTHETIC EVENTS (the episode's
44017
+ * event + timeline track) of the same sound on one camera — NOT a
44018
+ * notification floor. Notification spacing is the rule's own
44019
+ * `throttle.cooldownSec`, which already defaults to 60 s and is keyed
44020
+ * per-device; the two are independent brakes on different objects.
44021
+ *
44022
+ * Raised 30 → 60 s on 2026-09-03 by the operator, alongside the 2-in-10s
44023
+ * corroboration window above. */
44024
+ audioEpisodeCooldownSec: number().nonnegative().default(60)
43956
44025
  });
43957
44026
  var AUDIO_DETECTION_DEFAULTS = AudioDetectionSettingsSchema.parse({});
43958
44027
  /**
@@ -44129,14 +44198,45 @@ function observeAudioEpisode(state, sample, knobs) {
44129
44198
  };
44130
44199
  }
44131
44200
  //#endregion
44201
+ //#region src/pipeline-analytics/audio-confirm-window.ts
44202
+ function emptyAudioConfirmState() {
44203
+ return { hitsByClass: /* @__PURE__ */ new Map() };
44204
+ }
44205
+ function observeAudioConfirm(state, input, cfg) {
44206
+ if (cfg.confirmHits <= 1) return {
44207
+ confirmed: true,
44208
+ nextState: state
44209
+ };
44210
+ const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
44211
+ const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
44212
+ const next = new Map(state.hitsByClass);
44213
+ if (kept.length < cfg.confirmHits) {
44214
+ next.set(input.className, kept);
44215
+ return {
44216
+ confirmed: false,
44217
+ nextState: { hitsByClass: next }
44218
+ };
44219
+ }
44220
+ next.delete(input.className);
44221
+ return {
44222
+ confirmed: true,
44223
+ nextState: { hitsByClass: next }
44224
+ };
44225
+ }
44226
+ //#endregion
44132
44227
  //#region src/pipeline-analytics/audio-event-router.ts
44133
44228
  /**
44134
44229
  * Decide whether an audio frame is a (meaningful) classification event or a
44135
44230
  * level event. `silence` is never a classification — it falls to the level
44136
44231
  * path so only a genuine loudness peak/heartbeat can surface it.
44232
+ *
44233
+ * A class the operator did not allow takes the SAME route as a class that
44234
+ * failed the score: the level path. Both mean "not a classification event on
44235
+ * this camera", and giving the allowlist its own harder outcome (a silent
44236
+ * drop) would be a second arithmetic for one question.
44137
44237
  */
44138
44238
  function classifyAudioFrame(top, cfg) {
44139
- if (top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
44239
+ if ((cfg.allowedClasses.length === 0 || cfg.allowedClasses.includes(top?.className ?? "")) && top && top.className !== "silence" && top.score >= cfg.classificationMinScore) return {
44140
44240
  kind: "classification",
44141
44241
  className: top.className,
44142
44242
  score: top.score
@@ -63002,6 +63102,36 @@ function buildGlobalSettingsSchema(cap) {
63002
63102
  step: .05,
63003
63103
  default: AUDIO_DETECTION_DEFAULTS.classificationMinScore
63004
63104
  },
63105
+ {
63106
+ type: "multiselect",
63107
+ key: "audioAllowedClasses",
63108
+ label: "Event classes",
63109
+ 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.",
63110
+ options: AUDIO_MACRO_LABELS.filter((l) => l.id !== "silence").map((l) => ({
63111
+ value: l.id,
63112
+ label: l.name
63113
+ })),
63114
+ default: AUDIO_DETECTION_DEFAULTS.audioAllowedClasses
63115
+ },
63116
+ {
63117
+ type: "number",
63118
+ key: "audioConfirmHits",
63119
+ label: "Occurrences to confirm",
63120
+ 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.",
63121
+ min: 1,
63122
+ step: 1,
63123
+ default: AUDIO_DETECTION_DEFAULTS.audioConfirmHits
63124
+ },
63125
+ {
63126
+ type: "number",
63127
+ key: "audioConfirmWindowSec",
63128
+ label: "Confirmation window",
63129
+ description: "Width of the corroboration window. Occurrences are counted per class, so two different sounds never confirm each other. Ignored when occurrences is 1.",
63130
+ min: 1,
63131
+ step: 1,
63132
+ default: AUDIO_DETECTION_DEFAULTS.audioConfirmWindowSec,
63133
+ unit: "s"
63134
+ },
63005
63135
  {
63006
63136
  type: "number",
63007
63137
  key: "levelDeviationDb",
@@ -65848,6 +65978,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
65848
65978
  lastAudioInsertByDevice = /* @__PURE__ */ new Map();
65849
65979
  lastMotionInsertByDevice = /* @__PURE__ */ new Map();
65850
65980
  levelStateByDevice = /* @__PURE__ */ new Map();
65981
+ /** Per-device corroboration window for classified audio (see
65982
+ * `audio-confirm-window.ts`). Torn down wherever `levelStateByDevice` is —
65983
+ * a stale window on a removed device would confirm on its first frame back. */
65984
+ audioConfirmByDevice = /* @__PURE__ */ new Map();
65851
65985
  /**
65852
65986
  * Last ms a spatial track was alive on a device, any plane. Written on the
65853
65987
  * frame path (one `Map.set` per processed frame), read O(1) by the audio-
@@ -67362,7 +67496,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67362
67496
  mimeType: i.mimeType
67363
67497
  })),
67364
67498
  jsonSchema: request.jsonSchema,
67365
- temperature: request.temperature
67499
+ temperature: request.temperature,
67500
+ ...request.timeoutMs !== void 0 ? { timeoutMs: request.timeoutMs } : {}
67366
67501
  });
67367
67502
  return result.ok ? {
67368
67503
  ok: true,
@@ -68268,6 +68403,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68268
68403
  this.overlaySynthesisWarnAt.delete(data.deviceId);
68269
68404
  this.forgetDeviceProcessors(data.deviceId);
68270
68405
  this.levelStateByDevice.delete(data.deviceId);
68406
+ this.audioConfirmByDevice.delete(data.deviceId);
68271
68407
  this.settingsCaches.clearDevice(data.deviceId);
68272
68408
  this.rawDeviceBlobMirror.invalidate(data.deviceId);
68273
68409
  }
@@ -68286,6 +68422,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68286
68422
  this.overlaySynthesisWarnAt.delete(deviceId);
68287
68423
  this.forgetDeviceProcessors(deviceId);
68288
68424
  this.levelStateByDevice.delete(deviceId);
68425
+ this.audioConfirmByDevice.delete(deviceId);
68289
68426
  this.lastTrackActivityMs.delete(deviceId);
68290
68427
  this.motionEventSnapshots?.forgetDevice(deviceId);
68291
68428
  this.audioEpisodeByDevice.delete(deviceId);
@@ -68502,7 +68639,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68502
68639
  mimeType: image.mimeType
68503
68640
  })),
68504
68641
  jsonSchema: i.jsonSchema,
68505
- temperature: i.temperature
68642
+ temperature: i.temperature,
68643
+ ...i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : {}
68506
68644
  }),
68507
68645
  cancelVision: async (requestId) => {
68508
68646
  await api.llm.cancel.mutate({ requestId });
@@ -68797,6 +68935,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68797
68935
  this.nodeLiveness.clear();
68798
68936
  this.gatedNodeByDevice.clear();
68799
68937
  this.levelStateByDevice.clear();
68938
+ this.audioConfirmByDevice.clear();
68800
68939
  this.settingsCaches.clearAll();
68801
68940
  this.rawDeviceBlobMirror.invalidateAll();
68802
68941
  this.rawGlobalStoreMirror.invalidate();
@@ -70811,12 +70950,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70811
70950
  this.notificationCenter?.observeAudio(deviceId, {
70812
70951
  timestampMs: timestamp,
70813
70952
  ...level?.dbfs !== void 0 ? { dbfs: level.dbfs } : {},
70814
- labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass)
70953
+ labels: (frame.detections ?? []).filter((d) => d.score >= settings.classificationMinScore).map((d) => d.macroClass).filter((macroClass) => settings.audioAllowedClasses.length === 0 || settings.audioAllowedClasses.includes(macroClass))
70815
70954
  });
70816
70955
  const route = classifyAudioFrame(topClassification ? {
70817
70956
  className: topClassification.macroClass,
70818
70957
  score: topClassification.score
70819
- } : void 0, { classificationMinScore: settings.classificationMinScore });
70958
+ } : void 0, {
70959
+ classificationMinScore: settings.classificationMinScore,
70960
+ allowedClasses: settings.audioAllowedClasses
70961
+ });
70820
70962
  if (route.kind === "classification") {
70821
70963
  if (!topClassification) {
70822
70964
  this.ctx.logger.warn("classifyAudioFrame returned classification but topClassification is null", {
@@ -70825,6 +70967,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70825
70967
  });
70826
70968
  return;
70827
70969
  }
70970
+ const confirm = observeAudioConfirm(this.audioConfirmByDevice.get(deviceId) ?? emptyAudioConfirmState(), {
70971
+ className: route.className,
70972
+ timestampMs: timestamp
70973
+ }, {
70974
+ confirmHits: settings.audioConfirmHits,
70975
+ confirmWindowSec: settings.audioConfirmWindowSec
70976
+ });
70977
+ this.audioConfirmByDevice.set(deviceId, confirm.nextState);
70978
+ if (!confirm.confirmed) {
70979
+ this.ctx.logger.debug("audio event held for corroboration", {
70980
+ tags: { deviceId },
70981
+ meta: {
70982
+ deviceId,
70983
+ className: route.className,
70984
+ confirmHits: settings.audioConfirmHits,
70985
+ confirmWindowSec: settings.audioConfirmWindowSec
70986
+ }
70987
+ });
70988
+ return;
70989
+ }
70828
70990
  const classified = {
70829
70991
  deviceId,
70830
70992
  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-CUasEI3s.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C-Iu7OUr.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.167",
3
+ "version": "1.2.168",
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",