@camstack/addon-pipeline 0.1.8
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/audio-analyzer/index.js +723 -0
- package/dist/audio-analyzer/index.js.map +1 -0
- package/dist/audio-analyzer/index.mjs +683 -0
- package/dist/audio-analyzer/index.mjs.map +1 -0
- package/dist/audio-codec-nodeav/index.js +467 -0
- package/dist/audio-codec-nodeav/index.js.map +1 -0
- package/dist/audio-codec-nodeav/index.mjs +467 -0
- package/dist/audio-codec-nodeav/index.mjs.map +1 -0
- package/dist/decoder-nodeav/index.js +929 -0
- package/dist/decoder-nodeav/index.js.map +1 -0
- package/dist/decoder-nodeav/index.mjs +907 -0
- package/dist/decoder-nodeav/index.mjs.map +1 -0
- package/dist/detection-pipeline/index.js +5766 -0
- package/dist/detection-pipeline/index.js.map +1 -0
- package/dist/detection-pipeline/index.mjs +5725 -0
- package/dist/detection-pipeline/index.mjs.map +1 -0
- package/dist/index-D_cl0Qqb.js +5791 -0
- package/dist/index-D_cl0Qqb.js.map +1 -0
- package/dist/index-UbcdLS7a.mjs +5790 -0
- package/dist/index-UbcdLS7a.mjs.map +1 -0
- package/dist/motion-wasm/index.js +476 -0
- package/dist/motion-wasm/index.js.map +1 -0
- package/dist/motion-wasm/index.mjs +454 -0
- package/dist/motion-wasm/index.mjs.map +1 -0
- package/dist/pipeline-runner/index.js +1669 -0
- package/dist/pipeline-runner/index.js.map +1 -0
- package/dist/pipeline-runner/index.mjs +1647 -0
- package/dist/pipeline-runner/index.mjs.map +1 -0
- package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/StreamBrokerPanel.d.ts +21 -0
- package/dist/stream-broker/@mf-types/compiled-types/stream-broker/widgets/index.d.ts +13 -0
- package/dist/stream-broker/@mf-types/widgets.d.ts +2 -0
- package/dist/stream-broker/@mf-types.d.ts +3 -0
- package/dist/stream-broker/@mf-types.zip +0 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs +12 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-C-URP6DW.mjs +17 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-69eEmXwl.mjs +20 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-U1EUeEPs.mjs +104 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-DeouEaSs.mjs +85 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-DHUwjbb9.mjs +62 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DePVYdid.mjs +85 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CBlCGyx5.mjs +29 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-gBEZsQrp.mjs +36 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-DYEKzzY-.mjs +45 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-DZchZKbW.mjs +6 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-DICOtMTl.mjs +34 -0
- package/dist/stream-broker/_stub.js +752 -0
- package/dist/stream-broker/_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D6o1e2ed.mjs +156 -0
- package/dist/stream-broker/client-BK73l2KT.mjs +10063 -0
- package/dist/stream-broker/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +211 -0
- package/dist/stream-broker/hostInit-RCeroTVY.mjs +168 -0
- package/dist/stream-broker/index-BYclbfM0.mjs +15806 -0
- package/dist/stream-broker/index-BhXZh4lQ.mjs +1617 -0
- package/dist/stream-broker/index-BxHaCH3N.mjs +725 -0
- package/dist/stream-broker/index-D2-K2YJ7.mjs +19268 -0
- package/dist/stream-broker/index-IUYKHbxX.mjs +185 -0
- package/dist/stream-broker/index-Ss9m7Jum.mjs +2603 -0
- package/dist/stream-broker/index-ns1fRD30.mjs +435 -0
- package/dist/stream-broker/index-xncRG7-x.mjs +2713 -0
- package/dist/stream-broker/index.js +11171 -0
- package/dist/stream-broker/index.js.map +1 -0
- package/dist/stream-broker/index.mjs +11130 -0
- package/dist/stream-broker/index.mjs.map +1 -0
- package/dist/stream-broker/jsx-runtime-ZdY5pIZz.mjs +55 -0
- package/dist/stream-broker/remoteEntry.js +2973 -0
- package/dist/stream-broker/virtualExposes-pCd777Rp.mjs +42 -0
- package/package.json +258 -0
- package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
- package/python/inference_pool.py +1088 -0
- package/python/postprocessors/__init__.py +24 -0
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
- package/python/postprocessors/arcface.py +31 -0
- package/python/postprocessors/ctc.py +68 -0
- package/python/postprocessors/saliency.py +44 -0
- package/python/postprocessors/scrfd.py +212 -0
- package/python/postprocessors/softmax.py +43 -0
- package/python/postprocessors/yamnet.py +41 -0
- package/python/postprocessors/yolo.py +278 -0
- package/python/postprocessors/yolo_seg.py +247 -0
- package/python/requirements-coreml.txt +4 -0
- package/python/requirements-onnxruntime.txt +3 -0
- package/python/requirements-openvino.txt +3 -0
- package/python/requirements.txt +9 -0
- package/swift/audio-analyzer/apple-sound-classifier +0 -0
- package/swift/audio-analyzer/apple-sound-classifier.swift +213 -0
- package/swift/detection-pipeline/apple-sound-classifier +0 -0
- package/swift/detection-pipeline/apple-sound-classifier.swift +196 -0
- package/wasm/assembly/index.ts +290 -0
- package/wasm/assembly/tsconfig.json +4 -0
- package/wasm/motion.wasm +0 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
25
|
+
const types = require("@camstack/types");
|
|
26
|
+
const path = require("node:path");
|
|
27
|
+
const fs = require("node:fs");
|
|
28
|
+
const core = require("@camstack/core");
|
|
29
|
+
function _interopNamespaceDefault(e) {
|
|
30
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
31
|
+
if (e) {
|
|
32
|
+
for (const k in e) {
|
|
33
|
+
if (k !== "default") {
|
|
34
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
35
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: () => e[k]
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
n.default = e;
|
|
43
|
+
return Object.freeze(n);
|
|
44
|
+
}
|
|
45
|
+
const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
|
|
46
|
+
const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
|
|
47
|
+
async function createAudioPipeline(modelsDir, logger, options) {
|
|
48
|
+
const backend = options?.backend ?? (process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx");
|
|
49
|
+
if (backend === "apple-soundanalysis") {
|
|
50
|
+
return new AppleSoundAnalysisPipeline(logger);
|
|
51
|
+
}
|
|
52
|
+
return new YamnetOnnxPipeline(modelsDir, logger);
|
|
53
|
+
}
|
|
54
|
+
const YAMNET_MODEL_URL = `${types.HF_BASE_URL}/audioClassification/yamnet/onnx/camstack-yamnet.onnx`;
|
|
55
|
+
const YAMNET_LABELS_URL = `${types.HF_BASE_URL}/audioClassification/yamnet/camstack-yamnet-labels.json`;
|
|
56
|
+
class YamnetOnnxPipeline {
|
|
57
|
+
constructor(modelsDir, logger) {
|
|
58
|
+
this.modelsDir = modelsDir;
|
|
59
|
+
this.log = logger;
|
|
60
|
+
}
|
|
61
|
+
session = null;
|
|
62
|
+
inputName = "";
|
|
63
|
+
labels = [];
|
|
64
|
+
log;
|
|
65
|
+
async initialize() {
|
|
66
|
+
const ort = await import("onnxruntime-node");
|
|
67
|
+
const modelPath = path__namespace.join(this.modelsDir, "camstack-yamnet.onnx");
|
|
68
|
+
const labelsPath = path__namespace.join(this.modelsDir, "camstack-yamnet-labels.json");
|
|
69
|
+
if (!fs__namespace.existsSync(modelPath)) {
|
|
70
|
+
this.log.info("YAMNet ONNX model not found locally — downloading from HuggingFace", {
|
|
71
|
+
meta: { url: YAMNET_MODEL_URL, dest: modelPath }
|
|
72
|
+
});
|
|
73
|
+
await core.downloadFile(YAMNET_MODEL_URL, modelPath);
|
|
74
|
+
this.log.info("YAMNet ONNX model downloaded", {
|
|
75
|
+
meta: { sizeBytes: fs__namespace.statSync(modelPath).size }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!fs__namespace.existsSync(labelsPath)) {
|
|
79
|
+
this.log.info("YAMNet labels not found locally — downloading from HuggingFace", {
|
|
80
|
+
meta: { url: YAMNET_LABELS_URL, dest: labelsPath }
|
|
81
|
+
});
|
|
82
|
+
await core.downloadFile(YAMNET_LABELS_URL, labelsPath);
|
|
83
|
+
}
|
|
84
|
+
this.session = await ort.InferenceSession.create(modelPath);
|
|
85
|
+
this.inputName = this.session.inputNames[0] ?? "waveform";
|
|
86
|
+
if (fs__namespace.existsSync(labelsPath)) {
|
|
87
|
+
this.labels = JSON.parse(fs__namespace.readFileSync(labelsPath, "utf8"));
|
|
88
|
+
} else {
|
|
89
|
+
this.log.warn("YAMNet labels file not found — classifications will use numeric indices");
|
|
90
|
+
}
|
|
91
|
+
this.log.info(`YAMNet ONNX pipeline initialized (${this.labels.length} labels)`);
|
|
92
|
+
}
|
|
93
|
+
async classify(chunk) {
|
|
94
|
+
if (!this.session) {
|
|
95
|
+
throw new Error("YAMNet pipeline not initialized");
|
|
96
|
+
}
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
const ort = await import("onnxruntime-node");
|
|
99
|
+
const waveform = chunk.sampleRate === 16e3 && chunk.channels === 1 ? chunk.data : resampleMono16k(chunk);
|
|
100
|
+
const tensor = new ort.Tensor("float32", waveform, [waveform.length]);
|
|
101
|
+
const feeds = { [this.inputName]: tensor };
|
|
102
|
+
const results = await this.session.run(feeds);
|
|
103
|
+
const scoresData = results[this.session.outputNames[0]];
|
|
104
|
+
if (!scoresData) {
|
|
105
|
+
throw new Error("YAMNet returned no output");
|
|
106
|
+
}
|
|
107
|
+
const scores = scoresData.data;
|
|
108
|
+
const numClasses = 521;
|
|
109
|
+
const numFrames = scores.length / numClasses;
|
|
110
|
+
const avgScores = new Float32Array(numClasses);
|
|
111
|
+
for (let f = 0; f < numFrames; f++) {
|
|
112
|
+
for (let c = 0; c < numClasses; c++) {
|
|
113
|
+
avgScores[c] += scores[f * numClasses + c];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (let c = 0; c < numClasses; c++) {
|
|
117
|
+
avgScores[c] = avgScores[c] / numFrames;
|
|
118
|
+
}
|
|
119
|
+
const minScore = 0.05;
|
|
120
|
+
const classifications = [];
|
|
121
|
+
for (let c = 0; c < numClasses; c++) {
|
|
122
|
+
const score = avgScores[c];
|
|
123
|
+
if (score >= minScore) {
|
|
124
|
+
const label = c < this.labels.length ? this.labels[c] : String(c);
|
|
125
|
+
classifications.push({ className: label, score: Math.round(score * 1e3) / 1e3 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
classifications.sort((a, b) => b.score - a.score);
|
|
129
|
+
return {
|
|
130
|
+
classifications: classifications.slice(0, 10),
|
|
131
|
+
inferenceMs: Date.now() - start
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async dispose() {
|
|
135
|
+
if (this.session) {
|
|
136
|
+
await this.session.release();
|
|
137
|
+
this.session = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
class AppleSoundAnalysisPipeline {
|
|
142
|
+
log;
|
|
143
|
+
process = null;
|
|
144
|
+
receiveBuffer = Buffer.alloc(0);
|
|
145
|
+
pendingResolve = null;
|
|
146
|
+
pendingReject = null;
|
|
147
|
+
binaryPath = null;
|
|
148
|
+
debugCount = 0;
|
|
149
|
+
constructor(logger) {
|
|
150
|
+
this.log = logger;
|
|
151
|
+
}
|
|
152
|
+
async initialize() {
|
|
153
|
+
this.binaryPath = await this.resolveSwiftBinary();
|
|
154
|
+
if (!this.binaryPath) {
|
|
155
|
+
throw new Error("Apple SoundAnalysis: Swift CLI not found and compilation failed. macOS with Xcode CLI tools required.");
|
|
156
|
+
}
|
|
157
|
+
const { spawn } = await import("node:child_process");
|
|
158
|
+
this.process = spawn(this.binaryPath, ["--sample-rate=16000", "--top-k=10"], {
|
|
159
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
160
|
+
});
|
|
161
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
162
|
+
const lines = chunk.toString().split("\n");
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
if (trimmed) this.log.warn(trimmed);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
this.process.on("error", (err) => {
|
|
169
|
+
this.log.error("Swift process error", { meta: { error: err.message } });
|
|
170
|
+
this.pendingReject?.(err);
|
|
171
|
+
this.pendingReject = null;
|
|
172
|
+
this.pendingResolve = null;
|
|
173
|
+
});
|
|
174
|
+
this.process.on("exit", (code) => {
|
|
175
|
+
if (code !== 0 && code !== null) {
|
|
176
|
+
this.log.error("Swift process exited", { meta: { code } });
|
|
177
|
+
const err = new Error(`Apple SoundAnalysis: process exited with code ${code}`);
|
|
178
|
+
this.pendingReject?.(err);
|
|
179
|
+
this.pendingReject = null;
|
|
180
|
+
this.pendingResolve = null;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
this.process.stdout.on("data", (chunk) => {
|
|
184
|
+
this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]);
|
|
185
|
+
this.tryReceive();
|
|
186
|
+
});
|
|
187
|
+
const ready = await this.receiveMessage();
|
|
188
|
+
if (ready["status"] !== "ready") {
|
|
189
|
+
throw new Error(`Apple SoundAnalysis: unexpected init response: ${JSON.stringify(ready)}`);
|
|
190
|
+
}
|
|
191
|
+
this.log.info("Apple SoundAnalysis pipeline initialized (macOS built-in, Swift CLI bridge)");
|
|
192
|
+
}
|
|
193
|
+
async classify(chunk) {
|
|
194
|
+
if (!this.process?.stdin) {
|
|
195
|
+
throw new Error("Apple SoundAnalysis: process not initialized");
|
|
196
|
+
}
|
|
197
|
+
const waveform = chunk.sampleRate === 16e3 && chunk.channels === 1 ? chunk.data : resampleMono16k(chunk);
|
|
198
|
+
const audioBuffer = Buffer.from(waveform.buffer, waveform.byteOffset, waveform.byteLength);
|
|
199
|
+
const lengthBuf = Buffer.allocUnsafe(4);
|
|
200
|
+
lengthBuf.writeUInt32LE(audioBuffer.length, 0);
|
|
201
|
+
this.process.stdin.write(Buffer.concat([lengthBuf, audioBuffer]));
|
|
202
|
+
const result = await this.receiveMessage();
|
|
203
|
+
const classifications = result["classifications"] ?? [];
|
|
204
|
+
const inferenceMs = result["inferenceMs"] ?? 0;
|
|
205
|
+
if (this.debugCount < 3) {
|
|
206
|
+
const keys = Object.keys(result);
|
|
207
|
+
this.log.info("classify debug sample", {
|
|
208
|
+
meta: {
|
|
209
|
+
phase: "apple-sa",
|
|
210
|
+
index: this.debugCount,
|
|
211
|
+
keys,
|
|
212
|
+
classifications: classifications.length,
|
|
213
|
+
inferenceMs,
|
|
214
|
+
audioBytes: Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength).length,
|
|
215
|
+
sampleRate: chunk.sampleRate,
|
|
216
|
+
channels: chunk.channels
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
if (result["error"]) {
|
|
220
|
+
this.log.error("Swift error", { meta: { phase: "apple-sa", error: result["error"] } });
|
|
221
|
+
}
|
|
222
|
+
this.debugCount++;
|
|
223
|
+
}
|
|
224
|
+
return { classifications, inferenceMs };
|
|
225
|
+
}
|
|
226
|
+
async dispose() {
|
|
227
|
+
const proc = this.process;
|
|
228
|
+
if (!proc) return;
|
|
229
|
+
this.process = null;
|
|
230
|
+
proc.stdin?.end();
|
|
231
|
+
proc.kill("SIGTERM");
|
|
232
|
+
const exited = await new Promise((resolve) => {
|
|
233
|
+
const timer = setTimeout(() => resolve(false), 5e3);
|
|
234
|
+
proc.once("exit", () => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
resolve(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
if (!exited) {
|
|
240
|
+
try {
|
|
241
|
+
proc.kill("SIGKILL");
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
this.log.warn("Swift process did not exit gracefully — sent SIGKILL");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
receiveMessage() {
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
this.pendingResolve = resolve;
|
|
250
|
+
this.pendingReject = reject;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
tryReceive() {
|
|
254
|
+
if (this.receiveBuffer.length < 4) return;
|
|
255
|
+
const length = this.receiveBuffer.readUInt32LE(0);
|
|
256
|
+
if (this.receiveBuffer.length < 4 + length) return;
|
|
257
|
+
const jsonBytes = this.receiveBuffer.subarray(4, 4 + length);
|
|
258
|
+
this.receiveBuffer = this.receiveBuffer.subarray(4 + length);
|
|
259
|
+
const resolve = this.pendingResolve;
|
|
260
|
+
const reject = this.pendingReject;
|
|
261
|
+
this.pendingResolve = null;
|
|
262
|
+
this.pendingReject = null;
|
|
263
|
+
if (!resolve) return;
|
|
264
|
+
try {
|
|
265
|
+
const parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
266
|
+
resolve(parsed);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
reject?.(err instanceof Error ? err : new Error(String(err)));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Find pre-compiled binary or compile from Swift source. */
|
|
272
|
+
async resolveSwiftBinary() {
|
|
273
|
+
const candidates = [
|
|
274
|
+
path__namespace.join(__dirname, "../../swift/audio-analyzer/apple-sound-classifier"),
|
|
275
|
+
// Fallback for in-tree dev (src/<id>/swift/) and pre-merge layouts.
|
|
276
|
+
path__namespace.join(__dirname, "../swift/apple-sound-classifier"),
|
|
277
|
+
path__namespace.join(__dirname, "../../swift/apple-sound-classifier"),
|
|
278
|
+
path__namespace.join(__dirname, "../../../swift/apple-sound-classifier")
|
|
279
|
+
];
|
|
280
|
+
for (const p of candidates) {
|
|
281
|
+
if (fs__namespace.existsSync(p)) {
|
|
282
|
+
this.log.info("Found pre-compiled Swift CLI", { meta: { path: p } });
|
|
283
|
+
return p;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const sourceCandidates = [
|
|
287
|
+
path__namespace.join(__dirname, "../../swift/audio-analyzer/apple-sound-classifier.swift"),
|
|
288
|
+
path__namespace.join(__dirname, "../swift/apple-sound-classifier.swift"),
|
|
289
|
+
path__namespace.join(__dirname, "../../swift/apple-sound-classifier.swift"),
|
|
290
|
+
path__namespace.join(__dirname, "../../../swift/apple-sound-classifier.swift")
|
|
291
|
+
];
|
|
292
|
+
const sourcePath = sourceCandidates.find((p) => fs__namespace.existsSync(p));
|
|
293
|
+
if (!sourcePath) {
|
|
294
|
+
this.log.error("Swift source not found", { meta: { searched: sourceCandidates } });
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const outputPath = sourcePath.replace(".swift", "");
|
|
298
|
+
this.log.info("Compiling Swift CLI...", { meta: { source: sourcePath, output: outputPath } });
|
|
299
|
+
const { execFileSync } = await import("node:child_process");
|
|
300
|
+
try {
|
|
301
|
+
execFileSync("swiftc", ["-O", "-o", outputPath, sourcePath], {
|
|
302
|
+
timeout: 6e4,
|
|
303
|
+
stdio: "pipe"
|
|
304
|
+
});
|
|
305
|
+
this.log.info("Swift CLI compiled successfully");
|
|
306
|
+
return outputPath;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
this.log.error("Swift compilation failed — install Xcode Command Line Tools", {
|
|
309
|
+
meta: { error: types.errMsg(err) }
|
|
310
|
+
});
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function resampleMono16k(chunk) {
|
|
316
|
+
const { data, sampleRate, channels } = chunk;
|
|
317
|
+
const numSamples = data.length / channels;
|
|
318
|
+
const mono = new Float32Array(numSamples);
|
|
319
|
+
for (let i = 0; i < numSamples; i++) {
|
|
320
|
+
let sum = 0;
|
|
321
|
+
for (let c = 0; c < channels; c++) {
|
|
322
|
+
sum += data[i * channels + c];
|
|
323
|
+
}
|
|
324
|
+
mono[i] = sum / channels;
|
|
325
|
+
}
|
|
326
|
+
const ratio = 16e3 / sampleRate;
|
|
327
|
+
const outLen = Math.floor(numSamples * ratio);
|
|
328
|
+
const out = new Float32Array(outLen);
|
|
329
|
+
for (let i = 0; i < outLen; i++) {
|
|
330
|
+
const srcIdx = i / ratio;
|
|
331
|
+
const lo = Math.floor(srcIdx);
|
|
332
|
+
const hi = Math.min(lo + 1, numSamples - 1);
|
|
333
|
+
const frac = srcIdx - lo;
|
|
334
|
+
out[i] = mono[lo] * (1 - frac) + mono[hi] * frac;
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
const AUDIO_MODEL_OPTIONS = [
|
|
339
|
+
{ value: "", label: "Auto (matches backend)" },
|
|
340
|
+
{ value: "yamnet-onnx", label: "YAMNet (ONNX)" },
|
|
341
|
+
{ value: "apple-soundanalysis", label: "Apple SoundAnalysis (built-in)" }
|
|
342
|
+
];
|
|
343
|
+
const CLASSIFY_ERROR_SUPPRESS_MS = 3e4;
|
|
344
|
+
const CLASSIFY_MIN_INTERVAL_MS = 500;
|
|
345
|
+
const GLOBAL_DEVICE_KEY = -1;
|
|
346
|
+
class AudioAnalyzerProvider {
|
|
347
|
+
constructor(logger, pipeline, backendName, deviceSettingsResolver, deviceContributionResolver, deviceSettingsPatcher, reprobeImpl) {
|
|
348
|
+
this.pipeline = pipeline;
|
|
349
|
+
this.deviceSettingsResolver = deviceSettingsResolver;
|
|
350
|
+
this.deviceContributionResolver = deviceContributionResolver;
|
|
351
|
+
this.deviceSettingsPatcher = deviceSettingsPatcher;
|
|
352
|
+
this.reprobeImpl = reprobeImpl;
|
|
353
|
+
this.log = logger;
|
|
354
|
+
this.backendName = backendName;
|
|
355
|
+
}
|
|
356
|
+
log;
|
|
357
|
+
classifyCallCount = 0;
|
|
358
|
+
lastClassifyErrorMs = 0;
|
|
359
|
+
suppressedClassifyErrors = 0;
|
|
360
|
+
classifyCount = 0;
|
|
361
|
+
backendName = "unknown";
|
|
362
|
+
/** Per-camera in-flight state. Key = deviceId (or GLOBAL_DEVICE_KEY for legacy callers). */
|
|
363
|
+
cameraState = /* @__PURE__ */ new Map();
|
|
364
|
+
/** Global pipeline lock — Apple SA and ONNX are single-channel: only one classify() can
|
|
365
|
+
* run at a time. Without this, concurrent calls from different cameras overwrite the
|
|
366
|
+
* single pendingResolve slot in AppleSoundAnalysisPipeline, causing 30s timeouts. */
|
|
367
|
+
pipelineBusy = false;
|
|
368
|
+
// ── Device-details aggregator contribution ──────────────────────────────
|
|
369
|
+
async getDeviceSettingsContribution(input) {
|
|
370
|
+
return this.deviceContributionResolver(input.deviceId);
|
|
371
|
+
}
|
|
372
|
+
async getDeviceLiveContribution(_input) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
async applyDeviceSettingsPatch(input) {
|
|
376
|
+
await this.deviceSettingsPatcher(input.deviceId, input.patch);
|
|
377
|
+
return { success: true };
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Return the effective per-device audio-analyzer settings, resolved via
|
|
381
|
+
* the kernel's 3-level settings resolver (schema default → global →
|
|
382
|
+
* device override). Orchestrator consumers call this method so they
|
|
383
|
+
* never need to know the audio-analyzer schema field names.
|
|
384
|
+
*/
|
|
385
|
+
async resolveDeviceSettings({ deviceId }) {
|
|
386
|
+
return this.deviceSettingsResolver(deviceId);
|
|
387
|
+
}
|
|
388
|
+
async analyseChunk({
|
|
389
|
+
chunk,
|
|
390
|
+
settings
|
|
391
|
+
}) {
|
|
392
|
+
const samples = chunk.data;
|
|
393
|
+
let sumSquares = 0;
|
|
394
|
+
for (let i = 0; i < samples.length; i++) {
|
|
395
|
+
sumSquares += samples[i] * samples[i];
|
|
396
|
+
}
|
|
397
|
+
const rms = Math.sqrt(sumSquares / samples.length);
|
|
398
|
+
const dbfs = rms > 0 ? 20 * Math.log10(rms) : -96;
|
|
399
|
+
const level = {
|
|
400
|
+
rms: Math.round(rms * 1e4) / 1e4,
|
|
401
|
+
dbfs: Math.round(dbfs * 10) / 10
|
|
402
|
+
};
|
|
403
|
+
let classification;
|
|
404
|
+
try {
|
|
405
|
+
const result = await this.classify(chunk);
|
|
406
|
+
if (this.classifyCallCount < 3) {
|
|
407
|
+
const topRaw = result.labels.slice(0, 5).map((l) => `${l.className}(${(l.score * 100).toFixed(0)}%)`).join(", ");
|
|
408
|
+
this.log.info("classify debug sample", {
|
|
409
|
+
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
410
|
+
meta: {
|
|
411
|
+
index: this.classifyCallCount,
|
|
412
|
+
labelCount: result.labels.length,
|
|
413
|
+
top: topRaw,
|
|
414
|
+
inferenceMs: result.inferenceMs,
|
|
415
|
+
minConf: settings.minConfidence,
|
|
416
|
+
allowedClasses: settings.allowedClasses
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
this.classifyCallCount++;
|
|
421
|
+
if (result.inferenceMs > 0) {
|
|
422
|
+
const minConf = settings.minConfidence;
|
|
423
|
+
const allowedSet = settings.allowedClasses.length > 0 ? new Set(settings.allowedClasses.map((c) => c.toLowerCase())) : null;
|
|
424
|
+
let filtered = result.labels.filter((c) => c.score >= minConf);
|
|
425
|
+
if (allowedSet) {
|
|
426
|
+
filtered = filtered.filter((c) => allowedSet.has(c.className.toLowerCase()));
|
|
427
|
+
}
|
|
428
|
+
if (filtered.length > 0) {
|
|
429
|
+
classification = {
|
|
430
|
+
labels: filtered,
|
|
431
|
+
inferenceMs: result.inferenceMs
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
const sinceLastMs = now - this.lastClassifyErrorMs;
|
|
438
|
+
if (sinceLastMs >= CLASSIFY_ERROR_SUPPRESS_MS) {
|
|
439
|
+
const suppressed = this.suppressedClassifyErrors;
|
|
440
|
+
this.suppressedClassifyErrors = 0;
|
|
441
|
+
this.lastClassifyErrorMs = now;
|
|
442
|
+
const msg = types.errMsg(err);
|
|
443
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
444
|
+
this.log.warn("Audio classification failed", {
|
|
445
|
+
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
446
|
+
meta: { error: msg, stack, suppressedSince: suppressed > 0 ? suppressed : void 0 }
|
|
447
|
+
});
|
|
448
|
+
} else {
|
|
449
|
+
this.suppressedClassifyErrors++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { level, classification, timestamp: chunk.timestamp };
|
|
453
|
+
}
|
|
454
|
+
async classify(chunk) {
|
|
455
|
+
const camKey = chunk.deviceId ?? GLOBAL_DEVICE_KEY;
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
const state = this.cameraState.get(camKey);
|
|
458
|
+
if (state?.inProgress || state !== void 0 && now - state.lastEndMs < CLASSIFY_MIN_INTERVAL_MS) {
|
|
459
|
+
return { labels: [], rawLabels: [], inferenceMs: 0 };
|
|
460
|
+
}
|
|
461
|
+
if (this.pipelineBusy) {
|
|
462
|
+
return { labels: [], rawLabels: [], inferenceMs: 0 };
|
|
463
|
+
}
|
|
464
|
+
this.cameraState.set(camKey, { inProgress: true, lastEndMs: state?.lastEndMs ?? 0 });
|
|
465
|
+
this.pipelineBusy = true;
|
|
466
|
+
const result = await this.pipeline.classify({
|
|
467
|
+
data: chunk.data,
|
|
468
|
+
sampleRate: chunk.sampleRate,
|
|
469
|
+
channels: chunk.channels
|
|
470
|
+
}).finally(() => {
|
|
471
|
+
this.pipelineBusy = false;
|
|
472
|
+
this.cameraState.set(camKey, { inProgress: false, lastEndMs: Date.now() });
|
|
473
|
+
});
|
|
474
|
+
if (this.classifyCount < 3 || this.classifyCount % 100 === 0) {
|
|
475
|
+
const rawTop = result.classifications.slice(0, 5).map((c) => `"${c.className}"(${(c.score * 100).toFixed(0)}%)`).join(", ");
|
|
476
|
+
this.log.info("classify debug sample", {
|
|
477
|
+
tags: chunk.deviceId !== void 0 ? { deviceId: chunk.deviceId } : void 0,
|
|
478
|
+
meta: {
|
|
479
|
+
index: this.classifyCount,
|
|
480
|
+
engine: this.backendName,
|
|
481
|
+
rawLabelCount: result.classifications.length,
|
|
482
|
+
top: rawTop,
|
|
483
|
+
inferenceMs: result.inferenceMs
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
this.classifyCount++;
|
|
488
|
+
const macroAccum = /* @__PURE__ */ new Map();
|
|
489
|
+
for (const c of result.classifications) {
|
|
490
|
+
const macro = types.mapAudioLabelToMacro(c.className);
|
|
491
|
+
if (!macro) continue;
|
|
492
|
+
const prev = macroAccum.get(macro);
|
|
493
|
+
if (!prev || c.score > prev.score) {
|
|
494
|
+
macroAccum.set(macro, { score: c.score, rawTop: c.className });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const labels = [...macroAccum.entries()].sort((a, b) => b[1].score - a[1].score).map(([className, { score, rawTop }]) => ({ className, originalClass: rawTop, score }));
|
|
498
|
+
const rawLabels = [...result.classifications].sort((a, b) => b.score - a.score).map((c) => ({ className: c.className, originalClass: c.className, score: c.score }));
|
|
499
|
+
return { labels, rawLabels, inferenceMs: result.inferenceMs };
|
|
500
|
+
}
|
|
501
|
+
isReady() {
|
|
502
|
+
return this.pipeline !== null;
|
|
503
|
+
}
|
|
504
|
+
async dispose() {
|
|
505
|
+
await this.pipeline.dispose();
|
|
506
|
+
}
|
|
507
|
+
// Expose via the cap so the reprobe button in the UI reaches the
|
|
508
|
+
// right worker. Delegates to the addon-owned reprobe (it touches
|
|
509
|
+
// `ctx.settings.writeAddonStore` which only the addon has access to).
|
|
510
|
+
async reprobeAudioEngine() {
|
|
511
|
+
return this.reprobeImpl();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
class AudioAnalyzerAddon extends types.BaseAddon {
|
|
515
|
+
id = "audio-analyzer";
|
|
516
|
+
provider = null;
|
|
517
|
+
pipeline = null;
|
|
518
|
+
constructor() {
|
|
519
|
+
super(types.DEFAULT_AUDIO_ANALYZER_CONFIG);
|
|
520
|
+
}
|
|
521
|
+
globalSettingsSchema() {
|
|
522
|
+
return {
|
|
523
|
+
sections: [
|
|
524
|
+
{
|
|
525
|
+
id: "audio-engine",
|
|
526
|
+
title: "Audio",
|
|
527
|
+
// Co-located with detection-pipeline's `engine` section under
|
|
528
|
+
// a single "Engine" tab. Both sections handle inference-engine
|
|
529
|
+
// selection but for different modalities — distinct purposes
|
|
530
|
+
// are conveyed through section titles ("Detection engine" vs
|
|
531
|
+
// "Audio inference engine") and descriptions, not separate
|
|
532
|
+
// tabs.
|
|
533
|
+
tab: "engine",
|
|
534
|
+
// Renders after detection-pipeline's `engine` section
|
|
535
|
+
// (`order: 0`) on the "Inference Engine" tab.
|
|
536
|
+
order: 10,
|
|
537
|
+
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.',
|
|
538
|
+
// Field order — `probedBest*` lives at the top so the operator
|
|
539
|
+
// sees the auto-detected hint first and can compare it against
|
|
540
|
+
// their override below at a glance. Same convention as the
|
|
541
|
+
// detection-pipeline section.
|
|
542
|
+
fields: [
|
|
543
|
+
{
|
|
544
|
+
type: "text",
|
|
545
|
+
key: "probedBestAudioBackend",
|
|
546
|
+
label: "Probed best",
|
|
547
|
+
description: "Auto-detected best audio backend on this host. Click the refresh icon to re-run the probe.",
|
|
548
|
+
readonlyField: true,
|
|
549
|
+
default: "",
|
|
550
|
+
actions: [
|
|
551
|
+
{ action: "reprobe-audio-engine", icon: "refresh-cw", tooltip: "Re-probe audio engine" }
|
|
552
|
+
]
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
type: "select",
|
|
556
|
+
key: "audioBackend",
|
|
557
|
+
label: "Audio backend",
|
|
558
|
+
options: types.AUDIO_BACKEND_CHOICES.map((o) => ({ value: o.value, label: o.label })),
|
|
559
|
+
default: types.DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend,
|
|
560
|
+
immediate: true,
|
|
561
|
+
requiresRestart: true
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
type: "select",
|
|
565
|
+
key: "selectedAudioModel",
|
|
566
|
+
label: "Classification model",
|
|
567
|
+
description: "Empty = auto (matches backend). Device-level settings can only inherit / enable / disable this step; model + class filters live here at the node level.",
|
|
568
|
+
options: AUDIO_MODEL_OPTIONS.map((o) => ({ value: o.value, label: o.label })),
|
|
569
|
+
default: types.DEFAULT_AUDIO_ANALYZER_CONFIG.selectedAudioModel,
|
|
570
|
+
immediate: true,
|
|
571
|
+
requiresRestart: true
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
]
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Cascade override — narrow the `selectedAudioModel` options to the
|
|
580
|
+
* subset compatible with the currently-selected `audioBackend`.
|
|
581
|
+
*
|
|
582
|
+
* Same pattern as detection-pipeline's `engineRuntime → engineBackend
|
|
583
|
+
* → engineDevice` cascade: the base schema ships every option
|
|
584
|
+
* (Auto + YAMNet + Apple SA); this override drops the rows that
|
|
585
|
+
* belong to a backend the operator didn't pick. With `immediate:
|
|
586
|
+
* true` on the `audioBackend` select, the UI refetches schema after
|
|
587
|
+
* every flip and the model dropdown updates instantly.
|
|
588
|
+
*
|
|
589
|
+
* `overlay` carries the operator's tentative choices for benchmark/
|
|
590
|
+
* preview mode (operator typed but didn't save yet) — same
|
|
591
|
+
* semantics detection-pipeline relies on.
|
|
592
|
+
*/
|
|
593
|
+
async getGlobalSettings(overlay) {
|
|
594
|
+
const ctx = this.ctxIfReady;
|
|
595
|
+
const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
|
|
596
|
+
const merged = overlay ? { ...stored, ...overlay } : stored;
|
|
597
|
+
const operatorChoice = typeof merged.audioBackend === "string" ? merged.audioBackend : types.DEFAULT_AUDIO_ANALYZER_CONFIG.audioBackend;
|
|
598
|
+
const effectiveBackend = operatorChoice === "apple-soundanalysis" ? "apple-soundanalysis" : operatorChoice === "yamnet-onnx" ? "yamnet-onnx" : process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
|
|
599
|
+
const filteredModels = AUDIO_MODEL_OPTIONS.filter(
|
|
600
|
+
(o) => o.value === "" || o.value === effectiveBackend
|
|
601
|
+
);
|
|
602
|
+
const storedModel = typeof merged.selectedAudioModel === "string" ? merged.selectedAudioModel : "";
|
|
603
|
+
const validModel = filteredModels.find((o) => o.value === storedModel)?.value ?? "";
|
|
604
|
+
const raw = { ...merged, selectedAudioModel: validModel };
|
|
605
|
+
const schema = this.globalSettingsSchema();
|
|
606
|
+
const patched = {
|
|
607
|
+
...schema,
|
|
608
|
+
sections: schema.sections.map((section) => ({
|
|
609
|
+
...section,
|
|
610
|
+
fields: section.fields.map((field) => {
|
|
611
|
+
if (field.type === "select" && field.key === "selectedAudioModel") {
|
|
612
|
+
return { ...field, options: filteredModels.map((o) => ({ value: o.value, label: o.label })) };
|
|
613
|
+
}
|
|
614
|
+
return field;
|
|
615
|
+
})
|
|
616
|
+
}))
|
|
617
|
+
};
|
|
618
|
+
return types.hydrateSchema(patched, raw);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Re-run the platform probe and persist the detected backend into
|
|
622
|
+
* `probedBestAudioBackend`. Operator `audioBackend` setting is not
|
|
623
|
+
* touched — only the hint.
|
|
624
|
+
*/
|
|
625
|
+
async reprobeAudioEngine() {
|
|
626
|
+
const backend = process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
|
|
627
|
+
await this.ctx.settings?.writeAddonStore({ probedBestAudioBackend: backend });
|
|
628
|
+
this.ctx.logger.info("reprobeAudioEngine: wrote probedBestAudioBackend", { meta: { backend } });
|
|
629
|
+
return { backend };
|
|
630
|
+
}
|
|
631
|
+
/** Resolve the effective backend from the operator choice, falling back to the platform heuristic when 'auto'. */
|
|
632
|
+
resolveAudioBackend() {
|
|
633
|
+
const choice = this.config.audioBackend;
|
|
634
|
+
if (choice === "apple-soundanalysis") return "apple-soundanalysis";
|
|
635
|
+
if (choice === "yamnet-onnx") return "yamnet-onnx";
|
|
636
|
+
return process.platform === "darwin" ? "apple-soundanalysis" : "yamnet-onnx";
|
|
637
|
+
}
|
|
638
|
+
async onInitialize() {
|
|
639
|
+
const logger = this.ctx.logger;
|
|
640
|
+
const modelsDir = await this.ctx.api.storage.resolve.query({ location: "models", relativePath: "" }).catch(() => "camstack-data/models");
|
|
641
|
+
const backend = this.resolveAudioBackend();
|
|
642
|
+
logger.info("audio-analyzer: resolving pipeline", {
|
|
643
|
+
meta: { operatorChoice: this.config.audioBackend, effectiveBackend: backend, selectedModel: this.config.selectedAudioModel || null }
|
|
644
|
+
});
|
|
645
|
+
const p = await createAudioPipeline(modelsDir, logger, { backend });
|
|
646
|
+
await p.initialize();
|
|
647
|
+
this.pipeline = p;
|
|
648
|
+
if (!this.config.probedBestAudioBackend) {
|
|
649
|
+
this.reprobeAudioEngine().catch((err) => {
|
|
650
|
+
logger.warn("audio: auto-reprobe failed", {
|
|
651
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
const self = this;
|
|
656
|
+
const deviceSettingsResolver = async (deviceId) => {
|
|
657
|
+
try {
|
|
658
|
+
const resolved = await self.ctx.api.pipelineOrchestrator.resolvePipeline.query({ deviceId });
|
|
659
|
+
const stepSettings = resolved.audio?.settings ?? {};
|
|
660
|
+
const minConfidence = typeof stepSettings["minConfidence"] === "number" ? stepSettings["minConfidence"] : 0.3;
|
|
661
|
+
const allowedClasses = Array.isArray(stepSettings["enabledAudioClasses"]) ? stepSettings["enabledAudioClasses"] : [];
|
|
662
|
+
return { minConfidence, allowedClasses };
|
|
663
|
+
} catch (err) {
|
|
664
|
+
logger.warn("audio: resolveDeviceSettings via orchestrator failed", {
|
|
665
|
+
tags: { deviceId },
|
|
666
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
667
|
+
});
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const deviceContributionResolver = async (_deviceId) => {
|
|
672
|
+
return null;
|
|
673
|
+
};
|
|
674
|
+
const deviceSettingsPatcher = async (_deviceId, _patch) => {
|
|
675
|
+
};
|
|
676
|
+
this.provider = new AudioAnalyzerProvider(
|
|
677
|
+
logger,
|
|
678
|
+
this.pipeline,
|
|
679
|
+
backend,
|
|
680
|
+
deviceSettingsResolver,
|
|
681
|
+
deviceContributionResolver,
|
|
682
|
+
deviceSettingsPatcher,
|
|
683
|
+
() => this.reprobeAudioEngine()
|
|
684
|
+
);
|
|
685
|
+
return [
|
|
686
|
+
{ capability: types.audioAnalyzerCapability, provider: this.provider },
|
|
687
|
+
{
|
|
688
|
+
capability: types.audioAnalysisCapability,
|
|
689
|
+
provider: this.provider,
|
|
690
|
+
kind: "wrapper",
|
|
691
|
+
defaultActive: true
|
|
692
|
+
}
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
async onShutdown() {
|
|
696
|
+
if (this.provider) {
|
|
697
|
+
await this.provider.dispose();
|
|
698
|
+
this.provider = null;
|
|
699
|
+
}
|
|
700
|
+
this.pipeline = null;
|
|
701
|
+
}
|
|
702
|
+
// ── Standard ICamstackAddon — three-level settings API ─────
|
|
703
|
+
//
|
|
704
|
+
// Per-device audio settings (audio class filter + minConfidence) moved
|
|
705
|
+
// to the audio-classifier pipeline step's `getConfigSchema()` and the
|
|
706
|
+
// orchestrator owns the audio node assignment (`audioNodeId`). No
|
|
707
|
+
// device-level settings remain on this addon.
|
|
708
|
+
buildDeviceSchema() {
|
|
709
|
+
return { sections: [] };
|
|
710
|
+
}
|
|
711
|
+
async getDeviceSettings(deviceId) {
|
|
712
|
+
const raw = await this.ctx?.settings?.readDeviceStore(deviceId) ?? {};
|
|
713
|
+
return types.hydrateSchema(this.buildDeviceSchema(), raw);
|
|
714
|
+
}
|
|
715
|
+
async updateDeviceSettings(deviceId, patch) {
|
|
716
|
+
await this.ctx?.settings?.writeDeviceStore(deviceId, patch);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
exports.AudioAnalyzerAddon = AudioAnalyzerAddon;
|
|
720
|
+
exports.AudioAnalyzerProvider = AudioAnalyzerProvider;
|
|
721
|
+
exports.createAudioPipeline = createAudioPipeline;
|
|
722
|
+
exports.default = AudioAnalyzerAddon;
|
|
723
|
+
//# sourceMappingURL=index.js.map
|