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.
Files changed (238) hide show
  1. package/{etc/claude.md → AGENTS.md} +8 -3
  2. package/CHANGELOG.md +78 -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-t2t-simulator.d.ts → speechflow-node-t2a-kitten.d.ts} +5 -1
  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.css +1 -1
  209. package/speechflow-ui-db/dst/index.html +1 -1
  210. package/speechflow-ui-db/dst/index.js +15 -15
  211. package/speechflow-ui-db/etc/eslint.mjs +1 -1
  212. package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
  213. package/speechflow-ui-db/etc/stx.conf +1 -1
  214. package/speechflow-ui-db/etc/stylelint.js +1 -1
  215. package/speechflow-ui-db/etc/stylelint.yaml +1 -1
  216. package/speechflow-ui-db/etc/vite-client.mts +1 -1
  217. package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  218. package/speechflow-ui-db/package.json +22 -16
  219. package/speechflow-ui-db/src/app.styl +1 -1
  220. package/speechflow-ui-db/src/app.vue +4 -4
  221. package/speechflow-ui-db/src/index.html +1 -1
  222. package/speechflow-ui-db/src/index.ts +1 -1
  223. package/speechflow-ui-st/dst/index.html +1 -1
  224. package/speechflow-ui-st/dst/index.js +31 -31
  225. package/speechflow-ui-st/etc/eslint.mjs +1 -1
  226. package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
  227. package/speechflow-ui-st/etc/stx.conf +1 -1
  228. package/speechflow-ui-st/etc/stylelint.js +1 -1
  229. package/speechflow-ui-st/etc/stylelint.yaml +1 -1
  230. package/speechflow-ui-st/etc/vite-client.mts +1 -1
  231. package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
  232. package/speechflow-ui-st/package.json +23 -17
  233. package/speechflow-ui-st/src/app.styl +1 -1
  234. package/speechflow-ui-st/src/app.vue +9 -8
  235. package/speechflow-ui-st/src/index.html +1 -1
  236. package/speechflow-ui-st/src/index.ts +1 -1
  237. package/speechflow-cli/dst/speechflow-node-t2t-simulator.js +0 -128
  238. 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-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
 
