@camstack/addon-ai 0.4.98 → 0.4.99

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/addon.js CHANGED
@@ -5505,6 +5505,86 @@ ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5505
5505
  function number(params) {
5506
5506
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
5507
5507
  }
5508
+ //#endregion
5509
+ //#region ../types/dist/sleep-BnujYGPe.mjs
5510
+ /**
5511
+ * The audio chunk plane's byte format, and the ONE expansion from a coded
5512
+ * window to float samples (D455).
5513
+ *
5514
+ * ## Why a format at all
5515
+ *
5516
+ * D450 took the plane off its 8 → 16 kHz upsample: it carries the SOURCE
5517
+ * RATE, and the one consumer that needs 16 kHz resamples next to the model.
5518
+ * It left the FORMAT alone — the broker still turned each G.711 byte into a
5519
+ * 4-byte f32le sample before the bytes entered the transport, so every leg of
5520
+ * the plane carried four times the source. The plane crosses hub-main twice on
5521
+ * the way to the analyzer, and the fleet's G.711 cameras are ~79 % of it.
5522
+ *
5523
+ * So the plane carries the source BYTES too, and whoever needs floats expands
5524
+ * them where it needs them. That is the same argument D450 made for the rate,
5525
+ * one step further along the same wire.
5526
+ *
5527
+ * ## Why the expansion lives here
5528
+ *
5529
+ * Two packages need it and they must never disagree: `addon-pipeline`'s broker
5530
+ * (which still has to serve a subscriber that did NOT ask for coded bytes —
5531
+ * `AudioChunkPlane` expands per subscription) and
5532
+ * `addon-pipeline-orchestrator`'s `AudioWindowAccumulator` (which flushes an
5533
+ * f32le window to the analyzer cap, whose `AudioChunkInput` contract is
5534
+ * unchanged and stays f32le). Both bundle the bare `@camstack/types` entry
5535
+ * into their own dist (`self-contained` externals), so this travels with a
5536
+ * `camstack deploy` and needs no published server.
5537
+ *
5538
+ * A second μ-law table anywhere else is the defect this module exists to
5539
+ * prevent. (`stream-broker.ts`'s `mulawToPcm` / `alawToPcm` are the ENCODE
5540
+ * direction for the WebRTC egress — a different transform, not a copy.)
5541
+ *
5542
+ * ## Absent means f32le
5543
+ *
5544
+ * `format` is optional on the wire and its absence means `f32le` — today's
5545
+ * bytes, byte for byte. A peer that never heard of the field is served what it
5546
+ * has always been served, because the broker only emits a coded window to a
5547
+ * subscription that DECLARED it accepts one (`AudioSubscribeOptions.accept`).
5548
+ * That is the D448 `rawForward` negotiation, and it is what makes this
5549
+ * deployable one addon at a time across three nodes.
5550
+ */
5551
+ /** Every byte format the audio chunk plane can carry. `f32le` is the default. */
5552
+ var AUDIO_CHUNK_FORMATS = [
5553
+ "f32le",
5554
+ "pcmu",
5555
+ "pcma"
5556
+ ];
5557
+ /**
5558
+ * Build the μ-law decode table (ITU-T G.711). Each of the 256 byte values maps
5559
+ * to a 16-bit PCM sample, normalised to [-1.0, 1.0] for f32le output.
5560
+ *
5561
+ * Moved here verbatim from `audio-rtp-decoder.ts`, which no longer decodes:
5562
+ * it buffers the coded bytes and the plane's consumers expand.
5563
+ */
5564
+ function buildUlawTable() {
5565
+ const table = new Float32Array(256);
5566
+ for (let i = 0; i < 256; i++) {
5567
+ const complemented = ~i & 255;
5568
+ const sign = (complemented & 128) !== 0 ? -1 : 1;
5569
+ const exponent = complemented >> 4 & 7;
5570
+ table[i] = sign * ((8 * (complemented & 15) + 132 << exponent) - 132) / 32768;
5571
+ }
5572
+ return table;
5573
+ }
5574
+ /** Build the A-law decode table (ITU-T G.711). */
5575
+ function buildAlawTable() {
5576
+ const table = new Float32Array(256);
5577
+ for (let i = 0; i < 256; i++) {
5578
+ const xored = i ^ 85;
5579
+ const sign = (xored & 128) !== 0 ? 1 : -1;
5580
+ const exponent = xored >> 4 & 7;
5581
+ const mantissa = xored & 15;
5582
+ table[i] = sign * (exponent === 0 ? 16 * mantissa + 8 : 16 * mantissa + 264 << exponent - 1) / 32768;
5583
+ }
5584
+ return table;
5585
+ }
5586
+ buildUlawTable();
5587
+ buildAlawTable();
5508
5588
  Object.fromEntries([
5509
5589
  {
5510
5590
  id: "overview",
@@ -6797,11 +6877,20 @@ var SubscribeFramesResultSchema = object({
6797
6877
  * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
6798
6878
  * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
6799
6879
  */
6880
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
6800
6881
  var DecodedAudioChunkSchema = object({
6801
6882
  data: _instanceof(Uint8Array),
6802
6883
  sampleRate: number$1().int().positive(),
6803
6884
  channels: number$1().int().positive(),
6804
- timestamp: number$1()
6885
+ timestamp: number$1(),
6886
+ /**
6887
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
6888
+ * byte, for any peer that never heard of this field. A coded window
6889
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
6890
+ * subscription that DECLARED it accepts one, so absence can never mean
6891
+ * "coded bytes a consumer will read as floats" (D455).
6892
+ */
6893
+ format: AudioChunkFormatSchema.optional()
6805
6894
  });
6806
6895
  /**
6807
6896
  * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
@@ -6813,7 +6902,18 @@ var DecodedAudioChunkSchema = object({
6813
6902
  var SubscribeAudioChunksInputSchema = object({
6814
6903
  brokerId: string(),
6815
6904
  /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6816
- tag: string().optional()
6905
+ tag: string().optional(),
6906
+ /**
6907
+ * Byte formats this subscriber can READ, best first. The broker serves the
6908
+ * chunk's own format when it is in this list and expands to `f32le`
6909
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
6910
+ *
6911
+ * Absent (or without the source format) means `f32le` — the behaviour every
6912
+ * subscriber had before D455, unchanged. This is the negotiation half of
6913
+ * the source-bytes lever: it is what lets the broker and its consumers
6914
+ * deploy one at a time across three nodes.
6915
+ */
6916
+ accept: array(AudioChunkFormatSchema).readonly().optional()
6817
6917
  });
6818
6918
  /** Result of `stream-broker.subscribeAudioChunks`. */
6819
6919
  var SubscribeAudioChunksResultSchema = object({
@@ -10844,6 +10944,51 @@ var AudioAnalysisSettingsSchema = object({
10844
10944
  minConfidence: number$1().min(0).max(1).default(.3),
10845
10945
  allowedClasses: array(string()).default([])
10846
10946
  });
10947
+ /**
10948
+ * `attachDevice` — the analyzer PULLS a camera's audio from the broker (D461).
10949
+ *
10950
+ * Until D461 the orchestrator drained the broker's chunk plane, accumulated
10951
+ * ~1 s windows and pushed them back out as `analyseChunk`. It neither produced
10952
+ * nor consumed the audio: the PCM crossed hub-main twice for a process that
10953
+ * only buffered it. `attachDevice` inverts the direction — the analyzer opens
10954
+ * its own `subscribeAudioChunks` against the broker and the subscriber IS the
10955
+ * decoder, so the coded G.711 bytes D455 put on the plane stay coded all the
10956
+ * way to the one expansion that feeds the model.
10957
+ *
10958
+ * The orchestrator still owns the POLICY (the `audioMode` gate, the on-motion
10959
+ * window, the per-device node assignment, the settings read) and therefore
10960
+ * still owns the attach/detach pair. It no longer owns the bytes.
10961
+ */
10962
+ var AudioAttachDeviceInputSchema = object({
10963
+ deviceId: number$1(),
10964
+ /** Broker id (`<deviceId>/<camStreamId>`) carrying this camera's audio. */
10965
+ brokerId: string(),
10966
+ /**
10967
+ * `clusterRoles.ingestNode` — the node whose broker owns the source dial.
10968
+ * Every `streamBroker` call the attachment makes is pinned to it, exactly as
10969
+ * the orchestrator's poller pinned them before the move.
10970
+ */
10971
+ ingestNodeId: string(),
10972
+ /**
10973
+ * Resolved once by the orchestrator at attach time, exactly as it was read
10974
+ * once per subscription before D461. The analyzer does NOT re-resolve per
10975
+ * window: a settings change re-attaches, which is what always happened.
10976
+ */
10977
+ settings: AudioAnalysisSettingsSchema
10978
+ });
10979
+ var AudioAttachDeviceResultSchema = object({
10980
+ /** False only when the analyzer is shutting down and refused to attach. */
10981
+ attached: boolean(),
10982
+ /**
10983
+ * True when the attachment replaced a live one for the same device. An
10984
+ * attach is idempotent by REPLACEMENT — two pollers on one camera would
10985
+ * double the broker's fanout and neither would know about the other.
10986
+ */
10987
+ replaced: boolean()
10988
+ });
10989
+ var AudioDetachDeviceResultSchema = object({
10990
+ /** False when no attachment existed — detach is idempotent. */
10991
+ detached: boolean() });
10847
10992
  var AudioClassificationResultSchema = object({
10848
10993
  labels: array(AudioClassificationLabelSchema).readonly(),
10849
10994
  rawLabels: array(AudioClassificationLabelSchema).readonly().optional(),
@@ -10852,7 +10997,7 @@ var AudioClassificationResultSchema = object({
10852
10997
  method(object({
10853
10998
  chunk: AudioChunkInputSchema,
10854
10999
  settings: AudioAnalysisSettingsSchema
10855
- }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
11000
+ }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(AudioAttachDeviceInputSchema, AudioAttachDeviceResultSchema, { kind: "mutation" }), method(object({ deviceId: number$1() }), AudioDetachDeviceResultSchema, { kind: "mutation" }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
10856
11001
  kind: "mutation",
10857
11002
  auth: "admin"
10858
11003
  });
@@ -30841,12 +30986,24 @@ Object.freeze({
30841
30986
  addonId: null,
30842
30987
  access: "create"
30843
30988
  },
30989
+ "audioAnalyzer.attachDevice": {
30990
+ capName: "audio-analyzer",
30991
+ capScope: "system",
30992
+ addonId: null,
30993
+ access: "create"
30994
+ },
30844
30995
  "audioAnalyzer.classify": {
30845
30996
  capName: "audio-analyzer",
30846
30997
  capScope: "system",
30847
30998
  addonId: null,
30848
30999
  access: "view"
30849
31000
  },
31001
+ "audioAnalyzer.detachDevice": {
31002
+ capName: "audio-analyzer",
31003
+ capScope: "system",
31004
+ addonId: null,
31005
+ access: "create"
31006
+ },
30850
31007
  "audioAnalyzer.dispose": {
30851
31008
  capName: "audio-analyzer",
30852
31009
  capScope: "system",
@@ -36690,11 +36847,21 @@ Object.freeze({
36690
36847
  form: "single",
36691
36848
  optional: false
36692
36849
  }],
36850
+ "audioAnalyzer.attachDevice": [{
36851
+ name: "deviceId",
36852
+ form: "single",
36853
+ optional: false
36854
+ }],
36693
36855
  "audioAnalyzer.classify": [{
36694
36856
  name: "deviceId",
36695
36857
  form: "single",
36696
36858
  optional: true
36697
36859
  }],
36860
+ "audioAnalyzer.detachDevice": [{
36861
+ name: "deviceId",
36862
+ form: "single",
36863
+ optional: false
36864
+ }],
36698
36865
  "audioMetrics.getCurrentSnapshot": [{
36699
36866
  name: "deviceId",
36700
36867
  form: "single",
@@ -38546,6 +38713,52 @@ Object.freeze({
38546
38713
  "network-access": "ingress",
38547
38714
  "smtp-provider": "email"
38548
38715
  });
38716
+ var G711_SCALE_CORRECTION_DB = {
38717
+ PCMU: 20 * Math.log10(4),
38718
+ PCMA: 20 * Math.log10(8)
38719
+ };
38720
+ /**
38721
+ * Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
38722
+ * same intent on the ITU-T scale (D460).
38723
+ *
38724
+ * ## When this applies, and when it is the wrong thing to reach for
38725
+ *
38726
+ * An absolute-dBFS number in this repo is one of two things, and only one of
38727
+ * them converts:
38728
+ *
38729
+ * - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
38730
+ * is loud". It was true on the ITU-T scale before the epoch and it is true
38731
+ * after. The defect was never in the number; it was that 19 of this hub's
38732
+ * 25 cameras did not obey it. Converting such a number takes something
38733
+ * correct and makes it wrong, in order to preserve a bug.
38734
+ * - **A measurement taken through the old decoder** — a value someone read
38735
+ * off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
38736
+ * describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
38737
+ * louder. That is what this function is for.
38738
+ *
38739
+ * Telling the two apart is a question about PROVENANCE, not about arithmetic,
38740
+ * and it cannot be answered from the number. It is answered by the comment the
38741
+ * author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
38742
+ * mandatory.
38743
+ *
38744
+ * ## Why a function and not a typed-in number
38745
+ *
38746
+ * `-55 + 12.04` written into a source file is, six months later, completely
38747
+ * indistinguishable from a threshold somebody simply preferred. Calling this
38748
+ * keeps the derivation, the law, and the original measurement all visible at
38749
+ * the call site, so a future reader can disagree with the *premise* instead of
38750
+ * having to reverse-engineer the sum.
38751
+ *
38752
+ * **This is not a runtime gain.** It converts an authored CONSTANT once, where
38753
+ * it is declared. It must never be applied to a live sample or a stored
38754
+ * `AudioEvent.dbfs`: the decoder is correct now, and a second authority
38755
+ * adjusting numbers the decoder already got right is the original defect with
38756
+ * an extra place to argue with (D459).
38757
+ */
38758
+ function ituDbfsFromPreEpoch(law, authoredDbfs) {
38759
+ return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
38760
+ }
38761
+ Math.round(ituDbfsFromPreEpoch("PCMU", -55));
38549
38762
  /** Schema defaults — an untouched sub-field must author exactly these. */
38550
38763
  var NC_AUDIO_DEFAULTS = {
38551
38764
  hitPercent: 60,
package/dist/addon.mjs CHANGED
@@ -5532,6 +5532,86 @@ ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5532
5532
  function number(params) {
5533
5533
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
5534
5534
  }
5535
+ //#endregion
5536
+ //#region ../types/dist/sleep-BnujYGPe.mjs
5537
+ /**
5538
+ * The audio chunk plane's byte format, and the ONE expansion from a coded
5539
+ * window to float samples (D455).
5540
+ *
5541
+ * ## Why a format at all
5542
+ *
5543
+ * D450 took the plane off its 8 → 16 kHz upsample: it carries the SOURCE
5544
+ * RATE, and the one consumer that needs 16 kHz resamples next to the model.
5545
+ * It left the FORMAT alone — the broker still turned each G.711 byte into a
5546
+ * 4-byte f32le sample before the bytes entered the transport, so every leg of
5547
+ * the plane carried four times the source. The plane crosses hub-main twice on
5548
+ * the way to the analyzer, and the fleet's G.711 cameras are ~79 % of it.
5549
+ *
5550
+ * So the plane carries the source BYTES too, and whoever needs floats expands
5551
+ * them where it needs them. That is the same argument D450 made for the rate,
5552
+ * one step further along the same wire.
5553
+ *
5554
+ * ## Why the expansion lives here
5555
+ *
5556
+ * Two packages need it and they must never disagree: `addon-pipeline`'s broker
5557
+ * (which still has to serve a subscriber that did NOT ask for coded bytes —
5558
+ * `AudioChunkPlane` expands per subscription) and
5559
+ * `addon-pipeline-orchestrator`'s `AudioWindowAccumulator` (which flushes an
5560
+ * f32le window to the analyzer cap, whose `AudioChunkInput` contract is
5561
+ * unchanged and stays f32le). Both bundle the bare `@camstack/types` entry
5562
+ * into their own dist (`self-contained` externals), so this travels with a
5563
+ * `camstack deploy` and needs no published server.
5564
+ *
5565
+ * A second μ-law table anywhere else is the defect this module exists to
5566
+ * prevent. (`stream-broker.ts`'s `mulawToPcm` / `alawToPcm` are the ENCODE
5567
+ * direction for the WebRTC egress — a different transform, not a copy.)
5568
+ *
5569
+ * ## Absent means f32le
5570
+ *
5571
+ * `format` is optional on the wire and its absence means `f32le` — today's
5572
+ * bytes, byte for byte. A peer that never heard of the field is served what it
5573
+ * has always been served, because the broker only emits a coded window to a
5574
+ * subscription that DECLARED it accepts one (`AudioSubscribeOptions.accept`).
5575
+ * That is the D448 `rawForward` negotiation, and it is what makes this
5576
+ * deployable one addon at a time across three nodes.
5577
+ */
5578
+ /** Every byte format the audio chunk plane can carry. `f32le` is the default. */
5579
+ var AUDIO_CHUNK_FORMATS = [
5580
+ "f32le",
5581
+ "pcmu",
5582
+ "pcma"
5583
+ ];
5584
+ /**
5585
+ * Build the μ-law decode table (ITU-T G.711). Each of the 256 byte values maps
5586
+ * to a 16-bit PCM sample, normalised to [-1.0, 1.0] for f32le output.
5587
+ *
5588
+ * Moved here verbatim from `audio-rtp-decoder.ts`, which no longer decodes:
5589
+ * it buffers the coded bytes and the plane's consumers expand.
5590
+ */
5591
+ function buildUlawTable() {
5592
+ const table = new Float32Array(256);
5593
+ for (let i = 0; i < 256; i++) {
5594
+ const complemented = ~i & 255;
5595
+ const sign = (complemented & 128) !== 0 ? -1 : 1;
5596
+ const exponent = complemented >> 4 & 7;
5597
+ table[i] = sign * ((8 * (complemented & 15) + 132 << exponent) - 132) / 32768;
5598
+ }
5599
+ return table;
5600
+ }
5601
+ /** Build the A-law decode table (ITU-T G.711). */
5602
+ function buildAlawTable() {
5603
+ const table = new Float32Array(256);
5604
+ for (let i = 0; i < 256; i++) {
5605
+ const xored = i ^ 85;
5606
+ const sign = (xored & 128) !== 0 ? 1 : -1;
5607
+ const exponent = xored >> 4 & 7;
5608
+ const mantissa = xored & 15;
5609
+ table[i] = sign * (exponent === 0 ? 16 * mantissa + 8 : 16 * mantissa + 264 << exponent - 1) / 32768;
5610
+ }
5611
+ return table;
5612
+ }
5613
+ buildUlawTable();
5614
+ buildAlawTable();
5535
5615
  Object.fromEntries([
5536
5616
  {
5537
5617
  id: "overview",
@@ -6824,11 +6904,20 @@ var SubscribeFramesResultSchema = object({
6824
6904
  * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
6825
6905
  * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
6826
6906
  */
6907
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
6827
6908
  var DecodedAudioChunkSchema = object({
6828
6909
  data: _instanceof(Uint8Array),
6829
6910
  sampleRate: number$1().int().positive(),
6830
6911
  channels: number$1().int().positive(),
6831
- timestamp: number$1()
6912
+ timestamp: number$1(),
6913
+ /**
6914
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
6915
+ * byte, for any peer that never heard of this field. A coded window
6916
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
6917
+ * subscription that DECLARED it accepts one, so absence can never mean
6918
+ * "coded bytes a consumer will read as floats" (D455).
6919
+ */
6920
+ format: AudioChunkFormatSchema.optional()
6832
6921
  });
6833
6922
  /**
6834
6923
  * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
@@ -6840,7 +6929,18 @@ var DecodedAudioChunkSchema = object({
6840
6929
  var SubscribeAudioChunksInputSchema = object({
6841
6930
  brokerId: string(),
6842
6931
  /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6843
- tag: string().optional()
6932
+ tag: string().optional(),
6933
+ /**
6934
+ * Byte formats this subscriber can READ, best first. The broker serves the
6935
+ * chunk's own format when it is in this list and expands to `f32le`
6936
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
6937
+ *
6938
+ * Absent (or without the source format) means `f32le` — the behaviour every
6939
+ * subscriber had before D455, unchanged. This is the negotiation half of
6940
+ * the source-bytes lever: it is what lets the broker and its consumers
6941
+ * deploy one at a time across three nodes.
6942
+ */
6943
+ accept: array(AudioChunkFormatSchema).readonly().optional()
6844
6944
  });
6845
6945
  /** Result of `stream-broker.subscribeAudioChunks`. */
6846
6946
  var SubscribeAudioChunksResultSchema = object({
@@ -10871,6 +10971,51 @@ var AudioAnalysisSettingsSchema = object({
10871
10971
  minConfidence: number$1().min(0).max(1).default(.3),
10872
10972
  allowedClasses: array(string()).default([])
10873
10973
  });
10974
+ /**
10975
+ * `attachDevice` — the analyzer PULLS a camera's audio from the broker (D461).
10976
+ *
10977
+ * Until D461 the orchestrator drained the broker's chunk plane, accumulated
10978
+ * ~1 s windows and pushed them back out as `analyseChunk`. It neither produced
10979
+ * nor consumed the audio: the PCM crossed hub-main twice for a process that
10980
+ * only buffered it. `attachDevice` inverts the direction — the analyzer opens
10981
+ * its own `subscribeAudioChunks` against the broker and the subscriber IS the
10982
+ * decoder, so the coded G.711 bytes D455 put on the plane stay coded all the
10983
+ * way to the one expansion that feeds the model.
10984
+ *
10985
+ * The orchestrator still owns the POLICY (the `audioMode` gate, the on-motion
10986
+ * window, the per-device node assignment, the settings read) and therefore
10987
+ * still owns the attach/detach pair. It no longer owns the bytes.
10988
+ */
10989
+ var AudioAttachDeviceInputSchema = object({
10990
+ deviceId: number$1(),
10991
+ /** Broker id (`<deviceId>/<camStreamId>`) carrying this camera's audio. */
10992
+ brokerId: string(),
10993
+ /**
10994
+ * `clusterRoles.ingestNode` — the node whose broker owns the source dial.
10995
+ * Every `streamBroker` call the attachment makes is pinned to it, exactly as
10996
+ * the orchestrator's poller pinned them before the move.
10997
+ */
10998
+ ingestNodeId: string(),
10999
+ /**
11000
+ * Resolved once by the orchestrator at attach time, exactly as it was read
11001
+ * once per subscription before D461. The analyzer does NOT re-resolve per
11002
+ * window: a settings change re-attaches, which is what always happened.
11003
+ */
11004
+ settings: AudioAnalysisSettingsSchema
11005
+ });
11006
+ var AudioAttachDeviceResultSchema = object({
11007
+ /** False only when the analyzer is shutting down and refused to attach. */
11008
+ attached: boolean(),
11009
+ /**
11010
+ * True when the attachment replaced a live one for the same device. An
11011
+ * attach is idempotent by REPLACEMENT — two pollers on one camera would
11012
+ * double the broker's fanout and neither would know about the other.
11013
+ */
11014
+ replaced: boolean()
11015
+ });
11016
+ var AudioDetachDeviceResultSchema = object({
11017
+ /** False when no attachment existed — detach is idempotent. */
11018
+ detached: boolean() });
10874
11019
  var AudioClassificationResultSchema = object({
10875
11020
  labels: array(AudioClassificationLabelSchema).readonly(),
10876
11021
  rawLabels: array(AudioClassificationLabelSchema).readonly().optional(),
@@ -10879,7 +11024,7 @@ var AudioClassificationResultSchema = object({
10879
11024
  method(object({
10880
11025
  chunk: AudioChunkInputSchema,
10881
11026
  settings: AudioAnalysisSettingsSchema
10882
- }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
11027
+ }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(AudioAttachDeviceInputSchema, AudioAttachDeviceResultSchema, { kind: "mutation" }), method(object({ deviceId: number$1() }), AudioDetachDeviceResultSchema, { kind: "mutation" }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
10883
11028
  kind: "mutation",
10884
11029
  auth: "admin"
10885
11030
  });
@@ -30868,12 +31013,24 @@ Object.freeze({
30868
31013
  addonId: null,
30869
31014
  access: "create"
30870
31015
  },
31016
+ "audioAnalyzer.attachDevice": {
31017
+ capName: "audio-analyzer",
31018
+ capScope: "system",
31019
+ addonId: null,
31020
+ access: "create"
31021
+ },
30871
31022
  "audioAnalyzer.classify": {
30872
31023
  capName: "audio-analyzer",
30873
31024
  capScope: "system",
30874
31025
  addonId: null,
30875
31026
  access: "view"
30876
31027
  },
31028
+ "audioAnalyzer.detachDevice": {
31029
+ capName: "audio-analyzer",
31030
+ capScope: "system",
31031
+ addonId: null,
31032
+ access: "create"
31033
+ },
30877
31034
  "audioAnalyzer.dispose": {
30878
31035
  capName: "audio-analyzer",
30879
31036
  capScope: "system",
@@ -36717,11 +36874,21 @@ Object.freeze({
36717
36874
  form: "single",
36718
36875
  optional: false
36719
36876
  }],
36877
+ "audioAnalyzer.attachDevice": [{
36878
+ name: "deviceId",
36879
+ form: "single",
36880
+ optional: false
36881
+ }],
36720
36882
  "audioAnalyzer.classify": [{
36721
36883
  name: "deviceId",
36722
36884
  form: "single",
36723
36885
  optional: true
36724
36886
  }],
36887
+ "audioAnalyzer.detachDevice": [{
36888
+ name: "deviceId",
36889
+ form: "single",
36890
+ optional: false
36891
+ }],
36725
36892
  "audioMetrics.getCurrentSnapshot": [{
36726
36893
  name: "deviceId",
36727
36894
  form: "single",
@@ -38573,6 +38740,52 @@ Object.freeze({
38573
38740
  "network-access": "ingress",
38574
38741
  "smtp-provider": "email"
38575
38742
  });
38743
+ var G711_SCALE_CORRECTION_DB = {
38744
+ PCMU: 20 * Math.log10(4),
38745
+ PCMA: 20 * Math.log10(8)
38746
+ };
38747
+ /**
38748
+ * Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
38749
+ * same intent on the ITU-T scale (D460).
38750
+ *
38751
+ * ## When this applies, and when it is the wrong thing to reach for
38752
+ *
38753
+ * An absolute-dBFS number in this repo is one of two things, and only one of
38754
+ * them converts:
38755
+ *
38756
+ * - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
38757
+ * is loud". It was true on the ITU-T scale before the epoch and it is true
38758
+ * after. The defect was never in the number; it was that 19 of this hub's
38759
+ * 25 cameras did not obey it. Converting such a number takes something
38760
+ * correct and makes it wrong, in order to preserve a bug.
38761
+ * - **A measurement taken through the old decoder** — a value someone read
38762
+ * off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
38763
+ * describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
38764
+ * louder. That is what this function is for.
38765
+ *
38766
+ * Telling the two apart is a question about PROVENANCE, not about arithmetic,
38767
+ * and it cannot be answered from the number. It is answered by the comment the
38768
+ * author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
38769
+ * mandatory.
38770
+ *
38771
+ * ## Why a function and not a typed-in number
38772
+ *
38773
+ * `-55 + 12.04` written into a source file is, six months later, completely
38774
+ * indistinguishable from a threshold somebody simply preferred. Calling this
38775
+ * keeps the derivation, the law, and the original measurement all visible at
38776
+ * the call site, so a future reader can disagree with the *premise* instead of
38777
+ * having to reverse-engineer the sum.
38778
+ *
38779
+ * **This is not a runtime gain.** It converts an authored CONSTANT once, where
38780
+ * it is declared. It must never be applied to a live sample or a stored
38781
+ * `AudioEvent.dbfs`: the decoder is correct now, and a second authority
38782
+ * adjusting numbers the decoder already got right is the original defect with
38783
+ * an extra place to argue with (D459).
38784
+ */
38785
+ function ituDbfsFromPreEpoch(law, authoredDbfs) {
38786
+ return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
38787
+ }
38788
+ Math.round(ituDbfsFromPreEpoch("PCMU", -55));
38576
38789
  /** Schema defaults — an untouched sub-field must author exactly these. */
38577
38790
  var NC_AUDIO_DEFAULTS = {
38578
38791
  hitPercent: 60,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.98",
3
+ "version": "0.4.99",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",