speechflow 2.2.1 → 2.3.1

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 (242) hide show
  1. package/{etc/claude.md → AGENTS.md} +8 -3
  2. package/CHANGELOG.md +98 -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 +17 -19
  19. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  20. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +25 -8
  21. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  22. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +16 -13
  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 +7 -7
  27. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  28. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +7 -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 +21 -16
  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 +2 -2
  37. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  38. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +1 -1
  39. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +4 -3
  40. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +2 -2
  42. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
  43. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +19 -11
  44. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  45. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +8 -8
  46. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  47. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +33 -29
  48. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  49. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +6 -5
  50. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  51. package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +2 -1
  52. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +42 -23
  53. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
  54. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +13 -5
  55. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  56. package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +1 -0
  57. package/speechflow-cli/dst/speechflow-node-a2t-google.js +8 -2
  58. package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -1
  59. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +33 -27
  60. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
  61. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +16 -5
  62. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
  63. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +17 -5
  64. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  65. package/speechflow-cli/dst/speechflow-node-t2a-google.js +17 -5
  66. package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -1
  67. package/speechflow-cli/dst/speechflow-node-t2a-kitten.d.ts +15 -0
  68. package/speechflow-cli/dst/speechflow-node-t2a-kitten.js +194 -0
  69. package/speechflow-cli/dst/speechflow-node-t2a-kitten.js.map +1 -0
  70. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +24 -10
  71. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  72. package/speechflow-cli/dst/speechflow-node-t2a-openai.js +17 -5
  73. package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -1
  74. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +22 -7
  75. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +1 -1
  77. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
  78. package/speechflow-cli/dst/speechflow-node-t2t-format.js +1 -1
  79. package/speechflow-cli/dst/speechflow-node-t2t-google.js +4 -2
  80. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-modify.js +1 -1
  82. package/speechflow-cli/dst/speechflow-node-t2t-opus.js +10 -2
  83. package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -1
  84. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +1 -1
  85. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +1 -1
  86. package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +3 -0
  87. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +160 -57
  88. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js +1 -1
  90. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +34 -14
  91. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  92. package/speechflow-cli/dst/speechflow-node-t2t-summary.js +3 -3
  93. package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -1
  94. package/speechflow-cli/dst/speechflow-node-t2t-translate.js +1 -1
  95. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -2
  96. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  97. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +1 -1
  98. package/speechflow-cli/dst/speechflow-node-xio-device.js +18 -7
  99. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  100. package/speechflow-cli/dst/speechflow-node-xio-exec.js +27 -15
  101. package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
  102. package/speechflow-cli/dst/speechflow-node-xio-file.js +13 -7
  103. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  104. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +25 -12
  105. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  106. package/speechflow-cli/dst/speechflow-node-xio-vban.js +32 -20
  107. package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -1
  108. package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +1 -0
  109. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +84 -63
  110. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -1
  111. package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -0
  112. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +75 -20
  113. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  114. package/speechflow-cli/dst/speechflow-node.js +5 -7
  115. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  116. package/speechflow-cli/dst/speechflow-util-audio-wt.js +31 -5
  117. package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -1
  118. package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -1
  119. package/speechflow-cli/dst/speechflow-util-audio.js +28 -15
  120. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
  121. package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
  122. package/speechflow-cli/dst/speechflow-util-error.js +2 -2
  123. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
  124. package/speechflow-cli/dst/speechflow-util-llm.js +13 -3
  125. package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -1
  126. package/speechflow-cli/dst/speechflow-util-misc.d.ts +3 -2
  127. package/speechflow-cli/dst/speechflow-util-misc.js +63 -6
  128. package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
  129. package/speechflow-cli/dst/speechflow-util-queue.d.ts +9 -17
  130. package/speechflow-cli/dst/speechflow-util-queue.js +98 -78
  131. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
  132. package/speechflow-cli/dst/speechflow-util-stream.d.ts +1 -1
  133. package/speechflow-cli/dst/speechflow-util-stream.js +35 -8
  134. package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
  135. package/speechflow-cli/dst/speechflow-util.js +1 -1
  136. package/speechflow-cli/dst/speechflow.d.ts +1 -1
  137. package/speechflow-cli/dst/speechflow.js +1 -1
  138. package/speechflow-cli/etc/eslint.mjs +1 -1
  139. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  140. package/speechflow-cli/etc/stx.conf +8 -2
  141. package/speechflow-cli/package.d/@ericedouard+vad-node-realtime+0.2.0.patch +2 -1
  142. package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  143. package/speechflow-cli/package.d/kitten-tts-js+0.1.2.patch +24 -0
  144. package/speechflow-cli/package.d/speex-resampler+3.0.1.patch +56 -0
  145. package/speechflow-cli/package.json +40 -30
  146. package/speechflow-cli/src/lib.d.ts +19 -1
  147. package/speechflow-cli/src/speechflow-main-api.ts +64 -19
  148. package/speechflow-cli/src/speechflow-main-cli.ts +2 -2
  149. package/speechflow-cli/src/speechflow-main-config.ts +1 -1
  150. package/speechflow-cli/src/speechflow-main-graph.ts +56 -22
  151. package/speechflow-cli/src/speechflow-main-nodes.ts +1 -1
  152. package/speechflow-cli/src/speechflow-main-status.ts +6 -3
  153. package/speechflow-cli/src/speechflow-main.ts +1 -1
  154. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +19 -20
  155. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +31 -13
  156. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +17 -13
  157. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +6 -5
  158. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +9 -8
  159. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +8 -4
  160. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +1 -1
  161. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +22 -18
  162. package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +1 -1
  163. package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +43 -16
  164. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
  165. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +1 -1
  166. package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +4 -3
  167. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +2 -2
  168. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +24 -12
  169. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +10 -9
  170. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +38 -31
  171. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +6 -5
  172. package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +47 -25
  173. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -6
  174. package/speechflow-cli/src/speechflow-node-a2t-google.ts +12 -4
  175. package/speechflow-cli/src/speechflow-node-a2t-openai.ts +39 -31
  176. package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +16 -5
  177. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +17 -5
  178. package/speechflow-cli/src/speechflow-node-t2a-google.ts +17 -5
  179. package/speechflow-cli/src/speechflow-node-t2a-kitten.ts +178 -0
  180. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +24 -10
  181. package/speechflow-cli/src/speechflow-node-t2a-openai.ts +17 -5
  182. package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +22 -7
  183. package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +1 -1
  184. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
  185. package/speechflow-cli/src/speechflow-node-t2t-format.ts +1 -1
  186. package/speechflow-cli/src/speechflow-node-t2t-google.ts +4 -2
  187. package/speechflow-cli/src/speechflow-node-t2t-modify.ts +1 -1
  188. package/speechflow-cli/src/speechflow-node-t2t-opus.ts +10 -2
  189. package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +1 -1
  190. package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +1 -1
  191. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +215 -62
  192. package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +1 -1
  193. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +39 -15
  194. package/speechflow-cli/src/speechflow-node-t2t-summary.ts +3 -3
  195. package/speechflow-cli/src/speechflow-node-t2t-translate.ts +1 -1
  196. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -3
  197. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +1 -1
  198. package/speechflow-cli/src/speechflow-node-xio-device.ts +21 -7
  199. package/speechflow-cli/src/speechflow-node-xio-exec.ts +30 -16
  200. package/speechflow-cli/src/speechflow-node-xio-file.ts +15 -7
  201. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +28 -15
  202. package/speechflow-cli/src/speechflow-node-xio-vban.ts +35 -22
  203. package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +92 -70
  204. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +79 -22
  205. package/speechflow-cli/src/speechflow-node.ts +7 -8
  206. package/speechflow-cli/src/speechflow-util-audio-wt.ts +46 -7
  207. package/speechflow-cli/src/speechflow-util-audio.ts +31 -17
  208. package/speechflow-cli/src/speechflow-util-error.ts +3 -3
  209. package/speechflow-cli/src/speechflow-util-llm.ts +14 -3
  210. package/speechflow-cli/src/speechflow-util-misc.ts +63 -6
  211. package/speechflow-cli/src/speechflow-util-queue.ts +103 -81
  212. package/speechflow-cli/src/speechflow-util-stream.ts +40 -8
  213. package/speechflow-cli/src/speechflow-util.ts +1 -1
  214. package/speechflow-cli/src/speechflow.ts +1 -1
  215. package/speechflow-ui-db/dst/index.html +1 -1
  216. package/speechflow-ui-db/dst/index.js +15 -15
  217. package/speechflow-ui-db/etc/eslint.mjs +1 -1
  218. package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
  219. package/speechflow-ui-db/etc/stx.conf +1 -1
  220. package/speechflow-ui-db/etc/stylelint.js +1 -1
  221. package/speechflow-ui-db/etc/stylelint.yaml +1 -1
  222. package/speechflow-ui-db/etc/vite-client.mts +1 -1
  223. package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  224. package/speechflow-ui-db/package.json +22 -16
  225. package/speechflow-ui-db/src/app.styl +1 -1
  226. package/speechflow-ui-db/src/app.vue +1 -1
  227. package/speechflow-ui-db/src/index.html +1 -1
  228. package/speechflow-ui-db/src/index.ts +1 -1
  229. package/speechflow-ui-st/dst/index.html +1 -1
  230. package/speechflow-ui-st/dst/index.js +31 -31
  231. package/speechflow-ui-st/etc/eslint.mjs +1 -1
  232. package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
  233. package/speechflow-ui-st/etc/stx.conf +1 -1
  234. package/speechflow-ui-st/etc/stylelint.js +1 -1
  235. package/speechflow-ui-st/etc/stylelint.yaml +1 -1
  236. package/speechflow-ui-st/etc/vite-client.mts +1 -1
  237. package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  238. package/speechflow-ui-st/package.json +23 -17
  239. package/speechflow-ui-st/src/app.styl +1 -1
  240. package/speechflow-ui-st/src/app.vue +1 -1
  241. package/speechflow-ui-st/src/index.html +1 -1
  242. 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
 
