@camstack/addon-pipeline 0.1.20 → 0.2.0

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.
Files changed (101) hide show
  1. package/dist/audio-analyzer/index.js +736 -719
  2. package/dist/audio-analyzer/index.mjs +726 -679
  3. package/dist/audio-codec-nodeav/index.js +304 -461
  4. package/dist/audio-codec-nodeav/index.mjs +300 -462
  5. package/dist/chunk-BdkLduGY.mjs +5 -0
  6. package/dist/chunk-D6vf50IK.js +28 -0
  7. package/dist/codec-runtime-BOk-13PN.js +202 -0
  8. package/dist/codec-runtime-BsqlEjPi.mjs +197 -0
  9. package/dist/constants-B_b0a-6h.mjs +3119 -0
  10. package/dist/{index-CMcx_k6Y.js → constants-D65v6yp6.js} +3107 -2935
  11. package/dist/decoder-nodeav/index.js +1374 -1444
  12. package/dist/decoder-nodeav/index.mjs +1369 -1425
  13. package/dist/detection-pipeline/index.js +6462 -5613
  14. package/dist/detection-pipeline/index.mjs +6451 -5574
  15. package/dist/dist-7ewQjTle.js +22454 -0
  16. package/dist/dist-C5jnNl0n.mjs +22089 -0
  17. package/dist/motion-wasm/index.js +469 -467
  18. package/dist/motion-wasm/index.mjs +464 -446
  19. package/dist/pipeline-runner/index.js +2029 -1827
  20. package/dist/pipeline-runner/index.mjs +2025 -1811
  21. package/dist/recorder/index.js +2045 -2157
  22. package/dist/recorder/index.mjs +2042 -2156
  23. package/dist/stream-broker/_stub.js +1806 -1352
  24. package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D4-DHanK.mjs +156 -0
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Tf-HACFd.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C9WX5HNw.mjs +26 -0
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs +26 -0
  28. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs +26 -0
  29. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs +26 -0
  30. package/dist/stream-broker/dist-CYZr2fwk.mjs +2726 -0
  31. package/dist/stream-broker/hostInit-Di6vceAU.mjs +129 -0
  32. package/dist/stream-broker/index.js +17778 -15470
  33. package/dist/stream-broker/index.mjs +17769 -15465
  34. package/dist/stream-broker/remoteEntry.js +134 -2973
  35. package/dist/stream-broker/remoteEntry.ssr.js +33 -0
  36. package/dist/stream-broker/virtualExposes-dYNvIwoR.mjs +27 -0
  37. package/dist/stream-broker/virtual_mf-exposes-ssr___mfe_internal__addon_stream_broker_widgets__remoteEntry_js-Cmqfp4i_.mjs +10 -0
  38. package/embed-dist/assets/index-B8VlSD0-.js +150 -0
  39. package/embed-dist/assets/index-ZhDdp1Nd.css +2 -0
  40. package/embed-dist/index.html +13 -0
  41. package/package.json +25 -7
  42. package/wasm/assembly/index.ts +41 -16
  43. package/dist/audio-analyzer/index.js.map +0 -1
  44. package/dist/audio-analyzer/index.mjs.map +0 -1
  45. package/dist/audio-codec-nodeav/index.js.map +0 -1
  46. package/dist/audio-codec-nodeav/index.mjs.map +0 -1
  47. package/dist/decoder-nodeav/index.js.map +0 -1
  48. package/dist/decoder-nodeav/index.mjs.map +0 -1
  49. package/dist/detection-pipeline/index.js.map +0 -1
  50. package/dist/detection-pipeline/index.mjs.map +0 -1
  51. package/dist/index-5aYef068.mjs +0 -17514
  52. package/dist/index-5aYef068.mjs.map +0 -1
  53. package/dist/index-B36NMAdu.js +0 -17513
  54. package/dist/index-B36NMAdu.js.map +0 -1
  55. package/dist/index-CMcx_k6Y.js.map +0 -1
  56. package/dist/index-CYb7cFrv.mjs +0 -5790
  57. package/dist/index-CYb7cFrv.mjs.map +0 -1
  58. package/dist/motion-wasm/index.js.map +0 -1
  59. package/dist/motion-wasm/index.mjs.map +0 -1
  60. package/dist/pipeline-runner/index.js.map +0 -1
  61. package/dist/pipeline-runner/index.mjs.map +0 -1
  62. package/dist/recorder/index.js.map +0 -1
  63. package/dist/recorder/index.mjs.map +0 -1
  64. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/FfmpegParamsField.d.ts +0 -41
  65. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/GeometryBuilder.d.ts +0 -54
  66. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/StreamBrokerPanel.d.ts +0 -21
  67. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/format-ua.d.ts +0 -13
  68. package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/index.d.ts +0 -15
  69. package/dist/stream-broker/@mf-types/widgets.d.ts +0 -2
  70. package/dist/stream-broker/@mf-types.d.ts +0 -3
  71. package/dist/stream-broker/@mf-types.zip +0 -0
  72. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-lantnv8e.mjs +0 -12
  73. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-DJ3UNg7O.mjs +0 -30
  74. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-CYXy_bhS.mjs +0 -21
  75. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-U1EUeEPs.mjs +0 -104
  76. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-DeouEaSs.mjs +0 -85
  77. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-DHUwjbb9.mjs +0 -62
  78. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-CaDEYBIU.mjs +0 -89
  79. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-D6EROtlA.mjs +0 -29
  80. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-x6pP3Ghk.mjs +0 -36
  81. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-DYEKzzY-.mjs +0 -45
  82. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CcnN6sbA.mjs +0 -6
  83. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-DICOtMTl.mjs +0 -34
  84. package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CL9DR49k.mjs +0 -156
  85. package/dist/stream-broker/client-BvTmMOQu.mjs +0 -9836
  86. package/dist/stream-broker/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +0 -211
  87. package/dist/stream-broker/hostInit-ChmiMPS0.mjs +0 -168
  88. package/dist/stream-broker/index-BxsFuFmE.mjs +0 -2603
  89. package/dist/stream-broker/index-C-248uOU.mjs +0 -725
  90. package/dist/stream-broker/index-C05B6jqp.mjs +0 -185
  91. package/dist/stream-broker/index-CWkKuNLr.mjs +0 -232
  92. package/dist/stream-broker/index-DOJoSShD.mjs +0 -67784
  93. package/dist/stream-broker/index-DtOI1aTU.mjs +0 -18504
  94. package/dist/stream-broker/index-oMq6ilgR.mjs +0 -1641
  95. package/dist/stream-broker/index-vIWZQBIL.mjs +0 -435
  96. package/dist/stream-broker/index-xncRG7-x.mjs +0 -2713
  97. package/dist/stream-broker/index.js.map +0 -1
  98. package/dist/stream-broker/index.mjs.map +0 -1
  99. package/dist/stream-broker/jsx-runtime-BRT_HL0A.mjs +0 -55
  100. package/dist/stream-broker/schemas-B7L0qZtq.mjs +0 -3599
  101. package/dist/stream-broker/virtualExposes-pCd777Rp.mjs +0 -42
@@ -1,689 +1,736 @@
1
- import { e as errMsg, z as HF_BASE_URL, B as BaseAddon, F as DEFAULT_AUDIO_ANALYZER_CONFIG, G as AUDIO_BACKEND_CHOICES, r as hydrateSchema, I as audioAnalyzerCapability, J as audioAnalysisCapability, K as mapAudioLabelToMacro } from "../index-5aYef068.mjs";
2
- import * as path from "node:path";
1
+ import { L as mapAudioLabelToMacro, P as hydrateSchema, S as audioAnalyzerCapability, c as DEFAULT_AUDIO_ANALYZER_CONFIG, i as BaseAddon, j as errMsg, m as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, x as audioAnalysisCapability } from "../dist-C5jnNl0n.mjs";
3
2
  import * as fs from "node:fs";
3
+ import * as path$1 from "node:path";
4
4
  import { downloadFile } from "@camstack/core";
