@camstack/addon-post-analysis 1.2.75 → 1.2.76

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.
@@ -5427,12 +5427,6 @@ Object.fromEntries([
5427
5427
  icon: "shapes",
5428
5428
  order: 38
5429
5429
  },
5430
- {
5431
- id: "scenes",
5432
- label: "Scenes",
5433
- icon: "scan-eye",
5434
- order: 36
5435
- },
5436
5430
  {
5437
5431
  id: "analytics",
5438
5432
  label: "Analytics",
@@ -12706,6 +12700,17 @@ var LlmImageSchema = object({
12706
12700
  bytes: _instanceof(Uint8Array),
12707
12701
  mimeType: string()
12708
12702
  });
12703
+ /**
12704
+ * Retry policy. `enabled: false` is NOT the same as `maxAttempts: 1` in intent —
12705
+ * the flag is what a consumer table flips, the count is what the operator tunes.
12706
+ * A retry doubles the wall time of a call, so the two gates that run inside a
12707
+ * notification's budget keep it off (see `CONSUMER_RETRY_POLICY` in addon-ai).
12708
+ */
12709
+ var LlmRetryPolicySchema = object({
12710
+ enabled: boolean().default(false),
12711
+ /** Total attempts INCLUDING the first. 1 = no retry. */
12712
+ maxAttempts: number().int().min(1).max(5).default(1)
12713
+ });
12709
12714
  var LlmGenerateBaseInputSchema = object({
12710
12715
  /** Collection routing (the notification-output posture). */
12711
12716
  addonId: string().optional(),
@@ -12720,7 +12725,28 @@ var LlmGenerateBaseInputSchema = object({
12720
12725
  jsonSchema: record(string(), unknown()).optional(),
12721
12726
  /** Per-call override of the profile default. */
12722
12727
  maxTokens: number().int().positive().optional(),
12723
- temperature: number().optional()
12728
+ temperature: number().optional(),
12729
+ /** Per-call override of the profile default (nucleus sampling). */
12730
+ topP: number().min(0).max(1).optional(),
12731
+ /** Per-call override of the profile default (top-k sampling). */
12732
+ topK: number().int().positive().optional(),
12733
+ /** Per-call override of `profile.timeoutMs` — the total generation bound. */
12734
+ timeoutMs: number().int().positive().optional(),
12735
+ /** Per-call override; beats both the consumer table and the profile. */
12736
+ retry: LlmRetryPolicySchema.optional(),
12737
+ /**
12738
+ * Caller-minted id that makes this generation CANCELLABLE.
12739
+ *
12740
+ * Without it a caller that stops waiting cannot stop the work: the gates race
12741
+ * the call against 8 s and free their own slot when the timer wins, while the
12742
+ * generation upstream keeps running to `profile.timeoutMs` — 60 s by default,
12743
+ * on a single-threaded local model. The per-camera bound then counts WAITS,
12744
+ * not generations, and the real load is unbounded.
12745
+ *
12746
+ * `AbortSignal` cannot cross a process boundary; an id can. Pass one here and
12747
+ * `llm.cancel({ requestId })` tears the socket down.
12748
+ */
12749
+ requestId: string().optional()
12724
12750
  });
12725
12751
  /**
12726
12752
  * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
@@ -12759,8 +12785,49 @@ var ManagedRuntimeConfigSchema = object({
12759
12785
  gpuLayers: number().int().default(0),
12760
12786
  /** Default: cpus-2, clamped ≥1 (resolved node-side). */
12761
12787
  threads: number().int().optional(),
12762
- /** Concurrent slots. */
12788
+ /** Concurrent slots (`--parallel`). */
12763
12789
  parallel: number().int().default(1),
12790
+ /** Logical batch size (`-b`). Larger = faster prompt ingest, more RAM. */
12791
+ batchSize: number().int().positive().optional(),
12792
+ /** Physical batch / micro-batch (`-ub`). */
12793
+ ubatchSize: number().int().positive().optional(),
12794
+ /**
12795
+ * `--flash-attn`. Cuts KV-cache memory on the backends that implement it and
12796
+ * is a no-op elsewhere, so it is offered rather than assumed.
12797
+ */
12798
+ flashAttention: boolean().default(false),
12799
+ /**
12800
+ * `--mlock`. Pins the weights in RAM so the OS cannot page them out mid
12801
+ * inference. Costs the full model size in resident memory — which is exactly
12802
+ * what the RAM budget is counting.
12803
+ */
12804
+ mlock: boolean().default(false),
12805
+ /**
12806
+ * `--no-mmap`. Reads the whole GGUF up front instead of mapping it. Slower to
12807
+ * start, but avoids the page-fault stalls a network or spinning-disk model
12808
+ * store produces on every first token.
12809
+ */
12810
+ noMmap: boolean().default(false),
12811
+ /** `--cache-type-k` / `--cache-type-v` — quantising the KV cache is the
12812
+ * cheapest way to fit a longer context in the same RAM. */
12813
+ cacheTypeK: _enum([
12814
+ "f32",
12815
+ "f16",
12816
+ "q8_0",
12817
+ "q5_1",
12818
+ "q5_0",
12819
+ "q4_1",
12820
+ "q4_0"
12821
+ ]).optional(),
12822
+ cacheTypeV: _enum([
12823
+ "f32",
12824
+ "f16",
12825
+ "q8_0",
12826
+ "q5_1",
12827
+ "q5_0",
12828
+ "q4_1",
12829
+ "q4_0"
12830
+ ]).optional(),
12764
12831
  /** Else lazy: first generate boots it. */
12765
12832
  autoStart: boolean().default(false),
12766
12833
  /** 0 = never; frees RAM after quiet periods. */
@@ -12848,10 +12915,44 @@ var LlmProfileSchema = object({
12848
12915
  baseUrl: string().optional(),
12849
12916
  /** ConfigUISchema type:'password' — never round-trips (spec §5). */
12850
12917
  apiKey: string().optional(),
12918
+ /** Vision on/off. A vision call against a `false` profile is REFUSED, never
12919
+ * degraded to text — that shipped once and produced a confident answer to a
12920
+ * question about a picture nobody sent. */
12851
12921
  supportsVision: boolean(),
12852
12922
  temperature: number().min(0).max(2).optional(),
12923
+ /** Nucleus sampling. Every wire we speak has it. */
12924
+ topP: number().min(0).max(1).optional(),
12925
+ /** Top-k sampling. Carried only by the wires that have it — NEITHER OpenAI
12926
+ * wire does, and the client drops it there (measured: the request body gets
12927
+ * `top_p` and no `top_k`). The profile editor hides the field wherever it
12928
+ * would change nothing; `KINDS_WITH_TOP_K` is the single owner of that list. */
12929
+ topK: number().int().positive().optional(),
12853
12930
  maxTokens: number().int().positive().optional(),
12931
+ /** Prompt context window. Advisory for cloud kinds (they enforce their own);
12932
+ * for `managed-local` it is the llama.cpp `--ctx-size` the runtime starts
12933
+ * the model with, so it is the one field that changes a PROCESS. */
12934
+ contextLength: number().int().positive().optional(),
12935
+ /** Default system prompt. A caller's `system` REPLACES it (never appends —
12936
+ * two system prompts fighting is worse than either alone). */
12937
+ systemPrompt: string().optional(),
12938
+ /** Total generation bound — the only one a unary call has. */
12854
12939
  timeoutMs: number().int().positive().default(6e4),
12940
+ /** Wait for response headers only. */
12941
+ connectTimeoutMs: number().int().positive().default(1e4),
12942
+ /** Accepted, but no output yet — a cold GPU load lives here. */
12943
+ firstTokenTimeoutMs: number().int().positive().default(12e4),
12944
+ /** Output started then stopped. */
12945
+ idleTimeoutMs: number().int().positive().default(6e4),
12946
+ /** Profile-level default. The per-consumer table and a per-call override
12947
+ * both beat it — see `resolveRetryPolicy`. */
12948
+ retry: LlmRetryPolicySchema.default({
12949
+ enabled: false,
12950
+ maxAttempts: 1
12951
+ }),
12952
+ /** Whether this profile may use tools. The tool-call plumbing rides the
12953
+ * library; the REGISTRY of callable tools is ours and is empty in v1, so a
12954
+ * `true` here buys the wiring, not behaviour, until tools are registered. */
12955
+ toolsEnabled: boolean().default(false),
12855
12956
  extraHeaders: record(string(), string()).optional(),
12856
12957
  /** kind === 'managed-local' only (spec §4). */
12857
12958
  runtime: ManagedRuntimeConfigSchema.optional()
@@ -12913,7 +13014,10 @@ var ProfileRefInputSchema = object({
12913
13014
  addonId: string(),
12914
13015
  profileId: string()
12915
13016
  });
12916
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
13017
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({
13018
+ addonId: string().optional(),
13019
+ requestId: string()
13020
+ }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
12917
13021
  kind: "mutation",
12918
13022
  auth: "admin"
12919
13023
  }), method(ProfileRefInputSchema, _void(), {
@@ -14755,28 +14859,36 @@ var NcOccupancyConditionSchema = object({
14755
14859
  /**
14756
14860
  * Audio condition (IMMEDIATE trigger) — a rule on SOUND, not on a picture.
14757
14861
  *
14758
- * Operator-approved vocabulary (2026-08-12, option A — the same one the
14759
- * reference notifier uses, so an operator moving between them re-uses what
14760
- * they already know): a rule matches when, over a sampling window of
14761
- * `samplingSeconds`, at least `hitPercent`% of the audio samples in that
14762
- * window are HITS. A sample is a hit when it satisfies BOTH present filters:
14763
- *
14764
- * - `dbThreshold` — its level is at or above this many dBFS (see
14765
- * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale);
14766
- * - `labels` — the classifier put at least one of these labels on it.
14767
- *
14768
- * Both are OPTIONAL and independent, which is the point of the shape: a
14769
- * loudness rule ("something loud at 3am") needs no model to be right, and a
14770
- * label rule ("a dog barked") needs no threshold. **Fail-closed when NEITHER
14771
- * is given** — a window in which every sample is trivially a hit would fire on
14772
- * silence, so the engine refuses such a condition rather than notifying on
14773
- * nothing (the schema cannot express "at least one of" without becoming a
14774
- * ZodEffects the cap path would have to special-case).
14775
- *
14776
- * `hitPercent` is over the samples the window actually HOLDS, and the window
14777
- * must be FULL before it can match — a window that has been open for two
14778
- * seconds of its ten is 100% of nothing, and firing on it would make
14779
- * `samplingSeconds` decorative.
14862
+ * **TWO EXCLUSIVE MODES** (operator decision 2026-08-14, D157). Which one a
14863
+ * rule is in is not a stored field — it is WHICH FILTER the rule carries, so
14864
+ * there is no second switch that can disagree with the first and every rule
14865
+ * authored before the decision migrates for free (`audioModeOf`):
14866
+ *
14867
+ * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14868
+ * classifier labels with one of them. No window, no percentage:
14869
+ * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14870
+ * `throttle` cooldown is the only brake. The per-label confidence floor is
14871
+ * the analyzer's (`classificationMinScore`, per device) — a label only
14872
+ * reaches this condition if the classifier was already confident enough.
14873
+ * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14874
+ * the condition: at least `hitPercent`% of the samples over
14875
+ * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
14876
+ * {@link NC_AUDIO_DBFS_FLOOR}: negative-going, `0` = full scale). The window
14877
+ * must be FULL before it can match — a window open for two of its ten
14878
+ * seconds is 100% of nothing.
14879
+ *
14880
+ * **Why label mode has no window.** It had one, and it never fired: the
14881
+ * analyzer emits ~1 audio frame per second but YAMNet only LABELS one to three
14882
+ * of them per episode, even through continuous crying. The measured maximum
14883
+ * `hitPercent` over the whole live history was 40 — under the shipped default
14884
+ * of 60, so a label rule could not fire at all, ever. A percentage of frames is
14885
+ * the wrong question to ask of a sparse classifier.
14886
+ *
14887
+ * **Fail-closed when NEITHER is given** — every sample would be a trivial hit
14888
+ * and the rule would fire on silence. The schema cannot express "exactly one
14889
+ * of" without becoming a ZodEffects the cap path would have to special-case, so
14890
+ * the exclusivity is enforced where every editor writes (`patchAudio`) and a
14891
+ * legacy rule carrying both resolves to LABEL (the mode that fires).
14780
14892
  *
14781
14893
  * Labels are the audio macro classes (`AUDIO_MACRO_LABELS` / the NC taxonomy's
14782
14894
  * `audio-*` ids). Both spellings are accepted — the matcher normalizes the
@@ -14784,13 +14896,13 @@ var NcOccupancyConditionSchema = object({
14784
14896
  * an operator who typed `dog` mean the same thing.
14785
14897
  */
14786
14898
  var NcAudioConditionSchema = object({
14787
- /** Audio macro labels; absent = any sound (level-only rule). */
14899
+ /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14788
14900
  labels: array(string().min(1)).min(1).optional(),
14789
- /** Level floor in dBFS (negative-going, `0` = full scale); absent = any level. */
14901
+ /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14790
14902
  dbThreshold: number().min(-96).max(0).optional(),
14791
- /** Percentage of the window's samples that must be hits (1–100). */
14903
+ /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14792
14904
  hitPercent: number().int().min(1).max(100).default(60),
14793
- /** Length of the sampling window in seconds. */
14905
+ /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14794
14906
  samplingSeconds: number().int().min(1).max(300).default(10)
14795
14907
  });
14796
14908
  /**
@@ -27505,10 +27617,22 @@ method(object({
27505
27617
  * thing except the comparator: `similarity` (CLIP cosine at the same ROI coords
27506
27618
  * vs condition-tagged references) and `llm` (vision-LLM judgment over the crop).
27507
27619
  *
27508
- * D14 device-config archetype (`deviceConfig.ui.kind:'widget'`) — the framework
27509
- * derives the device-detail contribution; the provider carries NO hand-written
27510
- * settings-contribution methods. `status.kind:'push'` — the engine pushes on
27511
- * every hysteresis flip / availability change; consumers never poll.
27620
+ * **No `deviceConfig`, deliberately.** This shipped as the D14 widget archetype,
27621
+ * which put a "Scenes" tab on one camera's detail page. That is the wrong shape
27622
+ * for the thing: a scene is a standing question about the property ("is the bin
27623
+ * still out"), and the operator's question is "which of my scenes have tripped",
27624
+ * across every camera at once — not "what does camera 617 think". Buried one
27625
+ * camera deep it also could not be found. The surface is now a top-level admin
27626
+ * page (`/scenes`, `pages/Scenes.tsx`) that lists every scene on every camera and
27627
+ * picks the camera inside the create flow, the same shape Events and Faces have.
27628
+ *
27629
+ * The consequence to keep in mind: `host/scene-monitor-editor` is gone from
27630
+ * `HOST_WIDGETS` too. `scripts/check-host-widget-resolves.ts` asserts BOTH
27631
+ * directions, so a registration nobody declares fails exactly as loudly as a
27632
+ * declaration nobody registers. The editor is imported directly by the page.
27633
+ *
27634
+ * `status.kind:'push'` — the engine pushes on every hysteresis flip /
27635
+ * availability change; consumers never poll.
27512
27636
  */
27513
27637
  /** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
27514
27638
  * captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
@@ -27663,12 +27787,6 @@ var sceneMonitorCapability = {
27663
27787
  kind: "wrapper",
27664
27788
  defaultActive: true,
27665
27789
  deviceTypes: [DeviceType.Camera],
27666
- deviceConfig: { ui: {
27667
- kind: "widget",
27668
- widgetId: "host/scene-monitor-editor",
27669
- tab: "scenes",
27670
- label: "Scenes"
27671
- } },
27672
27790
  methods: {
27673
27791
  listScenes: method(object({ deviceId: number() }), SceneMonitorStatusSchema),
27674
27792
  createScene: method(object({
@@ -28257,12 +28375,64 @@ var NetworkAddressSchema = object({
28257
28375
  family: string(),
28258
28376
  internal: boolean()
28259
28377
  });
28378
+ /**
28379
+ * Provenance of the site coordinates, and the whole reason this is not just two
28380
+ * numbers.
28381
+ *
28382
+ * - `operator-set` — a human typed it, or accepted a detection. Authoritative;
28383
+ * nothing overwrites it.
28384
+ * - `derived-from-ip` — the hub geolocated its own public IP once, because a
28385
+ * default that is right to a few kilometres beats the coarse UTC clock split
28386
+ * the sun-times consumers otherwise fall back to.
28387
+ *
28388
+ * The UI shows which one it is. An operator who cannot tell a guess from their
28389
+ * own input will eventually trust the guess.
28390
+ */
28391
+ var SiteLocationSourceSchema = _enum(["operator-set", "derived-from-ip"]);
28392
+ /**
28393
+ * The read shape: the location plus the honest state of the one-shot derivation.
28394
+ *
28395
+ * `derivationAttemptedAt` is what makes the "one call, ever" contract
28396
+ * inspectable. When it is set and `location` is null, the geo-IP lookup ran and
28397
+ * failed; the hub will NOT try again on its own — the fallback is declared
28398
+ * (consumers degrade to their own last resort) and the operator either types the
28399
+ * coordinates or presses detect.
28400
+ */
28401
+ var SiteLocationStatusSchema = object({
28402
+ location: object({
28403
+ /** WGS84 decimal degrees. */
28404
+ latitude: number().min(-90).max(90),
28405
+ longitude: number().min(-180).max(180),
28406
+ source: SiteLocationSourceSchema,
28407
+ /** Epoch ms the value was last written. */
28408
+ updatedAt: number(),
28409
+ /**
28410
+ * Human-readable place the geo-IP service reported ("Napoli, IT"). Display
28411
+ * only — never parsed, never matched on. Absent for an operator-typed value.
28412
+ */
28413
+ label: string().optional()
28414
+ }).nullable(),
28415
+ derivationAttemptedAt: number().nullable(),
28416
+ /** Why the last derivation failed, for the UI to show instead of a shrug. */
28417
+ derivationError: string().nullable()
28418
+ });
28419
+ /** `null` clears the location and re-arms nothing — the derivation stays spent. */
28420
+ var SetSiteLocationInputSchema = object({
28421
+ latitude: number().min(-90).max(90),
28422
+ longitude: number().min(-180).max(180)
28423
+ }).nullable();
28260
28424
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
28261
28425
  kind: "mutation",
28262
28426
  auth: "admin"
28263
28427
  }), method(_void(), _void(), {
28264
28428
  kind: "mutation",
28265
28429
  auth: "admin"
28430
+ }), method(_void(), SiteLocationStatusSchema), method(SetSiteLocationInputSchema, SiteLocationStatusSchema, {
28431
+ kind: "mutation",
28432
+ auth: "admin"
28433
+ }), method(_void(), SiteLocationStatusSchema, {
28434
+ kind: "mutation",
28435
+ auth: "admin"
28266
28436
  });
28267
28437
  /**
28268
28438
  * Tamper / case-open detection sensor. Drives Home Assistant
@@ -32451,6 +32621,12 @@ Object.freeze({
32451
32621
  addonId: null,
32452
32622
  access: "create"
32453
32623
  },
32624
+ "llm.cancel": {
32625
+ capName: "llm",
32626
+ capScope: "system",
32627
+ addonId: null,
32628
+ access: "create"
32629
+ },
32454
32630
  "llm.deleteModel": {
32455
32631
  capName: "llm",
32456
32632
  capScope: "system",
@@ -35385,6 +35561,12 @@ Object.freeze({
35385
35561
  addonId: null,
35386
35562
  access: "create"
35387
35563
  },
35564
+ "system.detectSiteLocation": {
35565
+ capName: "system",
35566
+ capScope: "system",
35567
+ addonId: null,
35568
+ access: "create"
35569
+ },
35388
35570
  "system.featureFlags": {
35389
35571
  capName: "system",
35390
35572
  capScope: "system",
@@ -35403,6 +35585,12 @@ Object.freeze({
35403
35585
  addonId: null,
35404
35586
  access: "view"
35405
35587
  },
35588
+ "system.getSiteLocation": {
35589
+ capName: "system",
35590
+ capScope: "system",
35591
+ addonId: null,
35592
+ access: "view"
35593
+ },
35406
35594
  "system.health": {
35407
35595
  capName: "system",
35408
35596
  capScope: "system",
@@ -35427,6 +35615,12 @@ Object.freeze({
35427
35615
  addonId: null,
35428
35616
  access: "create"
35429
35617
  },
35618
+ "system.setSiteLocation": {
35619
+ capName: "system",
35620
+ capScope: "system",
35621
+ addonId: null,
35622
+ access: "create"
35623
+ },
35430
35624
  "terminalSession.adoptLegacyMonitor": {
35431
35625
  capName: "terminal-session",
35432
35626
  capScope: "system",
@@ -37684,6 +37878,26 @@ Object.freeze({
37684
37878
  "network-access": "ingress",
37685
37879
  "smtp-provider": "email"
37686
37880
  });
37881
+ /**
37882
+ * Which mode this condition is in — the ONE place that question is answered.
37883
+ *
37884
+ * The mode is NOT a stored field, deliberately. It is which filter the rule
37885
+ * carries, so every rule authored before the modes existed migrates for free
37886
+ * and there is no second switch that can disagree with the first (the failure
37887
+ * this repo has shipped twice). `null` = neither filter, which the engine
37888
+ * refuses (see {@link audioIsFailClosed}).
37889
+ *
37890
+ * A LEGACY rule carrying BOTH resolves to `label`: it is the mode that fires,
37891
+ * and the alternative is silently keeping a window the operator can no longer
37892
+ * see in the editor. Nothing new can reach this branch — {@link patchAudio}
37893
+ * clears the other filter on every write.
37894
+ */
37895
+ function audioModeOf(value) {
37896
+ if (value === void 0) return null;
37897
+ if (value.labels !== void 0 && value.labels.length > 0) return "label";
37898
+ if (value.dbThreshold !== void 0) return "level";
37899
+ return null;
37900
+ }
37687
37901
  new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
37688
37902
  new Set(["devices", "classes"]);
37689
37903
  var WEEKDAY_TO_DAY = {
@@ -38522,6 +38736,12 @@ Object.defineProperty(exports, "audioMetricsCapability", {
38522
38736
  return audioMetricsCapability;
38523
38737
  }
38524
38738
  });
38739
+ Object.defineProperty(exports, "audioModeOf", {
38740
+ enumerable: true,
38741
+ get: function() {
38742
+ return audioModeOf;
38743
+ }
38744
+ });
38525
38745
  Object.defineProperty(exports, "boolean", {
38526
38746
  enumerable: true,
38527
38747
  get: function() {