@camstack/addon-ai 0.4.98 → 0.4.100

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
  });
@@ -20482,6 +20627,14 @@ var NativeCropResultSchema = object({
20482
20627
  * set `encodeJpeg: true`; `bytes` is then absent.
20483
20628
  */
20484
20629
  jpeg: string().optional(),
20630
+ /**
20631
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20632
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20633
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20634
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20635
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20636
+ */
20637
+ jpegBytes: _instanceof(Uint8Array).optional(),
20485
20638
  width: number$1().int().positive(),
20486
20639
  height: number$1().int().positive(),
20487
20640
  /**
@@ -20548,7 +20701,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20548
20701
  })]);
20549
20702
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20550
20703
  var ParkedTrackFrameSchema = object({
20551
- jpeg: string(),
20704
+ /**
20705
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20706
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20707
+ * Exactly one of the two is present.
20708
+ */
20709
+ jpeg: string().optional(),
20710
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20711
+ jpegBytes: _instanceof(Uint8Array).optional(),
20552
20712
  width: number$1().int().positive(),
20553
20713
  height: number$1().int().positive(),
20554
20714
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21135,6 +21295,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21135
21295
  bbox: NativeCropBboxSchema,
21136
21296
  maxWidth: number$1().int().positive().optional(),
21137
21297
  /**
21298
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21299
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21300
+ * wire — never assume consent: a pre-D462 caller parses the field as
21301
+ * base64 and bytes would decode to garbage rather than fail.
21302
+ */
21303
+ acceptJpegBytes: boolean().optional(),
21304
+ /**
21138
21305
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21139
21306
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21140
21307
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21202,7 +21369,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21202
21369
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21203
21370
  deviceId: number$1(),
21204
21371
  trackId: string(),
21205
- kind: ParkedFrameKindSchema
21372
+ kind: ParkedFrameKindSchema,
21373
+ /**
21374
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21375
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21376
+ * wire — never assume consent: a pre-D462 caller parses the field as
21377
+ * base64 and bytes would decode to garbage rather than fail.
21378
+ */
21379
+ acceptJpegBytes: boolean().optional()
21206
21380
  }), ParkedTrackFrameSchema.nullable()), method(object({
21207
21381
  deviceId: number$1(),
21208
21382
  trackId: string()
@@ -30841,12 +31015,24 @@ Object.freeze({
30841
31015
  addonId: null,
30842
31016
  access: "create"
30843
31017
  },
31018
+ "audioAnalyzer.attachDevice": {
31019
+ capName: "audio-analyzer",
31020
+ capScope: "system",
31021
+ addonId: null,
31022
+ access: "create"
31023
+ },
30844
31024
  "audioAnalyzer.classify": {
30845
31025
  capName: "audio-analyzer",
30846
31026
  capScope: "system",
30847
31027
  addonId: null,
30848
31028
  access: "view"
30849
31029
  },
31030
+ "audioAnalyzer.detachDevice": {
31031
+ capName: "audio-analyzer",
31032
+ capScope: "system",
31033
+ addonId: null,
31034
+ access: "create"
31035
+ },
30850
31036
  "audioAnalyzer.dispose": {
30851
31037
  capName: "audio-analyzer",
30852
31038
  capScope: "system",
@@ -36690,11 +36876,21 @@ Object.freeze({
36690
36876
  form: "single",
36691
36877
  optional: false
36692
36878
  }],
36879
+ "audioAnalyzer.attachDevice": [{
36880
+ name: "deviceId",
36881
+ form: "single",
36882
+ optional: false
36883
+ }],
36693
36884
  "audioAnalyzer.classify": [{
36694
36885
  name: "deviceId",
36695
36886
  form: "single",
36696
36887
  optional: true
36697
36888
  }],
36889
+ "audioAnalyzer.detachDevice": [{
36890
+ name: "deviceId",
36891
+ form: "single",
36892
+ optional: false
36893
+ }],
36698
36894
  "audioMetrics.getCurrentSnapshot": [{
36699
36895
  name: "deviceId",
36700
36896
  form: "single",
@@ -38546,6 +38742,52 @@ Object.freeze({
38546
38742
  "network-access": "ingress",
38547
38743
  "smtp-provider": "email"
38548
38744
  });
38745
+ var G711_SCALE_CORRECTION_DB = {
38746
+ PCMU: 20 * Math.log10(4),
38747
+ PCMA: 20 * Math.log10(8)
38748
+ };
38749
+ /**
38750
+ * Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
38751
+ * same intent on the ITU-T scale (D460).
38752
+ *
38753
+ * ## When this applies, and when it is the wrong thing to reach for
38754
+ *
38755
+ * An absolute-dBFS number in this repo is one of two things, and only one of
38756
+ * them converts:
38757
+ *
38758
+ * - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
38759
+ * is loud". It was true on the ITU-T scale before the epoch and it is true
38760
+ * after. The defect was never in the number; it was that 19 of this hub's
38761
+ * 25 cameras did not obey it. Converting such a number takes something
38762
+ * correct and makes it wrong, in order to preserve a bug.
38763
+ * - **A measurement taken through the old decoder** — a value someone read
38764
+ * off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
38765
+ * describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
38766
+ * louder. That is what this function is for.
38767
+ *
38768
+ * Telling the two apart is a question about PROVENANCE, not about arithmetic,
38769
+ * and it cannot be answered from the number. It is answered by the comment the
38770
+ * author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
38771
+ * mandatory.
38772
+ *
38773
+ * ## Why a function and not a typed-in number
38774
+ *
38775
+ * `-55 + 12.04` written into a source file is, six months later, completely
38776
+ * indistinguishable from a threshold somebody simply preferred. Calling this
38777
+ * keeps the derivation, the law, and the original measurement all visible at
38778
+ * the call site, so a future reader can disagree with the *premise* instead of
38779
+ * having to reverse-engineer the sum.
38780
+ *
38781
+ * **This is not a runtime gain.** It converts an authored CONSTANT once, where
38782
+ * it is declared. It must never be applied to a live sample or a stored
38783
+ * `AudioEvent.dbfs`: the decoder is correct now, and a second authority
38784
+ * adjusting numbers the decoder already got right is the original defect with
38785
+ * an extra place to argue with (D459).
38786
+ */
38787
+ function ituDbfsFromPreEpoch(law, authoredDbfs) {
38788
+ return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
38789
+ }
38790
+ Math.round(ituDbfsFromPreEpoch("PCMU", -55));
38549
38791
  /** Schema defaults — an untouched sub-field must author exactly these. */
38550
38792
  var NC_AUDIO_DEFAULTS = {
38551
38793
  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
  });
@@ -20509,6 +20654,14 @@ var NativeCropResultSchema = object({
20509
20654
  * set `encodeJpeg: true`; `bytes` is then absent.
20510
20655
  */
20511
20656
  jpeg: string().optional(),
20657
+ /**
20658
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20659
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20660
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20661
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20662
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20663
+ */
20664
+ jpegBytes: _instanceof(Uint8Array).optional(),
20512
20665
  width: number$1().int().positive(),
20513
20666
  height: number$1().int().positive(),
20514
20667
  /**
@@ -20575,7 +20728,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20575
20728
  })]);
20576
20729
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20577
20730
  var ParkedTrackFrameSchema = object({
20578
- jpeg: string(),
20731
+ /**
20732
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20733
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20734
+ * Exactly one of the two is present.
20735
+ */
20736
+ jpeg: string().optional(),
20737
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20738
+ jpegBytes: _instanceof(Uint8Array).optional(),
20579
20739
  width: number$1().int().positive(),
20580
20740
  height: number$1().int().positive(),
20581
20741
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21162,6 +21322,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21162
21322
  bbox: NativeCropBboxSchema,
21163
21323
  maxWidth: number$1().int().positive().optional(),
21164
21324
  /**
21325
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21326
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21327
+ * wire — never assume consent: a pre-D462 caller parses the field as
21328
+ * base64 and bytes would decode to garbage rather than fail.
21329
+ */
21330
+ acceptJpegBytes: boolean().optional(),
21331
+ /**
21165
21332
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21166
21333
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21167
21334
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21229,7 +21396,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21229
21396
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21230
21397
  deviceId: number$1(),
21231
21398
  trackId: string(),
21232
- kind: ParkedFrameKindSchema
21399
+ kind: ParkedFrameKindSchema,
21400
+ /**
21401
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21402
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21403
+ * wire — never assume consent: a pre-D462 caller parses the field as
21404
+ * base64 and bytes would decode to garbage rather than fail.
21405
+ */
21406
+ acceptJpegBytes: boolean().optional()
21233
21407
  }), ParkedTrackFrameSchema.nullable()), method(object({
21234
21408
  deviceId: number$1(),
21235
21409
  trackId: string()
@@ -30868,12 +31042,24 @@ Object.freeze({
30868
31042
  addonId: null,
30869
31043
  access: "create"
30870
31044
  },
31045
+ "audioAnalyzer.attachDevice": {
31046
+ capName: "audio-analyzer",
31047
+ capScope: "system",
31048
+ addonId: null,
31049
+ access: "create"
31050
+ },
30871
31051
  "audioAnalyzer.classify": {
30872
31052
  capName: "audio-analyzer",
30873
31053
  capScope: "system",
30874
31054
  addonId: null,
30875
31055
  access: "view"
30876
31056
  },
31057
+ "audioAnalyzer.detachDevice": {
31058
+ capName: "audio-analyzer",
31059
+ capScope: "system",
31060
+ addonId: null,
31061
+ access: "create"
31062
+ },
30877
31063
  "audioAnalyzer.dispose": {
30878
31064
  capName: "audio-analyzer",
30879
31065
  capScope: "system",
@@ -36717,11 +36903,21 @@ Object.freeze({
36717
36903
  form: "single",
36718
36904
  optional: false
36719
36905
  }],
36906
+ "audioAnalyzer.attachDevice": [{
36907
+ name: "deviceId",
36908
+ form: "single",
36909
+ optional: false
36910
+ }],
36720
36911
  "audioAnalyzer.classify": [{
36721
36912
  name: "deviceId",
36722
36913
  form: "single",
36723
36914
  optional: true
36724
36915
  }],
36916
+ "audioAnalyzer.detachDevice": [{
36917
+ name: "deviceId",
36918
+ form: "single",
36919
+ optional: false
36920
+ }],
36725
36921
  "audioMetrics.getCurrentSnapshot": [{
36726
36922
  name: "deviceId",
36727
36923
  form: "single",
@@ -38573,6 +38769,52 @@ Object.freeze({
38573
38769
  "network-access": "ingress",
38574
38770
  "smtp-provider": "email"
38575
38771
  });
38772
+ var G711_SCALE_CORRECTION_DB = {
38773
+ PCMU: 20 * Math.log10(4),
38774
+ PCMA: 20 * Math.log10(8)
38775
+ };
38776
+ /**
38777
+ * Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
38778
+ * same intent on the ITU-T scale (D460).
38779
+ *
38780
+ * ## When this applies, and when it is the wrong thing to reach for
38781
+ *
38782
+ * An absolute-dBFS number in this repo is one of two things, and only one of
38783
+ * them converts:
38784
+ *
38785
+ * - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
38786
+ * is loud". It was true on the ITU-T scale before the epoch and it is true
38787
+ * after. The defect was never in the number; it was that 19 of this hub's
38788
+ * 25 cameras did not obey it. Converting such a number takes something
38789
+ * correct and makes it wrong, in order to preserve a bug.
38790
+ * - **A measurement taken through the old decoder** — a value someone read
38791
+ * off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
38792
+ * describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
38793
+ * louder. That is what this function is for.
38794
+ *
38795
+ * Telling the two apart is a question about PROVENANCE, not about arithmetic,
38796
+ * and it cannot be answered from the number. It is answered by the comment the
38797
+ * author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
38798
+ * mandatory.
38799
+ *
38800
+ * ## Why a function and not a typed-in number
38801
+ *
38802
+ * `-55 + 12.04` written into a source file is, six months later, completely
38803
+ * indistinguishable from a threshold somebody simply preferred. Calling this
38804
+ * keeps the derivation, the law, and the original measurement all visible at
38805
+ * the call site, so a future reader can disagree with the *premise* instead of
38806
+ * having to reverse-engineer the sum.
38807
+ *
38808
+ * **This is not a runtime gain.** It converts an authored CONSTANT once, where
38809
+ * it is declared. It must never be applied to a live sample or a stored
38810
+ * `AudioEvent.dbfs`: the decoder is correct now, and a second authority
38811
+ * adjusting numbers the decoder already got right is the original defect with
38812
+ * an extra place to argue with (D459).
38813
+ */
38814
+ function ituDbfsFromPreEpoch(law, authoredDbfs) {
38815
+ return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
38816
+ }
38817
+ Math.round(ituDbfsFromPreEpoch("PCMU", -55));
38576
38818
  /** Schema defaults — an untouched sub-field must author exactly these. */
38577
38819
  var NC_AUDIO_DEFAULTS = {
38578
38820
  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.100",
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",