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
 
@@ -39,13 +39,14 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
39
39
  /* internal state */
40
40
  private peerConnections = new Map<string, WebRTCConnection>()
41
41
  private httpServer: http.Server | null = null
42
- private chunkQueue: util.SingleQueue<SpeechFlowChunk> | null = null
42
+ private chunkQueue: util.AsyncQueue<SpeechFlowChunk> | null = null
43
43
  private opusEncoder: OpusEncoder | null = null
44
44
  private opusDecoder: OpusEncoder | null = null
45
45
  private pcmBuffer = Buffer.alloc(0)
46
46
  private rtpSequence = 0
47
47
  private rtpTimestamp = 0
48
48
  private rtpSSRC = 0
49
+ private rtpMarkerNext = true
49
50
  private maxConnections = 10
50
51
 
51
52
  /* Opus codec configuration: 48kHz, mono, 16-bit */
@@ -177,7 +178,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
177
178
  padding: false,
178
179
  paddingSize: 0,
179
180
  extension: false,
180
- marker: true,
181
+ marker: this.rtpMarkerNext,
181
182
  payloadType: 111, /* Opus payload type */
182
183
  sequenceNumber: this.rtpSequence++ & 0xFFFF,
183
184
  timestamp: this.rtpTimestamp,
@@ -186,6 +187,9 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
186
187
  extensions: []
187
188
  })
188
189
 
190
+ /* clear marker (set only on first packet of a talkspurt per RFC 3551) */
191
+ this.rtpMarkerNext = false
192
+
189
193
  /* build RTP packet */
190
194
  const rtpPacket = new RtpPacket(rtpHeader, opusPacket)
191
195
 
@@ -265,11 +269,11 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
265
269
  const resourceId = crypto.randomUUID()
266
270
  const { pc, subscription } = this.createPeerConnection(resourceId)
267
271
 
268
- /* protocol-specific setup */
269
- const track = setupFn(pc, resourceId)
270
-
271
272
  /* complete SDP offer/answer exchange and establish connection */
