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
 
@@ -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)
@@ -261,7 +262,26 @@ export class WebAudio {
261
262
  for (let i = 0; i < int16Array.length; i++)
262
263
  float32Data[i] = int16Array[i] / 32768.0
263
264
 
264
- /* start capture first */
265
+ /* register capture-ready handler first (before posting start-capture,
266
+ to avoid a race where capture-ready arrives before the listener
267
+ is in place) */
268
+ const readyHandler = (event: MessageEvent) => {
269
+ const { type: msgType, chunkId: msgChunkId } = event.data ?? {}
270
+ if (msgType === "capture-ready" && msgChunkId === chunkId) {
271
+ this.captureNode?.port.removeEventListener("message", readyHandler)
272
+
273
+ /* send input to source node */
274
+ this.sourceNode?.port.postMessage({
275
+ type: "input-chunk",
276
+ chunkId,
277
+ data: { pcmData: float32Data, channels: this.channels }
278
+ }, [ float32Data.buffer ])
279
+ }
280
+ }
281
+ if (this.captureNode !== null)
282
+ this.captureNode.port.addEventListener("message", readyHandler)
283
+
284
+ /* start capture after handler is registered */
265
285
  if (this.captureNode !== null) {
266
286
  this.captureNode.port.postMessage({
267
287
  type: "start-capture",
@@ -269,16 +289,6 @@ export class WebAudio {
269
289
  expectedSamples: int16Array.length
270
290
  })
271
291
  }
272
-
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)
282
292
  }
