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
 
@@ -21,8 +21,9 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
21
21
  public static name = "xio-websocket"
22
22
 
23
23
  /* internal state */
24
- private server: ws.WebSocketServer | null = null
25
- private client: ReconnWebSocket | null = null
24
+ private server: ws.WebSocketServer | null = null
25
+ private client: ReconnWebSocket | null = null
26
+ private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
26
27
 
27
28
  /* construct node */
28
29
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -63,7 +64,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
63
64
  /* listen locally on a Websocket port */
64
65
  const url = new URL(this.params.listen)
65
66
  const websockets = new Set<ws.WebSocket>()
66
- const chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
67
+ this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
67
68
  this.server = new ws.WebSocketServer({
68
69
  host: url.hostname,
69
70
  port: Number.parseInt(url.port, 10),
@@ -102,7 +103,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
102
103
  else
103
104
  buffer = Buffer.concat(data)
104
105
  const chunk = util.streamChunkDecode(buffer)
105
- chunkQueue.write(chunk)
106
+ this.chunkQueue?.write(chunk)
106
107
  })
107
108
  })
108
109
  this.server.on("error", (error) => {
@@ -124,9 +125,11 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
124
125
  callback(new Error("still no WebSocket connections available"))
125
126
  else {
126
127
  const data = util.streamChunkEncode(chunk)
127
- const results: Promise<void>[] = []
128
+ const sends: Promise<void>[] = []
129
+ const clients: ws.WebSocket[] = []
128
130
  for (const websocket of websockets.values()) {
129
- results.push(new Promise<void>((resolve, reject) => {
131
+ clients.push(websocket)
132
+ sends.push(new Promise<void>((resolve, reject) => {
130
133
  websocket.send(data, (error) => {
131
134
  if (error)
132
135
  reject(error)
@@ -135,10 +138,27 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
135
138
  })
136
139
  }))
137
140
  }
138
- Promise.all(results).then(() => {
139
- callback()
140
- }).catch((error: unknown) => {
141
- callback(util.ensureError(error))
141
+ Promise.allSettled(sends).then((results) => {
142
+ let lastError: Error | null = null
143
+ for (let i = 0; i < results.length; i++) {
144
+ if (results[i].status === "rejected") {
145
+ const error = util.ensureError((results[i] as PromiseRejectedResult).reason)
146
+ self.log("warning", `failed to send to WebSocket client: ${error.message}`)
147
+ websockets.delete(clients[i])
148
+ clients[i].terminate()
149
+ lastError = error
150
+ }
151
+ }
152
+ const failures = results.filter((r) => r.status === "rejected").length
153
+ if (failures > 0 && failures < results.length)
154
+ self.log("warning", "partial broadcast failure: " +
155
+ `${failures} of ${results.length} WebSocket clients failed`)
156
+ if (lastError !== null && failures === results.length)
157
+ callback(lastError)
158
+ else
159
+ callback()
160
+ }).catch((err: Error) => {
161
+ callback(err)
142
162
  })
143
163
  }
144
164
  },
@@ -147,12 +167,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
147
167
  callback()
148
168
  },
149
169
  read (size: number) {
150
- if (self.params.mode === "w")
151
- throw new Error("read operation on write-only node")
152
- reads.add(chunkQueue.read().then((chunk) => {
170
+ if (self.params.mode === "w") {
171
+ self.log("error", "read operation on write-only node")
172
+ this.push(null)
173
+ return
174
+ }
175
+ if (self.chunkQueue === null)
176
+ return
177
+ const queue = self.chunkQueue
178
+ reads.add(queue.read().then((chunk) => {
153
179
  this.push(chunk, "binary")
154
180
  }).catch((err: Error) => {
155
181
  self.log("warning", `read on chunk queue operation failed: ${err}`)
182
+ if (queue.destroyed)
183
+ this.push(null)
156
184
  }))
157
185
  }
158
186
  })
@@ -178,7 +206,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
178
206
  const error = util.ensureError(ev.error)
179
207
  this.log("error", `error of connection on URL ${this.params.connect}: ${error.message}`)
180
208
  })
181
- const chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
209
+ this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
182
210
  this.client.addEventListener("message", (ev: MessageEvent) => {
183
211
  if (this.params.mode === "w") {
184
212
  this.log("warning", `connection to URL ${this.params.connect}: ` +
@@ -192,7 +220,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
192
220
  }
193
221
  const buffer = Buffer.from(ev.data)
194
222
  const chunk = util.streamChunkDecode(buffer)
195
- chunkQueue.write(chunk)
223
+ this.chunkQueue?.write(chunk)
196
224
  })
197
225
  this.client.binaryType = "arraybuffer"
198
226
  const self = this
@@ -207,7 +235,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
207
235
  callback(new Error("write operation on read-only node"))
208
236
  else if (chunk.type !== self.params.type)
209
237
  callback(new Error(`written chunk is not of ${self.params.type} type`))
210
- else if (!self.client!.OPEN)
238
+ else if (self.client!.readyState !== ReconnWebSocket.OPEN)
211
239
  callback(new Error("still no WebSocket connection available"))
212
240
  else {
213
241
  const data = util.streamChunkEncode(chunk)
@@ -220,12 +248,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
220
248
  callback()
221
249
  },
222
250
  read (size: number) {
223
- if (self.params.mode === "w")
224
- throw new Error("read operation on write-only node")
225
- reads.add(chunkQueue.read().then((chunk) => {
251
+ if (self.params.mode === "w") {
252
+ self.log("error", "read operation on write-only node")
253
+ this.push(null)
254
+ return
255
+ }
256
+ if (self.chunkQueue === null)
257
+ return
258
+ const queue = self.chunkQueue
259
+ reads.add(queue.read().then((chunk) => {
226
260
  this.push(chunk, "binary")
227
261
  }).catch((err: Error) => {
228
262
  self.log("warning", `read on chunk queue operation failed: ${err}`)
263
+ if (queue.destroyed)
264
+ this.push(null)
229
265
  }))
230
266
  }
231
267
  })
@@ -234,8 +270,19 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
234
270
 
235
271
  /* close node */
236
272
  async close () {
273
+ /* drain and clear chunk queue reference */
274
+ if (this.chunkQueue !== null) {
275
+ this.chunkQueue.destroy()
276
+ this.chunkQueue = null
277
+ }
278
+
237
279
  /* close WebSocket server */
238
280
  if (this.server !== null) {
281
+ /* forcibly terminate all active client connections */
282
+ for (const client of this.server.clients)
283
+ client.terminate()
284
+
285
+ /* close connection */
239
286
  await new Promise<void>((resolve, reject) => {
240
287
  this.server!.close((error) => {
241
288
  if (error) reject(error)
@@ -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
 
@@ -9,7 +9,10 @@ import Events, { EventEmitter } from "node:events"
9
9
  import Stream from "node:stream"
10
10
 
11
11
  /* external dependencies */
12
- import { DateTime, Duration } from "luxon"
12
+ import { DateTime, Duration } from "luxon"
13
+
14
+ /* internal dependencies */
15
+ import { deepClone } from "./speechflow-util-misc"
13
16
 
14
17
  /* the definition of a single payload chunk passed through the SpeechFlow nodes */
15
18
  export class SpeechFlowChunk {
@@ -33,7 +36,7 @@ export class SpeechFlowChunk {
33
36
  this.kind,
34
37
  this.type,
35
38
  payload,
36
- new Map(this.meta)
39
+ deepClone(this.meta)
37
40
  )
38
41
  }
39
42
  }
@@ -140,16 +143,12 @@ export default class SpeechFlowNode extends Events.EventEmitter {
140
143
  if (typeof this.args[spec[name].pos] !== spec[name].type)
141
144
  throw new Error(`invalid type of positional parameter "${name}" ` +
142
145
  `(has to be ${spec[name].type})`)
143
- if ("match" in spec[name]
144
- && this.args[spec[name].pos].match(spec[name].match) === null)
145
- throw new Error(`invalid value of positional parameter "${name}" ` +
146
- `(has to match ${spec[name].match})`)
147
146
  if ("match" in spec[name]
148
147
  && ( ( spec[name].match instanceof RegExp
149
148
  && this.args[spec[name].pos].match(spec[name].match) === null)
150
149
  || ( typeof spec[name].match === "function"
151
150
  && !spec[name].match(this.args[spec[name].pos]) ) ))
152
- throw new Error(`invalid value "${this.opts[name]}" of positional parameter "${name}"`)
151
+ throw new Error(`invalid value "${this.args[spec[name].pos!]}" of positional parameter "${name}"`)
153
152
  this.params[name] = this.args[spec[name].pos]
154
153
  }
155
154
  else if ("val" in spec[name] && spec[name].val !== undefined)
@@ -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
 
@@ -15,7 +15,14 @@ interface StartCaptureMessage {
15
15
  chunkId: string
16
16
  expectedSamples: number
17
17
  }
18
- type WorkletMessage = InputChunkMessage | StartCaptureMessage
18
+ interface CancelCaptureMessage {
19
+ type: "cancel-capture"
20
+ chunkId: string
21
+ }
22
+ interface CancelAllCapturesMessage {
23
+ type: "cancel-all-captures"
24
+ }
25
+ type WorkletMessage = InputChunkMessage | StartCaptureMessage | CancelCaptureMessage | CancelAllCapturesMessage
19
26
  interface ChunkData {
20
27
  data: Float32Array
21
28
  chunkId: string
@@ -24,6 +31,10 @@ interface ChunkStartedMessage {
24
31
  type: "chunk-started"
25
32
  chunkId: string
26
33
  }
34
+ interface CaptureReadyMessage {
35
+ type: "capture-ready"
36
+ chunkId: string
37
+ }
27
38
  interface CaptureCompleteMessage {
28
39
  type: "capture-complete"
29
40
  chunkId: string
@@ -43,9 +54,12 @@ class AudioSourceProcessor extends AudioWorkletProcessor {
43
54
 
44
55
  /* receive input chunks */
45
56
  this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
46
- const { type, chunkId } = event.data
57
+ const { type } = event.data
47
58
  if (type === "input-chunk")
48
- this.pendingData.push({ data: event.data.data.pcmData, chunkId })
59
+ this.pendingData.push({
60
+ data: event.data.data.pcmData,
61
+ chunkId: event.data.chunkId
62
+ })
49
63
  })
50
64
  }
51
65
 
@@ -114,7 +128,8 @@ class AudioSourceProcessor extends AudioWorkletProcessor {
114
128
  /* audio capture node */
115
129
  class AudioCaptureProcessor extends AudioWorkletProcessor {
116
130
  /* internal state */
117
- private activeCaptures = new Map<string, { data: number[], expectedSamples: number }>()
131
+ private static readonly CAPTURE_TTL = 30 * 1000
132
+ private activeCaptures = new Map<string, { data: number[], expectedSamples: number, createdAt: number }>()
118
133
 
119
134
  /* node construction */
120
135
  constructor () {
@@ -122,13 +137,28 @@ class AudioCaptureProcessor extends AudioWorkletProcessor {
122
137
 
123
138
  /* receive start of capturing command */
124
139
  this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
125
- const { type, chunkId } = event.data
140
+ const { type } = event.data
126
141
  if (type === "start-capture") {
142
+ const chunkId = event.data.chunkId
127
143
  this.activeCaptures.set(chunkId, {
128
144
  data: [],
129
- expectedSamples: event.data.expectedSamples
145
+ expectedSamples: event.data.expectedSamples,
146
+ createdAt: Date.now()
130
147
  })
148
+
149
+ /* acknowledge capture registration */
150
+ const ready: CaptureReadyMessage = {
151
+ type: "capture-ready",
152
+ chunkId
153
+ }
154
+ this.port.postMessage(ready)
131
155
  }
156
+ else if (type === "cancel-capture") {
157
+ const chunkId = event.data.chunkId
158
+ this.activeCaptures.delete(chunkId)
159
+ }
160
+ else if (type === "cancel-all-captures")
161
+ this.activeCaptures.clear()
132
162
  })
133
163
  }
134
164
 
@@ -145,6 +175,15 @@ class AudioCaptureProcessor extends AudioWorkletProcessor {
145
175
  const frameCount = input[0].length
146
176
  const channelCount = input.length
147
177
 
178
+ /* evict stale captures (TTL safety net) */
179
+ const currentTime = Date.now()
180
+ for (const [ chunkId, capture ] of this.activeCaptures) {
181
+ if ((currentTime - capture.createdAt) > AudioCaptureProcessor.CAPTURE_TTL)
182
+ this.activeCaptures.delete(chunkId)
183
+ }
184
+ if (this.activeCaptures.size === 0)
185
+ return true
186
+
148
187
  /* iterate over all active captures */
149
188
  for (const [ chunkId, capture ] of this.activeCaptures) {
150
189
  /* convert planar to interleaved */
@@ -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
 
@@ -18,14 +18,11 @@ export function audioBufferDuration (
18
18
  buffer: Buffer,
19
19
  sampleRate = 48000,
20
20
  bitDepth = 16,
21
- channels = 1,
22
- littleEndian = true
21
+ channels = 1
23
22
  ) {
24
23
  /* sanity check parameters */
25
24
  if (!Buffer.isBuffer(buffer))
26
25
  throw new Error("invalid input (Buffer expected)")
27
- if (littleEndian !== true)
28
- throw new Error("only Little Endian supported")
29
26
  if (sampleRate <= 0)
30
27
  throw new Error("sample rate must be positive")
31
28
  if (bitDepth <= 0 || bitDepth % 8 !== 0)
@@ -234,7 +231,7 @@ export class WebAudio {
234
231
  this.pendingPromises.delete(chunkId)
235
232
  const int16Data = new Int16Array(data.length)
236
233
  for (let i = 0; i < data.length; i++)
237
- int16Data[i] = Math.max(-32768, Math.min(32767, Math.round(data[i] * 32767)))
234
+ int16Data[i] = Math.max(-32768, Math.min(32767, Math.round(data[i] * 32768)))
238
235
  promise.resolve(int16Data)
239
236
  }
240
237
  }
@@ -248,10 +245,14 @@ export class WebAudio {
248
245
 
249
246
  /* process single audio chunk */
250
247
  public async process (int16Array: Int16Array): Promise<Int16Array> {
248
+ if (this.sourceNode === null || this.captureNode === null)
249
+ throw new Error("WebAudio not ready (not yet setup or already destroyed)")
251
250
  const chunkId = `chunk_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
252
251
  return new Promise<Int16Array>((resolve, reject) => {
253
252
  const timeout = setTimeout(() => {
254
253
  this.pendingPromises.delete(chunkId)
254
+ if (this.captureNode !== null)
255
+ this.captureNode.port.postMessage({ type: "cancel-capture", chunkId })
255
256
  reject(new Error("processing timeout"))
256
257
  }, (int16Array.length / this.channels / this.audioContext.sampleRate) * 1000 + 250)
257
258
  if (this.captureNode !== null)
@@ -270,15 +271,22 @@ export class WebAudio {
270
271
  })
271
272
  }
272
273
 
273
- /* small delay to ensure capture is ready before sending data */
274
- setTimeout(() => {
275
- /* send input to source node */
276
- this.sourceNode?.port.postMessage({
277
- type: "input-chunk",
278
- chunkId,
279
- data: { pcmData: float32Data, channels: this.channels }
280
- }, [ float32Data.buffer ])
281
- }, 5)
274
+ /* wait for capture-ready acknowledgment before sending data */
275
+ const readyHandler = (event: MessageEvent) => {
276
+ const { type: msgType, chunkId: msgChunkId } = event.data ?? {}
277
+ if (msgType === "capture-ready" && msgChunkId === chunkId) {
278
+ this.captureNode?.port.removeEventListener("message", readyHandler)
279
+
280
+ /* send input to source node */
281
+ this.sourceNode?.port.postMessage({
282
+ type: "input-chunk",
283
+ chunkId,
284
+ data: { pcmData: float32Data, channels: this.channels }
285
+ }, [ float32Data.buffer ])
286
+ }
287
+ }
288
+ if (this.captureNode !== null)
289
+ this.captureNode.port.addEventListener("message", readyHandler)
282
290
  }
283
291
  catch (error) {
284
292
  clearTimeout(timeout)
@@ -291,6 +299,10 @@ export class WebAudio {
291
299
 
292
300
  /* destroy object */
293
301
  public async destroy (): Promise<void> {
302
+ /* cancel all worklet captures */
303
+ if (this.captureNode !== null)
304
+ this.captureNode.port.postMessage({ type: "cancel-all-captures" })
305
+
294
306
  /* reject all pending promises */
295
307
  shield(() => {
296
308
  this.pendingPromises.forEach(({ reject, timeout }) => {
@@ -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
 
@@ -193,7 +193,7 @@ export function runner<T> (
193
193
  export function shield<T extends (void | Promise<void>)> (op: () => T) {
194
194
  return run(
195
195
  "shielded operation",
196
- () => { op() },
197
- (_err) => { /* ignore error */ }
196
+ () => op(),
197
+ (_err) => undefined as T
198
198
  )
199
199
  }
@@ -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,23 +1,80 @@
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
 
7
+ /* external dependencies */
8
+ import { DateTime, Duration } from "luxon"
9
+
10
+ /* deep-clone a value while being aware of special class instances */
11
+ export const deepClone = (value: any): any => {
12
+ if (value === null || value === undefined || Number.isNaN(value))
13
+ return value
14
+ else if (typeof value !== "object")
15
+ return value
16
+ else if (Buffer.isBuffer(value))
17
+ return Buffer.from(value)
18
+ else if (value instanceof Uint8Array)
19
+ return new Uint8Array(value)
20
+ else if (value instanceof Duration)
21
+ return Duration.fromMillis(value.toMillis())
22
+ else if (value instanceof DateTime)
23
+ return DateTime.fromMillis(value.toMillis())
24
+ else if (Array.isArray(value))
25
+ return value.map((item) => deepClone(item))
26
+ else if (value instanceof Map) {
27
+ const result = new Map()
28
+ for (const [ k, v ] of value)
29
+ result.set(deepClone(k), deepClone(v))
30
+ return result
31
+ }
32
+ else if (value instanceof Set) {
33
+ const result = new Set()
34
+ for (const v of value)
35
+ result.add(deepClone(v))
36
+ return result
37
+ }
38
+ else if (Object.getPrototypeOf(value) === Object.prototype) {
39
+ const result: any = {}
40
+ for (const key of Object.keys(value))
41
+ result[key] = deepClone(value[key])
42
+ return result
43
+ }
44
+ else
45
+ return structuredClone(value)
46
+ }
47
+
7
48
  /* sleep: wait a duration of time and then resolve */
8
- export function sleep (durationMs: number) {
49
+ export function sleep (durationMs: number, signal?: AbortSignal) {
9
50
  return new Promise<void>((resolve) => {
10
- setTimeout(() => {
51
+ const ac = new AbortController()
52
+ const timer = setTimeout(() => {
53
+ ac.abort()
11
54
  resolve()
12
55
  }, durationMs)
56
+ timer.unref()
57
+ if (signal !== undefined)
58
+ signal.addEventListener("abort", () => {
59
+ clearTimeout(timer)
60
+ resolve()
61
+ }, { once: true, signal: ac.signal })
13
62
  })
14
63
  }
15
64
 
16
65
  /* timeout: wait a duration of time and then reject */
17
- export function timeout (durationMs: number, info = "timeout") {
18
- return new Promise<never>((_resolve, reject) => {
19
- setTimeout(() => {
66
+ export function timeout (durationMs: number, info = "timeout", signal?: AbortSignal) {
67
+ return new Promise<never>((resolve, reject) => {
68
+ const ac = new AbortController()
69
+ const timer = setTimeout(() => {
70
+ ac.abort()
20
71
  reject(new Error(info))
21
72
  }, durationMs)
73
+ timer.unref()
74
+ if (signal !== undefined)
75
+ signal.addEventListener("abort", () => {
76
+ clearTimeout(timer)
77
+ resolve(undefined as never)
78
+ }, { once: true, signal: ac.signal })
22
79
  })
23
80
  }