@@ -171,10 +171,13 @@ export class NodeGraph {
171
171
  /* open node */
172
172
  this.cli.log("info", `open node <${node.id}>`)
173
173
  node.setTimeZero(this.timeZero)
174
+ const ac = new AbortController()
174
175
  await Promise.race([
175
176
  node.open(),
176
- util.timeout(30 * 1000)
177
- ]).catch((err: Error) => {
177
+ util.timeout(30 * 1000, "timeout", ac.signal)
178
+ ]).finally(() => {
179
+ ac.abort()
180
+ }).catch((err: Error) => {
178
181
  this.cli.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`)
179
182
  throw new Error(`failed to open node <${node.id}>: ${err.message}`)
180
183
  })
@@ -211,8 +214,9 @@ export class NodeGraph {
211
214
  this.cli.log("info", `observe stream of node <${node.id}> for finish event`)
212
215
  this.activeNodes.add(node)
213
216
  const deactivateNode = (node: SpeechFlowNode, msg: string) => {
214
- if (this.activeNodes.has(node))
215
- this.activeNodes.delete(node)
217
+ if (!this.activeNodes.has(node))
218
+ return
219
+ this.activeNodes.delete(node)
216
220
  this.cli.log("info", `${msg} (${this.activeNodes.size} active nodes remaining)`)
217
221
  if (this.activeNodes.size === 0) {
218
222
  const timeFinished = DateTime.now()
@@ -220,7 +224,9 @@ export class NodeGraph {
220
224
  this.cli.log("info", "**** everything finished -- stream processing in SpeechFlow graph stops " +
221
225
  `(total duration: ${duration?.toFormat("hh:mm:ss.SSS") ?? "unknown"}) ****`)
222
226
  this.finishEvents.emit("finished")
223
- this.shutdown("finished", args, api)
227
+ this.shutdown("finished", args, api).catch((err: Error) => {
228
+ this.cli.log("error", `failed to shutdown: ${err.message}`)
229
+ })
224
230
  }
225
231
  }
226
232
  node.stream.on("error", (err: unknown) => {
@@ -228,12 +234,26 @@ export class NodeGraph {
228
234
  this.cli.log("warning", `stream of node <${node.id}> raised "error" event: ${error.message}`)
229
235
  api.sendErrorToDashboard(Date.now(), node.id, "warning", error.message)
230
236
  })
231
- node.stream.on("finish", () => {
232
- deactivateNode(node, `writable stream side (input) of node <${node.id}> raised "finish" event`)
233
- })
234
- node.stream.on("end", () => {
235
- deactivateNode(node, `readable stream side (output) of node <${node.id}> raised "end" event`)
236
- })
237
+
238
+ /* listen for the semantically correct completion event per stream type:
239
+ - for Duplex/Transform listen only for "end"
240
+ (readable-side, fires last, guarantees all output is drained),
241
+ - for pure Readable listen only for "end"
242
+ - for pure Writable listen only for "finish" */
243
+ if (node.stream instanceof Stream.Duplex)
244
+ node.stream.on("end", () => {
245
+ deactivateNode(node, `readable stream side (output) of node <${node.id}> raised "end" event`)
246
+ })
247
+ else if (node.stream instanceof Stream.Readable)
248
+ node.stream.on("end", () => {
249
+ deactivateNode(node, `readable stream side (output) of node <${node.id}> raised "end" event`)
250
+ })
251
+ else if (node.stream instanceof Stream.Writable)
252
+ node.stream.on("finish", () => {
253
+ deactivateNode(node, `writable stream side (input) of node <${node.id}> raised "finish" event`)
254
+ })
255
+ else
256
+ throw new Error(`stream of node <${node.id}> is neither of Duplex, Writable, nor Readable type`)
237
257
  }
238
258
 
239
259
  /* start of internal stream processing */
@@ -250,13 +270,16 @@ export class NodeGraph {
250
270
  const stream = node.stream
251
271
  if ((stream instanceof Stream.Writable || stream instanceof Stream.Duplex)
252
272
  && (!stream.writableEnded && !stream.destroyed)) {
273
+ const ac = new AbortController()
253
274
  drainPromises.push(
254
275
  Promise.race([
255
276
  new Promise<void>((resolve) => {
256
277
  stream.end(() => { resolve() })
257
278
  }),
258
- util.timeout(5000)
259
- ]).catch(() => {
279
+ util.timeout(5000, "timeout", ac.signal)
280
+ ]).finally(() => {
281
+ ac.abort()
282
+ }).catch(() => {
260
283
  /* ignore timeout -- stream will be destroyed later */
261
284
  })
262
285
  )
@@ -297,10 +320,13 @@ export class NodeGraph {
297
320
  async closeNodes (): Promise<void> {
298
321
  for (const node of this.graphNodes) {
299
322
  this.cli.log("info", `close node <${node.id}>`)
323
+ const ac = new AbortController()
300
324
  await Promise.race([
301
325
  node.close(),
302
- util.timeout(10 * 1000)
303
- ]).catch((err: Error) => {
326
+ util.timeout(10 * 1000, "timeout", ac.signal)
327
+ ]).finally(() => {
328
+ ac.abort()
329
+ }).catch((err: Error) => {
304
330
  this.cli.log("warning", `node <${node.id}> failed to close: ${err.message}`)
305
331
  })
306
332
  }
@@ -319,25 +345,33 @@ export class NodeGraph {
319
345
 
320
346
  /* graph destruction: PASS 5: destroy nodes */
321
347
  destroyNodes (): void {
322
- for (const node of this.graphNodes) {
348
+ for (const node of this.graphNodes)
323
349
  this.cli.log("info", `destroy node <${node.id}>`)
324
- this.graphNodes.delete(node)
325
- }
350
+ this.graphNodes.clear()
326
351
  }
327
352
 
328
353
  /* setup signal handling for shutdown */
329
354
  setupSignalHandlers (args: CLIOptions, api: APIServer): void {
330
355
  /* internal helper functions */
331
- const shutdownHandler = (signal: string) =>
332
- this.shutdown(signal, args, api)
333
356
  const logError = (error: Error) => {
334
357
  if (this.debug)
335
358
  this.cli.log("error", `${error.message}\n${error.stack}`)
336
359
  else
337
360
  this.cli.log("error", error.message)
338
361
  }
362
+ const shutdownHandler = (signal: string) => {
363
+ this.shutdown(signal, args, api).catch((err: unknown) => {
364
+ const error = util.ensureError(err, "shutdown error")
365
+ logError(error)
366
+ process.exit(1)
367
+ })
368
+ }
339
369
 
340
- /* hook into process signals */
370
+ /* re-hook into process signals */
371
+ process.removeAllListeners("SIGINT")
372
+ process.removeAllListeners("SIGUSR1")
373
+ process.removeAllListeners("SIGUSR2")
374
+ process.removeAllListeners("SIGTERM")
341
375
  process.on("SIGINT", () => { shutdownHandler("SIGINT") })
342
376
  process.on("SIGUSR1", () => { shutdownHandler("SIGUSR1") })
343
377
  process.on("SIGUSR2", () => { shutdownHandler("SIGUSR2") })
@@ -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
 
@@ -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
 
@@ -47,10 +47,13 @@ export class NodeStatusManager {
47
47
  this.cli.log("info", `gathering status of node <${name}>`)
48
48
  const node = new nodes[name](name, cfg, {}, [])
49
49
  node._accessBus = accessBus
50
+ const ac = new AbortController()
50
51
  const status = await Promise.race<{ [ key: string ]: string | number }>([
51
52
  node.status(),
52
- util.timeout(10 * 1000)
53
- ]).catch((err: Error) => {
53
+ util.timeout(10 * 1000, "timeout", ac.signal)
54
+ ]).finally(() => {
55
+ ac.abort()
56
+ }).catch((err: Error) => {
54
57
  this.cli.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
55
58
  return {} as { [ key: string ]: string | number }
56
59
  })
@@ -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
 
@@ -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
 
@@ -21,8 +21,7 @@ class CompressorProcessor extends AudioWorkletProcessor {
21
21
  { name: "ratio", defaultValue: 4.0, minValue: 1.0, maxValue: 20, automationRate: "k-rate" }, // compression ratio
22
22
  { name: "attack", defaultValue: 0.010, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
23
23
  { name: "release", defaultValue: 0.050, minValue: 0.0, maxValue: 1, automationRate: "k-rate" }, // seconds
24
- { name: "knee", defaultValue: 6.0, minValue: 0.0, maxValue: 40, automationRate: "k-rate" }, // dB
25
- { name: "makeup", defaultValue: 0.0, minValue: -24, maxValue: 24, automationRate: "k-rate" } // dB
24
+ { name: "knee", defaultValue: 6.0, minValue: 0.0, maxValue: 40, automationRate: "k-rate" } // dB
26
25
  ]
27
26
  }
28
27
 
@@ -94,21 +93,18 @@ class CompressorProcessor extends AudioWorkletProcessor {
94
93
  const kneeDB = parameters["knee"][0]
95
94
  const attackS = Math.max(parameters["attack"][0], 1 / this.sampleRate)
96
95
  const releaseS = Math.max(parameters["release"][0], 1 / this.sampleRate)
97
- const makeupDB = parameters["makeup"][0]
98
96
 
99
- /* update envelope per channel */
97
+ /* update envelope per channel and collect RMS values */
98
+ const rms = Array.from<number>({ length: nCh })
100
99
  for (let ch = 0; ch < nCh; ch++)
101
- this.env[ch] = util.updateEnvelopeForChannel(this.env, this.sampleRate, ch, input[ch], attackS, releaseS)
102
-
103
- /* determine linear value from decibel makeup value */
104
- const makeUpLin = util.dB2lin(makeupDB)
100
+ rms[ch] = util.updateEnvelopeForChannel(this.env, this.sampleRate, ch, input[ch], attackS, releaseS)
105
101
 
106
102
  /* iterate over all channels */
107
103
  this.reduction = 0
108
104
  for (let ch = 0; ch < nCh; ch++) {
109
- const levelDB = util.lin2dB(this.env[ch])
105
+ const levelDB = util.lin2dB(rms[ch])
110
106
  const gainDB = this.gainDBFor(levelDB, thresholdDB, ratio, kneeDB)
111
- const gainLin = util.dB2lin(gainDB) * makeUpLin
107
+ const gainLin = util.dB2lin(gainDB)
112
108
 
113
109
  /* on first channel, calculate reduction */
114
110
  if (ch === 0)
@@ -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
 
@@ -112,7 +112,6 @@ class AudioCompressor extends util.WebAudio {
112
112
  params.get("attack")!.setValueAtTime(this.config.attackMs / 1000, currentTime)
113
113
  params.get("release")!.setValueAtTime(this.config.releaseMs / 1000, currentTime)
114
114
  params.get("knee")!.setValueAtTime(this.config.kneeDb, currentTime)
115
- params.get("makeup")!.setValueAtTime(this.config.makeupDb, currentTime)
116
115
  }
117
116
 
118
117
  /* configure gain node */
@@ -244,7 +243,7 @@ export default class SpeechFlowNodeA2ACompressor extends SpeechFlowNode {
244
243
  callback(new Error("compressor not initialized"))
245
244
  else {
246
245
  /* compress chunk */
247
- const payload = util.convertBufToI16(chunk.payload)
246
+ const payload = util.convertBufToI16(chunk.payload, self.config.audioLittleEndian)
248
247
  self.compressor.process(payload).then((result) => {
249
248
  if (self.closing) {
250
249
  callback(new Error("stream already destroyed"))
@@ -253,10 +252,13 @@ export default class SpeechFlowNodeA2ACompressor extends SpeechFlowNode {
253
252
  if ((self.params.type === "standalone" && self.params.mode === "compress")
254
253
  || (self.params.type === "sidechain" && self.params.mode === "adjust")) {
255
254
  /* take over compressed data */
256
- const payload = util.convertI16ToBuf(result)
257
- chunk.payload = payload
255
+ const payload = util.convertI16ToBuf(result, self.config.audioLittleEndian)
256
+ const chunkNew = chunk.clone()
257
+ chunkNew.payload = payload
258
+ this.push(chunkNew)
258
259
  }
259
- this.push(chunk)
260
+ else
261
+ this.push(chunk)
260
262
  callback()
261
263
  }).catch((error: unknown) => {
262
264
  if (self.closing)
@@ -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
 
@@ -56,7 +56,7 @@ class ExpanderProcessor extends AudioWorkletProcessor {
56
56
  }
57
57
 
58
58
  /* determine target level */
59
- const targetOut = thresholdDB + (levelDB - thresholdDB) / ratio
59
+ const targetOut = thresholdDB + (levelDB - thresholdDB) * ratio
60
60
 
61
61
  /* return gain difference */
62
62
  return targetOut - levelDB
@@ -97,16 +97,17 @@ class ExpanderProcessor extends AudioWorkletProcessor {
97
97
  const releaseS = Math.max(parameters["release"][0], 1 / this.sampleRate)
98
98
  const makeupDB = parameters["makeup"][0]
99
99
 
100
- /* update envelope per channel */
100
+ /* update envelope per channel and collect RMS values */
101
+ const rms = Array.from<number>({ length: nCh })
101
102
  for (let ch = 0; ch < nCh; ch++)
102
- this.env[ch] = util.updateEnvelopeForChannel(this.env, this.sampleRate, ch, input[ch], attackS, releaseS)
103
+ rms[ch] = util.updateEnvelopeForChannel(this.env, this.sampleRate, ch, input[ch], attackS, releaseS)
103
104
 
104
105
  /* determine linear value from decibel makeup value */
105
106
  const makeUpLin = util.dB2lin(makeupDB)
106
107
 
107
108
  /* iterate over all channels */
108
109
  for (let ch = 0; ch < nCh; ch++) {
109
- const levelDB = util.lin2dB(this.env[ch])
110
+ const levelDB = util.lin2dB(rms[ch])
110
111
  const gainDB = this.gainDBFor(levelDB, thresholdDB, ratio, kneeDB)
111
112
  let gainLin = util.dB2lin(gainDB) * makeUpLin
112
113
 
@@ -117,6 +118,10 @@ class ExpanderProcessor extends AudioWorkletProcessor {
117
118
  gainLin *= util.dB2lin(neededLiftDB)
118
119
  }
119
120
 
121
+ /* guard against IEEE 754 edge case (0 * Infinity = NaN on silence) */
122
+ if (!Number.isFinite(gainLin))
123
+ gainLin = 0
124
+
120
125
  /* apply gain change to channel */
121
126
  const inp = input[ch]
122
127
  const out = output[ch]
@@ -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
 
@@ -168,7 +168,7 @@ export default class SpeechFlowNodeA2AExpander extends SpeechFlowNode {
168
168
  callback(new Error("expander not initialized"))
169
169
  else {
170
170
  /* expand chunk */
171
- const payload = util.convertBufToI16(chunk.payload)
171
+ const payload = util.convertBufToI16(chunk.payload, self.config.audioLittleEndian)
172
172
  self.expander.process(payload).then((result) => {
173
173
  if (self.closing) {
174
174
  callback(new Error("stream already destroyed"))
@@ -176,9 +176,10 @@ export default class SpeechFlowNodeA2AExpander extends SpeechFlowNode {
176
176
  }
177
177
 
178
178
  /* take over expanded data */
179
- const payload = util.convertI16ToBuf(result)
180
- chunk.payload = payload
181
- this.push(chunk)
179
+ const payload = util.convertI16ToBuf(result, self.config.audioLittleEndian)
180
+ const chunkNew = chunk.clone()
181
+ chunkNew.payload = payload
182
+ this.push(chunkNew)
182
183
  callback()
183
184
  }).catch((error: unknown) => {
184
185
  if (self.closing)
@@ -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
 
@@ -100,7 +100,8 @@ export default class SpeechFlowNodeA2AFFMPEG extends SpeechFlowNode {
100
100
 
101
101
  /* wrap streams with conversions for chunk vs plain audio */
102
102
  const wrapper1 = util.createTransformStreamForWritableSide("audio", 1)
103
- const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
103
+ const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero, undefined,
104
+ this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
104
105
  this.stream = Stream.compose(wrapper1, ffmpegStream, wrapper2)
105
106
  }
106
107
 
@@ -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
 
@@ -221,10 +221,8 @@ export default class SpeechFlowNodeA2AFiller extends SpeechFlowNode {
221
221
  self.log("info", "received EOF signal")
222
222
  this.push(null)
223
223
  }
224
- else if (!(chunk.payload instanceof Buffer)) {
224
+ else if (!Buffer.isBuffer(chunk.payload))
225
225
  self.log("warning", "invalid chunk (expected audio buffer)")
226
- this.push(null)
227
- }
228
226
  else {
229
227
  self.log("debug", `received data (${chunk.payload.byteLength} bytes)`)
230
228
  this.push(chunk)
@@ -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
 
@@ -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
 
@@ -105,10 +105,13 @@ export default class SpeechFlowNodeA2AGender extends SpeechFlowNode {
105
105
  device: "auto",
106
106
  progress_callback: progressCallback
107
107
  })
108
+ const ac = new AbortController()
108
109
  this.classifier = await Promise.race([
109
110
  pipelinePromise,
110
- util.timeout(30 * 1000, "model initialization timeout")
111
- ]) as Transformers.AudioClassificationPipeline
111
+ util.timeout(30 * 1000, "model initialization timeout", ac.signal)
112
+ ]).finally(() => {
113
+ ac.abort()
114
+ }) as Transformers.AudioClassificationPipeline
112
115
  }
113
116
  catch (error) {
114
117
  if (this.progressInterval) {
@@ -147,10 +150,13 @@ export default class SpeechFlowNodeA2AGender extends SpeechFlowNode {
147
150
  return genderLast
148
151
 
149
152
  /* classify audio */
153
+ const ac = new AbortController()
150
154
  const result = await Promise.race([
151
155
  this.classifier(data),
152
- util.timeout(30 * 1000, "classification timeout")
153
- ]) as Transformers.AudioClassificationOutput | Transformers.AudioClassificationOutput[]
156
+ util.timeout(30 * 1000, "classification timeout", ac.signal)
157
+ ]).finally(() => {
158
+ ac.abort()
159
+ }) as Transformers.AudioClassificationOutput | Transformers.AudioClassificationOutput[]
154
160
  const classified = Array.isArray(result) ?
155
161
  result as Transformers.AudioClassificationOutput :
156
162
  [ result ]
@@ -199,7 +205,7 @@ export default class SpeechFlowNodeA2AGender extends SpeechFlowNode {
199
205
  const element = this.queueAC.peek(pos)
200
206
  if (element === undefined || element.type !== "audio-frame")
201
207
  break
202
- if ((samples + element.data.length) < frameWindowSamples) {
208
+ if ((samples + element.data.length) <= frameWindowSamples) {
203
209
  data.set(element.data, samples)
204
210
  samples += element.data.length
205
211
  }
@@ -224,12 +230,13 @@ export default class SpeechFlowNodeA2AGender extends SpeechFlowNode {
224
230
  catch (error) {
225
231
  this.log("error", `gender classification error: ${error}`)
226
232
  }
227
-
228
- /* re-initiate working off round */
229
- workingOff = false
230
- if (!this.closing) {
231
- this.workingOffTimer = setTimeout(workOffQueue, 100)
232
- this.queue.once("write", workOffQueue)
233
+ finally {
234
+ /* re-initiate working off round */
235
+ workingOff = false
236
+ if (!this.closing) {
237
+ this.workingOffTimer = setTimeout(workOffQueue, 100)
238
+ this.queue.once("write", workOffQueue)
239
+ }
233
240
  }
234
241
  }
235
242
  this.queue.once("write", workOffQueue)
@@ -313,7 +320,7 @@ export default class SpeechFlowNodeA2AGender extends SpeechFlowNode {
313
320
  else if (element.type === "audio-frame"
314
321
  && element.gender === undefined)
315
322
  break
316
- const duration = util.audioArrayDuration(element.data)
323
+ const duration = util.audioArrayDuration(element.data, sampleRateTarget)
317
324
  const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
318
325
  const times = `start: ${fmtTime(element.chunk.timestampStart)}, ` +
319
326
  `end: ${fmtTime(element.chunk.timestampEnd)}`
@@ -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
 
@@ -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
 
@@ -114,17 +114,27 @@ export default class SpeechFlowNodeA2AGTCRN extends SpeechFlowNode {
114
114
  })
115
115
  })
116
116
 
117
- /* receive message from worker */
118
- const pending = new Map<string, (arr: Float32Array<ArrayBuffer>) => void>()
117
+ /* track pending promises */
118
+ const pending = new Map<string, {
119
+ resolve: (arr: Float32Array<ArrayBuffer>) => void,
120
+ reject: (err: Error) => void
121
+ }>()
122
+
123
+ /* reject all pending promises on worker exit */
119
124
  this.worker.on("exit", () => {
125
+ const err = new Error("worker terminated")
126
+ for (const cb of pending.values())
127
+ cb.reject(err)
120
128
  pending.clear()
121
129
  })
130
+
131
+ /* receive message from worker */
122
132
  this.worker.on("message", (msg: any) => {
123
133
  if (typeof msg === "object" && msg !== null && msg.type === "process-done") {
124
134
  const cb = pending.get(msg.id)
125
135
  pending.delete(msg.id)
126
136
  if (cb)
127
- cb(msg.data)
137
+ cb.resolve(msg.data)
128
138
  else
129
139
  this.log("warning", `GTCRN worker thread sent back unexpected id: ${msg.id}`)
130
140
  }
@@ -140,8 +150,8 @@ export default class SpeechFlowNodeA2AGTCRN extends SpeechFlowNode {
140
150
  if (this.closing)
141
151
  return samples
142
152
  const id = `${seq++}`
143
- return new Promise<Float32Array<ArrayBuffer>>((resolve) => {
144
- pending.set(id, (result) => { resolve(result) })
153
+ return new Promise<Float32Array<ArrayBuffer>>((resolve, reject) => {
154
+ pending.set(id, { resolve, reject })
145
155
  this.worker!.postMessage({ type: "process", id, samples }, [ samples.buffer ])
146
156
  })
147
157
  }
@@ -161,24 +171,37 @@ export default class SpeechFlowNodeA2AGTCRN extends SpeechFlowNode {
161
171
  callback(new Error("invalid chunk payload type"))
162
172
  else {
163
173
  /* resample Buffer from 48KHz (SpeechFlow) to 16KHz (GTCRN) */
164
- const resampledDown = self.resamplerDown!.processChunk(chunk.payload)
174
+ if (self.resamplerDown === null) {
175
+ callback(new Error("resamplerDown already destroyed"))
176
+ return
177
+ }
178
+ const resampledDown = self.resamplerDown.processChunk(chunk.payload)
165
179
 
166
180
  /* convert Buffer into Float32Array */
167
181
  const payload = util.convertBufToF32(resampledDown)
168
182
 
169
183
  /* process with GTCRN */
170
184
  workerProcess(payload).then((result: Float32Array<ArrayBuffer>) => {
185
+ /* short-circuit if already closing */
186
+ if (self.closing) {
187
+ callback()
188
+ return
189
+ }
190
+
171
191
  /* convert Float32Array into Buffer */
172
192
  const buf = util.convertF32ToBuf(result)
173
193
 
174
194
  /* resample Buffer from 16KHz (GTCRN) back to 48KHz (SpeechFlow) */
175
- const resampledUp = self.resamplerUp!.processChunk(buf)
176
-
177
- /* update chunk */
178
- chunk.payload = resampledUp
179
-
180
- /* forward updated chunk */
181
- this.push(chunk)
195
+ if (self.resamplerUp === null) {
196
+ callback(new Error("resamplerUp already destroyed"))
197
+ return
198
+ }
199
+ const resampledUp = self.resamplerUp.processChunk(buf)
200
+
201
+ /* forward cloned chunk with updated payload */
202
+ const chunkNew = chunk.clone()
203
+ chunkNew.payload = resampledUp
204
+ this.push(chunkNew)
182
205
  callback()
183
206
  }).catch((err: unknown) => {
184
207
  const error = util.ensureError(err)
@@ -211,9 +234,13 @@ export default class SpeechFlowNodeA2AGTCRN extends SpeechFlowNode {
211
234
  }
212
235
 
213
236
  /* destroy resamplers */
214
- if (this.resamplerDown !== null)
237
+ if (this.resamplerDown !== null) {
238
+ this.resamplerDown.destroy()
215
239
  this.resamplerDown = null
216
- if (this.resamplerUp !== null)
240
+ }
241
+ if (this.resamplerUp !== null) {
242
+ this.resamplerUp.destroy()
217
243
  this.resamplerUp = null
244
+ }
218
245
  }
219
246
  }
@@ -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
 
@@ -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
 
@@ -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
 
@@ -185,8 +185,9 @@ export default class SpeechFlowNodeA2APitch extends SpeechFlowNode {
185
185
 
186
186
  /* take over pitch-shifted data */
187
187
  const payload = util.convertI16ToBuf(result, self.config.audioLittleEndian)
188
- chunk.payload = payload
189
- this.push(chunk)
188
+ const chunkNew = chunk.clone()
189
+ chunkNew.payload = payload
190
+ this.push(chunkNew)
190
191
  callback()
191
192
  }).catch((error: unknown) => {
192
193
  if (self.closing)