@@ -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-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,7 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
96
96
  this.subprocess.on("error", (err) => {
97
97
  this.log("error", `subprocess error: ${err.message}`)
98
98
  this.emit("error", err)
99
- if (this.stream !== null)
99
+ if (this.stream !== null && !this.stream.destroyed)
100
100
  this.stream.emit("error", err)
101
101
  })
102
102
 
@@ -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,44 +163,55 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
160
163
  else resolve()
161
164
  })
162
165
  }),
163
- util.timeout(2000)
164
- ]).catch((err: unknown) => {
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
 
175
+ /* remove event listeners to prevent errors during kill sequence */
176
+ this.subprocess.removeAllListeners("error")
177
+ this.subprocess.removeAllListeners("exit")
178
+
170
179
  /* wait for subprocess to exit gracefully */
180
+ const ac2 = new AbortController()
171
181
  await Promise.race([
172
182
  this.subprocess,
173
- util.timeout(5000, "subprocess exit timeout")
174
- ]).catch(async (err: unknown) => {
183
+ util.timeout(5000, "subprocess exit timeout", ac2.signal)
184
+ ]).finally(() => {
185
+ ac2.abort()
186
+ }).catch(async (err: unknown) => {
175
187
  /* force kill with SIGTERM */
176
188
  const error = util.ensureError(err)
177
189
  if (error.message.includes("timeout")) {
178
190
  this.log("warning", "subprocess did not exit gracefully, forcing termination")
179
191
  this.subprocess!.kill("SIGTERM")
192
+ const ac3 = new AbortController()
180
193
  return Promise.race([
181
194
  this.subprocess,
182
- util.timeout(2000)
183
- ])
195
+ util.timeout(2000, "timeout", ac3.signal)
196
+ ]).finally(() => {
197
+ ac3.abort()
198
+ })
184
199
  }
185
200
  }).catch(async () => {
186
201
  /* force kill with SIGKILL */
187
202
  this.log("warning", "subprocess did not respond to SIGTERM, forcing SIGKILL")
188
203
  this.subprocess!.kill("SIGKILL")
204
+ const ac4 = new AbortController()
189
205
  return Promise.race([
190
206
  this.subprocess,
191
- util.timeout(1000)
192
- ])
207
+ util.timeout(1000, "timeout", ac4.signal)
208
+ ]).finally(() => {
209
+ ac4.abort()
210
+ })
193
211
  }).catch(() => {
194
212
  this.log("error", "subprocess did not terminate even after SIGKILL")
195
213
  })
196
214
 
197
- /* remove event listeners to prevent memory leaks */
198
- this.subprocess.removeAllListeners("error")
199
- this.subprocess.removeAllListeners("exit")
200
-
201
215
  /* clear subprocess reference */
202
216
  this.subprocess = null
203
217
  }
@@ -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
  }