speechflow 1.5.1 → 1.6.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 (232) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +192 -171
  3. package/etc/claude.md +83 -46
  4. package/etc/speechflow.yaml +84 -84
  5. package/package.json +3 -3
  6. package/speechflow-cli/dst/speechflow-main-api.d.ts +12 -0
  7. package/speechflow-cli/dst/speechflow-main-api.js +319 -0
  8. package/speechflow-cli/dst/speechflow-main-api.js.map +1 -0
  9. package/speechflow-cli/dst/speechflow-main-cli.d.ts +28 -0
  10. package/speechflow-cli/dst/speechflow-main-cli.js +271 -0
  11. package/speechflow-cli/dst/speechflow-main-cli.js.map +1 -0
  12. package/speechflow-cli/dst/speechflow-main-config.d.ts +9 -0
  13. package/speechflow-cli/dst/speechflow-main-config.js +27 -0
  14. package/speechflow-cli/dst/speechflow-main-config.js.map +1 -0
  15. package/speechflow-cli/dst/speechflow-main-graph.d.ts +34 -0
  16. package/speechflow-cli/dst/speechflow-main-graph.js +367 -0
  17. package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -0
  18. package/speechflow-cli/dst/speechflow-main-nodes.d.ts +10 -0
  19. package/speechflow-cli/dst/speechflow-main-nodes.js +60 -0
  20. package/speechflow-cli/dst/speechflow-main-nodes.js.map +1 -0
  21. package/speechflow-cli/dst/speechflow-main-status.d.ts +11 -0
  22. package/speechflow-cli/dst/speechflow-main-status.js +60 -0
  23. package/speechflow-cli/dst/speechflow-main-status.js.map +1 -0
  24. package/speechflow-cli/dst/speechflow-main.d.ts +7 -0
  25. package/speechflow-cli/dst/speechflow-main.js +127 -0
  26. package/speechflow-cli/dst/speechflow-main.js.map +1 -0
  27. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +4 -4
  28. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  29. package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +1 -1
  30. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +8 -9
  31. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  32. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +5 -5
  33. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +1 -1
  35. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +8 -9
  36. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  37. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.d.ts +1 -1
  38. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +8 -8
  39. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +1 -1
  41. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +6 -6
  42. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  43. package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +1 -1
  44. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +5 -5
  45. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
  46. package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +1 -1
  47. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +7 -7
  48. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  49. package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -1
  50. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +5 -5
  51. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-a2a-mute.d.ts +1 -1
  53. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +3 -3
  54. package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
  55. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +1 -1
  56. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +7 -7
  57. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  58. package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +1 -1
  59. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +7 -7
  60. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  61. package/speechflow-cli/dst/speechflow-node-a2a-vad.d.ts +1 -1
  62. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +7 -7
  63. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  64. package/speechflow-cli/dst/speechflow-node-a2a-wav.d.ts +1 -1
  65. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +3 -3
  66. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  67. package/speechflow-cli/dst/{speechflow-node-a2t-awstranscribe.d.ts → speechflow-node-a2t-amazon.d.ts} +1 -1
  68. package/speechflow-cli/dst/{speechflow-node-a2t-awstranscribe.js → speechflow-node-a2t-amazon.js} +11 -11
  69. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -0
  70. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +1 -1
  71. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +7 -7
  72. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  73. package/speechflow-cli/dst/{speechflow-node-a2t-openaitranscribe.d.ts → speechflow-node-a2t-openai.d.ts} +1 -1
  74. package/speechflow-cli/dst/{speechflow-node-a2t-openaitranscribe.js → speechflow-node-a2t-openai.js} +11 -11
  75. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -0
  76. package/speechflow-cli/dst/{speechflow-node-t2a-awspolly.d.ts → speechflow-node-t2a-amazon.d.ts} +1 -1
  77. package/speechflow-cli/dst/{speechflow-node-t2a-awspolly.js → speechflow-node-t2a-amazon.js} +9 -9
  78. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -0
  79. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.d.ts +1 -1
  80. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +5 -5
  81. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  82. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +1 -1
  83. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +7 -7
  84. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  85. package/speechflow-cli/dst/{speechflow-node-t2t-awstranslate.d.ts → speechflow-node-t2t-amazon.d.ts} +1 -1
  86. package/speechflow-cli/dst/{speechflow-node-t2t-awstranslate.js → speechflow-node-t2t-amazon.js} +7 -7
  87. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -0
  88. package/speechflow-cli/dst/speechflow-node-t2t-deepl.d.ts +1 -1
  89. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +5 -5
  90. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  91. package/speechflow-cli/dst/speechflow-node-t2t-format.d.ts +1 -1
  92. package/speechflow-cli/dst/speechflow-node-t2t-format.js +3 -3
  93. package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
  94. package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +1 -1
  95. package/speechflow-cli/dst/speechflow-node-t2t-google.js +8 -8
  96. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
  97. package/speechflow-cli/dst/{speechflow-node-a2a-dynamics.d.ts → speechflow-node-t2t-modify.d.ts} +1 -5
  98. package/speechflow-cli/dst/speechflow-node-t2t-modify.js +111 -0
  99. package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -0
  100. package/speechflow-cli/dst/speechflow-node-t2t-ollama.d.ts +1 -1
  101. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +5 -5
  102. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  103. package/speechflow-cli/dst/speechflow-node-t2t-openai.d.ts +1 -1
  104. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +5 -5
  105. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  106. package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +1 -1
  107. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +5 -5
  108. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  109. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.d.ts +1 -1
  110. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +5 -5
  111. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  112. package/speechflow-cli/dst/speechflow-node-t2t-transformers.d.ts +1 -1
  113. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +5 -5
  114. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  115. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -1
  116. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +5 -5
  117. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  118. package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -1
  119. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +3 -3
  120. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  121. package/speechflow-cli/dst/speechflow-node-xio-device.d.ts +1 -1
  122. package/speechflow-cli/dst/speechflow-node-xio-device.js +8 -8
  123. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  124. package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -1
  125. package/speechflow-cli/dst/speechflow-node-xio-file.js +50 -29
  126. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  127. package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -1
  128. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +7 -7
  129. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  130. package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -1
  131. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +10 -10
  132. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  133. package/speechflow-cli/dst/{speechflow-utils-audio-wt.js → speechflow-util-audio-wt.js} +1 -1
  134. package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -0
  135. package/speechflow-cli/dst/speechflow-util-audio.d.ts +22 -0
  136. package/speechflow-cli/dst/speechflow-util-audio.js +251 -0
  137. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -0
  138. package/speechflow-cli/dst/speechflow-util-error.d.ts +14 -0
  139. package/speechflow-cli/dst/speechflow-util-error.js +131 -0
  140. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -0
  141. package/speechflow-cli/dst/speechflow-util-queue.d.ts +68 -0
  142. package/speechflow-cli/dst/speechflow-util-queue.js +338 -0
  143. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -0
  144. package/speechflow-cli/dst/speechflow-util-stream.d.ts +18 -0
  145. package/speechflow-cli/dst/speechflow-util-stream.js +219 -0
  146. package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -0
  147. package/speechflow-cli/dst/speechflow-util-webaudio-wt.js +124 -0
  148. package/speechflow-cli/dst/speechflow-util-webaudio-wt.js.map +1 -0
  149. package/speechflow-cli/dst/{speechflow-utils-audio.js → speechflow-util-webaudio.js} +2 -2
  150. package/speechflow-cli/dst/speechflow-util-webaudio.js.map +1 -0
  151. package/speechflow-cli/dst/speechflow-util.d.ts +4 -0
  152. package/speechflow-cli/dst/speechflow-util.js +26 -0
  153. package/speechflow-cli/dst/speechflow-util.js.map +1 -0
  154. package/speechflow-cli/dst/speechflow.js +3 -906
  155. package/speechflow-cli/dst/speechflow.js.map +1 -1
  156. package/speechflow-cli/etc/oxlint.jsonc +4 -1
  157. package/speechflow-cli/package.json +12 -11
  158. package/speechflow-cli/src/speechflow-main-api.ts +315 -0
  159. package/speechflow-cli/src/speechflow-main-cli.ts +259 -0
  160. package/speechflow-cli/src/speechflow-main-config.ts +17 -0
  161. package/speechflow-cli/src/speechflow-main-graph.ts +372 -0
  162. package/speechflow-cli/src/speechflow-main-nodes.ts +61 -0
  163. package/speechflow-cli/src/speechflow-main-status.ts +70 -0
  164. package/speechflow-cli/src/speechflow-main.ts +106 -0
  165. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -4
  166. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +7 -8
  167. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +5 -5
  168. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +7 -8
  169. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +7 -7
  170. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +6 -6
  171. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +4 -4
  172. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +6 -6
  173. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +4 -4
  174. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +2 -2
  175. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +6 -6
  176. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +6 -6
  177. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +6 -6
  178. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +2 -2
  179. package/speechflow-cli/src/{speechflow-node-a2t-awstranscribe.ts → speechflow-node-a2t-amazon.ts} +10 -10
  180. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +7 -7
  181. package/speechflow-cli/src/{speechflow-node-a2t-openaitranscribe.ts → speechflow-node-a2t-openai.ts} +10 -10
  182. package/speechflow-cli/src/{speechflow-node-t2a-awspolly.ts → speechflow-node-t2a-amazon.ts} +7 -7
  183. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +4 -4
  184. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +6 -6
  185. package/speechflow-cli/src/{speechflow-node-t2t-awstranslate.ts → speechflow-node-t2t-amazon.ts} +5 -5
  186. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +4 -4
  187. package/speechflow-cli/src/speechflow-node-t2t-format.ts +2 -2
  188. package/speechflow-cli/src/speechflow-node-t2t-google.ts +7 -7
  189. package/speechflow-cli/src/speechflow-node-t2t-modify.ts +84 -0
  190. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +4 -4
  191. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +4 -4
  192. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +4 -4
  193. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +9 -9
  194. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +4 -4
  195. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -4
  196. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +2 -2
  197. package/speechflow-cli/src/speechflow-node-xio-device.ts +7 -7
  198. package/speechflow-cli/src/speechflow-node-xio-file.ts +49 -28
  199. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +7 -7
  200. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +9 -9
  201. package/speechflow-cli/src/{speechflow-utils-audio.ts → speechflow-util-audio.ts} +131 -1
  202. package/speechflow-cli/src/speechflow-util-error.ts +184 -0
  203. package/speechflow-cli/src/speechflow-util-queue.ts +320 -0
  204. package/speechflow-cli/src/speechflow-util-stream.ts +197 -0
  205. package/speechflow-cli/src/speechflow-util.ts +10 -0
  206. package/speechflow-cli/src/speechflow.ts +3 -947
  207. package/speechflow-ui-db/package.json +3 -3
  208. package/speechflow-ui-st/dst/app-font-fa-brands-400.woff2 +0 -0
  209. package/speechflow-ui-st/dst/app-font-fa-regular-400.woff2 +0 -0
  210. package/speechflow-ui-st/dst/app-font-fa-solid-900.woff2 +0 -0
  211. package/speechflow-ui-st/dst/app-font-fa-v4compatibility.woff2 +0 -0
  212. package/speechflow-ui-st/dst/index.css +2 -2
  213. package/speechflow-ui-st/dst/index.js +32 -33
  214. package/speechflow-ui-st/package.json +4 -4
  215. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +0 -208
  216. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +0 -1
  217. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +0 -312
  218. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +0 -1
  219. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +0 -1
  220. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +0 -1
  221. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +0 -1
  222. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +0 -1
  223. package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +0 -1
  224. package/speechflow-cli/dst/speechflow-utils-audio.js.map +0 -1
  225. package/speechflow-cli/dst/speechflow-utils.d.ts +0 -108
  226. package/speechflow-cli/dst/speechflow-utils.js +0 -740
  227. package/speechflow-cli/dst/speechflow-utils.js.map +0 -1
  228. package/speechflow-cli/src/speechflow-utils.ts +0 -804
  229. /package/speechflow-cli/dst/{speechflow-node-a2a-dynamics-wt.d.ts → speechflow-util-audio-wt.d.ts} +0 -0
  230. /package/speechflow-cli/dst/{speechflow-utils-audio-wt.d.ts → speechflow-util-webaudio-wt.d.ts} +0 -0
  231. /package/speechflow-cli/dst/{speechflow-utils-audio.d.ts → speechflow-util-webaudio.d.ts} +0 -0
  232. /package/speechflow-cli/src/{speechflow-utils-audio-wt.ts → speechflow-util-audio-wt.ts} +0 -0