272
273
  try {
274
+ /* protocol-specific setup */
275
+ const track = setupFn(pc, resourceId)
276
+
273
277
  /* set remote description (offer from client) */
274
278
  await pc.setRemoteDescription({ type: "offer", sdp: offer })
275
279
 
@@ -365,9 +369,10 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
365
369
  this.rtpSequence = Math.floor(Math.random() * 0x10000)
366
370
  this.rtpTimestamp = Math.floor(Math.random() * 0x100000000) >>> 0
367
371
  this.rtpSSRC = Math.floor(Math.random() * 0x100000000) >>> 0
372
+ this.rtpMarkerNext = true
368
373
 
369
374
  /* setup chunk queue for incoming audio */
370
- this.chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
375
+ this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
371
376
 
372
377
  /* parse listen address */
373
378
  const listen = this.parseAddress(this.params.listen, 8085)
@@ -375,77 +380,89 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
375
380
  /* setup HTTP server for WHIP/WHEP signaling */
376
381
  const self = this
377
382
  this.httpServer = http.createServer(async (req, res) => {
378
- /* determine URL */
379
- if (req.url === undefined) {
380
- res.writeHead(400, { "Content-Type": "text/plain" })
381
- res.end("Bad Request")
382
- return
383
- }
384
- const host = req.headers.host?.replace(/[^a-zA-Z0-9:.\-_]/g, "") ?? "localhost"
385
- const url = new URL(req.url, `http://${host}`)
386
- const pathMatch = url.pathname === self.params.path
387
- const resourceMatch = url.pathname.startsWith(self.params.path + "/")
388
-
389
- /* CORS headers for browser clients */
390
- res.setHeader("Access-Control-Allow-Origin", "*")
391
- res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, OPTIONS")
392
- res.setHeader("Access-Control-Allow-Headers", "Content-Type")
393
- res.setHeader("Access-Control-Expose-Headers", "Location")
394
-
395
- /* handle CORS preflight */
396
- if (req.method === "OPTIONS") {
397
- res.writeHead(204)
398
- res.end()
399
- return
400
- }
401
-
402
- /* handle requests... */
403
- if (req.method === "POST" && pathMatch) {
404
- /* handle WHIP/WHEP POST */
405
- const body = await self.readRequestBody(req)
406
-
407
- /* sanity check content type */
408
- const contentType = req.headers["content-type"]
409
- if (contentType !== "application/sdp") {
410
- res.writeHead(415, { "Content-Type": "text/plain" })
411
- res.end("Unsupported Media Type")
383
+ try {
384
+ /* determine URL */
385
+ if (req.url === undefined) {
386
+ res.writeHead(400, { "Content-Type": "text/plain" })
387
+ res.end("Bad Request")
388
+ return
389
+ }
390
+ const host = req.headers.host?.replace(/[^a-zA-Z0-9:.\-_]/g, "") ?? "localhost"
391
+ const url = new URL(req.url, `http://${host}`)
392
+ const pathMatch = url.pathname === self.params.path
393
+ const resourceMatch = url.pathname.startsWith(self.params.path + "/")
394
+
395
+ /* CORS headers for browser clients */
396
+ res.setHeader("Access-Control-Allow-Origin", "*")
397
+ res.setHeader("Access-Control-Allow-Methods", "POST, DELETE, OPTIONS")
398
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type")
399
+ res.setHeader("Access-Control-Expose-Headers", "Location")
400
+
401
+ /* handle CORS preflight */
402
+ if (req.method === "OPTIONS") {
403
+ res.writeHead(204)
404
+ res.end()
412
405
  return
413
406
  }
414
407
 
415
- /* determine if WHIP (receiving) or WHEP (sending) based on SDP content */
416
- const hasSendonly = /\ba=sendonly\b/m.test(body)
417
- const hasSendrecv = /\ba=sendrecv\b/m.test(body)
418
- const hasRecvonly = /\ba=recvonly\b/m.test(body)
419
- const isPublisher = hasSendonly || hasSendrecv
420
- const isViewer = hasRecvonly
421
-
422
- /* handle protocol based on mode */
423
- if (self.params.mode === "r" && isPublisher)
424
- /* in read mode, accept WHIP publishers */
425
- await self.handleWHIP(res, body)
426
- else if (self.params.mode === "w" && isViewer)
427
- /* in write mode, accept WHEP viewers */
428
- await self.handleWHEP(res, body)
408
+ /* handle requests... */
409
+ if (req.method === "POST" && pathMatch) {
410
+ /* handle WHIP/WHEP POST */
411
+ const body = await self.readRequestBody(req)
412
+
413
+ /* sanity check content type */
414
+ const contentType = req.headers["content-type"]
415
+ if (contentType !== "application/sdp") {
416
+ res.writeHead(415, { "Content-Type": "text/plain" })
417
+ res.end("Unsupported Media Type")
418
+ return
419
+ }
420
+
421
+ /* determine if WHIP (receiving) or WHEP (sending) based on SDP content */
422
+ const hasSendonly = /\ba=sendonly\b/m.test(body)
423
+ const hasSendrecv = /\ba=sendrecv\b/m.test(body)
424
+ const hasRecvonly = /\ba=recvonly\b/m.test(body)
425
+ const isPublisher = hasSendonly || hasSendrecv
426
+ const isViewer = hasRecvonly
427
+
428
+ /* handle protocol based on mode */
429
+ if (self.params.mode === "r" && isPublisher)
430
+ /* in read mode, accept WHIP publishers */
431
+ await self.handleWHIP(res, body)
432
+ else if (self.params.mode === "w" && isViewer)
433
+ /* in write mode, accept WHEP viewers */
434
+ await self.handleWHEP(res, body)
435
+ else {
436
+ res.writeHead(403, { "Content-Type": "text/plain" })
437
+ res.end("Forbidden")
438
+ }
439
+ }
440
+ else if (req.method === "DELETE" && resourceMatch) {
441
+ /* handle DELETE for connection teardown */
442
+ const resourceId = url.pathname.substring(self.params.path.length + 1)
443
+ self.handleDELETE(res, resourceId)
444
+ }
429
445
  else {
430
- res.writeHead(403, { "Content-Type": "text/plain" })
431
- res.end("Forbidden")
446
+ /* handle unknown requests */
447
+ res.writeHead(404, { "Content-Type": "text/plain" })
448
+ res.end("Not Found")
432
449
  }
433
450
  }
434
- else if (req.method === "DELETE" && resourceMatch) {
435
- /* handle DELETE for connection teardown */
436
- const resourceId = url.pathname.substring(self.params.path.length + 1)
437
- self.handleDELETE(res, resourceId)
438
- }
439
- else {
440
- /* handle unknown requests */
441
- res.writeHead(404, { "Content-Type": "text/plain" })
442
- res.end("Not Found")
451
+ catch (err: unknown) {
452
+ self.log("error", `HTTP request handler failed: ${util.ensureError(err).message}`)
453
+ if (!res.headersSent) {
454
+ res.writeHead(500, { "Content-Type": "text/plain" })
455
+ res.end("Internal Server Error")
456
+ }
443
457
  }
444
458
  })
445
459
 
446
460
  /* start HTTP server */
447
- await new Promise<void>((resolve) => {
461
+ await new Promise<void>((resolve, reject) => {
462
+ const onError = (err: Error) => { reject(err) }
463
+ this.httpServer!.once("error", onError)
448
464
  this.httpServer!.listen(listen.port, listen.host, () => {
465
+ this.httpServer!.removeListener("error", onError)
449
466
  const mode = this.params.mode === "r" ? "WHIP" : "WHEP"
450
467
  this.log("info", `WebRTC ${mode} server listening on http://${listen.host}:${listen.port}${this.params.path}`)
451
468
  resolve()
@@ -470,6 +487,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
470
487
  }
471
488
  if (self.peerConnections.size === 0) {
472
489
  /* silently drop if no viewers connected */
490
+ self.rtpMarkerNext = true
473
491
  callback()
474
492
  return
475
493
  }
@@ -486,11 +504,15 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
486
504
  this.push(null)
487
505
  return
488
506
  }
489
- reads.add(self.chunkQueue!.read().then((chunk) => {
507
+ if (self.chunkQueue === null)
508
+ return
509
+ const queue = self.chunkQueue
510
+ reads.add(queue.read().then((chunk) => {
490
511
  this.push(chunk, "binary")
491
512
  }).catch((err: Error) => {
492
513
  self.log("warning", `read on chunk queue operation failed: ${err}`)
493
- this.push(null)
514
+ if (queue.destroyed)
515
+ this.push(null)
494
516
  }))
495
517
  }
496
518
  })
@@ -517,7 +539,7 @@ export default class SpeechFlowNodeXIOWebRTC extends SpeechFlowNode {
517
539
 
518
540
  /* drain and clear chunk queue */
519
541
  if (this.chunkQueue !== null) {
520
- this.chunkQueue.drain()
542
+ this.chunkQueue.destroy()
521
543
  this.chunkQueue = null
522
544
  }
523
545
 
@@ -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),
@@ -101,8 +102,13 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
101
102
  buffer = Buffer.from(data)
102
103
  else
103
104
  buffer = Buffer.concat(data)
104
- const chunk = util.streamChunkDecode(buffer)
105
- chunkQueue.write(chunk)
105
+ try {
106
+ const chunk = util.streamChunkDecode(buffer)
107
+ this.chunkQueue?.write(chunk)
108
+ }
109
+ catch (_err: unknown) {
110
+ this.log("warning", `received invalid CBOR chunk on URL ${this.params.listen} from peer ${peer}`)
111
+ }
106
112
  })
107
113
  })
108
114
  this.server.on("error", (error) => {
@@ -124,9 +130,11 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
124
130
  callback(new Error("still no WebSocket connections available"))
125
131
  else {
126
132
  const data = util.streamChunkEncode(chunk)
127
- const results: Promise<void>[] = []
133
+ const sends: Promise<void>[] = []
134
+ const clients: ws.WebSocket[] = []
128
135
  for (const websocket of websockets.values()) {
129
- results.push(new Promise<void>((resolve, reject) => {
136
+ clients.push(websocket)
137
+ sends.push(new Promise<void>((resolve, reject) => {
130
138
  websocket.send(data, (error) => {
131
139
  if (error)
132
140
  reject(error)
@@ -135,10 +143,27 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
135
143
  })
136
144
  }))
137
145
  }
138
- Promise.all(results).then(() => {
139
- callback()
140
- }).catch((error: unknown) => {
141
- callback(util.ensureError(error))
146
+ Promise.allSettled(sends).then((results) => {
147
+ let lastError: Error | null = null
148
+ for (let i = 0; i < results.length; i++) {
149
+ if (results[i].status === "rejected") {
150
+ const error = util.ensureError((results[i] as PromiseRejectedResult).reason)
151
+ self.log("warning", `failed to send to WebSocket client: ${error.message}`)
152
+ websockets.delete(clients[i])
153
+ clients[i].terminate()
154
+ lastError = error
155
+ }
156
+ }
157
+ const failures = results.filter((r) => r.status === "rejected").length
158
+ if (failures > 0 && failures < results.length)
159
+ self.log("warning", "partial broadcast failure: " +
160
+ `${failures} of ${results.length} WebSocket clients failed`)
161
+ if (lastError !== null && failures === results.length)
162
+ callback(lastError)
163
+ else
164
+ callback()
165
+ }).catch((err: Error) => {
166
+ callback(err)
142
167
  })
143
168
  }
144
169
  },
@@ -147,12 +172,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
147
172
  callback()
148
173
  },
149
174
  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) => {
175
+ if (self.params.mode === "w") {
176
+ self.log("error", "read operation on write-only node")
177
+ this.push(null)
178
+ return
179
+ }
180
+ if (self.chunkQueue === null)
181
+ return
182
+ const queue = self.chunkQueue
183
+ reads.add(queue.read().then((chunk) => {
153
184
  this.push(chunk, "binary")
154
185
  }).catch((err: Error) => {
155
186
  self.log("warning", `read on chunk queue operation failed: ${err}`)
187
+ if (queue.destroyed)
188
+ this.push(null)
156
189
  }))
157
190
  }
158
191
  })
@@ -178,7 +211,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
178
211
  const error = util.ensureError(ev.error)
179
212
  this.log("error", `error of connection on URL ${this.params.connect}: ${error.message}`)
180
213
  })
181
- const chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
214
+ this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
182
215
  this.client.addEventListener("message", (ev: MessageEvent) => {
183
216
  if (this.params.mode === "w") {
184
217
  this.log("warning", `connection to URL ${this.params.connect}: ` +
@@ -191,8 +224,13 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
191
224
  return
192
225
  }
193
226
  const buffer = Buffer.from(ev.data)
194
- const chunk = util.streamChunkDecode(buffer)
195
- chunkQueue.write(chunk)
227
+ try {
228
+ const chunk = util.streamChunkDecode(buffer)
229
+ this.chunkQueue?.write(chunk)
230
+ }
231
+ catch (_err: unknown) {
232
+ this.log("warning", `received invalid CBOR chunk from URL ${this.params.connect}`)
233
+ }
196
234
  })
197
235
  this.client.binaryType = "arraybuffer"
198
236
  const self = this
@@ -207,7 +245,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
207
245
  callback(new Error("write operation on read-only node"))
208
246
  else if (chunk.type !== self.params.type)
209
247
  callback(new Error(`written chunk is not of ${self.params.type} type`))
210
- else if (!self.client!.OPEN)
248
+ else if (self.client!.readyState !== ReconnWebSocket.OPEN)
211
249
  callback(new Error("still no WebSocket connection available"))
212
250
  else {
213
251
  const data = util.streamChunkEncode(chunk)
@@ -220,12 +258,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
220
258
  callback()
221
259
  },
222
260
  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) => {
261
+ if (self.params.mode === "w") {
262
+ self.log("error", "read operation on write-only node")
263
+ this.push(null)
264
+ return
265
+ }
266
+ if (self.chunkQueue === null)
267
+ return
268
+ const queue = self.chunkQueue
269
+ reads.add(queue.read().then((chunk) => {
226
270
  this.push(chunk, "binary")
227
271
  }).catch((err: Error) => {
228
272
  self.log("warning", `read on chunk queue operation failed: ${err}`)
273
+ if (queue.destroyed)
274
+ this.push(null)
229
275
  }))
230
276
  }
231
277
  })
@@ -234,8 +280,19 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
234
280
 
235
281
  /* close node */
236
282
  async close () {
283
+ /* drain and clear chunk queue reference */
284
+ if (this.chunkQueue !== null) {
285
+ this.chunkQueue.destroy()
286
+ this.chunkQueue = null
287
+ }
288
+
237
289
  /* close WebSocket server */
238
290
  if (this.server !== null) {
291
+ /* forcibly terminate all active client connections */
292
+ for (const client of this.server.clients)
293
+ client.terminate()
294
+
295
+ /* close connection */
239
296
  await new Promise<void>((resolve, reject) => {
240
297
  this.server!.close((error) => {
241
298
  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 */