283
293
  catch (error) {
284
294
  clearTimeout(timeout)
@@ -291,6 +301,10 @@ export class WebAudio {
291
301
 
292
302
  /* destroy object */
293
303
  public async destroy (): Promise<void> {
304
+ /* cancel all worklet captures */
305
+ if (this.captureNode !== null)
306
+ this.captureNode.port.postMessage({ type: "cancel-all-captures" })
307
+
294
308
  /* reject all pending promises */
295
309
  shield(() => {
296
310
  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,12 +1,15 @@
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
7
  /* standard dependencies */
8
8
  import EventEmitter from "node:events"
9
9
 
10
+ /* internal dependencies */
11
+ import * as util from "./speechflow-util-misc"
12
+
10
13
  /* external dependencies */
11
14
  import OpenAI from "openai"
12
15
  import Anthropic from "@anthropic-ai/sdk"
@@ -353,8 +356,16 @@ export class LLM extends EventEmitter {
353
356
  this.ollama?.abort()
354
357
  this.ollama = null
355
358
  }
356
- else if (this.config.provider === "transformers") {
357
- this.transformer?.dispose()
359
+ else if (this.config.provider === "transformers" && this.transformer !== null) {
360
+ const ac = new AbortController()
361
+ await Promise.race([
362
+ this.transformer.dispose(),
363
+ util.timeout(5000, "transformer dispose timeout", ac.signal)
364
+ ]).finally(() => {
365
+ ac.abort()
366
+ }).catch((error) => {
367
+ this.log("warning", `error during transformer cleanup: ${error}`)
368
+ })
358
369
  this.transformer = null
359
370
  }
360
371
  this.initialized = false
@@ -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
  }
@@ -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
 
@@ -26,70 +26,6 @@ export function importObject<T> (name: string, arg: object | string, validator:
26
26
  return result as T
27
27
  }
28
28
 
29
- /* helper class for single item queue */
30
- export class SingleQueue<T> extends EventEmitter {
31
- private queue = new Array<T>()
32
- write (item: T) {
33
- this.queue.unshift(item)
34
- this.emit("dequeue")
35
- }
36
- read () {
37
- return new Promise<T>((resolve) => {
38
- const tryToConsume = () => {
39
- const item = this.queue.pop()
40
- if (item !== undefined)
41
- resolve(item)
42
- else
43
- this.once("dequeue", tryToConsume)
44
- }
45
- tryToConsume()
46
- })
47
- }
48
- drain () {
49
- const items = this.queue
50
- this.queue = new Array<T>()
51
- return items
52
- }
53
- }
54
-
55
- /* helper class for double-item queue */
56
- export class DoubleQueue<T0, T1> extends EventEmitter {
57
- private queue0 = new Array<T0>()
58
- private queue1 = new Array<T1>()
59
- private notify () {
60
- if (this.queue0.length > 0 && this.queue1.length > 0)
61
- this.emit("dequeue")
62
- }
63
- write0 (item: T0) {
64
- this.queue0.unshift(item)
65
- this.notify()
66
- }
67
- write1 (item: T1) {
68
- this.queue1.unshift(item)
69
- this.notify()
70
- }
71
- read () {
72
- return new Promise<[ T0, T1 ]>((resolve) => {
73
- const consume = (): [ T0, T1 ] | undefined => {
74
- if (this.queue0.length > 0 && this.queue1.length > 0) {
75
- const item0 = this.queue0.pop() as T0
76
- const item1 = this.queue1.pop() as T1
77
- return [ item0, item1 ]
78
- }
79
- return undefined
80
- }
81
- const tryToConsume = () => {
82
- const items = consume()
83
- if (items !== undefined)
84
- resolve(items)
85
- else
86
- this.once("dequeue", tryToConsume)
87
- }
88
- tryToConsume()
89
- })
90
- }
91
- }
92
-
93
29
  /* queue element */
94
30
  export type QueueElement = { type: string }
95
31
 
@@ -112,6 +48,15 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
112
48
  silent (silence: boolean) {
113
49
  this.silence = silence
114
50
  }
51
+ silently<R> (fn: () => R): R {
52
+ this.silence = true
53
+ try {
54
+ return fn()
55
+ }
56
+ finally {
57
+ this.silence = false
58
+ }
59
+ }
115
60
 
116
61
  /* notify about operation */
117
62
  notify (event: string, info: any) {
@@ -126,7 +71,7 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
126
71
  position (index?: number): number {
127
72
  if (index !== undefined) {
128
73
  this.index = Math.max(0, Math.min(index, this.queue.elements.length))
129
- this.notify("position", this.index)
74
+ this.notify("position", { start: this.index })
130
75
  }
131
76
  return this.index
132
77
  }
@@ -139,35 +84,53 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
139
84
  if (this.index !== indexOld)
140
85
  this.notify("position", { start: this.index })
141
86
  }
142
- walkForwardUntil (type: T["type"]) {
87
+ walkForwardUntil (type: T["type"]): boolean {
143
88
  while (this.index < this.queue.elements.length
144
89
  && this.queue.elements[this.index].type !== type)
145
90
  this.index++
146
91
  this.notify("position", { start: this.index })
92
+ return this.index < this.queue.elements.length
93
+ && this.queue.elements[this.index].type === type
147
94
  }
148
- walkBackwardUntil (type: T["type"]) {
149
- while (this.index > 0
150
- && this.queue.elements[this.index].type !== type)
95
+ walkBackwardUntil (type: T["type"]): boolean {
96
+ if (this.index === this.queue.elements.length && this.index > 0)
97
+ this.index--
98
+ while (this.index < this.queue.elements.length
99
+ && this.queue.elements[this.index].type !== type) {
100
+ if (this.index === 0)
101
+ break
151
102
  this.index--
103
+ }
152
104
  this.notify("position", { start: this.index })
105
+ return this.index < this.queue.elements.length
106
+ && this.queue.elements[this.index].type === type
153
107
  }
154
108
 
155
109
  /* search operations */
156
- searchForward (type: T["type"]) {
110
+ searchForward (type: T["type"]): number {
157
111
  let position = this.index
158
112
  while (position < this.queue.elements.length
159
113
  && this.queue.elements[position].type !== type)
160
114
  position++
161
- this.notify("search", { start: this.index, end: position })
162
- return position
115
+ const found = position < this.queue.elements.length
116
+ && this.queue.elements[position].type === type
117
+ this.notify("search", { start: this.index, end: found ? position : this.index })
118
+ return found ? position : -1
163
119
  }
164
- searchBackward (type: T["type"]) {
120
+ searchBackward (type: T["type"]): number {
165
121
  let position = this.index
166
- while (position > 0
167
- && this.queue.elements[position].type !== type)
122
+ if (position === this.queue.elements.length && position > 0)
168
123
  position--
169
- this.notify("search", { start: position, end: this.index })
170
- return position
124
+ while (position < this.queue.elements.length
125
+ && this.queue.elements[position].type !== type) {
126
+ if (position === 0)
127
+ break
128
+ position--
129
+ }
130
+ const found = position < this.queue.elements.length
131
+ && this.queue.elements[position].type === type
132
+ this.notify("search", { start: found ? position : this.index, end: this.index })
133
+ return found ? position : -1
171
134
  }
172
135
 
173
136
  /* reading operations */
@@ -217,12 +180,14 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
217
180
  }
218
181
  insert (element: T) {
219
182
  this.queue.elements.splice(this.index, 0, element)
183
+ this.queue.adjustPointers(this, this.index, "insert")
220
184
  this.queue.notify("write", { start: this.index, end: this.index, op: "insert" })
221
185
  }
222
186
  delete () {
223
187
  if (this.index >= this.queue.elements.length)
224
188
  throw new Error("cannot delete after last element")
225
189
  this.queue.elements.splice(this.index, 1)
190
+ this.queue.adjustPointers(this, this.index, "delete")
226
191
  this.queue.notify("write", { start: this.index, end: this.index, op: "delete" })
227
192
  }
228
193
  }
@@ -239,6 +204,15 @@ export class Queue<T extends QueueElement> extends EventEmitter {
239
204
  silent (silence: boolean) {
240
205
  this.silence = silence
241
206
  }
207
+ silently<R> (fn: () => R): R {
208
+ this.silence = true
209
+ try {
210
+ return fn()
211
+ }
212
+ finally {
213
+ this.silence = false
214
+ }
215
+ }
242
216
  notify (event: string, info: any) {
243
217
  if (!this.silence)
244
218
  this.emit(event, info)
@@ -253,6 +227,24 @@ export class Queue<T extends QueueElement> extends EventEmitter {
253
227
  throw new Error("pointer does not exist")
254
228
  this.pointers.delete(name)
255
229
  }
230
+
231
+ /* adjust all sibling pointer positions (after insert/delete splice)
232
+ NOTICE: for insert, pointers AT the index are shifted forward to preserve
233
+ their pointer-to-element binding; for delete, pointers AT the index are
234
+ intentionally NOT adjusted, causing them to silently advance to the next
235
+ element (which shifted into the deleted position). */
236
+ adjustPointers (exclude: QueuePointer<T>, index: number, op: "insert" | "delete"): void {
237
+ for (const pointer of this.pointers.values()) {
238
+ if (pointer === exclude)
239
+ continue
240
+ const pos = pointer.position()
241
+ if (op === "insert" && pos >= index)
242
+ pointer.position(pos + 1)
243
+ else if (op === "delete" && pos > index)
244
+ pointer.position(pos - 1)
245
+ }
246
+ }
247
+
256
248
  trim (): void {
257
249
  /* determine minimum pointer position */
258
250
  let min = this.elements.length
@@ -269,9 +261,15 @@ export class Queue<T extends QueueElement> extends EventEmitter {
269
261
  for (const pointer of this.pointers.values())
270
262
  pointer.position(pointer.position() - min)
271
263
 
264
+ /* notify (start/end refer to pre-splice indices) */
272
265
  this.notify("write", { start: 0, end: min, op: "trim" })
273
266
  }
274
267
  }
268
+ clear (): void {
269
+ this.elements.length = 0
270
+ for (const pointer of this.pointers.values())
271
+ pointer.position(0)
272
+ }
275
273
  }
276
274
 
277
275
  /* meta store */
@@ -301,9 +299,12 @@ export class TimeStore<T> extends EventEmitter {
301
299
 
302
300
  /* asynchronous queue */
303
301
  export class AsyncQueue<T> {
304
- private queue: Array<T> = []
302
+ private queue = new Array<T>()
305
303
  private resolvers: { resolve: (v: T) => void, reject: (err: Error) => void }[] = []
304
+ public destroyed = false
306
305
  write (v: T) {
306
+ if (this.destroyed)
307
+ return
307
308
  const resolver = this.resolvers.shift()
308
309
  if (resolver)
309
310
  resolver.resolve(v)
@@ -319,7 +320,13 @@ export class AsyncQueue<T> {
319
320
  empty () {
320
321
  return this.queue.length === 0
321
322
  }
323
+ drain () {
324
+ const items = this.queue
325
+ this.queue = new Array<T>()
326
+ return items
327
+ }
322
328
  destroy () {
329
+ this.destroyed = true
323
330
  for (const resolver of this.resolvers)
324
331
  resolver.reject(new Error("AsyncQueue destroyed"))
325
332
  this.resolvers = []
@@ -359,7 +366,22 @@ export class PromiseSet<T> {
359
366
  this.promises.delete(promise)
360
367
  }).catch(() => {})
361
368
  }
362
- async awaitAll () {
363
- await Promise.all(this.promises)
369
+ async awaitAll (timeout = 0) {
370
+ const deadline = timeout > 0 ? Date.now() + timeout : 0
371
+ while (this.promises.size > 0) {
372
+ const snapshot = [ ...this.promises ]
373
+ const remaining = deadline > 0 ? deadline - Date.now() : 0
374
+ if (deadline > 0 && remaining <= 0)
375
+ break
376
+ if (deadline > 0)
377
+ await Promise.race([
378
+ Promise.all(snapshot),
379
+ new Promise((resolve) => { setTimeout(resolve, remaining) })
380
+ ])
381
+ else
382
+ await Promise.all(snapshot)
383
+ if (deadline > 0 && Date.now() >= deadline)
384
+ break
385
+ }
364
386
  }
365
387
  }
@@ -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
 
@@ -59,7 +59,10 @@ export function ensureStreamChunk (type: "audio" | "text", chunk: SpeechFlowChun
59
59
  export function createTransformStreamForReadableSide (
60
60
  type: "text" | "audio",
61
61
  getTimeZero: () => DateTime,
62
- writableHighWaterMark?: number
62
+ writableHighWaterMark?: number,
63
+ sampleRate?: number,
64
+ bitDepth?: number,
65
+ channels?: number
63
66
  ) {
64
67
  return new Stream.Transform({
65
68
  writableObjectMode: false,
@@ -72,7 +75,8 @@ export function createTransformStreamForReadableSide (
72
75
  const start = DateTime.now().diff(timeZero)
73
76
  let end = start
74
77
  if (type === "audio") {
75
- const duration = util.audioBufferDuration(chunk as Buffer)
78
+ const duration = util.audioBufferDuration(chunk as Buffer,
79
+ sampleRate, bitDepth, channels)
76
80
  end = start.plus(duration * 1000)
77
81
  }
78
82
  const payload = ensureStreamChunk(type, chunk) as Buffer | string
@@ -185,9 +189,33 @@ export class StreamWrapper extends Stream.Transform {
185
189
  return
186
190
  }
187
191
  try {
188
- if (typeof this.foreignStream.end === "function")
192
+ if (typeof this.foreignStream.end === "function") {
189
193
  this.foreignStream.end()
190
- callback()
194
+
195
+ /* wait for the foreign stream to finish flushing
196
+ before signaling completion of this Transform stream */
197
+ const ac = new AbortController()
198
+ Promise.race([
199
+ new Promise<void>((resolve, reject) => {
200
+ if (typeof this.foreignStream.once === "function") {
201
+ this.foreignStream.once("finish", () => { resolve() })
202
+ this.foreignStream.once("error", (err: Error) => { reject(err) })
203
+ }
204
+ else
205
+ resolve()
206
+ }),
207
+ util.timeout(5000, "foreign stream flush timeout", ac.signal)
208
+ ]).finally(() => {
209
+ ac.abort()
210
+ }).then(() => {
211
+ callback()
212
+ }).catch(() => {
213
+ /* ignore timeout -- stream will be destroyed anyway */
214
+ callback()
215
+ })
216
+ }
217
+ else
218
+ callback()
191
219
  }
192
220
  catch (err: unknown) {
193
221
  callback(util.ensureError(err))
@@ -211,15 +239,19 @@ export async function destroyStream (
211
239
  if (( stream instanceof Stream.Duplex
212
240
  || stream instanceof Stream.Transform
213
241
  || stream instanceof Stream.Writable )
214
- && (!stream.writableEnded && !stream.destroyed))
242
+ && (!stream.writableEnded && !stream.destroyed)) {
243
+ const ac = new AbortController()
215
244
  await Promise.race([
216
245
  new Promise<void>((resolve) => {
217
246
  stream.end(() => { resolve() })
218
247
  }),
219
- util.timeout(5000, "stream end timeout")
220
- ]).catch(() => {
248
+ util.timeout(5000, "stream end timeout", ac.signal)
249
+ ]).finally(() => {
250
+ ac.abort()
251
+ }).catch(() => {
221
252
  /* ignore timeout -- stream will be destroyed anyway */
222
253
  })
254
+ }
223
255
 
224
256
  /* destroy the stream */
225
257
  stream.destroy()
@@ -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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /*!
3
3
  ** SpeechFlow - Speech Processing Flow Graph
4
- ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
5
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
6
  */
7
7
 
@@ -2,7 +2,7 @@
2
2
  <!--
3
3
  **
4
4
  ** SpeechFlow - Speech Processing Flow Graph
5
- ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
6
6
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
7
7
  **
8
8
  -->