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
 
@@ -106,9 +106,13 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
106
106
  else if (chunk.payload === "")
107
107
  callback()
108
108
  else {
109
+ let callbackCalled = false
109
110
  let processTimeout: ReturnType<typeof setTimeout> | null = setTimeout(() => {
110
111
  processTimeout = null
111
- callback(new Error("OpenAI TTS API timeout"))
112
+ if (!callbackCalled) {
113
+ callbackCalled = true
114
+ callback(new Error("OpenAI TTS API timeout"))
115
+ }
112
116
  }, 60 * 1000)
113
117
  const clearProcessTimeout = () => {
114
118
  if (processTimeout !== null) {
@@ -119,12 +123,16 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
119
123
  try {
120
124
  if (self.closing) {
121
125
  clearProcessTimeout()
126
+ callbackCalled = true
122
127
  callback(new Error("stream destroyed during processing"))
123
128
  return
124
129
  }
125
130
  const buffer = await textToSpeech(chunk.payload as string)
131
+ clearProcessTimeout()
132
+ if (callbackCalled)
133
+ return
134
+ callbackCalled = true
126
135
  if (self.closing) {
127
- clearProcessTimeout()
128
136
  callback(new Error("stream destroyed during processing"))
129
137
  return
130
138
  }
@@ -138,12 +146,14 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
138
146
  chunkNew.type = "audio"
139
147
  chunkNew.payload = buffer
140
148
  chunkNew.timestampEnd = Duration.fromMillis(chunkNew.timestampStart.toMillis() + durationMs)
141
- clearProcessTimeout()
142
149
  this.push(chunkNew)
143
150
  callback()
144
151
  }
145
152
  catch (error) {
146
153
  clearProcessTimeout()
154
+ if (callbackCalled)
155
+ return
156
+ callbackCalled = true
147
157
  callback(util.ensureError(error, "OpenAI TTS processing failed"))
148
158
  }
149
159
  }
@@ -166,8 +176,10 @@ export default class SpeechFlowNodeT2AOpenAI extends SpeechFlowNode {
166
176
  }
167
177
 
168
178
  /* destroy resampler */
169
- if (this.resampler !== null)
179
+ if (this.resampler !== null) {
180
+ this.resampler.destroy()
170
181
  this.resampler = null
182
+ }
171
183
 
172
184
  /* destroy OpenAI API */
173
185
  if (this.openai !== null)
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  ** SpeechFlow - Speech Processing Flow Graph
3
- ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
3
+ ** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
@@ -138,8 +138,11 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
138
138
  throw new Error("unexpected Supertonic result: sampling_rate is not a number")
139
139
  const samples = result.audio
140
140
  const outputSampleRate = result.sampling_rate
141
- if (outputSampleRate !== this.sampleRate)
142
- this.log("warning", `unexpected sample rate ${outputSampleRate}Hz (expected ${this.sampleRate}Hz)`)
141
+ if (outputSampleRate !== this.sampleRate) {
142
+ this.log("warning", `unexpected sample rate change ${this.sampleRate}Hz -> ${outputSampleRate}Hz (recreating resampler)`)
143
+ this.sampleRate = outputSampleRate
144
+ this.resampler = new SpeexResampler(1, this.sampleRate, this.config.audioSampleRate, 7)
145
+ }
143
146
 
144
147
  /* calculate duration */
145
148
  const duration = samples.length / outputSampleRate
@@ -169,9 +172,13 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
169
172
  else if (chunk.payload === "")
170
173
  callback()
171
174
  else {
175
+ let callbackCalled = false
172
176
  let processTimeout: ReturnType<typeof setTimeout> | null = setTimeout(() => {
173
177
  processTimeout = null
174
- callback(new Error("Supertonic TTS timeout"))
178
+ if (!callbackCalled) {
179
+ callbackCalled = true
180
+ callback(new Error("Supertonic TTS timeout"))
181
+ }
175
182
  }, 120 * 1000)
176
183
  const clearProcessTimeout = () => {
177
184
  if (processTimeout !== null) {
@@ -180,8 +187,11 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
180
187
  }
181
188
  }
182
189
  text2speech(chunk.payload as string).then((buffer) => {
190
+ clearProcessTimeout()
191
+ if (callbackCalled)
192
+ return
193
+ callbackCalled = true
183
194
  if (self.closing) {
184
- clearProcessTimeout()
185
195
  callback(new Error("stream destroyed during processing"))
186
196
  return
187
197
  }
@@ -196,11 +206,13 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
196
206
  chunkNew.type = "audio"
197
207
  chunkNew.payload = buffer
198
208
  chunkNew.timestampEnd = Duration.fromMillis(chunkNew.timestampStart.toMillis() + durationMs)
199
- clearProcessTimeout()
200
209
  this.push(chunkNew)
201
210
  callback()
202
211
  }).catch((error: unknown) => {
203
212
  clearProcessTimeout()
213
+ if (callbackCalled)
214
+ return
215
+ callbackCalled = true
204
216
  callback(util.ensureError(error, "Supertonic processing failed"))
205
217
  })
206
218
  }
@@ -223,8 +235,10 @@ export default class SpeechFlowNodeT2ASupertonic extends SpeechFlowNode {
223
235
  }
224
236
 
225
237
  /* destroy resampler */
226
- if (this.resampler !== null)
238
+ if (this.resampler !== null) {
239
+ this.resampler.destroy()
227
240
  this.resampler = null
241
+ }
228
242
 
229
243
  /* destroy TTS pipeline */
230
244
  if (this.tts !== null) {
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  ** SpeechFlow - Speech Processing Flow Graph
3
- ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
3
+ ** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
@@ -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
 
@@ -125,7 +125,9 @@ export default class SpeechFlowNodeT2TGoogle extends SpeechFlowNode {
125
125
 
126
126
  /* shutdown Google Translate client */
127
127
  if (this.client !== null) {
128
- this.client.close()
128
+ await this.client.close().catch((error) => {
129
+ this.log("warning", `error closing Google Translate client: ${error}`)
130
+ })
129
131
  this.client = null
130
132
  }
131
133
  }
@@ -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
 
@@ -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
 
@@ -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,6 +112,9 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
112
112
  /* produce SRT/VTT blocks */
113
113
  let output = convertSingle(timestampStart, timestampEnd, chunk.payload)
114
114
  if (this.params.words) {
115
+ if (words.length === 0)
116
+ this.log("warning", "word-level subtitle highlighting requested but no word-level timing data available")
117
+
115
118
  /* produce additional SRT/VTT blocks with each word highlighted */
116
119
  const occurrences = new Map<string, number>()
117
120
  for (const word of words) {
@@ -166,7 +169,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
166
169
  else if (this.params.mode === "import") {
167
170
  /* parse timestamp in SRT format ("HH:MM:SS,mmm") or VTT format ("HH:MM:SS.mmm") */
168
171
  const parseTimestamp = (ts: string): Duration => {
169
- const match = ts.match(/^(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/)
172
+ const match = ts.match(/^(\d{2,}):(\d{2}):(\d{2})[,.](\d{3})$/)
170
173
  if (!match)
171
174
  throw new Error(`invalid timestamp format: "${ts}"`)
172
175
  const hours = Number.parseInt(match[1], 10)
@@ -202,7 +205,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
202
205
 
203
206
  /* parse timestamp line */
204
207
  const timeLine = lines[lineIdx]
205
- const timeMatch = timeLine.match(/^(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/)
208
+ const timeMatch = timeLine.match(/^(\d{2,}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2,}:\d{2}:\d{2},\d{3})/)
206
209
  if (!timeMatch) {
207
210
  this.log("warning", "SRT contains invalid timestamp line")
208
211
  continue
@@ -230,8 +233,8 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
230
233
  const blocks = content.trim().split(/\r?\n\r?\n+/)
231
234
  for (const block of blocks) {
232
235
  const lines = block.trim().split(/\r?\n/)
233
- if (lines.length < 1) {
234
- this.log("warning", "VTT block contains fewer than 1 line")
236
+ if (lines.length < 2) {
237
+ this.log("warning", "VTT block contains fewer than 2 lines")
235
238
  continue
236
239
  }
237
240
 
@@ -244,7 +247,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
244
247
 
245
248
  /* parse timestamp line */
246
249
  const timeLine = lines[lineIdx]
247
- const timeMatch = timeLine.match(/^(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})/)
250
+ const timeMatch = timeLine.match(/^(\d{2,}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2,}:\d{2}:\d{2}\.\d{3})/)
248
251
  if (!timeMatch) {
249
252
  this.log("warning", "VTT contains invalid timestamp line")
250
253
  continue
@@ -288,25 +291,42 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
288
291
  /* accumulate input */
289
292
  buffer += chunk.payload
290
293
 
291
- /* parse accumulated input */
294
+ /* find the last double-newline boundary to separate
295
+ complete blocks from a potentially incomplete trailing block */
296
+ const boundary = /\r?\n\r?\n/g
297
+ let lastBoundaryEnd = -1
298
+ let match: RegExpExecArray | null
299
+ while ((match = boundary.exec(buffer)) !== null)
300
+ lastBoundaryEnd = match.index + match[0].length
301
+
302
+ /* if no complete block boundary found, wait for more data */
303
+ if (lastBoundaryEnd < 0) {
304
+ callback()
305
+ return
306
+ }
307
+
308
+ /* split buffer into complete portion and remainder */
309
+ const complete = buffer.substring(0, lastBoundaryEnd)
310
+ const remainder = buffer.substring(lastBoundaryEnd)
311
+
312
+ /* parse only the complete portion */
292
313
  try {
293
314
  /* parse entries */
294
- const entries = (self.params.format === "srt" ? parseSRT(buffer) : parseVTT(buffer))
315
+ const entries = (self.params.format === "srt" ? parseSRT(complete) : parseVTT(complete))
295
316
 
296
317
  /* emit parsed entries as individual chunks */
297
318
  for (const entry of entries) {
298
319
  const chunkNew = new SpeechFlowChunk(entry.start, entry.end, "final", "text", entry.text)
299
320
  this.push(chunkNew)
300
321
  }
301
-
302
- /* clear buffer after successful parse */
303
- buffer = ""
304
- callback()
305
322
  }
306
323
  catch (error: unknown) {
307
- buffer = ""
308
- callback(util.ensureError(error))
324
+ self.log("warning", `subtitle parse error: ${util.ensureError(error).message}`)
309
325
  }
326
+
327
+ /* keep only the unparsed remainder in the buffer */
328
+ buffer = remainder
329
+ callback()
310
330
  },
311
331
  final (callback) {
312
332
  /* process any remaining buffer content */
@@ -402,7 +422,11 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
402
422
  const emit = (chunk: SpeechFlowChunk) => {
403
423
  const data = JSON.stringify(chunk)
404
424
  for (const info of wsPeers.values())
405
- info.ws.send(data)
425
+ if (info.ws.readyState === WebSocket.OPEN)
426
+ info.ws.send(data, (err) => {
427
+ if (err)
428
+ this.log("warning", `HAPI: WebSocket: subtitle send failed: ${err.message}`)
429
+ })
406
430
  }
407
431
 
408
432
  /* establish writable stream */
@@ -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
 
@@ -103,7 +103,7 @@ export default class SpeechFlowNodeT2TSummary extends SpeechFlowNode {
103
103
 
104
104
  /* count sentences in text */
105
105
  private countSentences (text: string): number {
106
- const matches = text.match(/[.;?!]/g)
106
+ const matches = text.match(/[.;?!]+(?:\s|$)/g)
107
107
  return matches ? matches.length : 0
108
108
  }
109
109
 
@@ -186,7 +186,7 @@ export default class SpeechFlowNodeT2TSummary extends SpeechFlowNode {
186
186
  },
187
187
  final (callback) {
188
188
  /* generate final summary if there is accumulated text */
189
- if (self.accumulatedText.length > 0 && self.sentencesSinceLastSummary > 0) {
189
+ if (self.accumulatedText.length > 0) {
190
190
  self.sentencesSinceLastSummary = 0
191
191
  self.log("info", "generating final summary of accumulated text")
192
192
  const textToSummarize = self.accumulatedText
@@ -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,6 +47,7 @@ export default class SpeechFlowNodeX2XFilter extends SpeechFlowNode {
47
47
 
48
48
  /* helper function for comparing two values */
49
49
  const comparison = (val1: any, op: string, val2: any) => {
50
+ val1 ??= ""
50
51
  if (op === "==" || op === "!=") {
51
52
  /* equal comparison */
52
53
  const str1 = (typeof val1 === "string" ? val1 : val1.toString())
@@ -73,8 +74,8 @@ export default class SpeechFlowNodeX2XFilter extends SpeechFlowNode {
73
74
  /* non-equal comparison */
74
75
  const coerceNum = (val: any) =>
75
76
  typeof val === "number" ? val : (
76
- typeof val === "string" && val.match(/^[\d+-]+$/) ? Number.parseInt(val, 10) : (
77
- typeof val === "string" && val.match(/^[\d.+-]+$/) ?
77
+ typeof val === "string" && val.match(/^[+-]?\d+$/) ? Number.parseInt(val, 10) : (
78
+ typeof val === "string" && val.match(/^[+-]?(\d+\.?\d*|\d*\.?\d+)$/) ?
78
79
  Number.parseFloat(val) :
79
80
  Number(val)
80
81
  )
@@ -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
 
@@ -116,7 +116,8 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
116
116
 
117
117
  /* convert regular stream into object-mode stream */
118
118
  const wrapper1 = util.createTransformStreamForWritableSide("audio", 1)
119
- const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark)
119
+ const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark,
120
+ this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
120
121
  this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
121
122
  }
122
123
 
@@ -137,7 +138,8 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
137
138
  this.stream = this.io as unknown as Stream.Readable
138
139
 
139
140
  /* convert regular stream into object-mode stream */
140
- const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark)
141
+ const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark,
142
+ this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
141
143
  this.stream = Stream.compose(this.stream, wrapper)
142
144
  }
143
145
 
@@ -207,24 +209,36 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
207
209
  if (!error.message.match(/AudioIO Quit expects 1 argument/))
208
210
  throw error
209
211
  }
212
+ const ac1 = new AbortController()
210
213
  await Promise.race([
211
- util.timeout(2 * 1000, "PortAudio abort timeout"),
214
+ util.timeout(2 * 1000, "PortAudio abort timeout", ac1.signal),
212
215
  new Promise<void>((resolve) => {
213
216
  this.io!.abort(() => {
214
217
  resolve()
215
218
  })
216
219
  }).catch(catchHandler)
217
- ])
220
+ ]).finally(() => {
221
+ ac1.abort()
222
+ })
223
+ const ac2 = new AbortController()
218
224
  await Promise.race([
219
- util.timeout(2 * 1000, "PortAudio quit timeout"),
225
+ util.timeout(2 * 1000, "PortAudio quit timeout", ac2.signal),
220
226
  new Promise<void>((resolve) => {
221
227
  this.io!.quit(() => {
222
228
  resolve()
223
229
  })
224
230
  }).catch(catchHandler)
225
- ])
231
+ ]).finally(() => {
232
+ ac2.abort()
233
+ })
226
234
  this.io = null
227
235
  }
236
+
237
+ /* shutdown stream */
238
+ if (this.stream !== null) {
239
+ await util.destroyStream(this.stream)
240
+ this.stream = null
241
+ }
228
242
  }
229
243
  }
230
244
 
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  ** SpeechFlow - Speech Processing Flow Graph
3
- ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
3
+ ** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
@@ -129,13 +129,15 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
129
129
  })
130
130
  const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, highWaterMark)
131
131
  const wrapper2 = util.createTransformStreamForReadableSide(
132
- this.params.type, () => this.timeZero, highWaterMark)
132
+ this.params.type, () => this.timeZero, highWaterMark,
133
+ this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
133
134
  this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
134
135
  }
135
136
  else if (this.params.mode === "r") {
136
137
  /* read-only mode: stdout only */
137
138
  const wrapper = util.createTransformStreamForReadableSide(
138
- this.params.type, () => this.timeZero, highWaterMark)
139
+ this.params.type, () => this.timeZero, highWaterMark,
140
+ this.config.audioSampleRate, this.config.audioBitDepth, this.config.audioChannels)
139
141
  this.stream = Stream.compose(this.subprocess.stdout!, wrapper)
140
142
  }
141
143
  else if (this.params.mode === "w") {
@@ -153,6 +155,7 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
153
155
  /* gracefully end stdin if in write or read/write mode */
154
156
  if ((this.params.mode === "w" || this.params.mode === "rw") && this.subprocess.stdin
155
157
  && !this.subprocess.stdin.destroyed && !this.subprocess.stdin.writableEnded) {
158
+ const ac1 = new AbortController()
156
159
  await Promise.race([
157
160
  new Promise<void>((resolve, reject) => {
158
161
  this.subprocess!.stdin!.end((err?: Error) => {
@@ -160,36 +163,47 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
160
163
  else resolve()
161
164
  })
162
165
  }),
163
- util.timeout(2000)
164
- ]).catch((err: unknown) => {
166
+ util.timeout(2000, "timeout", ac1.signal)
167
+ ]).finally(() => {
168
+ ac1.abort()
169
+ }).catch((err: unknown) => {
165
170
  const error = util.ensureError(err)
166
171
  this.log("warning", `failed to gracefully close stdin: ${error.message}`)
167
172
  })
168
173
  }
169
174
 
170
175
  /* wait for subprocess to exit gracefully */
176
+ const ac2 = new AbortController()
171
177
  await Promise.race([
172
178
  this.subprocess,
173
- util.timeout(5000, "subprocess exit timeout")
174
- ]).catch(async (err: unknown) => {
179
+ util.timeout(5000, "subprocess exit timeout", ac2.signal)
180
+ ]).finally(() => {
181
+ ac2.abort()
182
+ }).catch(async (err: unknown) => {
175
183
  /* force kill with SIGTERM */
176
184
  const error = util.ensureError(err)
177
185
  if (error.message.includes("timeout")) {
178
186
  this.log("warning", "subprocess did not exit gracefully, forcing termination")
179
187
  this.subprocess!.kill("SIGTERM")
188
+ const ac3 = new AbortController()
180
189
  return Promise.race([
181
190
  this.subprocess,
182
- util.timeout(2000)
183
- ])
191
+ util.timeout(2000, "timeout", ac3.signal)
192
+ ]).finally(() => {
193
+ ac3.abort()
194
+ })
184
195
  }
185
196
  }).catch(async () => {
186
197
  /* force kill with SIGKILL */
187
198
  this.log("warning", "subprocess did not respond to SIGTERM, forcing SIGKILL")
188
199
  this.subprocess!.kill("SIGKILL")
200
+ const ac4 = new AbortController()
189
201
  return Promise.race([
190
202
  this.subprocess,
191
- util.timeout(1000)
192
- ])
203
+ util.timeout(1000, "timeout", ac4.signal)
204
+ ]).finally(() => {
205
+ ac4.abort()
206
+ })
193
207
  }).catch(() => {
194
208
  this.log("error", "subprocess did not terminate even after SIGKILL")
195
209
  })