speechflow 2.2.1 → 2.3.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.
- package/{etc/claude.md → AGENTS.md} +8 -3
- package/CHANGELOG.md +70 -1
- package/README.md +28 -4
- package/etc/speechflow.yaml +3 -1
- package/etc/stx.conf +1 -1
- package/package.json +6 -6
- package/speechflow-cli/dst/speechflow-main-api.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-main-api.js +57 -16
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-cli.js +2 -2
- package/speechflow-cli/dst/speechflow-main-config.js +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.js +55 -21
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-nodes.js +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +6 -3
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +7 -10
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +8 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +9 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +2 -4
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +20 -12
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js +33 -11
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +4 -3
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +19 -11
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +8 -8
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +33 -29
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +34 -20
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +13 -5
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +3 -2
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +33 -27
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +16 -5
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js +194 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +21 -9
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +21 -7
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +4 -2
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +34 -14
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +3 -3
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +18 -7
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +23 -11
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +13 -7
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +25 -12
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +32 -20
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +78 -62
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +63 -18
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.js +5 -7
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio-wt.js +31 -5
- package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +25 -14
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +2 -2
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.js +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +3 -2
- package/speechflow-cli/dst/speechflow-util-misc.js +63 -6
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.d.ts +5 -17
- package/speechflow-cli/dst/speechflow-util-queue.js +57 -78
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +35 -8
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.js +1 -1
- package/speechflow-cli/dst/speechflow.d.ts +1 -1
- package/speechflow-cli/dst/speechflow.js +1 -1
- package/speechflow-cli/etc/eslint.mjs +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/etc/stx.conf +8 -2
- package/speechflow-cli/package.d/@ericedouard+vad-node-realtime+0.2.0.patch +2 -1
- package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-cli/package.d/kitten-tts-js+0.1.2.patch +24 -0
- package/speechflow-cli/package.d/speex-resampler+3.0.1.patch +56 -0
- package/speechflow-cli/package.json +40 -30
- package/speechflow-cli/src/lib.d.ts +19 -1
- package/speechflow-cli/src/speechflow-main-api.ts +64 -19
- package/speechflow-cli/src/speechflow-main-cli.ts +2 -2
- package/speechflow-cli/src/speechflow-main-config.ts +1 -1
- package/speechflow-cli/src/speechflow-main-graph.ts +56 -22
- package/speechflow-cli/src/speechflow-main-nodes.ts +1 -1
- package/speechflow-cli/src/speechflow-main-status.ts +6 -3
- package/speechflow-cli/src/speechflow-main.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +7 -11
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +8 -6
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +10 -5
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -2
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +2 -4
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +20 -13
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +43 -16
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +4 -3
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +24 -12
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +10 -9
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +38 -31
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +35 -22
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -6
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +5 -4
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +39 -31
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +16 -5
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-kitten.ts +178 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +21 -9
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +21 -7
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +4 -2
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +39 -15
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +3 -3
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +1 -1
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +1 -1
- package/speechflow-cli/src/speechflow-node-xio-device.ts +21 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +25 -11
- package/speechflow-cli/src/speechflow-node-xio-file.ts +15 -7
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +28 -15
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +35 -22
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +85 -69
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +67 -20
- package/speechflow-cli/src/speechflow-node.ts +7 -8
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +46 -7
- package/speechflow-cli/src/speechflow-util-audio.ts +27 -15
- package/speechflow-cli/src/speechflow-util-error.ts +3 -3
- package/speechflow-cli/src/speechflow-util-llm.ts +1 -1
- package/speechflow-cli/src/speechflow-util-misc.ts +63 -6
- package/speechflow-cli/src/speechflow-util-queue.ts +60 -81
- package/speechflow-cli/src/speechflow-util-stream.ts +40 -8
- package/speechflow-cli/src/speechflow-util.ts +1 -1
- package/speechflow-cli/src/speechflow.ts +1 -1
- package/speechflow-ui-db/dst/index.html +1 -1
- package/speechflow-ui-db/dst/index.js +15 -15
- package/speechflow-ui-db/etc/eslint.mjs +1 -1
- package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-db/etc/stx.conf +1 -1
- package/speechflow-ui-db/etc/stylelint.js +1 -1
- package/speechflow-ui-db/etc/stylelint.yaml +1 -1
- package/speechflow-ui-db/etc/vite-client.mts +1 -1
- package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-db/package.json +22 -16
- package/speechflow-ui-db/src/app.styl +1 -1
- package/speechflow-ui-db/src/app.vue +1 -1
- package/speechflow-ui-db/src/index.html +1 -1
- package/speechflow-ui-db/src/index.ts +1 -1
- package/speechflow-ui-st/dst/index.html +1 -1
- package/speechflow-ui-st/dst/index.js +31 -31
- package/speechflow-ui-st/etc/eslint.mjs +1 -1
- package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-st/etc/stx.conf +1 -1
- package/speechflow-ui-st/etc/stylelint.js +1 -1
- package/speechflow-ui-st/etc/stylelint.yaml +1 -1
- package/speechflow-ui-st/etc/vite-client.mts +1 -1
- package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-st/package.json +23 -17
- package/speechflow-ui-st/src/app.styl +1 -1
- package/speechflow-ui-st/src/app.vue +1 -1
- package/speechflow-ui-st/src/index.html +1 -1
- package/speechflow-ui-st/src/index.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -96,7 +96,8 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
96
96
|
chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
97
97
|
}
|
|
98
98
|
const wrapper = util.createTransformStreamForReadableSide(
|
|
99
|
-
this.params.type, () => this.timeZero
|
|
99
|
+
this.params.type, () => this.timeZero, undefined,
|
|
100
|
+
this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
|
|
100
101
|
this.stream = Stream.compose(process.stdin, chunker, wrapper)
|
|
101
102
|
}
|
|
102
103
|
else {
|
|
@@ -109,7 +110,8 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
109
110
|
readable = fs.createReadStream(this.params.path,
|
|
110
111
|
{ highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
|
|
111
112
|
const wrapper = util.createTransformStreamForReadableSide(
|
|
112
|
-
this.params.type, () => this.timeZero
|
|
113
|
+
this.params.type, () => this.timeZero, undefined,
|
|
114
|
+
this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
|
|
113
115
|
this.stream = Stream.compose(readable, wrapper)
|
|
114
116
|
}
|
|
115
117
|
}
|
|
@@ -158,8 +160,9 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
158
160
|
},
|
|
159
161
|
destroy (err, callback) {
|
|
160
162
|
if (self.fd !== null) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
const fd = self.fd
|
|
164
|
+
self.fd = null
|
|
165
|
+
fs.close(fd, () => {
|
|
163
166
|
callback(err)
|
|
164
167
|
})
|
|
165
168
|
}
|
|
@@ -199,6 +202,7 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
199
202
|
const stream = this.stream
|
|
200
203
|
if ((stream instanceof Stream.Writable || stream instanceof Stream.Duplex)
|
|
201
204
|
&& (!stream.writableEnded && !stream.destroyed)) {
|
|
205
|
+
const ac = new AbortController()
|
|
202
206
|
await Promise.race([
|
|
203
207
|
new Promise<void>((resolve, reject) => {
|
|
204
208
|
stream.end((err?: Error) => {
|
|
@@ -208,8 +212,12 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
208
212
|
resolve()
|
|
209
213
|
})
|
|
210
214
|
}),
|
|
211
|
-
util.timeout(5000)
|
|
212
|
-
])
|
|
215
|
+
util.timeout(5000, "timeout", ac.signal)
|
|
216
|
+
]).finally(() => {
|
|
217
|
+
ac.abort()
|
|
218
|
+
}).catch(() => {
|
|
219
|
+
/* ignore timeout -- stdio stream cannot be destroyed */
|
|
220
|
+
})
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
this.stream = null
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
23
23
|
/* internal state */
|
|
24
24
|
private broker: MQTT.MqttClient | null = null
|
|
25
25
|
private clientId: string = (new UUID(1)).format()
|
|
26
|
-
private chunkQueue: util.
|
|
26
|
+
private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
|
|
27
27
|
|
|
28
28
|
/* construct node */
|
|
29
29
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
@@ -100,13 +100,15 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
100
100
|
const reasonCode = packet.reasonCode ?? 0
|
|
101
101
|
this.log("info", `connection closed to MQTT ${this.params.url} (reason code: ${reasonCode})`)
|
|
102
102
|
})
|
|
103
|
-
this.chunkQueue = new util.
|
|
103
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
104
104
|
this.broker.on("message", (topic: string, payload: Buffer, packet: MQTT.IPublishPacket) => {
|
|
105
105
|
if (topic !== this.params.topicRead || this.params.mode === "w")
|
|
106
106
|
return
|
|
107
|
+
if (this.chunkQueue === null)
|
|
108
|
+
return
|
|
107
109
|
try {
|
|
108
110
|
const chunk = util.streamChunkDecode(payload)
|
|
109
|
-
this.chunkQueue
|
|
111
|
+
this.chunkQueue.write(chunk)
|
|
110
112
|
}
|
|
111
113
|
catch (_err: unknown) {
|
|
112
114
|
this.log("warning", `received invalid CBOR chunk from MQTT ${this.params.url}`)
|
|
@@ -141,12 +143,20 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
141
143
|
callback()
|
|
142
144
|
},
|
|
143
145
|
read (size: number) {
|
|
144
|
-
if (self.params.mode === "w")
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
if (self.params.mode === "w") {
|
|
147
|
+
self.log("error", "read operation on write-only node")
|
|
148
|
+
this.push(null)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (self.chunkQueue === null)
|
|
152
|
+
return
|
|
153
|
+
const queue = self.chunkQueue
|
|
154
|
+
reads.add(queue.read().then((chunk) => {
|
|
147
155
|
this.push(chunk, "binary")
|
|
148
156
|
}).catch((err: Error) => {
|
|
149
157
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
158
|
+
if (queue.destroyed)
|
|
159
|
+
this.push(null)
|
|
150
160
|
}))
|
|
151
161
|
}
|
|
152
162
|
})
|
|
@@ -154,14 +164,10 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
154
164
|
|
|
155
165
|
/* close node */
|
|
156
166
|
async close () {
|
|
157
|
-
/* clear chunk queue reference */
|
|
158
|
-
this.chunkQueue
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (this.broker !== null) {
|
|
162
|
-
if (this.broker.connected)
|
|
163
|
-
this.broker.end()
|
|
164
|
-
this.broker = null
|
|
167
|
+
/* drain and clear chunk queue reference */
|
|
168
|
+
if (this.chunkQueue !== null) {
|
|
169
|
+
this.chunkQueue.destroy()
|
|
170
|
+
this.chunkQueue = null
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
/* shutdown stream */
|
|
@@ -169,5 +175,12 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
169
175
|
await util.destroyStream(this.stream)
|
|
170
176
|
this.stream = null
|
|
171
177
|
}
|
|
178
|
+
|
|
179
|
+
/* close MQTT broker */
|
|
180
|
+
if (this.broker !== null) {
|
|
181
|
+
if (this.broker.connected)
|
|
182
|
+
this.broker.end()
|
|
183
|
+
this.broker = null
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
186
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
32
32
|
|
|
33
33
|
/* internal state */
|
|
34
34
|
private server: VBANServer | null = null
|
|
35
|
-
private chunkQueue: util.
|
|
35
|
+
private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
|
|
36
36
|
private frameCounter = 0
|
|
37
37
|
private targetAddress = ""
|
|
38
38
|
private targetPort = 0
|
|
@@ -99,7 +99,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
/* setup chunk queue for incoming audio */
|
|
102
|
-
this.chunkQueue = new util.
|
|
102
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
103
103
|
|
|
104
104
|
/* determine target for sending */
|
|
105
105
|
if (this.params.connect !== "") {
|
|
@@ -128,6 +128,12 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
128
128
|
}
|
|
129
129
|
const data = packet.data
|
|
130
130
|
|
|
131
|
+
/* check sample rate compatibility */
|
|
132
|
+
if (packet.sr !== this.config.audioSampleRate) {
|
|
133
|
+
this.log("warning", `incompatible VBAN sample rate: packet=${packet.sr}Hz, configured=${this.config.audioSampleRate}Hz`)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
131
137
|
/* convert audio format if necessary */
|
|
132
138
|
let audioBuffer: Buffer
|
|
133
139
|
const bitResolution = packet.bitResolution
|
|
@@ -139,7 +145,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
139
145
|
/* 8-bit unsigned to 16-bit signed */
|
|
140
146
|
audioBuffer = Buffer.alloc(data.length * 2)
|
|
141
147
|
for (let i = 0; i < data.length; i++) {
|
|
142
|
-
const sample = ((data[i] - 128) / 128) *
|
|
148
|
+
const sample = ((data[i] - 128) / 128) * 32768
|
|
143
149
|
audioBuffer.writeInt16LE(Math.round(sample), i * 2)
|
|
144
150
|
}
|
|
145
151
|
}
|
|
@@ -153,7 +159,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
153
159
|
const b2 = data[i * 3 + 2]
|
|
154
160
|
const value = ((b2 << 16) | (b1 << 8) | b0) & 0xFFFFFF
|
|
155
161
|
const signed = value > 0x7FFFFF ? value - 0x1000000 : value
|
|
156
|
-
const sample = (signed / 0x800000) *
|
|
162
|
+
const sample = (signed / 0x800000) * 32768
|
|
157
163
|
audioBuffer.writeInt16LE(Math.round(sample), i * 2)
|
|
158
164
|
}
|
|
159
165
|
}
|
|
@@ -163,7 +169,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
163
169
|
audioBuffer = Buffer.alloc(samples * 2)
|
|
164
170
|
for (let i = 0; i < samples; i++) {
|
|
165
171
|
const value = data.readInt32LE(i * 4)
|
|
166
|
-
const sample = (value / 0x80000000) *
|
|
172
|
+
const sample = (value / 0x80000000) * 32768
|
|
167
173
|
audioBuffer.writeInt16LE(Math.round(sample), i * 2)
|
|
168
174
|
}
|
|
169
175
|
}
|
|
@@ -173,7 +179,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
173
179
|
audioBuffer = Buffer.alloc(samples * 2)
|
|
174
180
|
for (let i = 0; i < samples; i++) {
|
|
175
181
|
const value = data.readFloatLE(i * 4)
|
|
176
|
-
const sample = Math.max(-32768, Math.min(32767, Math.round(value *
|
|
182
|
+
const sample = Math.max(-32768, Math.min(32767, Math.round(value * 32768)))
|
|
177
183
|
audioBuffer.writeInt16LE(sample, i * 2)
|
|
178
184
|
}
|
|
179
185
|
}
|
|
@@ -183,7 +189,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
183
189
|
audioBuffer = Buffer.alloc(samples * 2)
|
|
184
190
|
for (let i = 0; i < samples; i++) {
|
|
185
191
|
const value = data.readDoubleLE(i * 8)
|
|
186
|
-
const sample = Math.max(-32768, Math.min(32767, Math.round(value *
|
|
192
|
+
const sample = Math.max(-32768, Math.min(32767, Math.round(value * 32768)))
|
|
187
193
|
audioBuffer.writeInt16LE(sample, i * 2)
|
|
188
194
|
}
|
|
189
195
|
}
|
|
@@ -227,11 +233,11 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
227
233
|
/* bind to listen port */
|
|
228
234
|
if (this.params.listen !== "") {
|
|
229
235
|
const listen = this.parseAddress(this.params.listen, 6980)
|
|
230
|
-
this.server.bind(listen.port, listen.host)
|
|
236
|
+
await this.server.bind(listen.port, listen.host)
|
|
231
237
|
}
|
|
232
238
|
else
|
|
233
239
|
/* still need to bind for sending */
|
|
234
|
-
this.server.bind(0)
|
|
240
|
+
await this.server.bind(0)
|
|
235
241
|
|
|
236
242
|
/* create duplex stream */
|
|
237
243
|
const self = this
|
|
@@ -279,7 +285,7 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
279
285
|
nbChannel: self.config.audioChannels - 1,
|
|
280
286
|
bitResolution: EBitsResolutions.VBAN_DATATYPE_INT16,
|
|
281
287
|
codec: ECodecs.VBAN_CODEC_PCM,
|
|
282
|
-
frameCounter: self.frameCounter++
|
|
288
|
+
frameCounter: self.frameCounter++ & 0xFFFFFFFF
|
|
283
289
|
}, audioBuffer)
|
|
284
290
|
|
|
285
291
|
/* send packet */
|
|
@@ -292,13 +298,20 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
292
298
|
callback()
|
|
293
299
|
},
|
|
294
300
|
read (size: number) {
|
|
295
|
-
if (self.params.mode === "w")
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
if (self.params.mode === "w") {
|
|
302
|
+
self.log("error", "read operation on write-only node")
|
|
303
|
+
this.push(null)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
if (self.chunkQueue === null)
|
|
307
|
+
return
|
|
308
|
+
const queue = self.chunkQueue
|
|
309
|
+
reads.add(queue.read().then((chunk) => {
|
|
298
310
|
this.push(chunk, "binary")
|
|
299
311
|
}).catch((err: Error) => {
|
|
300
312
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
301
|
-
|
|
313
|
+
if (queue.destroyed)
|
|
314
|
+
this.push(null)
|
|
302
315
|
}))
|
|
303
316
|
}
|
|
304
317
|
})
|
|
@@ -308,20 +321,20 @@ export default class SpeechFlowNodeXIOVBAN extends SpeechFlowNode {
|
|
|
308
321
|
async close () {
|
|
309
322
|
/* drain and clear chunk queue reference */
|
|
310
323
|
if (this.chunkQueue !== null) {
|
|
311
|
-
this.chunkQueue.
|
|
324
|
+
this.chunkQueue.destroy()
|
|
312
325
|
this.chunkQueue = null
|
|
313
326
|
}
|
|
314
327
|
|
|
315
|
-
/* close VBAN server */
|
|
316
|
-
if (this.server !== null) {
|
|
317
|
-
this.server.close()
|
|
318
|
-
this.server = null
|
|
319
|
-
}
|
|
320
|
-
|
|
321
328
|
/* shutdown stream */
|
|
322
329
|
if (this.stream !== null) {
|
|
323
330
|
await util.destroyStream(this.stream)
|
|
324
331
|
this.stream = null
|
|
325
332
|
}
|
|
333
|
+
|
|
334
|
+
/* close VBAN server */
|
|
335
|
+
if (this.server !== null) {
|
|
336
|
+
await this.server.close()
|
|
337
|
+
this.server = null
|
|
338
|
+
}
|
|
326
339
|
}
|
|
327
340
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
39
39
|
/* internal state */
|
|
40
40
|
private peerConnections = new Map<string, WebRTCConnection>()
|
|
41
41
|
private httpServer: http.Server | null = null
|
|
42
|
-
private chunkQueue: util.
|
|
42
|
+
private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
|
|
43
43
|
private opusEncoder: OpusEncoder | null = null
|
|
44
44
|
private opusDecoder: OpusEncoder | null = null
|
|
45
45
|
private pcmBuffer = Buffer.alloc(0)
|
|
@@ -265,11 +265,11 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
265
265
|
const resourceId = crypto.randomUUID()
|
|
266
266
|
const { pc, subscription } = this.createPeerConnection(resourceId)
|
|
267
267
|
|
|
268
|
-
/* protocol-specific setup */
|
|
269
|
-
const track = setupFn(pc, resourceId)
|
|
270
|
-
|
|
271
268
|
/* complete SDP offer/answer exchange and establish connection */
|
|
272
269
|
try {
|
|
270
|
+
/* protocol-specific setup */
|
|
271
|
+
const track = setupFn(pc, resourceId)
|
|
272
|
+
|
|
273
273
|
/* set remote description (offer from client) */
|
|
274
274
|
await pc.setRemoteDescription({ type: "offer", sdp: offer })
|
|
275
275
|
|
|
@@ -367,7 +367,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
367
367
|
this.rtpSSRC = Math.floor(Math.random() * 0x100000000) >>> 0
|
|
368
368
|
|
|
369
369
|
/* setup chunk queue for incoming audio */
|
|
370
|
-
this.chunkQueue = new util.
|
|
370
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
371
371
|
|
|
372
372
|
/* parse listen address */
|
|
373
373
|
const listen = this.parseAddress(this.params.listen, 8085)
|
|
@@ -375,77 +375,89 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
375
375
|
/* setup HTTP server for WHIP/WHEP signaling */
|
|
376
376
|
const self = this
|
|
377
377
|
this.httpServer = http.createServer(async (req, res) => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/* handle requests... */
|
|
403
|
-
if (req.method === "POST" && pathMatch) {
|
|
404
|
-
/* handle WHIP/WHEP POST */
|
|
405
|
-
const body = await self.readRequestBody(req)
|
|
406
|
-
|
|
407
|
-
/* sanity check content type */
|
|
408
|
-
const contentType = req.headers["content-type"]
|
|
409
|
-
if (contentType !== "application/sdp") {
|
|
410
|
-
res.writeHead(415, { "Content-Type": "text/plain" })
|
|
411
|
-
res.end("Unsupported Media Type")
|
|
378
|
+
try {
|
|
379
|
+
/* determine URL */
|
|
380
|
+
if (req.url === undefined) {
|
|
381
|
+
res.writeHead(400, { "Content-Type": "text/plain" })
|
|
382
|
+
res.end("Bad Request")
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
const host = req.headers.host?.replace(/[^a-zA-Z0-9:.\-_]/g, "") ?? "localhost"
|
|
386
|
+
const url = new URL(req.url, `http://${host}`)
|
|
387
|
+
const pathMatch = url.pathname === self.params.path
|
|
388
|
+
const resourceMatch = url.pathname.startsWith(self.params.path + "/")
|
|
389
|
+
|
|
390
|
+
/* CORS headers for browser clients */
|
|
391
|
+
res.setHeader("Access-Control-Allow-Origin", "*")
|
|
392
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, OPTIONS")
|
|
393
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
|
394
|
+
res.setHeader("Access-Control-Expose-Headers", "Location")
|
|
395
|
+
|
|
396
|
+
/* handle CORS preflight */
|
|
397
|
+
if (req.method === "OPTIONS") {
|
|
398
|
+
res.writeHead(204)
|
|
399
|
+
res.end()
|
|
412
400
|
return
|
|
413
401
|
}
|
|
414
402
|
|
|
415
|
-
/*
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
403
|
+
/* handle requests... */
|
|
404
|
+
if (req.method === "POST" && pathMatch) {
|
|
405
|
+
/* handle WHIP/WHEP POST */
|
|
406
|
+
const body = await self.readRequestBody(req)
|
|
407
|
+
|
|
408
|
+
/* sanity check content type */
|
|
409
|
+
const contentType = req.headers["content-type"]
|
|
410
|
+
if (contentType !== "application/sdp") {
|
|
411
|
+
res.writeHead(415, { "Content-Type": "text/plain" })
|
|
412
|
+
res.end("Unsupported Media Type")
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* determine if WHIP (receiving) or WHEP (sending) based on SDP content */
|
|
417
|
+
const hasSendonly = /\ba=sendonly\b/m.test(body)
|
|
418
|
+
const hasSendrecv = /\ba=sendrecv\b/m.test(body)
|
|
419
|
+
const hasRecvonly = /\ba=recvonly\b/m.test(body)
|
|
420
|
+
const isPublisher = hasSendonly || hasSendrecv
|
|
421
|
+
const isViewer = hasRecvonly
|
|
422
|
+
|
|
423
|
+
/* handle protocol based on mode */
|
|
424
|
+
if (self.params.mode === "r" && isPublisher)
|
|
425
|
+
/* in read mode, accept WHIP publishers */
|
|
426
|
+
await self.handleWHIP(res, body)
|
|
427
|
+
else if (self.params.mode === "w" && isViewer)
|
|
428
|
+
/* in write mode, accept WHEP viewers */
|
|
429
|
+
await self.handleWHEP(res, body)
|
|
430
|
+
else {
|
|
431
|
+
res.writeHead(403, { "Content-Type": "text/plain" })
|
|
432
|
+
res.end("Forbidden")
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else if (req.method === "DELETE" && resourceMatch) {
|
|
436
|
+
/* handle DELETE for connection teardown */
|
|
437
|
+
const resourceId = url.pathname.substring(self.params.path.length + 1)
|
|
438
|
+
self.handleDELETE(res, resourceId)
|
|
439
|
+
}
|
|
429
440
|
else {
|
|
430
|
-
|
|
431
|
-
res.
|
|
441
|
+
/* handle unknown requests */
|
|
442
|
+
res.writeHead(404, { "Content-Type": "text/plain" })
|
|
443
|
+
res.end("Not Found")
|
|
432
444
|
}
|
|
433
445
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
/* handle unknown requests */
|
|
441
|
-
res.writeHead(404, { "Content-Type": "text/plain" })
|
|
442
|
-
res.end("Not Found")
|
|
446
|
+
catch (err: unknown) {
|
|
447
|
+
self.log("error", `HTTP request handler failed: ${util.ensureError(err).message}`)
|
|
448
|
+
if (!res.headersSent) {
|
|
449
|
+
res.writeHead(500, { "Content-Type": "text/plain" })
|
|
450
|
+
res.end("Internal Server Error")
|
|
451
|
+
}
|
|
443
452
|
}
|
|
444
453
|
})
|
|
445
454
|
|
|
446
455
|
/* start HTTP server */
|
|
447
|
-
await new Promise<void>((resolve) => {
|
|
456
|
+
await new Promise<void>((resolve, reject) => {
|
|
457
|
+
const onError = (err: Error) => { reject(err) }
|
|
458
|
+
this.httpServer!.once("error", onError)
|
|
448
459
|
this.httpServer!.listen(listen.port, listen.host, () => {
|
|
460
|
+
this.httpServer!.removeListener("error", onError)
|
|
449
461
|
const mode = this.params.mode === "r" ? "WHIP" : "WHEP"
|
|
450
462
|
this.log("info", `WebRTC ${mode} server listening on http://${listen.host}:${listen.port}${this.params.path}`)
|
|
451
463
|
resolve()
|
|
@@ -486,11 +498,15 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
486
498
|
this.push(null)
|
|
487
499
|
return
|
|
488
500
|
}
|
|
489
|
-
|
|
501
|
+
if (self.chunkQueue === null)
|
|
502
|
+
return
|
|
503
|
+
const queue = self.chunkQueue
|
|
504
|
+
reads.add(queue.read().then((chunk) => {
|
|
490
505
|
this.push(chunk, "binary")
|
|
491
506
|
}).catch((err: Error) => {
|
|
492
507
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
493
|
-
|
|
508
|
+
if (queue.destroyed)
|
|
509
|
+
this.push(null)
|
|
494
510
|
}))
|
|
495
511
|
}
|
|
496
512
|
})
|
|
@@ -517,7 +533,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
|
|
|
517
533
|
|
|
518
534
|
/* drain and clear chunk queue */
|
|
519
535
|
if (this.chunkQueue !== null) {
|
|
520
|
-
this.chunkQueue.
|
|
536
|
+
this.chunkQueue.destroy()
|
|
521
537
|
this.chunkQueue = null
|
|
522
538
|
}
|
|
523
539
|
|