5
+ //#region src/audio-analyzer/audio-pipeline.ts
6
+ /**
7
+ * Create the appropriate audio pipeline.
8
+ *
9
+ * - 'yamnet-onnx': Cross-platform YAMNet ONNX (requires model download)
10
+ * - 'apple-soundanalysis': macOS 12+ Apple SoundAnalysis (zero model download, Neural Engine)
11
+ * - undefined: auto-detect (Apple SA on macOS, YAMNet on Linux)
12
+ */
5
13
  async function createAudioPipeline(modelsDir, logger, options) {
6
- const backend = options?.backend ?? (process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx");
7
- if (backend === "apple-soundanalysis") {
8
- return new AppleSoundAnalysisPipeline(logger);
9
- }
10
- return new YamnetOnnxPipeline(modelsDir, logger);
11
- }
12
- const YAMNET_MODEL_URL = `${HF_BASE_URL}/audioClassification/yamnet/onnx/camstack-yamnet.onnx`;
13
- const YAMNET_LABELS_URL = `${HF_BASE_URL}/audioClassification/yamnet/camstack-yamnet-labels.json`;
14
- class YamnetOnnxPipeline {
15
- constructor(modelsDir, logger) {
16
- this.modelsDir = modelsDir;
17
- this.log = logger;
18
- }
19
- session = null;
20
- inputName = "";
21
- labels = [];
22
- log;
23
- async initialize() {
24
- const ort = await import("onnxruntime-node");
25
- const modelPath = path.join(this.modelsDir, "camstack-yamnet.onnx");
26
- const labelsPath = path.join(this.modelsDir, "camstack-yamnet-labels.json");
27
- if (!fs.existsSync(modelPath)) {
28
- this.log.info("YAMNet ONNX model not found locally — downloading from HuggingFace", {
29
- meta: { url: YAMNET_MODEL_URL, dest: modelPath }
30
- });
31
- await downloadFile(YAMNET_MODEL_URL, modelPath);
32
- this.log.info("YAMNet ONNX model downloaded", {
33
- meta: { sizeBytes: fs.statSync(modelPath).size }
34
- });
35
- }
36
- if (!fs.existsSync(labelsPath)) {
37
- this.log.info("YAMNet labels not found locally — downloading from HuggingFace", {
38
- meta: { url: YAMNET_LABELS_URL, dest: labelsPath }
39
- });
40
- await downloadFile(YAMNET_LABELS_URL, labelsPath);
41
- }
42
- this.session = await ort.InferenceSession.create(modelPath);
43
- this.inputName = this.session.inputNames[0] ?? "waveform";
44
- if (fs.existsSync(labelsPath)) {
45
- this.labels = JSON.parse(fs.readFileSync(labelsPath, "utf8"));
46
- } else {
47
- this.log.warn("YAMNet labels file not found — classifications will use numeric indices");
48
- }
49
- this.log.info(`YAMNet ONNX pipeline initialized (${this.labels.length} labels)`);
50
- }
51
- async classify(chunk) {
52
- if (!this.session) {
53
- throw new Error("YAMNet pipeline not initialized");
54
- }
55
- const start = Date.now();
56
- const ort = await import("onnxruntime-node");
57
- const waveform = chunk.sampleRate === 16e3 && chunk.channels === 1 ? chunk.data : resampleMono16k(chunk);
58
- const tensor = new ort.Tensor("float32", waveform, [waveform.length]);
59
- const feeds = { [this.inputName]: tensor };
60
- const results = await this.session.run(feeds);
61
- const scoresData = results[this.session.outputNames[0]];
62
- if (!scoresData) {
63
- throw new Error("YAMNet returned no output");
64
- }
65
- const scores = scoresData.data;
66
- const numClasses = 521;
67
- const numFrames = scores.length / numClasses;
68
- const avgScores = new Float32Array(numClasses);
69
- for (let f = 0; f < numFrames; f++) {
70
- for (let c = 0; c < numClasses; c++) {
71
- avgScores[c] += scores[f * numClasses + c];
72
- }
73
- }
74
- for (let c = 0; c < numClasses; c++) {
75
- avgScores[c] = avgScores[c] / numFrames;
76
- }
77
- const minScore = 0.05;
78
- const classifications = [];
79
- for (let c = 0; c < numClasses; c++) {
80
- const score = avgScores[c];
81
- if (score >= minScore) {
82
- const label = c < this.labels.length ? this.labels[c] : String(c);
83
- classifications.push({ className: label, score: Math.round(score * 1e3) / 1e3 });
84
- }
85
- }
86
- classifications.sort((a, b) => b.score - a.score);
87
- return {
88
- classifications: classifications.slice(0, 10),
89
- inferenceMs: Date.now() - start
90
- };
91
- }
92
- async dispose() {
93
- if (this.session) {
94
- await this.session.release();
95
- this.session = null;
96
- }
97
- }
98
- }
99
- class AppleSoundAnalysisPipeline {
100
- log;
101
- process = null;
102
- receiveBuffer = Buffer.alloc(0);
103
- pendingResolve = null;
104
- pendingReject = null;
105
- binaryPath = null;
106
- debugCount = 0;
107
- constructor(logger) {
108
- this.log = logger;
109
- }
110
- async initialize() {
111
- this.binaryPath = await this.resolveSwiftBinary();
112
- if (!this.binaryPath) {
113
- throw new Error("Apple SoundAnalysis: Swift CLI not found and compilation failed. macOS with Xcode CLI tools required.");
114
- }
115
- const { spawn } = await import("node:child_process");
116
- this.process = spawn(this.binaryPath, ["--sample-rate=16000", "--top-k=10"], {
117
- stdio: ["pipe", "pipe", "pipe"]
118
- });
119
- this.process.stderr?.on("data", (chunk) => {
120
- const lines = chunk.toString().split("\n");
121
- for (const line of lines) {
122
- const trimmed = line.trim();
123
- if (trimmed) this.log.warn(trimmed);
124
- }
125
- });
126
- this.process.on("error", (err) => {
127
- this.log.error("Swift process error", { meta: { error: err.message } });
128
- this.pendingReject?.(err);
129
- this.pendingReject = null;
130
- this.pendingResolve = null;
131
- });
132
- this.process.on("exit", (code) => {
133
- if (code !== 0 && code !== null) {
134
- this.log.error("Swift process exited", { meta: { code } });
135
- const err = new Error(`Apple SoundAnalysis: process exited with code ${code}`);
136
- this.pendingReject?.(err);
137
- this.pendingReject = null;
138
- this.pendingResolve = null;
139
- }
140
- });
141
- this.process.stdout.on("data", (chunk) => {
142
- this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
143
- this.tryReceive();
144
- });
145
- const ready = await this.receiveMessage();
146
- if (ready["status"] !== "ready") {
147
- throw new Error(`Apple SoundAnalysis: unexpected init response: ${JSON.stringify(ready)}`);
148
- }
149
- this.log.info("Apple SoundAnalysis pipeline initialized (macOS built-in, Swift CLI bridge)");
150
- }
151
- async classify(chunk) {
152
- if (!this.process?.stdin) {
153
- throw new Error("Apple SoundAnalysis: process not initialized");
154
- }
155
- const waveform = chunk.sampleRate === 16e3 && chunk.channels === 1 ? chunk.data : resampleMono16k(chunk);
156
- const audioBuffer = Buffer.from(waveform.buffer, waveform.byteOffset, waveform.byteLength);
157
- const lengthBuf = Buffer.allocUnsafe(4);
158
- lengthBuf.writeUInt32LE(audioBuffer.length, 0);
159
- this.process.stdin.write(Buffer.concat([lengthBuf, audioBuffer]));
160
- const result = await this.receiveMessage();
161
- const classifications = result["classifications"] ?? [];
162
- const inferenceMs = result["inferenceMs"] ?? 0;
163
- if (this.debugCount < 3) {
164
- const keys = Object.keys(result);
165
- this.log.info("classify debug sample", {
166
- meta: {
167
- phase: "apple-sa",
168
- index: this.debugCount,
169
- keys,
170
- classifications: classifications.length,
171
- inferenceMs,
172
- audioBytes: Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength).length,
173
- sampleRate: chunk.sampleRate,
174
- channels: chunk.channels
175
- }
176
- });
177
- if (result["error"]) {
178
- this.log.error("Swift error", { meta: { phase: "apple-sa", error: result["error"] } });
179
- }
180
- this.debugCount++;
181
- }
182
- return { classifications, inferenceMs };
183
- }
184
- async dispose() {
185
- const proc = this.process;
186
- if (!proc) return;
187
- this.process = null;
188
- proc.stdin?.end();
189
- proc.kill("SIGTERM");
190
- const exited = await new Promise((resolve) => {
191
- const timer = setTimeout(() => resolve(false), 5e3);
192
- proc.once("exit", () => {
193
- clearTimeout(timer);
194
- resolve(true);
195
- });
196
- });
197
- if (!exited) {
198
- try {
199
- proc.kill("SIGKILL");
200
- } catch {
201
- }
202
- this.log.warn("Swift process did not exit gracefully — sent SIGKILL");
203
- }
204
- }
205
- receiveMessage() {
206
- return new Promise((resolve, reject) => {
207
- this.pendingResolve = resolve;
208
- this.pendingReject = reject;
209
- });
210
- }
211
- tryReceive() {
212
- if (this.receiveBuffer.length < 4) return;
213
- const length = this.receiveBuffer.readUInt32LE(0);
214
- if (this.receiveBuffer.length < 4 + length) return;
215
- const jsonBytes = this.receiveBuffer.subarray(4, 4 + length);
216
- this.receiveBuffer = this.receiveBuffer.subarray(4 + length);
217
- const resolve = this.pendingResolve;
218
- const reject = this.pendingReject;
219
- this.pendingResolve = null;
220
- this.pendingReject = null;
221
- if (!resolve) return;
222
- try {
223
- const parsed = JSON.parse(jsonBytes.toString("utf8"));
224
- resolve(parsed);
225
- } catch (err) {
226
- reject?.(err instanceof Error ? err : new Error(String(err)));
227
- }
228
- }
229
- /** Find pre-compiled binary or compile from Swift source. */
230
- async resolveSwiftBinary() {
231
- const candidates = [
232
- path.join(__dirname, "../../swift/audio-analyzer/apple-sound-classifier"),
233
- // Fallback for in-tree dev (src/<id>/swift/) and pre-merge layouts.
234
- path.join(__dirname, "../swift/apple-sound-classifier"),
235
- path.join(__dirname, "../../swift/apple-sound-classifier"),
236
- path.join(__dirname, "../../../swift/apple-sound-classifier")
237
- ];
238
- for (const p of candidates) {
239
- if (fs.existsSync(p)) {
240
- this.log.info("Found pre-compiled Swift CLI", { meta: { path: p } });
241
- return p;
242
- }
243
- }
244
- const sourceCandidates = [
245
- path.join(__dirname, "../../swift/audio-analyzer/apple-sound-classifier.swift"),
246
- path.join(__dirname, "../swift/apple-sound-classifier.swift"),
247
- path.join(__dirname, "../../swift/apple-sound-classifier.swift"),
248
- path.join(__dirname, "../../../swift/apple-sound-classifier.swift")
249
- ];
250
- const sourcePath = sourceCandidates.find((p) => fs.existsSync(p));
251
- if (!sourcePath) {
252
- this.log.error("Swift source not found", { meta: { searched: sourceCandidates } });
253
- return null;
254
- }
255
- const outputPath = sourcePath.replace(".swift", "");
256
- this.log.info("Compiling Swift CLI...", { meta: { source: sourcePath, output: outputPath } });
257
- const { execFileSync } = await import("node:child_process");
258
- try {
259
- execFileSync("swiftc", ["-O", "-o", outputPath, sourcePath], {
260
- timeout: 6e4,
261
- stdio: "pipe"
262
- });
263
- this.log.info("Swift CLI compiled successfully");
264
- return outputPath;
265
- } catch (err) {
266
- this.log.error("Swift compilation failed — install Xcode Command Line Tools", {
267
- meta: { error: errMsg(err) }
268
- });
269
- return null;
270
- }
271
- }
14
+ if ((options?.backend ?? (process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx")) === "apple-soundanalysis") return new AppleSoundAnalysisPipeline(logger);
15
+ return new YamnetOnnxPipeline(modelsDir, logger);
272
16
  }
17
+ /**
18
+ * Canonical model URLs on the camstack HuggingFace mirror. Mirrors the
19
+ * convention every detection model follows (single point of truth =
20
+ * `HF_BASE_URL` from `@camstack/types`); the auto-download path uses
21
+ * `downloadFile` from `@camstack/core`, the SAME helper detection-
22
+ * pipeline uses to materialise its YOLO/face/plate models. Missing
23
+ * model on disk → fetch from HF; cached file → no-op.
24
+ *
25
+ * Repo layout follows the detection-pipeline pattern:
26
+ * {domain}/{family}/{format}/{filename}
27
+ * For YAMNet that's `audioClassification/yamnet/onnx/camstack-yamnet.onnx`,
28
+ * with the labels JSON sitting one level up (`audioClassification/yamnet/`)
29
+ * because they're format-agnostic (same 521 AudioSet class names whether
30
+ * the runtime is ONNX, OpenVINO, or TF).
31
+ */
32
+ var YAMNET_MODEL_URL = `${HF_BASE_URL}/audioClassification/yamnet/onnx/camstack-yamnet.onnx`;
33
+ var YAMNET_LABELS_URL = `${HF_BASE_URL}/audioClassification/yamnet/camstack-yamnet-labels.json`;
34
+ var YamnetOnnxPipeline = class {
35
+ modelsDir;
36
+ session = null;
37
+ inputName = "";
38
+ labels = [];
39
+ log;
40
+ constructor(modelsDir, logger) {
41
+ this.modelsDir = modelsDir;
42
+ this.log = logger;
43
+ }
44
+ async initialize() {
45
+ const ort = await import("onnxruntime-node");
46
+ const modelPath = path$1.join(this.modelsDir, "camstack-yamnet.onnx");
47
+ const labelsPath = path$1.join(this.modelsDir, "camstack-yamnet-labels.json");
48
+ if (!fs.existsSync(modelPath)) {
49
+ this.log.info("YAMNet ONNX model not found locally — downloading from HuggingFace", { meta: {
50
+ url: YAMNET_MODEL_URL,
51
+ dest: modelPath
52
+ } });
53
+ await downloadFile(YAMNET_MODEL_URL, modelPath);
54
+ this.log.info("YAMNet ONNX model downloaded", { meta: { sizeBytes: fs.statSync(modelPath).size } });
55
+ }
56
+ if (!fs.existsSync(labelsPath)) {
57
+ this.log.info("YAMNet labels not found locally — downloading from HuggingFace", { meta: {
58
+ url: YAMNET_LABELS_URL,
59
+ dest: labelsPath
60
+ } });
61
+ await downloadFile(YAMNET_LABELS_URL, labelsPath);
62
+ }
63
+ this.session = await ort.InferenceSession.create(modelPath);
64
+ this.inputName = this.session.inputNames[0] ?? "waveform";
65
+ if (fs.existsSync(labelsPath)) this.labels = JSON.parse(fs.readFileSync(labelsPath, "utf8"));
66
+ else this.log.warn("YAMNet labels file not found — classifications will use numeric indices");
67
+ this.log.info(`YAMNet ONNX pipeline initialized (${this.labels.length} labels)`);
68
+ }
69
+ async classify(chunk) {
70
+ if (!this.session) throw new Error("YAMNet pipeline not initialized");
71
+ const start = Date.now();
72
+ const ort = await import("onnxruntime-node");
73
+ const waveform = chunk.sampleRate === 16e3 && chunk.channels === 1 ? chunk.data : resampleMono16k(chunk);
74
+ const tensor = new ort.Tensor("float32", waveform, [waveform.length]);
75
+ const feeds = { [this.inputName]: tensor };
76
+ const scoresData = (await this.session.run(feeds))[this.session.outputNames[0]];
77
+ if (!scoresData) throw new Error("YAMNet returned no output");
78
+ const scores = scoresData.data;
79
+ const numClasses = 521;
80
+ const numFrames = scores.length / numClasses;
81
+ const avgScores = new Float32Array(numClasses);
82
+ for (let f = 0; f < numFrames; f++) for (let c = 0; c < numClasses; c++) avgScores[c] += scores[f * numClasses + c];
83
+ for (let c = 0; c < numClasses; c++) avgScores[c] = avgScores[c] / numFrames;
84
+ const minScore = .05;
85
+ const classifications = [];
86
+ for (let c = 0; c < numClasses; c++) {
87
+ const score = avgScores[c];
88
+ if (score >= minScore) {
89
+ const label = c < this.labels.length ? this.labels[c] : String(c);
90
+ classifications.push({
91
+ className: label,
92
+ score: Math.round(score * 1e3) / 1e3
93
+ });
94
+ }
95
+ }
96
+ classifications.sort((a, b) => b.score - a.score);
97
+ return {
98
+ classifications: classifications.slice(0, 10),
99
+ inferenceMs: Date.now() - start
100
+ };
101
+ }
102
+ async dispose() {
103
+ if (this.session) {
104
+ await this.session.release();
105
+ this.session = null;
106
+ }
107
+ }
108
+ };
109
+ var AppleSoundAnalysisPipeline = class {
110
+ log;
111
+ process = null;
112
+ receiveBuffer = Buffer.alloc(0);
113
+ pendingResolve = null;
114
+ pendingReject = null;
115
+ binaryPath = null;
116
+ debugCount = 0;
117
+ constructor(logger) {
118
+ this.log = logger;
119
+ }
120
+ async initialize() {
121
+ this.binaryPath = await this.resolveSwiftBinary();
122
+ if (!this.binaryPath) throw new Error("Apple SoundAnalysis: Swift CLI not found and compilation failed. macOS with Xcode CLI tools required.");
123
+ const { spawn } = await import("node:child_process");
124
+ this.process = spawn(this.binaryPath, ["--sample-rate=16000", "--top-k=10"], { stdio: [
125
+ "pipe",
126
+ "pipe",
127
+ "pipe"
128
+ ] });
129
+ this.process.stderr?.on("data", (chunk) => {
130
+ const lines = chunk.toString().split("\n");
131
+ for (const line of lines) {
132
+ const trimmed = line.trim();
133
+ if (trimmed) this.log.warn(trimmed);
134
+ }
135
+ });
136
+ this.process.on("error", (err) => {
137
+ this.log.error("Swift process error", { meta: { error: err.message } });
138
+ this.pendingReject?.(err);
139
+ this.pendingReject = null;
140
+ this.pendingResolve = null;
141
+ });
142
+ this.process.on("exit", (code) => {
143
+ if (code !== 0 && code !== null) {
144
+ this.log.error("Swift process exited", { meta: { code } });
145
+ const err = /* @__PURE__ */ new Error(`Apple SoundAnalysis: process exited with code ${code}`);
146
+ this.pendingReject?.(err);
147
+ this.pendingReject = null;
148
+ this.pendingResolve = null;
149
+ }
150
+ });
151
+ this.process.stdout.on("data", (chunk) => {
152
+ this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
153
+ this.tryReceive();
154
+ });
155
+ const ready = await this.receiveMessage();
156
+ if (ready["status"] !== "ready") throw new Error(`Apple SoundAnalysis: unexpected init response: ${JSON.stringify(ready)}`);
157
+ this.log.info("Apple SoundAnalysis pipeline initialized (macOS built-in, Swift CLI bridge)");
158
+ }
159
+ async classify(chunk) {
160
+ if (!this.process?.stdin) throw new Error("Apple SoundAnalysis: process not initialized");
161
+ const waveform = chunk.sampleRate === 16e3 && chunk.channels === 1 ? chunk.data : resampleMono16k(chunk);
162
+ const audioBuffer = Buffer.from(waveform.buffer, waveform.byteOffset, waveform.byteLength);
163
+ const lengthBuf = Buffer.allocUnsafe(4);
164
+ lengthBuf.writeUInt32LE(audioBuffer.length, 0);
165
+ this.process.stdin.write(Buffer.concat([lengthBuf, audioBuffer]));
166
+ const result = await this.receiveMessage();
167
+ const classifications = result["classifications"] ?? [];
168
+ const inferenceMs = result["inferenceMs"] ?? 0;
169
+ if (this.debugCount < 3) {
170
+ const keys = Object.keys(result);
171
+ this.log.info("classify debug sample", { meta: {
172
+ phase: "apple-sa",
173
+ index: this.debugCount,
174
+ keys,
175
+ classifications: classifications.length,
176
+ inferenceMs,
177
+ audioBytes: Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength).length,
178
+ sampleRate: chunk.sampleRate,
179
+ channels: chunk.channels
180
+ } });
181
+ if (result["error"]) this.log.error("Swift error", { meta: {
182
+ phase: "apple-sa",
183
+ error: result["error"]
184
+ } });
185
+ this.debugCount++;
186
+ }
187
+ return {
188
+ classifications,
189
+ inferenceMs
190
+ };
191
+ }
192
+ async dispose() {
193
+ const proc = this.process;
194
+ if (!proc) return;
195
+ this.process = null;
196
+ proc.stdin?.end();
197
+ proc.kill("SIGTERM");
198
+ if (!await new Promise((resolve) => {
199
+ const timer = setTimeout(() => resolve(false), 5e3);
200
+ proc.once("exit", () => {
201
+ clearTimeout(timer);
202
+ resolve(true);
203
+ });
204
+ })) {
205
+ try {
206
+ proc.kill("SIGKILL");
207
+ } catch {}
208
+ this.log.warn("Swift process did not exit gracefully — sent SIGKILL");
209
+ }
210
+ }
211
+ receiveMessage() {
212
+ return new Promise((resolve, reject) => {
213
+ this.pendingResolve = resolve;
214
+ this.pendingReject = reject;
215
+ });
216
+ }
217
+ tryReceive() {
218
+ if (this.receiveBuffer.length < 4) return;
219
+ const length = this.receiveBuffer.readUInt32LE(0);
220
+ if (this.receiveBuffer.length < 4 + length) return;
221
+ const jsonBytes = this.receiveBuffer.subarray(4, 4 + length);
222
+ this.receiveBuffer = this.receiveBuffer.subarray(4 + length);
223
+ const resolve = this.pendingResolve;
224
+ const reject = this.pendingReject;
225
+ this.pendingResolve = null;
226
+ this.pendingReject = null;
227
+ if (!resolve) return;
228
+ try {
229
+ resolve(JSON.parse(jsonBytes.toString("utf8")));
230
+ } catch (err) {
231
+ reject?.(err instanceof Error ? err : new Error(String(err)));
232
+ }
233
+ }
234
+ /** Find pre-compiled binary or compile from Swift source. */
235
+ async resolveSwiftBinary() {
236
+ const candidates = [
237
+ path$1.join(__dirname, "../../swift/audio-analyzer/apple-sound-classifier"),
238
+ path$1.join(__dirname, "../swift/apple-sound-classifier"),
239
+ path$1.join(__dirname, "../../swift/apple-sound-classifier"),
240
+ path$1.join(__dirname, "../../../swift/apple-sound-classifier")
241
+ ];
242
+ for (const p of candidates) if (fs.existsSync(p)) {
243
+ this.log.info("Found pre-compiled Swift CLI", { meta: { path: p } });
244
+ return p;
245
+ }
246
+ const sourceCandidates = [
247
+ path$1.join(__dirname, "../../swift/audio-analyzer/apple-sound-classifier.swift"),
248
+ path$1.join(__dirname, "../swift/apple-sound-classifier.swift"),
249
+ path$1.join(__dirname, "../../swift/apple-sound-classifier.swift"),
250
+ path$1.join(__dirname, "../../../swift/apple-sound-classifier.swift")
251
+ ];
252
+ const sourcePath = sourceCandidates.find((p) => fs.existsSync(p));
253
+ if (!sourcePath) {
254
+ this.log.error("Swift source not found", { meta: { searched: sourceCandidates } });
255
+ return null;
256
+ }
257
+ const outputPath = sourcePath.replace(".swift", "");
258
+ this.log.info("Compiling Swift CLI...", { meta: {
259
+ source: sourcePath,
260
+ output: outputPath
261
+ } });
262
+ const { execFileSync } = await import("node:child_process");
263
+ try {
264
+ execFileSync("swiftc", [
265
+ "-O",
266
+ "-o",
267
+ outputPath,
268
+ sourcePath
269
+ ], {
270
+ timeout: 6e4,
271
+ stdio: "pipe"
272
+ });
273
+ this.log.info("Swift CLI compiled successfully");
274
+ return outputPath;
275
+ } catch (err) {
276
+ this.log.error("Swift compilation failed — install Xcode Command Line Tools", { meta: { error: errMsg(err) } });
277
+ return null;
278
+ }
279
+ }
280
+ };
281
+ /** Simple resample to 16kHz mono by linear interpolation. */
273
282
  function resampleMono16k(chunk) {
274
- const { data, sampleRate, channels } = chunk;
275
- const numSamples = data.length / channels;
276
- const mono = new Float32Array(numSamples);
277
- for (let i = 0; i < numSamples; i++) {
278
- let sum = 0;
279
- for (let c = 0; c < channels; c++) {
280
- sum += data[i * channels + c];
281
- }
282
- mono[i] = sum / channels;
283
- }
284
- const ratio = 16e3 / sampleRate;
285
- const outLen = Math.floor(numSamples * ratio);
286
- const out = new Float32Array(outLen);
287
- for (let i = 0; i < outLen; i++) {
288
- const srcIdx = i / ratio;
289
- const lo = Math.floor(srcIdx);
290
- const hi = Math.min(lo + 1, numSamples - 1);
291
- const frac = srcIdx - lo;
292
- out[i] = mono[lo] * (1 - frac) + mono[hi] * frac;
293
- }
294
- return out;
283
+ const { data, sampleRate, channels } = chunk;
284
+ const numSamples = data.length / channels;
285
+ const mono = new Float32Array(numSamples);
286
+ for (let i = 0; i < numSamples; i++) {
287
+ let sum = 0;
288
+ for (let c = 0; c < channels; c++) sum += data[i * channels + c];
289
+ mono[i] = sum / channels;
290
+ }
291
+ const ratio = 16e3 / sampleRate;
292
+ const outLen = Math.floor(numSamples * ratio);
293
+ const out = new Float32Array(outLen);
294
+ for (let i = 0; i < outLen; i++) {
295
+ const srcIdx = i / ratio;
296
+ const lo = Math.floor(srcIdx);
297
+ const hi = Math.min(lo + 1, numSamples - 1);
298
+ const frac = srcIdx - lo;
299
+ out[i] = mono[lo] * (1 - frac) + mono[hi] * frac;
300
+ }
301
+ return out;
295
302
  }
296
- const AUDIO_MODEL_OPTIONS = [
297
- { value: "", label: "Auto (matches backend)" },
298
- { value: "yamnet-onnx", label: "YAMNet (ONNX)" },
299
- { value: "apple-soundanalysis", label: "Apple SoundAnalysis (built-in)" }
303
+ //#endregion
304
+ //#region src/audio-analyzer/addons/analyzer/index.ts
305
+ /**
306
+ * Choices presented in the Audio Model dropdown. YAMNet runs via ONNX
307
+ * when backend=yamnet-onnx; Apple SoundAnalysis is a built-in macOS
308
+ * model and has no swappable modelId — the backend IS the model.
309
+ */
310
+ var AUDIO_MODEL_OPTIONS = [
311
+ {
312
+ value: "",
313
+ label: "Auto (matches backend)"
314
+ },
315
+ {
316
+ value: "yamnet-onnx",
317
+ label: "YAMNet (ONNX)"
318
+ },
319
+ {
320
+ value: "apple-soundanalysis",
321
+ label: "Apple SoundAnalysis (built-in)"
322
+ }
300
323
  ];
301
- const CLASSIFY_ERROR_SUPPRESS_MS = 3e4;
302
- const CLASSIFY_MIN_INTERVAL_MS = 500;
303
- const GLOBAL_DEVICE_KEY = -1;
324
+ /**
325
+ * AudioAnalyzerProvider implements IAudioAnalyzer.
326
+ *
327
+ * Computes dB/RMS on every chunk and classifies via the in-process
328
+ * IAudioPipeline (YAMNet ONNX / Apple SoundAnalysis). No tRPC roundtrip
329
+ * to a separate audio-classifier addon.
330
+ */
331
+ var CLASSIFY_ERROR_SUPPRESS_MS = 3e4;
332
+ var CLASSIFY_MIN_INTERVAL_MS = 500;
333
+ var GLOBAL_DEVICE_KEY = -1;
334
+ /**
335
+ * Reconstruct a Float32 view over f32le PCM bytes carried in a Uint8Array.
336
+ *
337
+ * `@msgpack/msgpack` decodes a binary blob into a Uint8Array that is a subview
338
+ * of its internal decode buffer, whose `byteOffset` is NOT guaranteed to be
339
+ * 4-byte aligned (observed e.g. 9). `new Float32Array(buf, offset, …)` then
340
+ * throws "start offset of Float32Array should be a multiple of 4". When the
341
+ * offset is misaligned we copy into a fresh 0-offset buffer; the aligned
342
+ * fast-path reuses the existing view with no copy.
343
+ */
304
344
  function float32FromBytes(raw) {
305
- const bytes = raw.byteOffset % 4 === 0 ? raw : new Uint8Array(raw);
306
- return new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
345
+ const bytes = raw.byteOffset % 4 === 0 ? raw : new Uint8Array(raw);
346
+ return new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
307
347
  }
308
- class AudioAnalyzerProvider {
309
- constructor(logger, pipeline, backendName, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
310
- this.pipeline = pipeline;
311
- this.deviceSettingsResolver = deviceSettingsResolver;
312
- this.deviceContributionResolver = deviceContributionResolver;
313
- this.deviceSettingsPatcher = deviceSettingsPatcher;
314
- this.reprobeImpl = reprobeImpl;
315
- this.log = logger;
316
- this.backendName = backendName;
317
- }
318
- log;
319
- classifyCallCount = 0;
320
- lastClassifyErrorMs = 0;
321
- suppressedClassifyErrors = 0;
322
- classifyCount = 0;
323
- backendName = "unknown";
324
- /** When true, logs a raw-label sample every 100 classifications (opt-in
325
- * debug aid). Off by default the watchdog heartbeat covers liveness. */
326
- debugClassifySamples = false;
327
- /** Per-camera in-flight state. Key = deviceId (or GLOBAL_DEVICE_KEY for legacy callers). */
328
- cameraState = /* @__PURE__ */ new Map();
329
- /** Global pipeline lock Apple SA and ONNX are single-channel: only one classify() can
330
- * run at a time. Without this, concurrent calls from different cameras overwrite the
331
- * single pendingResolve slot in AppleSoundAnalysisPipeline, causing 30s timeouts. */
332
- pipelineBusy = false;
333
- // ── Device-details aggregator contribution ──────────────────────────────
334
- async getDeviceSettingsContribution(input) {
335
- return this.deviceContributionResolver(input.deviceId);
336
- }
337
- async getDeviceLiveContribution(_input) {
338
- return null;
339
- }
340
- async applyDeviceSettingsPatch(input) {
341
- await this.deviceSettingsPatcher(input.deviceId, input.patch);
342
- return { success: true };
343
- }
344
- /**
345
- * Return the effective per-device audio-analyzer settings, resolved via
346
- * the kernel's 3-level settings resolver (schema default → global →
347
- * device override). Orchestrator consumers call this method so they
348
- * never need to know the audio-analyzer schema field names.
349
- */
350
- async resolveDeviceSettings({ deviceId }) {
351
- return this.deviceSettingsResolver(deviceId);
352
- }
353
- async analyseChunk({
354
- chunk,
355
- settings
356
- }) {
357
- const samples = float32FromBytes(chunk.data);
358
- let sumSquares = 0;
359
- for (let i = 0; i < samples.length; i++) {
360
- sumSquares += samples[i] * samples[i];
361
- }
362
- const rms = Math.sqrt(sumSquares / samples.length);
363
- const dbfs = rms > 0 ? 20 * Math.log10(rms) : -96;
364
- const level = {
365
- rms: Math.round(rms * 1e4) / 1e4,
366
- dbfs: Math.round(dbfs * 10) / 10
367
- };
368
- let classification;
369
- try {
370
- const result = await this.classify(chunk);
371
- if (this.classifyCallCount < 3) {
372
- const topRaw = result.labels.slice(0, 5).map((l) => `${l.className}(${(l.score * 100).toFixed(0)}%)`).join(", ");
373
- this.log.info("classify debug sample", {
374
- tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
375
- meta: {
376
- index: this.classifyCallCount,
377
- labelCount: result.labels.length,
378
- top: topRaw,
379
- inferenceMs: result.inferenceMs,
380
- minConf: settings.minConfidence,
381
- allowedClasses: settings.allowedClasses
382
- }
383
- });
384
- }
385
- this.classifyCallCount++;
386
- if (result.inferenceMs > 0) {
387
- const minConf = settings.minConfidence;
388
- const allowedSet = settings.allowedClasses.length > 0 ? new Set(settings.allowedClasses.map((c) => c.toLowerCase())) : null;
389
- let filtered = result.labels.filter((c) => c.score >= minConf);
390
- if (allowedSet) {
391
- filtered = filtered.filter((c) => allowedSet.has(c.className.toLowerCase()));
392
- }
393
- if (filtered.length > 0) {
394
- classification = {
395
- labels: filtered,
396
- inferenceMs: result.inferenceMs
397
- };
398
- }
399
- }
400
- } catch (err) {
401
- const now = Date.now();
402
- const sinceLastMs = now - this.lastClassifyErrorMs;
403
- if (sinceLastMs >= CLASSIFY_ERROR_SUPPRESS_MS) {
404
- const suppressed = this.suppressedClassifyErrors;
405
- this.suppressedClassifyErrors = 0;
406
- this.lastClassifyErrorMs = now;
407
- const msg = errMsg(err);
408
- const stack = err instanceof Error ? err.stack : void 0;
409
- this.log.warn("Audio classification failed", {
410
- tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
411
- meta: { error: msg, stack, suppressedSince: suppressed > 0 ? suppressed : void 0 }
412
- });
413
- } else {
414
- this.suppressedClassifyErrors++;
415
- }
416
- }
417
- return { level, classification, timestamp: chunk.timestamp };
418
- }
419
- async classify(chunk) {
420
- const camKey = chunk.deviceId ?? GLOBAL_DEVICE_KEY;
421
- const now = Date.now();
422
- const state = this.cameraState.get(camKey);
423
- if (state?.inProgress || state !== void 0 && now - state.lastEndMs < CLASSIFY_MIN_INTERVAL_MS) {
424
- return { labels: [], rawLabels: [], inferenceMs: 0 };
425
- }
426
- if (this.pipelineBusy) {
427
- return { labels: [], rawLabels: [], inferenceMs: 0 };
428
- }
429
- this.cameraState.set(camKey, { inProgress: true, lastEndMs: state?.lastEndMs ?? 0 });
430
- this.pipelineBusy = true;
431
- const f32Data = float32FromBytes(chunk.data);
432
- const result = await this.pipeline.classify({
433
- data: f32Data,
434
- sampleRate: chunk.sampleRate,
435
- channels: chunk.channels
436
- }).finally(() => {
437
- this.pipelineBusy = false;
438
- this.cameraState.set(camKey, { inProgress: false, lastEndMs: Date.now() });
439
- });
440
- if (this.debugClassifySamples && (this.classifyCount < 3 || this.classifyCount % 100 === 0)) {
441
- const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
442
- this.log.info("classify debug sample", {
443
- tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
444
- meta: {
445
- index: this.classifyCount,
446
- engine: this.backendName,
447
- rawLabelCount: result.classifications.length,
448
- top: rawTop,
449
- inferenceMs: result.inferenceMs
450
- }
451
- });
452
- }
453
- this.classifyCount++;
454
- const macroAccum = /* @__PURE__ */ new Map();
455
- for (const c of result.classifications) {
456
- const macro = mapAudioLabelToMacro(c.className);
457
- if (!macro) continue;
458
- const prev = macroAccum.get(macro);
459
- if (!prev || c.score > prev.score) {
460
- macroAccum.set(macro, { score: c.score, rawTop: c.className });
461
- }
462
- }
463
- const labels = [...macroAccum.entries()].sort((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({ className, originalClass: rawTop, score }));
464
- const rawLabels = [...result.classifications].sort((a, b) => b.score - a.score).map((c) => ({ className: c.className, originalClass: c.className, score: c.score }));
465
- return { labels, rawLabels, inferenceMs: result.inferenceMs };
466
- }
467
- isReady() {
468
- return this.pipeline !== null;
469
- }
470
- async dispose() {
471
- await this.pipeline.dispose();
472
- }
473
- // Expose via the cap so the reprobe button in the UI reaches the
474
- // right worker. Delegates to the addon-owned reprobe (it touches
475
- // `ctx.settings.writeAddonStore` which only the addon has access to).
476
- async reprobeAudioEngine() {
477
- return this.reprobeImpl();
478
- }
479
- }
480
- class AudioAnalyzerAddon extends BaseAddon {
481
- id = "audio-analyzer";
482
- provider = null;
483
- pipeline = null;
484
- constructor() {
485
- super(DEFAULT_AUDIO_ANALYZER_CONFIG);
486
- }
487
- globalSettingsSchema() {
488
- return {
489
- sections: [
490
- {
491
- id: "audio-engine",
492
- title: "Audio",
493
- // Co-located with detection-pipeline's `engine` section under
494
- // a single "Engine" tab. Both sections handle inference-engine
495
- // selection but for different modalities — distinct purposes
496
- // are conveyed through section titles ("Detection engine" vs
497
- // "Audio inference engine") and descriptions, not separate
498
- // tabs.
499
- tab: "engine",
500
- // Renders after detection-pipeline's `engine` section
501
- // (`order: 0`) on the "Inference Engine" tab.
502
- order: 10,
503
- description: 'Audio classification backend (Apple SoundAnalysis or YAMNet ONNX). Independent from the vision-detection engine above. "Auto" picks Apple SoundAnalysis on macOS, YAMNet on Linux. Click the refresh icon next to "Probed best" to re-run the probe.',
504
- // Field order `probedBest*` lives at the top so the operator
505
- // sees the auto-detected hint first and can compare it against
506
- // their override below at a glance. Same convention as the
507
- // detection-pipeline section.
508
- fields: [
509
- {
510
- type: "text",
511
- key: "probedBestAudioBackend",
512
- label: "Probed best",
513
- description: "Auto-detected best audio backend on this host. Click the refresh icon to re-run the probe.",
514
- readonlyField: true,
515
- default: "",
516
- actions: [
517
- { action: "reprobe-audio-engine", icon: "refresh-cw", tooltip: "Re-probe audio engine" }
518
- ]
519
- },
520
- {
521
- type: "select",
522
- key: "audioBackend",
523
- label: "Audio backend",
524
- options: AUDIO_BACKEND_CHOICES.map((o) => ({ value: o.value, label: o.label })),
525
- default: DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend,
526
- immediate: true,
527
- requiresRestart: true
528
- },
529
- {
530
- type: "select",
531
- key: "selectedAudioModel",
532
- label: "Classification model",
533
- description: "Empty = auto (matches backend). Device-level settings can only inherit / enable / disable this step; model + class filters live here at the node level.",
534
- options: AUDIO_MODEL_OPTIONS.map((o) => ({ value: o.value, label: o.label })),
535
- default: DEFAULT_AUDIO_ANALYZER_CONFIG.selectedAudioModel,
536
- immediate: true,
537
- requiresRestart: true
538
- }
539
- ]
540
- }
541
- ]
542
- };
543
- }
544
- /**
545
- * Cascade override — narrow the `selectedAudioModel` options to the
546
- * subset compatible with the currently-selected `audioBackend`.
547
- *
548
- * Same pattern as detection-pipeline's `engineRuntime engineBackend
549
- * → engineDevice` cascade: the base schema ships every option
550
- * (Auto + YAMNet + Apple SA); this override drops the rows that
551
- * belong to a backend the operator didn't pick. With `immediate:
552
- * true` on the `audioBackend` select, the UI refetches schema after
553
- * every flip and the model dropdown updates instantly.
554
- *
555
- * `overlay` carries the operator's tentative choices for benchmark/
556
- * preview mode (operator typed but didn't save yet) — same
557
- * semantics detection-pipeline relies on.
558
- */
559
- async getGlobalSettings(overlay) {
560
- const ctx = this.ctxIfReady;
561
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
562
- const merged = overlay ? { ...stored, ...overlay } : stored;
563
- const operatorChoice = typeof merged.audioBackend === "string" ? merged.audioBackend : DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
564
- const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
565
- const filteredModels = AUDIO_MODEL_OPTIONS.filter(
566
- (o) => o.value === "" || o.value === effectiveBackend
567
- );
568
- const storedModel = typeof merged.selectedAudioModel === "string" ? merged.selectedAudioModel : "";
569
- const validModel = filteredModels.find((o) => o.value === storedModel)?.value ?? "";
570
- const raw = { ...merged, selectedAudioModel: validModel };
571
- const schema = this.globalSettingsSchema();
572
- const patched = {
573
- ...schema,
574
- sections: schema.sections.map((section) => ({
575
- ...section,
576
- fields: section.fields.map((field) => {
577
- if (field.type === "select" && field.key === "selectedAudioModel") {
578
- return { ...field, options: filteredModels.map((o) => ({ value: o.value, label: o.label })) };
579
- }
580
- return field;
581
- })
582
- }))
583
- };
584
- return hydrateSchema(patched, raw);
585
- }
586
- /**
587
- * Re-run the platform probe and persist the detected backend into
588
- * `probedBestAudioBackend`. Operator `audioBackend` setting is not
589
- * touched — only the hint.
590
- */
591
- async reprobeAudioEngine() {
592
- const backend = process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
593
- await this.ctx.settings?.writeAddonStore({ probedBestAudioBackend: backend });
594
- this.ctx.logger.info("reprobeAudioEngine: wrote probedBestAudioBackend", { meta: { backend } });
595
- return { backend };
596
- }
597
- /** Resolve the effective backend from the operator choice, falling back to the platform heuristic when 'auto'. */
598
- resolveAudioBackend() {
599
- const choice = this.config.audioBackend;
600
- if (choice === "apple-soundanalysis") return "apple-soundanalysis";
601
- if (choice === "yamnet-onnx") return "yamnet-onnx";
602
- return process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
603
- }
604
- async onInitialize() {
605
- const logger = this.ctx.logger;
606
- const modelsDir = await this.ctx.api.storage.resolve.query({ location: "models", relativePath: "" }).catch(() => "camstack-data/models");
607
- const backend = this.resolveAudioBackend();
608
- logger.info("audio-analyzer: resolving pipeline", {
609
- meta: { operatorChoice: this.config.audioBackend, effectiveBackend: backend, selectedModel: this.config.selectedAudioModel || null }
610
- });
611
- const p = await createAudioPipeline(modelsDir, logger, { backend });
612
- await p.initialize();
613
- this.pipeline = p;
614
- if (!this.config.probedBestAudioBackend) {
615
- this.reprobeAudioEngine().catch((err) => {
616
- logger.warn("audio: auto-reprobe failed", {
617
- meta: { error: err instanceof Error ? err.message : String(err) }
618
- });
619
- });
620
- }
621
- const self = this;
622
- const deviceSettingsResolver = async (deviceId) => {
623
- try {
624
- const resolved = await self.ctx.api.pipelineOrchestrator.resolvePipeline.query({ deviceId });
625
- const stepSettings = resolved.audio?.settings ?? {};
626
- const minConfidence = typeof stepSettings["minConfidence"] === "number" ? stepSettings["minConfidence"] : 0.3;
627
- const allowedClasses = Array.isArray(stepSettings["enabledAudioClasses"]) ? stepSettings["enabledAudioClasses"] : [];
628
- return { minConfidence, allowedClasses };
629
- } catch (err) {
630
- logger.warn("audio: resolveDeviceSettings via orchestrator failed", {
631
- tags: { deviceId },
632
- meta: { error: err instanceof Error ? err.message : String(err) }
633
- });
634
- return null;
635
- }
636
- };
637
- const deviceContributionResolver = async (_deviceId) => {
638
- return null;
639
- };
640
- const deviceSettingsPatcher = async (_deviceId, _patch) => {
641
- };
642
- this.provider = new AudioAnalyzerProvider(
643
- logger,
644
- this.pipeline,
645
- backend,
646
- deviceSettingsResolver,
647
- deviceContributionResolver,
648
- deviceSettingsPatcher,
649
- () => this.reprobeAudioEngine()
650
- );
651
- return [
652
- { capability: audioAnalyzerCapability, provider: this.provider },
653
- {
654
- capability: audioAnalysisCapability,
655
- provider: this.provider
656
- }
657
- ];
658
- }
659
- async onShutdown() {
660
- if (this.provider) {
661
- await this.provider.dispose();
662
- this.provider = null;
663
- }
664
- this.pipeline = null;
665
- }
666
- // ── Standard ICamstackAddon — three-level settings API ─────
667
- //
668
- // Per-device audio settings (audio class filter + minConfidence) moved
669
- // to the audio-classifier pipeline step's `getConfigSchema()` and the
670
- // orchestrator owns the audio node assignment (`audioNodeId`). No
671
- // device-level settings remain on this addon.
672
- buildDeviceSchema() {
673
- return { sections: [] };
674
- }
675
- async getDeviceSettings(deviceId) {
676
- const raw = await this.ctx?.settings?.readDeviceStore(deviceId) ?? {};
677
- return hydrateSchema(this.buildDeviceSchema(), raw);
678
- }
679
- async updateDeviceSettings(deviceId, patch) {
680
- await this.ctx?.settings?.writeDeviceStore(deviceId, patch);
681
- }
682
- }
683
- export {
684
- AudioAnalyzerAddon,
685
- AudioAnalyzerProvider,
686
- createAudioPipeline,
687
- AudioAnalyzerAddon as default
348
+ var AudioAnalyzerProvider = class {
349
+ pipeline;
350
+ deviceSettingsResolver;
351
+ deviceContributionResolver;
352
+ deviceSettingsPatcher;
353
+ reprobeImpl;
354
+ log;
355
+ classifyCallCount = 0;
356
+ lastClassifyErrorMs = 0;
357
+ suppressedClassifyErrors = 0;
358
+ classifyCount = 0;
359
+ backendName = "unknown";
360
+ /** When true, logs a raw-label sample every 100 classifications (opt-in
361
+ * debug aid). Off by default — the watchdog heartbeat covers liveness. */
362
+ debugClassifySamples = false;
363
+ /** Per-camera in-flight state. Key = deviceId (or GLOBAL_DEVICE_KEY for legacy callers). */
364
+ cameraState = /* @__PURE__ */ new Map();
365
+ /** Global pipeline lockApple SA and ONNX are single-channel: only one classify() can
366
+ * run at a time. Without this, concurrent calls from different cameras overwrite the
367
+ * single pendingResolve slot in AppleSoundAnalysisPipeline, causing 30s timeouts. */
368
+ pipelineBusy = false;
369
+ constructor(logger, pipeline, backendName, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
370
+ this.pipeline = pipeline;
371
+ this.deviceSettingsResolver = deviceSettingsResolver;
372
+ this.deviceContributionResolver = deviceContributionResolver;
373
+ this.deviceSettingsPatcher = deviceSettingsPatcher;
374
+ this.reprobeImpl = reprobeImpl;
375
+ this.log = logger;
376
+ this.backendName = backendName;
377
+ }
378
+ async getDeviceSettingsContribution(input) {
379
+ return this.deviceContributionResolver(input.deviceId);
380
+ }
381
+ async getDeviceLiveContribution(_input) {
382
+ return null;
383
+ }
384
+ async applyDeviceSettingsPatch(input) {
385
+ await this.deviceSettingsPatcher(input.deviceId, input.patch);
386
+ return { success: true };
387
+ }
388
+ /**
389
+ * Return the effective per-device audio-analyzer settings, resolved via
390
+ * the kernel's 3-level settings resolver (schema default global →
391
+ * device override). Orchestrator consumers call this method so they
392
+ * never need to know the audio-analyzer schema field names.
393
+ */
394
+ async resolveDeviceSettings({ deviceId }) {
395
+ return this.deviceSettingsResolver(deviceId);
396
+ }
397
+ async analyseChunk({ chunk, settings }) {
398
+ const samples = float32FromBytes(chunk.data);
399
+ let sumSquares = 0;
400
+ for (let i = 0; i < samples.length; i++) sumSquares += samples[i] * samples[i];
401
+ const rms = Math.sqrt(sumSquares / samples.length);
402
+ const dbfs = rms > 0 ? 20 * Math.log10(rms) : -96;
403
+ const level = {
404
+ rms: Math.round(rms * 1e4) / 1e4,
405
+ dbfs: Math.round(dbfs * 10) / 10
406
+ };
407
+ let classification;
408
+ try {
409
+ const result = await this.classify(chunk);
410
+ if (this.classifyCallCount < 3) {
411
+ const topRaw = result.labels.slice(0, 5).map((l) => `${l.className}(${(l.score * 100).toFixed(0)}%)`).join(", ");
412
+ this.log.info("classify debug sample", {
413
+ tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
414
+ meta: {
415
+ index: this.classifyCallCount,
416
+ labelCount: result.labels.length,
417
+ top: topRaw,
418
+ inferenceMs: result.inferenceMs,
419
+ minConf: settings.minConfidence,
420
+ allowedClasses: settings.allowedClasses
421
+ }
422
+ });
423
+ }
424
+ this.classifyCallCount++;
425
+ if (result.inferenceMs > 0) {
426
+ const minConf = settings.minConfidence;
427
+ const allowedSet = settings.allowedClasses.length > 0 ? new Set(settings.allowedClasses.map((c) => c.toLowerCase())) : null;
428
+ let filtered = result.labels.filter((c) => c.score >= minConf);
429
+ if (allowedSet) filtered = filtered.filter((c) => allowedSet.has(c.className.toLowerCase()));
430
+ if (filtered.length > 0) classification = {
431
+ labels: filtered,
432
+ inferenceMs: result.inferenceMs
433
+ };
434
+ }
435
+ } catch (err) {
436
+ const now = Date.now();
437
+ if (now - this.lastClassifyErrorMs >= CLASSIFY_ERROR_SUPPRESS_MS) {
438
+ const suppressed = this.suppressedClassifyErrors;
439
+ this.suppressedClassifyErrors = 0;
440
+ this.lastClassifyErrorMs = now;
441
+ const msg = errMsg(err);
442
+ const stack = err instanceof Error ? err.stack : void 0;
443
+ this.log.warn("Audio classification failed", {
444
+ tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
445
+ meta: {
446
+ error: msg,
447
+ stack,
448
+ suppressedSince: suppressed > 0 ? suppressed : void 0
449
+ }
450
+ });
451
+ } else this.suppressedClassifyErrors++;
452
+ }
453
+ return {
454
+ level,
455
+ classification,
456
+ timestamp: chunk.timestamp
457
+ };
458
+ }
459
+ async classify(chunk) {
460
+ const camKey = chunk.deviceId ?? GLOBAL_DEVICE_KEY;
461
+ const now = Date.now();
462
+ const state = this.cameraState.get(camKey);
463
+ if (state?.inProgress || state !== void 0 && now - state.lastEndMs < CLASSIFY_MIN_INTERVAL_MS) return {
464
+ labels: [],
465
+ rawLabels: [],
466
+ inferenceMs: 0
467
+ };
468
+ if (this.pipelineBusy) return {
469
+ labels: [],
470
+ rawLabels: [],
471
+ inferenceMs: 0
472
+ };
473
+ this.cameraState.set(camKey, {
474
+ inProgress: true,
475
+ lastEndMs: state?.lastEndMs ?? 0
476
+ });
477
+ this.pipelineBusy = true;
478
+ const f32Data = float32FromBytes(chunk.data);
479
+ const result = await this.pipeline.classify({
480
+ data: f32Data,
481
+ sampleRate: chunk.sampleRate,
482
+ channels: chunk.channels
483
+ }).finally(() => {
484
+ this.pipelineBusy = false;
485
+ this.cameraState.set(camKey, {
486
+ inProgress: false,
487
+ lastEndMs: Date.now()
488
+ });
489
+ });
490
+ if (this.debugClassifySamples && (this.classifyCount < 3 || this.classifyCount % 100 === 0)) {
491
+ const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
492
+ this.log.info("classify debug sample", {
493
+ tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
494
+ meta: {
495
+ index: this.classifyCount,
496
+ engine: this.backendName,
497
+ rawLabelCount: result.classifications.length,
498
+ top: rawTop,
499
+ inferenceMs: result.inferenceMs
500
+ }
501
+ });
502
+ }
503
+ this.classifyCount++;
504
+ const macroAccum = /* @__PURE__ */ new Map();
505
+ for (const c of result.classifications) {
506
+ const macro = mapAudioLabelToMacro(c.className);
507
+ if (!macro) continue;
508
+ const prev = macroAccum.get(macro);
509
+ if (!prev || c.score > prev.score) macroAccum.set(macro, {
510
+ score: c.score,
511
+ rawTop: c.className
512
+ });
513
+ }
514
+ return {
515
+ labels: [...macroAccum.entries()].toSorted((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({
516
+ className,
517
+ originalClass: rawTop,
518
+ score
519
+ })),
520
+ rawLabels: [...result.classifications].toSorted((a, b) => b.score - a.score).map((c) => ({
521
+ className: c.className,
522
+ originalClass: c.className,
523
+ score: c.score
524
+ })),
525
+ inferenceMs: result.inferenceMs
526
+ };
527
+ }
528
+ isReady() {
529
+ return this.pipeline !== null;
530
+ }
531
+ async dispose() {
532
+ await this.pipeline.dispose();
533
+ }
534
+ async reprobeAudioEngine() {
535
+ return this.reprobeImpl();
536
+ }
537
+ };
538
+ /**
539
+ * Audio Analyzer addon — provides the `audio-analyzer` capability.
540
+ *
541
+ * Owns the IAudioPipeline directly no tRPC roundtrip to a separate
542
+ * audio-classifier addon.
543
+ */
544
+ var AudioAnalyzerAddon = class extends BaseAddon {
545
+ id = "audio-analyzer";
546
+ provider = null;
547
+ pipeline = null;
548
+ constructor() {
549
+ super(DEFAULT_AUDIO_ANALYZER_CONFIG);
550
+ }
551
+ globalSettingsSchema() {
552
+ return { sections: [{
553
+ id: "audio-engine",
554
+ title: "Audio",
555
+ tab: "engine",
556
+ order: 10,
557
+ description: "Audio classification backend (Apple SoundAnalysis or YAMNet ONNX). Independent from the vision-detection engine above. \"Auto\" picks Apple SoundAnalysis on macOS, YAMNet on Linux. Click the refresh icon next to \"Probed best\" to re-run the probe.",
558
+ fields: [
559
+ {
560
+ type: "text",
561
+ key: "probedBestAudioBackend",
562
+ label: "Probed best",
563
+ description: "Auto-detected best audio backend on this host. Click the refresh icon to re-run the probe.",
564
+ readonlyField: true,
565
+ default: "",
566
+ actions: [{
567
+ action: "reprobe-audio-engine",
568
+ icon: "refresh-cw",
569
+ tooltip: "Re-probe audio engine"
570
+ }]
571
+ },
572
+ {
573
+ type: "select",
574
+ key: "audioBackend",
575
+ label: "Audio backend",
576
+ options: AUDIO_BACKEND_CHOICES.map((o) => ({
577
+ value: o.value,
578
+ label: o.label
579
+ })),
580
+ default: DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend,
581
+ immediate: true,
582
+ requiresRestart: true
583
+ },
584
+ {
585
+ type: "select",
586
+ key: "selectedAudioModel",
587
+ label: "Classification model",
588
+ description: "Empty = auto (matches backend). Device-level settings can only inherit / enable / disable this step; model + class filters live here at the node level.",
589
+ options: AUDIO_MODEL_OPTIONS.map((o) => ({
590
+ value: o.value,
591
+ label: o.label
592
+ })),
593
+ default: DEFAULT_AUDIO_ANALYZER_CONFIG.selectedAudioModel,
594
+ immediate: true,
595
+ requiresRestart: true
596
+ }
597
+ ]
598
+ }] };
599
+ }
600
+ /**
601
+ * Cascade override narrow the `selectedAudioModel` options to the
602
+ * subset compatible with the currently-selected `audioBackend`.
603
+ *
604
+ * Same pattern as detection-pipeline's `engineRuntime engineBackend
605
+ * → engineDevice` cascade: the base schema ships every option
606
+ * (Auto + YAMNet + Apple SA); this override drops the rows that
607
+ * belong to a backend the operator didn't pick. With `immediate:
608
+ * true` on the `audioBackend` select, the UI refetches schema after
609
+ * every flip and the model dropdown updates instantly.
610
+ *
611
+ * `overlay` carries the operator's tentative choices for benchmark/
612
+ * preview mode (operator typed but didn't save yet) — same
613
+ * semantics detection-pipeline relies on.
614
+ */
615
+ async getGlobalSettings(overlay) {
616
+ const ctx = this.ctxIfReady;
617
+ const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
618
+ const merged = overlay ? {
619
+ ...stored,
620
+ ...overlay
621
+ } : stored;
622
+ const operatorChoice = typeof merged.audioBackend === "string" ? merged.audioBackend : DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
623
+ const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
624
+ const filteredModels = AUDIO_MODEL_OPTIONS.filter((o) => o.value === "" || o.value === effectiveBackend);
625
+ const storedModel = typeof merged.selectedAudioModel === "string" ? merged.selectedAudioModel : "";
626
+ const validModel = filteredModels.find((o) => o.value === storedModel)?.value ?? "";
627
+ const raw = {
628
+ ...merged,
629
+ selectedAudioModel: validModel
630
+ };
631
+ const schema = this.globalSettingsSchema();
632
+ return hydrateSchema({
633
+ ...schema,
634
+ sections: schema.sections.map((section) => ({
635
+ ...section,
636
+ fields: section.fields.map((field) => {
637
+ if (field.type === "select" && field.key === "selectedAudioModel") return {
638
+ ...field,
639
+ options: filteredModels.map((o) => ({
640
+ value: o.value,
641
+ label: o.label
642
+ }))
643
+ };
644
+ return field;
645
+ })
646
+ }))
647
+ }, raw);
648
+ }
649
+ /**
650
+ * Re-run the platform probe and persist the detected backend into
651
+ * `probedBestAudioBackend`. Operator `audioBackend` setting is not
652
+ * touched — only the hint.
653
+ */
654
+ async reprobeAudioEngine() {
655
+ const backend = process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
656
+ await this.ctx.settings?.writeAddonStore({ probedBestAudioBackend: backend });
657
+ this.ctx.logger.info("reprobeAudioEngine: wrote probedBestAudioBackend", { meta: { backend } });
658
+ return { backend };
659
+ }
660
+ /** Resolve the effective backend from the operator choice, falling back to the platform heuristic when 'auto'. */
661
+ resolveAudioBackend() {
662
+ const choice = this.config.audioBackend;
663
+ if (choice === "apple-soundanalysis") return "apple-soundanalysis";
664
+ if (choice === "yamnet-onnx") return "yamnet-onnx";
665
+ return process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
666
+ }
667
+ async onInitialize() {
668
+ const logger = this.ctx.logger;
669
+ const modelsDir = await this.ctx.api.storage.resolve.query({
670
+ location: "models",
671
+ relativePath: ""
672
+ }).catch(() => "camstack-data/models");
673
+ const backend = this.resolveAudioBackend();
674
+ logger.info("audio-analyzer: resolving pipeline", { meta: {
675
+ operatorChoice: this.config.audioBackend,
676
+ effectiveBackend: backend,
677
+ selectedModel: this.config.selectedAudioModel || null
678
+ } });
679
+ const p = await createAudioPipeline(modelsDir, logger, { backend });
680
+ await p.initialize();
681
+ this.pipeline = p;
682
+ if (!this.config.probedBestAudioBackend) this.reprobeAudioEngine().catch((err) => {
683
+ logger.warn("audio: auto-reprobe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
684
+ });
685
+ const self = this;
686
+ const deviceSettingsResolver = async (deviceId) => {
687
+ try {
688
+ const stepSettings = (await self.ctx.api.pipelineOrchestrator.resolvePipeline.query({ deviceId })).audio?.settings ?? {};
689
+ return {
690
+ minConfidence: typeof stepSettings["minConfidence"] === "number" ? stepSettings["minConfidence"] : .3,
691
+ allowedClasses: Array.isArray(stepSettings["enabledAudioClasses"]) ? stepSettings["enabledAudioClasses"] : []
692
+ };
693
+ } catch (err) {
694
+ logger.warn("audio: resolveDeviceSettings via orchestrator failed", {
695
+ tags: { deviceId },
696
+ meta: { error: err instanceof Error ? err.message : String(err) }
697
+ });
698
+ return null;
699
+ }
700
+ };
701
+ const deviceContributionResolver = async (_deviceId) => {
702
+ return null;
703
+ };
704
+ const deviceSettingsPatcher = async (_deviceId, _patch) => {};
705
+ this.provider = new AudioAnalyzerProvider(logger, this.pipeline, backend, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, () => this.reprobeAudioEngine());
706
+ return [{
707
+ capability: audioAnalyzerCapability,
708
+ provider: this.provider
709
+ }, {
710
+ capability: audioAnalysisCapability,
711
+ provider: this.provider
712
+ }];
713
+ }
714
+ async onShutdown() {
715
+ if (this.provider) {
716
+ await this.provider.dispose();
717
+ this.provider = null;
718
+ }
719
+ this.pipeline = null;
720
+ }
721
+ buildDeviceSchema() {
722
+ return { sections: [] };
723
+ }
724
+ async getDeviceSettings(deviceId) {
725
+ const raw = await this.ctx?.settings?.readDeviceStore(deviceId) ?? {};
726
+ return hydrateSchema(this.buildDeviceSchema(), raw);
727
+ }
728
+ async updateDeviceSettings(deviceId, patch) {
729
+ await this.ctx?.settings?.writeDeviceStore(deviceId, patch);
730
+ }
688
731
  };
689
- //# sourceMappingURL=index.mjs.map
732
+ //#endregion
733
+ //#region src/audio-analyzer/index.ts
734
+ var audio_analyzer_default = AudioAnalyzerAddon;
735
+ //#endregion
736
+ export { AudioAnalyzerAddon, AudioAnalyzerProvider, createAudioPipeline, audio_analyzer_default as default };