speechflow 2.2.0 → 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 +78 -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-t2t-simulator.d.ts → speechflow-node-t2a-kitten.d.ts} +5 -1
- 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.css +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 +4 -4
- 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 +9 -8
- package/speechflow-ui-st/src/index.html +1 -1
- package/speechflow-ui-st/src/index.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-simulator.js +0 -128
- package/speechflow-cli/dst/speechflow-node-t2t-simulator.js.map +0 -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
|
|
|
@@ -106,9 +106,13 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
|
|
|
106
106
|
else if (chunk.payload === "")
|
|
107
107
|
callback()
|
|
108
108
|
else {
|
|
109
|
+
let callbackCalled = false
|
|
109
110
|
let processTimeout: ReturnType<typeof setTimeout> | null = setTimeout(() => {
|
|
110
111
|
processTimeout = null
|
|
111
|
-
|
|
112
|
+
if (!callbackCalled) {
|
|
113
|
+
callbackCalled = true
|
|
114
|
+
callback(new Error("OpenAI TTS API timeout"))
|
|
115
|
+
}
|
|
112
116
|
}, 60 * 1000)
|
|
113
117
|
const clearProcessTimeout = () => {
|
|
114
118
|
if (processTimeout !== null) {
|
|
@@ -119,12 +123,16 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
|
|
|
119
123
|
try {
|
|
120
124
|
if (self.closing) {
|
|
121
125
|
clearProcessTimeout()
|
|
126
|
+
callbackCalled = true
|
|
122
127
|
callback(new Error("stream destroyed during processing"))
|
|
123
128
|
return
|
|
124
129
|
}
|
|
125
130
|
const buffer = await textToSpeech(chunk.payload as string)
|
|
131
|
+
clearProcessTimeout()
|
|
132
|
+
if (callbackCalled)
|
|
133
|
+
return
|
|
134
|
+
callbackCalled = true
|
|
126
135
|
if (self.closing) {
|
|
127
|
-
clearProcessTimeout()
|
|
128
136
|
callback(new Error("stream destroyed during processing"))
|
|
129
137
|
return
|
|
130
138
|
}
|
|
@@ -138,12 +146,14 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
|
|
|
138
146
|
chunkNew.type = "audio"
|
|
139
147
|
chunkNew.payload = buffer
|
|
140
148
|
chunkNew.timestampEnd = Duration.fromMillis(chunkNew.timestampStart.toMillis() + durationMs)
|
|
141
|
-
clearProcessTimeout()
|
|
142
149
|
this.push(chunkNew)
|
|
143
150
|
callback()
|
|
144
151
|
}
|
|
145
152
|
catch (error) {
|
|
146
153
|
clearProcessTimeout()
|
|
154
|
+
if (callbackCalled)
|
|
155
|
+
return
|
|
156
|
+
callbackCalled = true
|
|
147
157
|
callback(util.ensureError(error, "OpenAI TTS processing failed"))
|
|
148
158
|
}
|
|
149
159
|
}
|
|
@@ -166,8 +176,10 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
|
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
/* destroy resampler */
|
|
169
|
-
if (this.resampler !== null)
|
|
179
|
+
if (this.resampler !== null) {
|
|
180
|
+
this.resampler.destroy()
|
|
170
181
|
this.resampler = null
|
|
182
|
+
}
|
|
171
183
|
|
|
172
184
|
/* destroy OpenAI API */
|
|
173
185
|
if (this.openai !== 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
|
|
|
@@ -138,8 +138,11 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
|
|
|
138
138
|
throw new Error("unexpected Supertonic result: sampling_rate is not a number")
|
|
139
139
|
const samples = result.audio
|
|
140
140
|
const outputSampleRate = result.sampling_rate
|
|
141
|
-
if (outputSampleRate !== this.sampleRate)
|
|
142
|
-
this.log("warning", `unexpected sample rate ${
|
|
141
|
+
if (outputSampleRate !== this.sampleRate) {
|
|
142
|
+
this.log("warning", `unexpected sample rate change ${this.sampleRate}Hz -> ${outputSampleRate}Hz (recreating resampler)`)
|
|
143
|
+
this.sampleRate = outputSampleRate
|
|
144
|
+
this.resampler = new SpeexResampler(1, this.sampleRate, this.config.audioSampleRate, 7)
|
|
145
|
+
}
|
|
143
146
|
|
|
144
147
|
/* calculate duration */
|
|
145
148
|
const duration = samples.length / outputSampleRate
|
|
@@ -169,9 +172,13 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
|
|
|
169
172
|
else if (chunk.payload === "")
|
|
170
173
|
callback()
|
|
171
174
|
else {
|
|
175
|
+
let callbackCalled = false
|
|
172
176
|
let processTimeout: ReturnType<typeof setTimeout> | null = setTimeout(() => {
|
|
173
177
|
processTimeout = null
|
|
174
|
-
|
|
178
|
+
if (!callbackCalled) {
|
|
179
|
+
callbackCalled = true
|
|
180
|
+
callback(new Error("Supertonic TTS timeout"))
|
|
181
|
+
}
|
|
175
182
|
}, 120 * 1000)
|
|
176
183
|
const clearProcessTimeout = () => {
|
|
177
184
|
if (processTimeout !== null) {
|
|
@@ -180,8 +187,11 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
|
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
text2speech(chunk.payload as string).then((buffer) => {
|
|
190
|
+
clearProcessTimeout()
|
|
191
|
+
if (callbackCalled)
|
|
192
|
+
return
|
|
193
|
+
callbackCalled = true
|
|
183
194
|
if (self.closing) {
|
|
184
|
-
clearProcessTimeout()
|
|
185
195
|
callback(new Error("stream destroyed during processing"))
|
|
186
196
|
return
|
|
187
197
|
}
|
|
@@ -196,11 +206,13 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
|
|
|
196
206
|
chunkNew.type = "audio"
|
|
197
207
|
chunkNew.payload = buffer
|
|
198
208
|
chunkNew.timestampEnd = Duration.fromMillis(chunkNew.timestampStart.toMillis() + durationMs)
|
|
199
|
-
clearProcessTimeout()
|
|
200
209
|
this.push(chunkNew)
|
|
201
210
|
callback()
|
|
202
211
|
}).catch((error: unknown) => {
|
|
203
212
|
clearProcessTimeout()
|
|
213
|
+
if (callbackCalled)
|
|
214
|
+
return
|
|
215
|
+
callbackCalled = true
|
|
204
216
|
callback(util.ensureError(error, "Supertonic processing failed"))
|
|
205
217
|
})
|
|
206
218
|
}
|
|
@@ -223,8 +235,10 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
|
|
|
223
235
|
}
|
|
224
236
|
|
|
225
237
|
/* destroy resampler */
|
|
226
|
-
if (this.resampler !== null)
|
|
238
|
+
if (this.resampler !== null) {
|
|
239
|
+
this.resampler.destroy()
|
|
227
240
|
this.resampler = null
|
|
241
|
+
}
|
|
228
242
|
|
|
229
243
|
/* destroy TTS pipeline */
|
|
230
244
|
if (this.tts !== 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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -125,7 +125,9 @@ export default class SpeechFlowNodeT2TGoogle extends SpeechFlowNode {
|
|
|
125
125
|
|
|
126
126
|
/* shutdown Google Translate client */
|
|
127
127
|
if (this.client !== null) {
|
|
128
|
-
this.client.close()
|
|
128
|
+
await this.client.close().catch((error) => {
|
|
129
|
+
this.log("warning", `error closing Google Translate client: ${error}`)
|
|
130
|
+
})
|
|
129
131
|
this.client = null
|
|
130
132
|
}
|
|
131
133
|
}
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -112,6 +112,9 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
112
112
|
/* produce SRT/VTT blocks */
|
|
113
113
|
let output = convertSingle(timestampStart, timestampEnd, chunk.payload)
|
|
114
114
|
if (this.params.words) {
|
|
115
|
+
if (words.length === 0)
|
|
116
|
+
this.log("warning", "word-level subtitle highlighting requested but no word-level timing data available")
|
|
117
|
+
|
|
115
118
|
/* produce additional SRT/VTT blocks with each word highlighted */
|
|
116
119
|
const occurrences = new Map<string, number>()
|
|
117
120
|
for (const word of words) {
|
|
@@ -166,7 +169,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
166
169
|
else if (this.params.mode === "import") {
|
|
167
170
|
/* parse timestamp in SRT format ("HH:MM:SS,mmm") or VTT format ("HH:MM:SS.mmm") */
|
|
168
171
|
const parseTimestamp = (ts: string): Duration => {
|
|
169
|
-
const match = ts.match(/^(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/)
|
|
172
|
+
const match = ts.match(/^(\d{2,}):(\d{2}):(\d{2})[,.](\d{3})$/)
|
|
170
173
|
if (!match)
|
|
171
174
|
throw new Error(`invalid timestamp format: "${ts}"`)
|
|
172
175
|
const hours = Number.parseInt(match[1], 10)
|
|
@@ -202,7 +205,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
202
205
|
|
|
203
206
|
/* parse timestamp line */
|
|
204
207
|
const timeLine = lines[lineIdx]
|
|
205
|
-
const timeMatch = timeLine.match(/^(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/)
|
|
208
|
+
const timeMatch = timeLine.match(/^(\d{2,}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2,}:\d{2}:\d{2},\d{3})/)
|
|
206
209
|
if (!timeMatch) {
|
|
207
210
|
this.log("warning", "SRT contains invalid timestamp line")
|
|
208
211
|
continue
|
|
@@ -230,8 +233,8 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
230
233
|
const blocks = content.trim().split(/\r?\n\r?\n+/)
|
|
231
234
|
for (const block of blocks) {
|
|
232
235
|
const lines = block.trim().split(/\r?\n/)
|
|
233
|
-
if (lines.length <
|
|
234
|
-
this.log("warning", "VTT block contains fewer than
|
|
236
|
+
if (lines.length < 2) {
|
|
237
|
+
this.log("warning", "VTT block contains fewer than 2 lines")
|
|
235
238
|
continue
|
|
236
239
|
}
|
|
237
240
|
|
|
@@ -244,7 +247,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
244
247
|
|
|
245
248
|
/* parse timestamp line */
|
|
246
249
|
const timeLine = lines[lineIdx]
|
|
247
|
-
const timeMatch = timeLine.match(/^(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})/)
|
|
250
|
+
const timeMatch = timeLine.match(/^(\d{2,}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2,}:\d{2}:\d{2}\.\d{3})/)
|
|
248
251
|
if (!timeMatch) {
|
|
249
252
|
this.log("warning", "VTT contains invalid timestamp line")
|
|
250
253
|
continue
|
|
@@ -288,25 +291,42 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
288
291
|
/* accumulate input */
|
|
289
292
|
buffer += chunk.payload
|
|
290
293
|
|
|
291
|
-
/*
|
|
294
|
+
/* find the last double-newline boundary to separate
|
|
295
|
+
complete blocks from a potentially incomplete trailing block */
|
|
296
|
+
const boundary = /\r?\n\r?\n/g
|
|
297
|
+
let lastBoundaryEnd = -1
|
|
298
|
+
let match: RegExpExecArray | null
|
|
299
|
+
while ((match = boundary.exec(buffer)) !== null)
|
|
300
|
+
lastBoundaryEnd = match.index + match[0].length
|
|
301
|
+
|
|
302
|
+
/* if no complete block boundary found, wait for more data */
|
|
303
|
+
if (lastBoundaryEnd < 0) {
|
|
304
|
+
callback()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* split buffer into complete portion and remainder */
|
|
309
|
+
const complete = buffer.substring(0, lastBoundaryEnd)
|
|
310
|
+
const remainder = buffer.substring(lastBoundaryEnd)
|
|
311
|
+
|
|
312
|
+
/* parse only the complete portion */
|
|
292
313
|
try {
|
|
293
314
|
/* parse entries */
|
|
294
|
-
const entries = (self.params.format === "srt" ? parseSRT(
|
|
315
|
+
const entries = (self.params.format === "srt" ? parseSRT(complete) : parseVTT(complete))
|
|
295
316
|
|
|
296
317
|
/* emit parsed entries as individual chunks */
|
|
297
318
|
for (const entry of entries) {
|
|
298
319
|
const chunkNew = new SpeechFlowChunk(entry.start, entry.end, "final", "text", entry.text)
|
|
299
320
|
this.push(chunkNew)
|
|
300
321
|
}
|
|
301
|
-
|
|
302
|
-
/* clear buffer after successful parse */
|
|
303
|
-
buffer = ""
|
|
304
|
-
callback()
|
|
305
322
|
}
|
|
306
323
|
catch (error: unknown) {
|
|
307
|
-
|
|
308
|
-
callback(util.ensureError(error))
|
|
324
|
+
self.log("warning", `subtitle parse error: ${util.ensureError(error).message}`)
|
|
309
325
|
}
|
|
326
|
+
|
|
327
|
+
/* keep only the unparsed remainder in the buffer */
|
|
328
|
+
buffer = remainder
|
|
329
|
+
callback()
|
|
310
330
|
},
|
|
311
331
|
final (callback) {
|
|
312
332
|
/* process any remaining buffer content */
|
|
@@ -402,7 +422,11 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
|
|
|
402
422
|
const emit = (chunk: SpeechFlowChunk) => {
|
|
403
423
|
const data = JSON.stringify(chunk)
|
|
404
424
|
for (const info of wsPeers.values())
|
|
405
|
-
info.ws.
|
|
425
|
+
if (info.ws.readyState === WebSocket.OPEN)
|
|
426
|
+
info.ws.send(data, (err) => {
|
|
427
|
+
if (err)
|
|
428
|
+
this.log("warning", `HAPI: WebSocket: subtitle send failed: ${err.message}`)
|
|
429
|
+
})
|
|
406
430
|
}
|
|
407
431
|
|
|
408
432
|
/* establish writable stream */
|
|
@@ -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
|
|
|
@@ -103,7 +103,7 @@ export default class SpeechFlowNodeT2TSummary extends SpeechFlowNode {
|
|
|
103
103
|
|
|
104
104
|
/* count sentences in text */
|
|
105
105
|
private countSentences (text: string): number {
|
|
106
|
-
const matches = text.match(/[.;?!]/g)
|
|
106
|
+
const matches = text.match(/[.;?!]+(?:\s|$)/g)
|
|
107
107
|
return matches ? matches.length : 0
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -186,7 +186,7 @@ export default class SpeechFlowNodeT2TSummary extends SpeechFlowNode {
|
|
|
186
186
|
},
|
|
187
187
|
final (callback) {
|
|
188
188
|
/* generate final summary if there is accumulated text */
|
|
189
|
-
if (self.accumulatedText.length > 0
|
|
189
|
+
if (self.accumulatedText.length > 0) {
|
|
190
190
|
self.sentencesSinceLastSummary = 0
|
|
191
191
|
self.log("info", "generating final summary of accumulated text")
|
|
192
192
|
const textToSummarize = self.accumulatedText
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -47,6 +47,7 @@ export default class SpeechFlowNodeX2XFilter extends SpeechFlowNode {
|
|
|
47
47
|
|
|
48
48
|
/* helper function for comparing two values */
|
|
49
49
|
const comparison = (val1: any, op: string, val2: any) => {
|
|
50
|
+
val1 ??= ""
|
|
50
51
|
if (op === "==" || op === "!=") {
|
|
51
52
|
/* equal comparison */
|
|
52
53
|
const str1 = (typeof val1 === "string" ? val1 : val1.toString())
|
|
@@ -73,8 +74,8 @@ export default class SpeechFlowNodeX2XFilter extends SpeechFlowNode {
|
|
|
73
74
|
/* non-equal comparison */
|
|
74
75
|
const coerceNum = (val: any) =>
|
|
75
76
|
typeof val === "number" ? val : (
|
|
76
|
-
typeof val === "string" && val.match(/^[
|
|
77
|
-
typeof val === "string" && val.match(/^[\d
|
|
77
|
+
typeof val === "string" && val.match(/^[+-]?\d+$/) ? Number.parseInt(val, 10) : (
|
|
78
|
+
typeof val === "string" && val.match(/^[+-]?(\d+\.?\d*|\d*\.?\d+)$/) ?
|
|
78
79
|
Number.parseFloat(val) :
|
|
79
80
|
Number(val)
|
|
80
81
|
)
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -116,7 +116,8 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
|
|
|
116
116
|
|
|
117
117
|
/* convert regular stream into object-mode stream */
|
|
118
118
|
const wrapper1 = util.createTransformStreamForWritableSide("audio", 1)
|
|
119
|
-
const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark
|
|
119
|
+
const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark,
|
|
120
|
+
this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
|
|
120
121
|
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
121
122
|
}
|
|
122
123
|
|
|
@@ -137,7 +138,8 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
|
|
|
137
138
|
this.stream = this.io as unknown as Stream.Readable
|
|
138
139
|
|
|
139
140
|
/* convert regular stream into object-mode stream */
|
|
140
|
-
const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark
|
|
141
|
+
const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark,
|
|
142
|
+
this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
|
|
141
143
|
this.stream = Stream.compose(this.stream, wrapper)
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -207,24 +209,36 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
|
|
|
207
209
|
if (!error.message.match(/AudioIO Quit expects 1 argument/))
|
|
208
210
|
throw error
|
|
209
211
|
}
|
|
212
|
+
const ac1 = new AbortController()
|
|
210
213
|
await Promise.race([
|
|
211
|
-
util.timeout(2 * 1000, "PortAudio abort timeout"),
|
|
214
|
+
util.timeout(2 * 1000, "PortAudio abort timeout", ac1.signal),
|
|
212
215
|
new Promise<void>((resolve) => {
|
|
213
216
|
this.io!.abort(() => {
|
|
214
217
|
resolve()
|
|
215
218
|
})
|
|
216
219
|
}).catch(catchHandler)
|
|
217
|
-
])
|
|
220
|
+
]).finally(() => {
|
|
221
|
+
ac1.abort()
|
|
222
|
+
})
|
|
223
|
+
const ac2 = new AbortController()
|
|
218
224
|
await Promise.race([
|
|
219
|
-
util.timeout(2 * 1000, "PortAudio quit timeout"),
|
|
225
|
+
util.timeout(2 * 1000, "PortAudio quit timeout", ac2.signal),
|
|
220
226
|
new Promise<void>((resolve) => {
|
|
221
227
|
this.io!.quit(() => {
|
|
222
228
|
resolve()
|
|
223
229
|
})
|
|
224
230
|
}).catch(catchHandler)
|
|
225
|
-
])
|
|
231
|
+
]).finally(() => {
|
|
232
|
+
ac2.abort()
|
|
233
|
+
})
|
|
226
234
|
this.io = null
|
|
227
235
|
}
|
|
236
|
+
|
|
237
|
+
/* shutdown stream */
|
|
238
|
+
if (this.stream !== null) {
|
|
239
|
+
await util.destroyStream(this.stream)
|
|
240
|
+
this.stream = null
|
|
241
|
+
}
|
|
228
242
|
}
|
|
229
243
|
}
|
|
230
244
|
|
|
@@ -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
|
|
|
@@ -129,13 +129,15 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
|
|
|
129
129
|
})
|
|
130
130
|
const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, highWaterMark)
|
|
131
131
|
const wrapper2 = util.createTransformStreamForReadableSide(
|
|
132
|
-
this.params.type, () => this.timeZero, highWaterMark
|
|
132
|
+
this.params.type, () => this.timeZero, highWaterMark,
|
|
133
|
+
this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
|
|
133
134
|
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
134
135
|
}
|
|
135
136
|
else if (this.params.mode === "r") {
|
|
136
137
|
/* read-only mode: stdout only */
|
|
137
138
|
const wrapper = util.createTransformStreamForReadableSide(
|
|
138
|
-
this.params.type, () => this.timeZero, highWaterMark
|
|
139
|
+
this.params.type, () => this.timeZero, highWaterMark,
|
|
140
|
+
this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
|
|
139
141
|
this.stream = Stream.compose(this.subprocess.stdout!, wrapper)
|
|
140
142
|
}
|
|
141
143
|
else if (this.params.mode === "w") {
|
|
@@ -153,6 +155,7 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
|
|
|
153
155
|
/* gracefully end stdin if in write or read/write mode */
|
|
154
156
|
if ((this.params.mode === "w" || this.params.mode === "rw") && this.subprocess.stdin
|
|
155
157
|
&& !this.subprocess.stdin.destroyed && !this.subprocess.stdin.writableEnded) {
|
|
158
|
+
const ac1 = new AbortController()
|
|
156
159
|
await Promise.race([
|
|
157
160
|
new Promise<void>((resolve, reject) => {
|
|
158
161
|
this.subprocess!.stdin!.end((err?: Error) => {
|
|
@@ -160,36 +163,47 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
|
|
|
160
163
|
else resolve()
|
|
161
164
|
})
|
|
162
165
|
}),
|
|
163
|
-
util.timeout(2000)
|
|
164
|
-
]).
|
|
166
|
+
util.timeout(2000, "timeout", ac1.signal)
|
|
167
|
+
]).finally(() => {
|
|
168
|
+
ac1.abort()
|
|
169
|
+
}).catch((err: unknown) => {
|
|
165
170
|
const error = util.ensureError(err)
|
|
166
171
|
this.log("warning", `failed to gracefully close stdin: ${error.message}`)
|
|
167
172
|
})
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/* wait for subprocess to exit gracefully */
|
|
176
|
+
const ac2 = new AbortController()
|
|
171
177
|
await Promise.race([
|
|
172
178
|
this.subprocess,
|
|
173
|
-
util.timeout(5000, "subprocess exit timeout")
|
|
174
|
-
]).
|
|
179
|
+
util.timeout(5000, "subprocess exit timeout", ac2.signal)
|
|
180
|
+
]).finally(() => {
|
|
181
|
+
ac2.abort()
|
|
182
|
+
}).catch(async (err: unknown) => {
|
|
175
183
|
/* force kill with SIGTERM */
|
|
176
184
|
const error = util.ensureError(err)
|
|
177
185
|
if (error.message.includes("timeout")) {
|
|
178
186
|
this.log("warning", "subprocess did not exit gracefully, forcing termination")
|
|
179
187
|
this.subprocess!.kill("SIGTERM")
|
|
188
|
+
const ac3 = new AbortController()
|
|
180
189
|
return Promise.race([
|
|
181
190
|
this.subprocess,
|
|
182
|
-
util.timeout(2000)
|
|
183
|
-
])
|
|
191
|
+
util.timeout(2000, "timeout", ac3.signal)
|
|
192
|
+
]).finally(() => {
|
|
193
|
+
ac3.abort()
|
|
194
|
+
})
|
|
184
195
|
}
|
|
185
196
|
}).catch(async () => {
|
|
186
197
|
/* force kill with SIGKILL */
|
|
187
198
|
this.log("warning", "subprocess did not respond to SIGTERM, forcing SIGKILL")
|
|
188
199
|
this.subprocess!.kill("SIGKILL")
|
|
200
|
+
const ac4 = new AbortController()
|
|
189
201
|
return Promise.race([
|
|
190
202
|
this.subprocess,
|
|
191
|
-
util.timeout(1000)
|
|
192
|
-
])
|
|
203
|
+
util.timeout(1000, "timeout", ac4.signal)
|
|
204
|
+
]).finally(() => {
|
|
205
|
+
ac4.abort()
|
|
206
|
+
})
|
|
193
207
|
}).catch(() => {
|
|
194
208
|
this.log("error", "subprocess did not terminate even after SIGKILL")
|
|
195
209
|
})
|