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.
Files changed (235) hide show
  1. package/{etc/claude.md → AGENTS.md} +8 -3
  2. package/CHANGELOG.md +70 -1
  3. package/README.md +28 -4
  4. package/etc/speechflow.yaml +3 -1
  5. package/etc/stx.conf +1 -1
  6. package/package.json +6 -6
  7. package/speechflow-cli/dst/speechflow-main-api.d.ts +2 -1
  8. package/speechflow-cli/dst/speechflow-main-api.js +57 -16
  9. package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
  10. package/speechflow-cli/dst/speechflow-main-cli.js +2 -2
  11. package/speechflow-cli/dst/speechflow-main-config.js +1 -1
  12. package/speechflow-cli/dst/speechflow-main-graph.js +55 -21
  13. package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
  14. package/speechflow-cli/dst/speechflow-main-nodes.js +1 -1
  15. package/speechflow-cli/dst/speechflow-main-status.js +6 -3
  16. package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
  17. package/speechflow-cli/dst/speechflow-main.js +1 -1
  18. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +7 -10
  19. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  20. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +8 -6
  21. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  22. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +9 -5
  23. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
  24. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +6 -5
  25. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  26. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +2 -2
  27. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  28. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +2 -4
  29. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  30. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +1 -1
  31. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +20 -12
  32. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  33. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js +1 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js +33 -11
  35. package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +1 -1
  37. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +1 -1
  38. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +4 -3
  39. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +2 -2
  41. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
  42. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +19 -11
  43. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  44. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +8 -8
  45. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  46. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +33 -29
  47. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  48. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +6 -5
  49. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  50. package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +2 -1
  51. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +34 -20
  52. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
  53. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +13 -5
  54. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  55. package/speechflow-cli/dst/speechflow-node-a2t-google.js +3 -2
  56. package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -1
  57. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +33 -27
  58. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
  59. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +16 -5
  60. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
  61. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +17 -5
  62. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  63. package/speechflow-cli/dst/speechflow-node-t2a-google.js +17 -5
  64. package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -1
  65. package/speechflow-cli/dst/speechflow-node-t2a-kitten.d.ts +15 -0
  66. package/speechflow-cli/dst/speechflow-node-t2a-kitten.js +194 -0
  67. package/speechflow-cli/dst/speechflow-node-t2a-kitten.js.map +1 -0
  68. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +21 -9
  69. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  70. package/speechflow-cli/dst/speechflow-node-t2a-openai.js +17 -5
  71. package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -1
  72. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +21 -7
  73. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -1
  74. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +1 -1
  75. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-format.js +1 -1
  77. package/speechflow-cli/dst/speechflow-node-t2t-google.js +4 -2
  78. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
  79. package/speechflow-cli/dst/speechflow-node-t2t-modify.js +1 -1
  80. package/speechflow-cli/dst/speechflow-node-t2t-opus.js +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +1 -1
  82. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +1 -1
  83. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +1 -1
  84. package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js +1 -1
  85. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +34 -14
  86. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  87. package/speechflow-cli/dst/speechflow-node-t2t-summary.js +3 -3
  88. package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-t2t-translate.js +1 -1
  90. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -2
  91. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  92. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +1 -1
  93. package/speechflow-cli/dst/speechflow-node-xio-device.js +18 -7
  94. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  95. package/speechflow-cli/dst/speechflow-node-xio-exec.js +23 -11
  96. package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
  97. package/speechflow-cli/dst/speechflow-node-xio-file.js +13 -7
  98. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  99. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +25 -12
  100. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-node-xio-vban.js +32 -20
  102. package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -1
  103. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +78 -62
  104. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -1
  105. package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -0
  106. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +63 -18
  107. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  108. package/speechflow-cli/dst/speechflow-node.js +5 -7
  109. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  110. package/speechflow-cli/dst/speechflow-util-audio-wt.js +31 -5
  111. package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -1
  112. package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -1
  113. package/speechflow-cli/dst/speechflow-util-audio.js +25 -14
  114. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
  115. package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
  116. package/speechflow-cli/dst/speechflow-util-error.js +2 -2
  117. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
  118. package/speechflow-cli/dst/speechflow-util-llm.js +1 -1
  119. package/speechflow-cli/dst/speechflow-util-misc.d.ts +3 -2
  120. package/speechflow-cli/dst/speechflow-util-misc.js +63 -6
  121. package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
  122. package/speechflow-cli/dst/speechflow-util-queue.d.ts +5 -17
  123. package/speechflow-cli/dst/speechflow-util-queue.js +57 -78
  124. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
  125. package/speechflow-cli/dst/speechflow-util-stream.d.ts +1 -1
  126. package/speechflow-cli/dst/speechflow-util-stream.js +35 -8
  127. package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
  128. package/speechflow-cli/dst/speechflow-util.js +1 -1
  129. package/speechflow-cli/dst/speechflow.d.ts +1 -1
  130. package/speechflow-cli/dst/speechflow.js +1 -1
  131. package/speechflow-cli/etc/eslint.mjs +1 -1
  132. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  133. package/speechflow-cli/etc/stx.conf +8 -2
  134. package/speechflow-cli/package.d/@ericedouard+vad-node-realtime+0.2.0.patch +2 -1
  135. package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  136. package/speechflow-cli/package.d/kitten-tts-js+0.1.2.patch +24 -0
  137. package/speechflow-cli/package.d/speex-resampler+3.0.1.patch +56 -0
  138. package/speechflow-cli/package.json +40 -30
  139. package/speechflow-cli/src/lib.d.ts +19 -1
  140. package/speechflow-cli/src/speechflow-main-api.ts +64 -19
  141. package/speechflow-cli/src/speechflow-main-cli.ts +2 -2
  142. package/speechflow-cli/src/speechflow-main-config.ts +1 -1
  143. package/speechflow-cli/src/speechflow-main-graph.ts +56 -22
  144. package/speechflow-cli/src/speechflow-main-nodes.ts +1 -1
  145. package/speechflow-cli/src/speechflow-main-status.ts +6 -3
  146. package/speechflow-cli/src/speechflow-main.ts +1 -1
  147. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +7 -11
  148. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +8 -6
  149. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +10 -5
  150. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +6 -5
  151. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -2
  152. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +2 -4
  153. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +1 -1
  154. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +20 -13
  155. package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +1 -1
  156. package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +43 -16
  157. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +1 -1
  158. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +1 -1
  159. package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +4 -3
  160. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +2 -2
  161. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +24 -12
  162. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +10 -9
  163. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +38 -31
  164. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +6 -5
  165. package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +35 -22
  166. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -6
  167. package/speechflow-cli/src/speechflow-node-a2t-google.ts +5 -4
  168. package/speechflow-cli/src/speechflow-node-a2t-openai.ts +39 -31
  169. package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +16 -5
  170. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +17 -5
  171. package/speechflow-cli/src/speechflow-node-t2a-google.ts +17 -5
  172. package/speechflow-cli/src/speechflow-node-t2a-kitten.ts +178 -0
  173. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +21 -9
  174. package/speechflow-cli/src/speechflow-node-t2a-openai.ts +17 -5
  175. package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +21 -7
  176. package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +1 -1
  177. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
  178. package/speechflow-cli/src/speechflow-node-t2t-format.ts +1 -1
  179. package/speechflow-cli/src/speechflow-node-t2t-google.ts +4 -2
  180. package/speechflow-cli/src/speechflow-node-t2t-modify.ts +1 -1
  181. package/speechflow-cli/src/speechflow-node-t2t-opus.ts +1 -1
  182. package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +1 -1
  183. package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +1 -1
  184. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +1 -1
  185. package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +1 -1
  186. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +39 -15
  187. package/speechflow-cli/src/speechflow-node-t2t-summary.ts +3 -3
  188. package/speechflow-cli/src/speechflow-node-t2t-translate.ts +1 -1
  189. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -3
  190. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +1 -1
  191. package/speechflow-cli/src/speechflow-node-xio-device.ts +21 -7
  192. package/speechflow-cli/src/speechflow-node-xio-exec.ts +25 -11
  193. package/speechflow-cli/src/speechflow-node-xio-file.ts +15 -7
  194. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +28 -15
  195. package/speechflow-cli/src/speechflow-node-xio-vban.ts +35 -22
  196. package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +85 -69
  197. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +67 -20
  198. package/speechflow-cli/src/speechflow-node.ts +7 -8
  199. package/speechflow-cli/src/speechflow-util-audio-wt.ts +46 -7
  200. package/speechflow-cli/src/speechflow-util-audio.ts +27 -15
  201. package/speechflow-cli/src/speechflow-util-error.ts +3 -3
  202. package/speechflow-cli/src/speechflow-util-llm.ts +1 -1
  203. package/speechflow-cli/src/speechflow-util-misc.ts +63 -6
  204. package/speechflow-cli/src/speechflow-util-queue.ts +60 -81
  205. package/speechflow-cli/src/speechflow-util-stream.ts +40 -8
  206. package/speechflow-cli/src/speechflow-util.ts +1 -1
  207. package/speechflow-cli/src/speechflow.ts +1 -1
  208. package/speechflow-ui-db/dst/index.html +1 -1
  209. package/speechflow-ui-db/dst/index.js +15 -15
  210. package/speechflow-ui-db/etc/eslint.mjs +1 -1
  211. package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
  212. package/speechflow-ui-db/etc/stx.conf +1 -1
  213. package/speechflow-ui-db/etc/stylelint.js +1 -1
  214. package/speechflow-ui-db/etc/stylelint.yaml +1 -1
  215. package/speechflow-ui-db/etc/vite-client.mts +1 -1
  216. package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  217. package/speechflow-ui-db/package.json +22 -16
  218. package/speechflow-ui-db/src/app.styl +1 -1
  219. package/speechflow-ui-db/src/app.vue +1 -1
  220. package/speechflow-ui-db/src/index.html +1 -1
  221. package/speechflow-ui-db/src/index.ts +1 -1
  222. package/speechflow-ui-st/dst/index.html +1 -1
  223. package/speechflow-ui-st/dst/index.js +31 -31
  224. package/speechflow-ui-st/etc/eslint.mjs +1 -1
  225. package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
  226. package/speechflow-ui-st/etc/stx.conf +1 -1
  227. package/speechflow-ui-st/etc/stylelint.js +1 -1
  228. package/speechflow-ui-st/etc/stylelint.yaml +1 -1
  229. package/speechflow-ui-st/etc/vite-client.mts +1 -1
  230. package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  231. package/speechflow-ui-st/package.json +23 -17
  232. package/speechflow-ui-st/src/app.styl +1 -1
  233. package/speechflow-ui-st/src/app.vue +1 -1
  234. package/speechflow-ui-st/src/index.html +1 -1
  235. 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-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
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
- fs.close(self.fd, () => {
162
- self.fd = null
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-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
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.SingleQueue<SpeechFlowChunk> | null = null
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.SingleQueue<SpeechFlowChunk>()
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!.write(chunk)
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
- throw new Error("read operation on write-only node")
146
- reads.add(self.chunkQueue!.read().then((chunk) => {
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 = null
159
-
160
- /* close MQTT broker */
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-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
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.SingleQueue<SpeechFlowChunk> | null = null
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.SingleQueue<SpeechFlowChunk>()
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) * 32767
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) * 32767
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) * 32767
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 * 32767)))
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 * 32767)))
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
- throw new Error("read operation on write-only node")
297
- reads.add(self.chunkQueue!.read().then((chunk) => {
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
- this.push(null)
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.drain()
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-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
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.SingleQueue<SpeechFlowChunk> | null = null
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.SingleQueue<SpeechFlowChunk>()
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
- /* determine URL */
379
- if (req.url === undefined) {
380
- res.writeHead(400, { "Content-Type": "text/plain" })
381
- res.end("Bad Request")
382
- return
383
- }
384
- const host = req.headers.host?.replace(/[^a-zA-Z0-9:.\-_]/g, "") ?? "localhost"
385
- const url = new URL(req.url, `http://${host}`)
386
- const pathMatch = url.pathname === self.params.path
387
- const resourceMatch = url.pathname.startsWith(self.params.path + "/")
388
-
389
- /* CORS headers for browser clients */
390
- res.setHeader("Access-Control-Allow-Origin", "*")
391
- res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, OPTIONS")
392
- res.setHeader("Access-Control-Allow-Headers", "Content-Type")
393
- res.setHeader("Access-Control-Expose-Headers", "Location")
394
-
395
- /* handle CORS preflight */
396
- if (req.method === "OPTIONS") {
397
- res.writeHead(204)
398
- res.end()
399
- return
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
- /* determine if WHIP (receiving) or WHEP (sending) based on SDP content */
416
- const hasSendonly = /\ba=sendonly\b/m.test(body)
417
- const hasSendrecv = /\ba=sendrecv\b/m.test(body)
418
- const hasRecvonly = /\ba=recvonly\b/m.test(body)
419
- const isPublisher = hasSendonly || hasSendrecv
420
- const isViewer = hasRecvonly
421
-
422
- /* handle protocol based on mode */
423
- if (self.params.mode === "r" && isPublisher)
424
- /* in read mode, accept WHIP publishers */
425
- await self.handleWHIP(res, body)
426
- else if (self.params.mode === "w" && isViewer)
427
- /* in write mode, accept WHEP viewers */
428
- await self.handleWHEP(res, body)
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
- res.writeHead(403, { "Content-Type": "text/plain" })
431
- res.end("Forbidden")
441
+ /* handle unknown requests */
442
+ res.writeHead(404, { "Content-Type": "text/plain" })
443
+ res.end("Not Found")
432
444
  }
433
445
  }
434
- else if (req.method === "DELETE" && resourceMatch) {
435
- /* handle DELETE for connection teardown */
436
- const resourceId = url.pathname.substring(self.params.path.length + 1)
437
- self.handleDELETE(res, resourceId)
438
- }
439
- else {
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
- reads.add(self.chunkQueue!.read().then((chunk) => {
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
- this.push(null)
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.drain()
536
+ this.chunkQueue.destroy()
521
537
  this.chunkQueue = null
522
538
  }
523
539