@@ -0,0 +1,184 @@
1
+ /*
2
+ ** SpeechFlow - Speech Processing Flow Graph
3
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+
7
+ /* helper function for retrieving an Error object */
8
+ export function ensureError (error: unknown, prefix?: string, debug = false): Error {
9
+ if (error instanceof Error && prefix === undefined && debug === false)
10
+ return error
11
+ let msg = error instanceof Error ?
12
+ error.message : String(error)
13
+ if (prefix)
14
+ msg = `${prefix}: ${msg}`
15
+ if (debug && error instanceof Error)
16
+ msg = `${msg}\n${error.stack}`
17
+ return new Error(msg, { cause: error })
18
+ }
19
+
20
+ /* helper function for retrieving a Promise object */
21
+ export function ensurePromise<T> (arg: T | Promise<T>): Promise<T> {
22
+ if (!(arg instanceof Promise))
23
+ arg = Promise.resolve(arg)
24
+ return arg
25
+ }
26
+
27
+ /* helper function for running the finally code of "run" */
28
+ function runFinally (onfinally?: () => void) {
29
+ if (!onfinally)
30
+ return
31
+ try { onfinally() }
32
+ catch (_error: unknown) { /* ignored */ }
33
+ }
34
+
35
+ /* helper type for ensuring T contains no Promise */
36
+ type runNoPromise<T> =
37
+ [ T ] extends [ Promise<any> ] ? never : T
38
+
39
+ /* run a synchronous or asynchronous action */
40
+ export function run<T, X extends runNoPromise<T> | never> (
41
+ action: () => X,
42
+ oncatch?: (error: Error) => X | never,
43
+ onfinally?: () => void
44
+ ): X
45
+ export function run<T, X extends runNoPromise<T> | never> (
46
+ description: string,
47
+ action: () => X,
48
+ oncatch?: (error: Error) => X | never,
49
+ onfinally?: () => void
50
+ ): X
51
+ export function run<T, X extends (T | Promise<T>)> (
52
+ action: () => X,
53
+ oncatch?: (error: Error) => X,
54
+ onfinally?: () => void
55
+ ): Promise<T>
56
+ export function run<T, X extends (T | Promise<T>)> (
57
+ description: string,
58
+ action: () => X,
59
+ oncatch?: (error: Error) => X,
60
+ onfinally?: () => void
61
+ ): Promise<T>
62
+ export function run<T> (
63
+ ...args: any[]
64
+ ): T | Promise<T> | never {
65
+ /* support overloaded signatures */
66
+ let description: string | undefined
67
+ let action: () => T | Promise<T> | never
68
+ let oncatch: (error: Error) => T | Promise<T> | never
69
+ let onfinally: () => void
70
+ if (typeof args[0] === "string") {
71
+ description = args[0]
72
+ action = args[1]
73
+ oncatch = args[2]
74
+ onfinally = args[3]
75
+ }
76
+ else {
77
+ action = args[0]
78
+ oncatch = args[1]
79
+ onfinally = args[2]
80
+ }
81
+
82
+ /* perform the action */
83
+ let result: T | Promise<T>
84
+ try {
85
+ result = action()
86
+ }
87
+ catch (arg: unknown) {
88
+ /* synchronous case (error branch) */
89
+ let error = ensureError(arg, description)
90
+ if (oncatch) {
91
+ try {
92
+ result = oncatch(error)
93
+ }
94
+ catch (arg: unknown) {
95
+ error = ensureError(arg, description)
96
+ runFinally(onfinally)
97
+ throw error
98
+ }
99
+ runFinally(onfinally)
100
+ return result
101
+ }
102
+ runFinally(onfinally)
103
+ throw error
104
+ }
105
+ if (result instanceof Promise) {
106
+ /* asynchronous case (result or error branch) */
107
+ return result.catch((arg: unknown) => {
108
+ /* asynchronous case (error branch) */
109
+ let error = ensureError(arg, description)
110
+ if (oncatch) {
111
+ try {
112
+ return oncatch(error)
113
+ }
114
+ catch (arg: unknown) {
115
+ error = ensureError(arg, description)
116
+ throw error
117
+ }
118
+ }
119
+ throw error
120
+ }).finally(() => {
121
+ /* asynchronous case (result and error branch) */
122
+ runFinally(onfinally)
123
+ })
124
+ }
125
+ else {
126
+ /* synchronous case (result branch) */
127
+ runFinally(onfinally)
128
+ return result
129
+ }
130
+ }
131
+
132
+ /* run a synchronous or asynchronous action */
133
+ /* eslint @typescript-eslint/unified-signatures: off */
134
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
135
+ action: F,
136
+ oncatch?: (error: Error) => X | never,
137
+ onfinally?: () => void
138
+ ): F
139
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
140
+ description: string,
141
+ action: F,
142
+ oncatch?: (error: Error) => X | never,
143
+ onfinally?: () => void
144
+ ): F
145
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
146
+ action: F,
147
+ oncatch?: (error: Error) => X,
148
+ onfinally?: () => void
149
+ ): F
150
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
151
+ description: string,
152
+ action: F,
153
+ oncatch?: (error: Error) => X,
154
+ onfinally?: () => void
155
+ ): F
156
+ export function runner<T> (
157
+ ...args: any[]
158
+ ): (...args: any[]) => T | Promise<T> | never {
159
+ /* support overloaded signatures */
160
+ let description: string | undefined
161
+ let action: (...args: any[]) => T | Promise<T> | never
162
+ let oncatch: (error: Error) => T | Promise<T> | never
163
+ let onfinally: () => void
164
+ if (typeof args[0] === "string") {
165
+ description = args[0]
166
+ action = args[1]
167
+ oncatch = args[2]
168
+ onfinally = args[3]
169
+ }
170
+ else {
171
+ action = args[0]
172
+ oncatch = args[1]
173
+ onfinally = args[2]
174
+ }
175
+
176
+ /* wrap the "run" operation on "action" into function
177
+ which exposes the signature of "action" */
178
+ return (...args: any[]) => {
179
+ if (description)
180
+ return run(description, () => action(...args), oncatch, onfinally)
181
+ else
182
+ return run(() => action(...args), oncatch, onfinally)
183
+ }
184
+ }
@@ -0,0 +1,320 @@
1
+ /*
2
+ ** SpeechFlow - Speech Processing Flow Graph
3
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+
7
+ /* standard dependencies */
8
+ import { EventEmitter } from "node:events"
9
+ import { type, type Type } from "arktype"
10
+
11
+ /* external dependencies */
12
+ import { Duration } from "luxon"
13
+ import * as IntervalTree from "node-interval-tree"
14
+
15
+ /* internal dependencies */
16
+ import * as util from "./speechflow-util"
17
+
18
+ /* import an object with parsing and strict error handling */
19
+ export function importObject<T>(name: string, arg: object | string, validator: Type<T, {}>): T {
20
+ const obj: object = typeof arg === "string" ?
21
+ util.run(`${name}: parsing JSON`, () => JSON.parse(arg)) :
22
+ arg
23
+ const result = validator(obj)
24
+ if (result instanceof type.errors)
25
+ throw new Error(`${name}: validation: ${result.summary}`)
26
+ return result as T
27
+ }
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, reject) => {
38
+ const consume = () =>
39
+ this.queue.length > 0 ? this.queue.pop()! : null
40
+ const tryToConsume = () => {
41
+ const item = consume()
42
+ if (item !== null)
43
+ resolve(item)
44
+ else
45
+ this.once("dequeue", tryToConsume)
46
+ }
47
+ tryToConsume()
48
+ })
49
+ }
50
+ }
51
+
52
+ /* helper class for double-item queue */
53
+ export class DoubleQueue<T0, T1> extends EventEmitter {
54
+ private queue0 = new Array<T0>()
55
+ private queue1 = new Array<T1>()
56
+ private notify () {
57
+ if (this.queue0.length > 0 && this.queue1.length > 0)
58
+ this.emit("dequeue")
59
+ }
60
+ write0 (item: T0) {
61
+ this.queue0.unshift(item)
62
+ this.notify()
63
+ }
64
+ write1 (item: T1) {
65
+ this.queue1.unshift(item)
66
+ this.notify()
67
+ }
68
+ read () {
69
+ return new Promise<[ T0, T1 ]>((resolve, reject) => {
70
+ const consume = (): [ T0, T1 ] | null => {
71
+ if (this.queue0.length > 0 && this.queue1.length > 0) {
72
+ const item0 = this.queue0.pop() as T0
73
+ const item1 = this.queue1.pop() as T1
74
+ return [ item0, item1 ]
75
+ }
76
+ return null
77
+ }
78
+ const tryToConsume = () => {
79
+ const items = consume()
80
+ if (items !== null)
81
+ resolve(items)
82
+ else
83
+ this.once("dequeue", tryToConsume)
84
+ }
85
+ tryToConsume()
86
+ })
87
+ }
88
+ }
89
+
90
+ /* queue element */
91
+ export type QueueElement = { type: string }
92
+
93
+ /* queue pointer */
94
+ export class QueuePointer<T extends QueueElement> extends EventEmitter {
95
+ /* internal state */
96
+ private index = 0
97
+
98
+ /* construction */
99
+ constructor (
100
+ private name: string,
101
+ private queue: Queue<T>
102
+ ) {
103
+ super()
104
+ this.setMaxListeners(100)
105
+ }
106
+
107
+ /* positioning operations */
108
+ maxPosition () {
109
+ return this.queue.elements.length
110
+ }
111
+ position (index?: number): number {
112
+ if (index !== undefined) {
113
+ this.index = Math.max(0, Math.min(index, this.queue.elements.length))
114
+ this.emit("position", this.index)
115
+ }
116
+ return this.index
117
+ }
118
+ walk (num: number) {
119
+ const indexOld = this.index
120
+ if (num > 0)
121
+ this.index = Math.min(this.index + num, this.queue.elements.length)
122
+ else if (num < 0)
123
+ this.index = Math.max(this.index + num, 0)
124
+ if (this.index !== indexOld)
125
+ this.emit("position", { start: this.index })
126
+ }
127
+ walkForwardUntil (type: T["type"]) {
128
+ while (this.index < this.queue.elements.length
129
+ && this.queue.elements[this.index].type !== type)
130
+ this.index++
131
+ this.emit("position", { start: this.index })
132
+ }
133
+ walkBackwardUntil (type: T["type"]) {
134
+ while (this.index > 0
135
+ && this.queue.elements[this.index].type !== type)
136
+ this.index--
137
+ this.emit("position", { start: this.index })
138
+ }
139
+
140
+ /* search operations */
141
+ searchForward (type: T["type"]) {
142
+ let position = this.index
143
+ while (position < this.queue.elements.length
144
+ && this.queue.elements[position].type !== type)
145
+ position++
146
+ this.emit("search", { start: this.index, end: position })
147
+ return position
148
+ }
149
+ searchBackward (type: T["type"]) {
150
+ let position = this.index
151
+ while (position > 0
152
+ && this.queue.elements[position].type !== type)
153
+ position--
154
+ this.emit("search", { start: position, end: this.index })
155
+ return position
156
+ }
157
+
158
+ /* reading operations */
159
+ peek (position?: number) {
160
+ if (position === undefined)
161
+ position = this.index
162
+ position = Math.max(0, Math.min(position, this.queue.elements.length))
163
+ const element = this.queue.elements[position]
164
+ this.queue.emit("read", { start: position, end: position })
165
+ return element
166
+ }
167
+ read () {
168
+ const element = this.queue.elements[this.index]
169
+ if (this.index < this.queue.elements.length)
170
+ this.index++
171
+ this.queue.emit("read", { start: this.index - 1, end: this.index - 1 })
172
+ return element
173
+ }
174
+ slice (size?: number) {
175
+ let slice: T[]
176
+ const start = this.index
177
+ if (size !== undefined) {
178
+ size = Math.max(0, Math.min(size, this.queue.elements.length - this.index))
179
+ slice = this.queue.elements.slice(this.index, this.index + size)
180
+ this.index += size
181
+ }
182
+ else {
183
+ slice = this.queue.elements.slice(this.index)
184
+ this.index = this.queue.elements.length
185
+ }
186
+ this.queue.emit("read", { start, end: this.index })
187
+ return slice
188
+ }
189
+
190
+ /* writing operations */
191
+ touch () {
192
+ if (this.index >= this.queue.elements.length)
193
+ throw new Error("cannot touch after last element")
194
+ this.queue.emit("write", { start: this.index, end: this.index + 1 })
195
+ }
196
+ append (element: T) {
197
+ this.queue.elements.push(element)
198
+ this.index = this.queue.elements.length
199
+ this.queue.emit("write", { start: this.index - 1, end: this.index - 1 })
200
+ }
201
+ insert (element: T) {
202
+ this.queue.elements.splice(this.index, 0, element)
203
+ this.queue.emit("write", { start: this.index - 1, end: this.index })
204
+ }
205
+ delete () {
206
+ if (this.index >= this.queue.elements.length)
207
+ throw new Error("cannot delete after last element")
208
+ this.queue.elements.splice(this.index, 1)
209
+ this.queue.emit("write", { start: this.index, end: this.index })
210
+ }
211
+ }
212
+
213
+ /* queue */
214
+ export class Queue<T extends QueueElement> extends EventEmitter {
215
+ public elements: T[] = []
216
+ private pointers = new Map<string, QueuePointer<T>>()
217
+ constructor () {
218
+ super()
219
+ this.setMaxListeners(100)
220
+ }
221
+ pointerUse (name: string): QueuePointer<T> {
222
+ if (!this.pointers.has(name))
223
+ this.pointers.set(name, new QueuePointer<T>(name, this))
224
+ return this.pointers.get(name)!
225
+ }
226
+ pointerDelete (name: string): void {
227
+ if (!this.pointers.has(name))
228
+ throw new Error("pointer not exists")
229
+ this.pointers.delete(name)
230
+ }
231
+ trim (): void {
232
+ /* determine minimum pointer position */
233
+ let min = this.elements.length
234
+ for (const pointer of this.pointers.values())
235
+ if (min > pointer.position())
236
+ min = pointer.position()
237
+
238
+ /* trim the maximum amount of first elements */
239
+ if (min > 0) {
240
+ this.elements.splice(0, min)
241
+
242
+ /* shift all pointers */
243
+ for (const pointer of this.pointers.values())
244
+ pointer.position(pointer.position() - min)
245
+ }
246
+ }
247
+ }
248
+
249
+ /* meta store */
250
+ interface TimeStoreInterval<T> extends IntervalTree.Interval {
251
+ item: T
252
+ }
253
+ export class TimeStore<T> extends EventEmitter {
254
+ private tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
255
+ store (start: Duration, end: Duration, item: T): void {
256
+ this.tree.insert({ low: start.toMillis(), high: end.toMillis(), item })
257
+ }
258
+ fetch (start: Duration, end: Duration): T[] {
259
+ const intervals = this.tree.search(start.toMillis(), end.toMillis())
260
+ return intervals.map((interval) => interval.item)
261
+ }
262
+ prune (_before: Duration): void {
263
+ const before = _before.toMillis()
264
+ const intervals = this.tree.search(0, before - 1)
265
+ for (const interval of intervals)
266
+ if (interval.low < before && interval.high < before)
267
+ this.tree.remove(interval)
268
+ }
269
+ clear (): void {
270
+ this.tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
271
+ }
272
+ }
273
+
274
+ /* asynchronous queue */
275
+ export class AsyncQueue<T> {
276
+ private queue: Array<T | null> = []
277
+ private resolvers: ((v: T | null) => void)[] = []
278
+ write (v: T | null) {
279
+ const resolve = this.resolvers.shift()
280
+ if (resolve)
281
+ resolve(v)
282
+ else
283
+ this.queue.push(v)
284
+ }
285
+ async read () {
286
+ if (this.queue.length > 0)
287
+ return this.queue.shift()!
288
+ else
289
+ return new Promise<T | null>((resolve) => this.resolvers.push(resolve))
290
+ }
291
+ destroy () {
292
+ for (const resolve of this.resolvers)
293
+ resolve(null)
294
+ this.resolvers = []
295
+ this.queue = []
296
+ }
297
+ }
298
+
299
+ /* cached regular expression class */
300
+ export class CachedRegExp {
301
+ private cache = new Map<string, RegExp>()
302
+ compile (pattern: string): RegExp | null {
303
+ if (this.cache.has(pattern))
304
+ return this.cache.get(pattern)!
305
+ try {
306
+ const regex = new RegExp(pattern)
307
+ this.cache.set(pattern, regex)
308
+ return regex
309
+ }
310
+ catch (_error) {
311
+ return null
312
+ }
313
+ }
314
+ clear (): void {
315
+ this.cache.clear()
316
+ }
317
+ size (): number {
318
+ return this.cache.size
319
+ }
320
+ }
@@ -0,0 +1,197 @@
1
+ /*
2
+ ** SpeechFlow - Speech Processing Flow Graph
3
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+
7
+ /* standard dependencies */
8
+ import Stream from "node:stream"
9
+
10
+ /* external dependencies */
11
+ import { DateTime, Duration } from "luxon"
12
+ import * as CBOR from "cbor2"
13
+
14
+ /* internal dependencies */
15
+ import { SpeechFlowChunk } from "./speechflow-node"
16
+ import * as util from "./speechflow-util"
17
+
18
+ /* create a Duplex/Transform stream which has
19
+ object-mode on Writable side and buffer/string-mode on Readable side */
20
+ export function createTransformStreamForWritableSide () {
21
+ return new Stream.Transform({
22
+ readableObjectMode: true,
23
+ writableObjectMode: true,
24
+ decodeStrings: false,
25
+ highWaterMark: 1,
26
+ transform (chunk: SpeechFlowChunk, encoding, callback) {
27
+ this.push(chunk.payload)
28
+ callback()
29
+ },
30
+ final (callback) {
31
+ this.push(null)
32
+ callback()
33
+ }
34
+ })
35
+ }
36
+
37
+ /* create a Duplex/Transform stream which has
38
+ object-mode on Readable side and buffer/string-mode on Writable side */
39
+ export function createTransformStreamForReadableSide (type: "text" | "audio", getTimeZero: () => DateTime) {
40
+ return new Stream.Transform({
41
+ readableObjectMode: true,
42
+ writableObjectMode: true,
43
+ decodeStrings: false,
44
+ highWaterMark: (type === "audio" ? 19200 : 65536), /* audio: 400ms @ 48kHz/16bit/mono, text: 64KB */
45
+ transform (chunk: Buffer | string, encoding, callback) {
46
+ if (chunk === null) {
47
+ this.push(null)
48
+ callback()
49
+ return
50
+ }
51
+ const timeZero = getTimeZero()
52
+ const start = DateTime.now().diff(timeZero)
53
+ let end = start
54
+ if (type === "audio") {
55
+ const duration = util.audioBufferDuration(chunk as Buffer)
56
+ end = start.plus(duration * 1000)
57
+ }
58
+ const payload = ensureStreamChunk(type, chunk) as Buffer | string
59
+ const obj = new SpeechFlowChunk(start, end, "final", type, payload)
60
+ this.push(obj)
61
+ callback()
62
+ },
63
+ final (callback) {
64
+ this.push(null)
65
+ callback()
66
+ }
67
+ })
68
+ }
69
+
70
+ /* ensure a chunk is of a certain type and format */
71
+ export function ensureStreamChunk (type: "audio" | "text", chunk: SpeechFlowChunk | Buffer | string) {
72
+ if (chunk instanceof SpeechFlowChunk) {
73
+ if (chunk.type !== type)
74
+ throw new Error(`invalid payload chunk (expected ${type} type, received ${chunk.type} type)`)
75
+ }
76
+ else {
77
+ if (type === "text" && Buffer.isBuffer(chunk))
78
+ chunk = chunk.toString("utf8")
79
+ else if (type === "audio" && !Buffer.isBuffer(chunk))
80
+ chunk = Buffer.from(chunk)
81
+ }
82
+ return chunk
83
+ }
84
+
85
+ /* type of a serialized SpeechFlow chunk */
86
+ type SpeechFlowChunkSerialized = {
87
+ timestampStart: number,
88
+ timestampEnd: number,
89
+ kind: string,
90
+ type: string,
91
+ payload: Uint8Array
92
+ }
93
+
94
+ /* encode/serialize chunk of data */
95
+ export function streamChunkEncode (chunk: SpeechFlowChunk) {
96
+ let payload: Uint8Array
97
+ if (Buffer.isBuffer(chunk.payload))
98
+ payload = new Uint8Array(chunk.payload)
99
+ else {
100
+ const encoder = new TextEncoder()
101
+ payload = encoder.encode(chunk.payload)
102
+ }
103
+ const data = {
104
+ timestampStart: chunk.timestampStart.toMillis(),
105
+ timestampEnd: chunk.timestampEnd.toMillis(),
106
+ kind: chunk.kind,
107
+ type: chunk.type,
108
+ payload
109
+ } satisfies SpeechFlowChunkSerialized
110
+ const _data = CBOR.encode(data)
111
+ return _data
112
+ }
113
+
114
+ /* decode/unserialize chunk of data */
115
+ export function streamChunkDecode (_data: Uint8Array) {
116
+ let data: SpeechFlowChunkSerialized
117
+ try {
118
+ data = CBOR.decode<SpeechFlowChunkSerialized>(_data)
119
+ }
120
+ catch (err: unknown) {
121
+ throw util.ensureError(err, "CBOR decoding failed")
122
+ }
123
+ let payload: Buffer | string
124
+ if (data.type === "audio")
125
+ payload = Buffer.from(data.payload)
126
+ else
127
+ payload = (new TextDecoder()).decode(data.payload)
128
+ const chunk = new SpeechFlowChunk(
129
+ Duration.fromMillis(data.timestampStart),
130
+ Duration.fromMillis(data.timestampEnd),
131
+ data.kind as "intermediate" | "final",
132
+ data.type as "audio" | "text",
133
+ payload
134
+ )
135
+ return chunk
136
+ }
137
+
138
+ /* utility class for wrapping a custom stream into a regular Transform stream */
139
+ export class StreamWrapper extends Stream.Transform {
140
+ private foreignStream: any
141
+ private onData = (chunk: any) => { this.push(chunk) }
142
+ private onError = (err: Error) => { this.emit("error", err) }
143
+ private onEnd = () => { this.push(null) }
144
+ constructor (foreignStream: any, options: Stream.TransformOptions = {}) {
145
+ options.readableObjectMode = true
146
+ options.writableObjectMode = true
147
+ super(options)
148
+ this.foreignStream = foreignStream
149
+ if (typeof this.foreignStream.on === "function") {
150
+ this.foreignStream.on("data", this.onData)
151
+ this.foreignStream.on("error", this.onError)
152
+ this.foreignStream.on("end", this.onEnd)
153
+ }
154
+ }
155
+ _transform (chunk: any, encoding: BufferEncoding, callback: Stream.TransformCallback): void {
156
+ if (this.destroyed) {
157
+ callback(new Error("stream already destroyed"))
158
+ return
159
+ }
160
+ try {
161
+ if (typeof this.foreignStream.write === "function") {
162
+ const canContinue = this.foreignStream.write(chunk)
163
+ if (canContinue)
164
+ callback()
165
+ else
166
+ this.foreignStream.once("drain", callback)
167
+ }
168
+ else
169
+ throw new Error("foreign stream lacks write method")
170
+ }
171
+ catch (err: unknown) {
172
+ callback(util.ensureError(err))
173
+ }
174
+ }
175
+ _flush (callback: Stream.TransformCallback): void {
176
+ if (this.destroyed) {
177
+ callback(new Error("stream already destroyed"))
178
+ return
179
+ }
180
+ try {
181
+ if (typeof this.foreignStream.end === "function")
182
+ this.foreignStream.end()
183
+ callback()
184
+ }
185
+ catch (err: unknown) {
186
+ callback(util.ensureError(err))
187
+ }
188
+ }
189
+ _destroy (error: Error | null, callback: Stream.TransformCallback): void {
190
+ if (typeof this.foreignStream.removeListener === "function") {
191
+ this.foreignStream.removeListener("data", this.onData)
192
+ this.foreignStream.removeListener("error", this.onError)
193
+ this.foreignStream.removeListener("end", this.onEnd)
194
+ }
195
+ super._destroy(error, callback)
196
+ }
197
+ }