speechflow 1.5.0 → 1.6.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 (177) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +210 -167
  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-node-a2a-compressor.d.ts +1 -1
  7. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +4 -4
  8. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  9. package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +1 -1
  10. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +4 -4
  11. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  12. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.d.ts +1 -1
  13. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +5 -15
  14. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  15. package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +1 -1
  16. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +4 -4
  17. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  18. package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +1 -1
  19. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +3 -3
  20. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
  21. package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +1 -1
  22. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +3 -3
  23. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  24. package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -1
  25. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +3 -3
  26. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  27. package/speechflow-cli/dst/speechflow-node-a2a-mute.d.ts +1 -1
  28. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +3 -3
  29. package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
  30. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +1 -1
  31. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +3 -3
  32. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  33. package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +1 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +3 -3
  35. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-vad.d.ts +1 -1
  37. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
  38. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  39. package/speechflow-cli/dst/speechflow-node-a2a-wav.d.ts +1 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +3 -3
  41. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  42. package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +18 -0
  43. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +312 -0
  44. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -0
  45. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +1 -1
  46. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +7 -12
  47. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -1
  48. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +1 -1
  49. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +4 -4
  50. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  51. package/speechflow-cli/dst/speechflow-node-a2t-openai.d.ts +19 -0
  52. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +351 -0
  53. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -0
  54. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +1 -1
  55. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +6 -6
  56. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -1
  57. package/speechflow-cli/dst/speechflow-node-t2a-amazon.d.ts +16 -0
  58. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +204 -0
  59. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -0
  60. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +1 -1
  61. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +40 -7
  62. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -1
  63. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.d.ts +1 -1
  64. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +5 -5
  65. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  66. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +1 -1
  67. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +41 -7
  68. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  69. package/speechflow-cli/dst/speechflow-node-t2t-amazon.d.ts +13 -0
  70. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +175 -0
  71. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -0
  72. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +1 -1
  73. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +39 -5
  74. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -1
  75. package/speechflow-cli/dst/speechflow-node-t2t-deepl.d.ts +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +6 -5
  77. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  78. package/speechflow-cli/dst/speechflow-node-t2t-format.d.ts +1 -1
  79. package/speechflow-cli/dst/speechflow-node-t2t-format.js +3 -3
  80. package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +13 -0
  82. package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
  83. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
  84. package/speechflow-cli/dst/speechflow-node-t2t-modify.d.ts +11 -0
  85. package/speechflow-cli/dst/speechflow-node-t2t-modify.js +111 -0
  86. package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -0
  87. package/speechflow-cli/dst/speechflow-node-t2t-ollama.d.ts +1 -1
  88. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +39 -5
  89. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  90. package/speechflow-cli/dst/speechflow-node-t2t-openai.d.ts +1 -1
  91. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +39 -5
  92. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  93. package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +1 -1
  94. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +3 -3
  95. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.d.ts +1 -1
  97. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +6 -5
  98. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  99. package/speechflow-cli/dst/speechflow-node-t2t-transformers.d.ts +1 -1
  100. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +6 -5
  101. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  102. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -1
  103. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -3
  104. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  105. package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -1
  106. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +3 -3
  107. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  108. package/speechflow-cli/dst/speechflow-node-xio-device.d.ts +1 -1
  109. package/speechflow-cli/dst/speechflow-node-xio-device.js +3 -3
  110. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  111. package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -1
  112. package/speechflow-cli/dst/speechflow-node-xio-file.js +43 -22
  113. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  114. package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -1
  115. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +3 -3
  116. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  117. package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -1
  118. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +3 -3
  119. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  120. package/speechflow-cli/dst/speechflow-utils.d.ts +16 -0
  121. package/speechflow-cli/dst/speechflow-utils.js +140 -1
  122. package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
  123. package/speechflow-cli/dst/speechflow.js +19 -19
  124. package/speechflow-cli/dst/speechflow.js.map +1 -1
  125. package/speechflow-cli/etc/biome.jsonc +2 -1
  126. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  127. package/speechflow-cli/package.json +16 -15
  128. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +4 -4
  129. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +4 -4
  130. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +4 -14
  131. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +4 -4
  132. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +2 -2
  133. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +2 -2
  134. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
  135. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +2 -2
  136. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +2 -2
  137. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +2 -2
  138. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +2 -2
  139. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +2 -2
  140. package/speechflow-cli/src/{speechflow-node-a2t-awstranscribe.ts → speechflow-node-a2t-amazon.ts} +11 -13
  141. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +4 -4
  142. package/speechflow-cli/src/{speechflow-node-a2t-openaitranscribe.ts → speechflow-node-a2t-openai.ts} +7 -7
  143. package/speechflow-cli/src/{speechflow-node-t2a-awspolly.ts → speechflow-node-t2a-amazon.ts} +8 -8
  144. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +4 -4
  145. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +7 -6
  146. package/speechflow-cli/src/{speechflow-node-t2t-awstranslate.ts → speechflow-node-t2t-amazon.ts} +6 -5
  147. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +5 -4
  148. package/speechflow-cli/src/speechflow-node-t2t-format.ts +2 -2
  149. package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
  150. package/speechflow-cli/src/speechflow-node-t2t-modify.ts +84 -0
  151. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +5 -4
  152. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +5 -4
  153. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +2 -2
  154. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +10 -9
  155. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +5 -4
  156. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -2
  157. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +2 -2
  158. package/speechflow-cli/src/speechflow-node-xio-device.ts +2 -2
  159. package/speechflow-cli/src/speechflow-node-xio-file.ts +43 -21
  160. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +2 -2
  161. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +2 -2
  162. package/speechflow-cli/src/speechflow-utils.ts +196 -1
  163. package/speechflow-cli/src/speechflow.ts +22 -22
  164. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  165. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  166. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  167. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  168. package/speechflow-ui-db/dst/index.css +2 -2
  169. package/speechflow-ui-db/dst/index.js +37 -38
  170. package/speechflow-ui-db/package.json +10 -10
  171. package/speechflow-ui-st/dst/app-font-fa-brands-400.woff2 +0 -0
  172. package/speechflow-ui-st/dst/app-font-fa-regular-400.woff2 +0 -0
  173. package/speechflow-ui-st/dst/app-font-fa-solid-900.woff2 +0 -0
  174. package/speechflow-ui-st/dst/app-font-fa-v4compatibility.woff2 +0 -0
  175. package/speechflow-ui-st/dst/index.css +2 -2
  176. package/speechflow-ui-st/dst/index.js +32 -33
  177. package/speechflow-ui-st/package.json +11 -11
@@ -18,6 +18,7 @@ import HAPIWebSocket from "hapi-plugin-websocket"
18
18
 
19
19
  /* internal dependencies */
20
20
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
21
+ import * as utils from "./speechflow-utils"
21
22
 
22
23
  type wsPeerCtx = {
23
24
  peer: string
@@ -29,9 +30,9 @@ type wsPeerInfo = {
29
30
  }
30
31
 
31
32
  /* SpeechFlow node for subtitle (text-to-text) "translations" */
32
- export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
33
+ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
33
34
  /* declare official node name */
34
- public static name = "subtitle"
35
+ public static name = "t2t-subtitle"
35
36
 
36
37
  /* internal state */
37
38
  private sequenceNo = 1
@@ -43,11 +44,11 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
43
44
 
44
45
  /* declare node configuration parameters */
45
46
  this.configure({
46
- format: { type: "string", pos: 0, val: "srt", match: /^(?:srt|vtt)$/ },
47
- words: { type: "boolean", val: false },
48
- mode: { type: "string", val: "export", match: /^(?:export|render)$/ },
49
- addr: { type: "string", val: "127.0.0.1" },
50
- port: { type: "number", val: 8585 }
47
+ format: { type: "string", pos: 0, val: "srt", match: /^(?:srt|vtt)$/ },
48
+ words: { type: "boolean", val: false },
49
+ mode: { type: "string", val: "export", match: /^(?:export|render)$/ },
50
+ addr: { type: "string", val: "127.0.0.1" },
51
+ port: { type: "number", val: 8585 }
51
52
  })
52
53
 
53
54
  /* declare node input/output format */
@@ -145,8 +146,8 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
145
146
  chunkNew.payload = payload
146
147
  this.push(chunkNew)
147
148
  callback()
148
- }).catch((err) => {
149
- callback(err)
149
+ }).catch((error: unknown) => {
150
+ callback(utils.ensureError(error))
150
151
  })
151
152
  }
152
153
  }
@@ -13,15 +13,16 @@ import * as Transformers from "@huggingface/transformers"
13
13
 
14
14
  /* internal dependencies */
15
15
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
16
+ import * as utils from "./speechflow-utils"
16
17
 
17
18
  /* internal utility types */
18
19
  type ConfigEntry = { systemPrompt: string, chat: Array<{ role: string, content: string }> }
19
20
  type Config = { [ key: string ]: ConfigEntry }
20
21
 
21
22
  /* SpeechFlow node for Transformers text-to-text translation */
22
- export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
23
+ export default class SpeechFlowNodeT2TTransformers extends SpeechFlowNode {
23
24
  /* declare official node name */
24
- public static name = "transformers"
25
+ public static name = "t2t-transformers"
25
26
 
26
27
  /* internal state */
27
28
  private translator: Transformers.TranslationPipeline | null = null
@@ -212,8 +213,8 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
212
213
  chunk.payload = payload
213
214
  this.push(chunk)
214
215
  callback()
215
- }).catch((err) => {
216
- callback(err)
216
+ }).catch((error: unknown) => {
217
+ callback(utils.ensureError(error))
217
218
  })
218
219
  }
219
220
  },
@@ -12,9 +12,9 @@ import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
12
12
  import * as utils from "./speechflow-utils"
13
13
 
14
14
  /* SpeechFlow node for data flow filtering (based on meta information) */
15
- export default class SpeechFlowNodeFilter extends SpeechFlowNode {
15
+ export default class SpeechFlowNodeX2XFilter extends SpeechFlowNode {
16
16
  /* declare official node name */
17
- public static name = "filter"
17
+ public static name = "x2x-filter"
18
18
 
19
19
  /* cached regular expression instance */
20
20
  private cachedRegExp = new utils.CachedRegExp()
@@ -14,9 +14,9 @@ import { Duration } from "luxon"
14
14
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
15
15
 
16
16
  /* SpeechFlow node for data flow tracing */
17
- export default class SpeechFlowNodeTrace extends SpeechFlowNode {
17
+ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
18
18
  /* declare official node name */
19
- public static name = "trace"
19
+ public static name = "x2x-trace"
20
20
 
21
21
  /* construct node */
22
22
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -15,9 +15,9 @@ import SpeechFlowNode from "./speechflow-node"
15
15
  import * as utils from "./speechflow-utils"
16
16
 
17
17
  /* SpeechFlow node for device access */
18
- export default class SpeechFlowNodeDevice extends SpeechFlowNode {
18
+ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
19
19
  /* declare official node name */
20
- public static name = "device"
20
+ public static name = "xio-device"
21
21
 
22
22
  /* internal state */
23
23
  private io: PortAudio.IoStreamRead
@@ -13,9 +13,9 @@ import SpeechFlowNode from "./speechflow-node"
13
13
  import * as utils from "./speechflow-utils"
14
14
 
15
15
  /* SpeechFlow node for file access */
16
- export default class SpeechFlowNodeFile extends SpeechFlowNode {
16
+ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
17
17
  /* declare official node name */
18
- public static name = "file"
18
+ public static name = "xio-file"
19
19
 
20
20
  /* construct node */
21
21
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -59,6 +59,28 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
59
59
  if (this.params.path === "")
60
60
  throw new Error("required parameter \"path\" has to be given")
61
61
 
62
+ /* utility function: create a writable stream as chunker that
63
+ writes to process.stdout but properly handles finish events.
64
+ This ensures the writable side of the composed stream below
65
+ properly signals completion while keeping process.stdout open
66
+ (as it's a global stream that shouldn't be closed by individual nodes). */
67
+ const createStdoutChunker = () => {
68
+ return new Stream.Writable({
69
+ highWaterMark: this.params.type === "audio" ?
70
+ highWaterMarkAudio : highWaterMarkText,
71
+ write (chunk: Buffer | string, encoding, callback) {
72
+ const canContinue = process.stdout.write(chunk, encoding)
73
+ if (canContinue)
74
+ callback()
75
+ else
76
+ process.stdout.once("drain", callback)
77
+ },
78
+ final (callback) {
79
+ callback()
80
+ }
81
+ })
82
+ }
83
+
62
84
  /* dispatch according to mode and path */
63
85
  if (this.params.mode === "rw") {
64
86
  if (this.params.path === "-") {
@@ -145,17 +167,13 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
145
167
  else if (this.params.mode === "w") {
146
168
  if (this.params.path === "-") {
147
169
  /* standard I/O */
148
- let chunker: Stream.PassThrough
149
- if (this.params.type === "audio") {
170
+ if (this.params.type === "audio")
150
171
  process.stdout.setEncoding()
151
- chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
152
- }
153
- else {
172
+ else
154
173
  process.stdout.setEncoding(this.config.textEncoding)
155
- chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
156
- }
174
+ const chunker = createStdoutChunker()
157
175
  const wrapper = utils.createTransformStreamForWritableSide()
158
- this.stream = Stream.compose(wrapper, chunker, process.stdout)
176
+ this.stream = Stream.compose(wrapper, chunker)
159
177
  }
160
178
  else {
161
179
  /* file I/O */
@@ -178,18 +196,22 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
178
196
  async close () {
179
197
  /* shutdown stream */
180
198
  if (this.stream !== null) {
181
- await new Promise<void>((resolve, reject) => {
182
- if (this.stream instanceof Stream.Writable || this.stream instanceof Stream.Duplex) {
183
- this.stream.end((err?: Error) => {
184
- if (err)
185
- reject(err)
186
- else
199
+ await Promise.race([
200
+ new Promise<void>((resolve, reject) => {
201
+ if (this.stream instanceof Stream.Writable || this.stream instanceof Stream.Duplex) {
202
+ if (this.stream.writableEnded || this.stream.destroyed)
187
203
  resolve()
188
- })
189
- }
190
- else
191
- resolve()
192
- })
204
+ else
205
+ this.stream.end((err?: Error) => {
206
+ if (err) reject(err)
207
+ else resolve()
208
+ })
209
+ }
210
+ else
211
+ resolve()
212
+ }),
213
+ new Promise<void>((resolve) => setTimeout(() => resolve(), 5000))
214
+ ])
193
215
  if (this.params.path !== "-")
194
216
  this.stream.destroy()
195
217
  this.stream = null
@@ -16,9 +16,9 @@ import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
16
16
  import * as utils from "./speechflow-utils"
17
17
 
18
18
  /* SpeechFlow node for MQTT networking */
19
- export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
19
+ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
20
20
  /* declare official node name */
21
- public static name = "mqtt"
21
+ public static name = "xio-mqtt"
22
22
 
23
23
  /* internal state */
24
24
  private broker: MQTT.MqttClient | null = null
@@ -16,9 +16,9 @@ import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
16
16
  import * as utils from "./speechflow-utils"
17
17
 
18
18
  /* SpeechFlow node for Websocket networking */
19
- export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
19
+ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
20
20
  /* declare official node name */
21
- public static name = "websocket"
21
+ public static name = "xio-websocket"
22
22
 
23
23
  /* internal state */
24
24
  private server: ws.WebSocketServer | null = null
@@ -7,6 +7,7 @@
7
7
  /* standard dependencies */
8
8
  import Stream from "node:stream"
9
9
  import { EventEmitter } from "node:events"
10
+ import { type, type Type } from "arktype"
10
11
 
11
12
  /* external dependencies */
12
13
  import { DateTime, Duration } from "luxon"
@@ -16,6 +17,194 @@ import * as IntervalTree from "node-interval-tree"
16
17
  /* internal dependencies */
17
18
  import { SpeechFlowChunk } from "./speechflow-node"
18
19
 
20
+ /* helper function for retrieving an Error object */
21
+ export function ensureError (error: unknown, prefix?: string): Error {
22
+ if (error instanceof Error && prefix === undefined)
23
+ return error
24
+ let msg = error instanceof Error ?
25
+ error.message : String(error)
26
+ if (prefix)
27
+ msg = `${prefix}: ${msg}`
28
+ return new Error(msg, { cause: error })
29
+ }
30
+
31
+ /* helper function for retrieving a Promise object */
32
+ export function ensurePromise<T> (arg: T | Promise<T>): Promise<T> {
33
+ if (!(arg instanceof Promise))
34
+ arg = Promise.resolve(arg)
35
+ return arg
36
+ }
37
+
38
+ /* helper function for running the finally code of "run" */
39
+ function runFinally (onfinally?: () => void) {
40
+ if (!onfinally)
41
+ return
42
+ try { onfinally() }
43
+ catch (_error: unknown) { /* ignored */ }
44
+ }
45
+
46
+ /* helper type for ensuring T contains no Promise */
47
+ type runNoPromise<T> =
48
+ [ T ] extends [ Promise<any> ] ? never : T
49
+
50
+ /* run a synchronous or asynchronous action */
51
+ export function run<T, X extends runNoPromise<T> | never> (
52
+ action: () => X,
53
+ oncatch?: (error: Error) => X | never,
54
+ onfinally?: () => void
55
+ ): X
56
+ export function run<T, X extends runNoPromise<T> | never> (
57
+ description: string,
58
+ action: () => X,
59
+ oncatch?: (error: Error) => X | never,
60
+ onfinally?: () => void
61
+ ): X
62
+ export function run<T, X extends (T | Promise<T>)> (
63
+ action: () => X,
64
+ oncatch?: (error: Error) => X,
65
+ onfinally?: () => void
66
+ ): Promise<T>
67
+ export function run<T, X extends (T | Promise<T>)> (
68
+ description: string,
69
+ action: () => X,
70
+ oncatch?: (error: Error) => X,
71
+ onfinally?: () => void
72
+ ): Promise<T>
73
+ export function run<T> (
74
+ ...args: any[]
75
+ ): T | Promise<T> | never {
76
+ /* support overloaded signatures */
77
+ let description: string | undefined
78
+ let action: () => T | Promise<T> | never
79
+ let oncatch: (error: Error) => T | Promise<T> | never
80
+ let onfinally: () => void
81
+ if (typeof args[0] === "string") {
82
+ description = args[0]
83
+ action = args[1]
84
+ oncatch = args[2]
85
+ onfinally = args[3]
86
+ }
87
+ else {
88
+ action = args[0]
89
+ oncatch = args[1]
90
+ onfinally = args[2]
91
+ }
92
+
93
+ /* perform the action */
94
+ let result: T | Promise<T>
95
+ try {
96
+ result = action()
97
+ }
98
+ catch (arg: unknown) {
99
+ /* synchronous case (error branch) */
100
+ let error = ensureError(arg, description)
101
+ if (oncatch) {
102
+ try {
103
+ result = oncatch(error)
104
+ }
105
+ catch (arg: unknown) {
106
+ error = ensureError(arg, description)
107
+ runFinally(onfinally)
108
+ throw error
109
+ }
110
+ runFinally(onfinally)
111
+ return result
112
+ }
113
+ runFinally(onfinally)
114
+ throw error
115
+ }
116
+ if (result instanceof Promise) {
117
+ /* asynchronous case (result or error branch) */
118
+ return result.catch((arg: unknown) => {
119
+ /* asynchronous case (error branch) */
120
+ let error = ensureError(arg, description)
121
+ if (oncatch) {
122
+ try {
123
+ return oncatch(error)
124
+ }
125
+ catch (arg: unknown) {
126
+ error = ensureError(arg, description)
127
+ throw error
128
+ }
129
+ }
130
+ throw error
131
+ }).finally(() => {
132
+ /* asynchronous case (result and error branch) */
133
+ runFinally(onfinally)
134
+ })
135
+ }
136
+ else {
137
+ /* synchronous case (result branch) */
138
+ runFinally(onfinally)
139
+ return result
140
+ }
141
+ }
142
+
143
+ /* run a synchronous or asynchronous action */
144
+ /* eslint @typescript-eslint/unified-signatures: off */
145
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
146
+ action: F,
147
+ oncatch?: (error: Error) => X | never,
148
+ onfinally?: () => void
149
+ ): F
150
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
151
+ description: string,
152
+ action: F,
153
+ oncatch?: (error: Error) => X | never,
154
+ onfinally?: () => void
155
+ ): F
156
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
157
+ action: F,
158
+ oncatch?: (error: Error) => X,
159
+ onfinally?: () => void
160
+ ): F
161
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
162
+ description: string,
163
+ action: F,
164
+ oncatch?: (error: Error) => X,
165
+ onfinally?: () => void
166
+ ): F
167
+ export function runner<T> (
168
+ ...args: any[]
169
+ ): (...args: any[]) => T | Promise<T> | never {
170
+ /* support overloaded signatures */
171
+ let description: string | undefined
172
+ let action: (...args: any[]) => T | Promise<T> | never
173
+ let oncatch: (error: Error) => T | Promise<T> | never
174
+ let onfinally: () => void
175
+ if (typeof args[0] === "string") {
176
+ description = args[0]
177
+ action = args[1]
178
+ oncatch = args[2]
179
+ onfinally = args[3]
180
+ }
181
+ else {
182
+ action = args[0]
183
+ oncatch = args[1]
184
+ onfinally = args[2]
185
+ }
186
+
187
+ /* wrap the "run" operation on "action" into function
188
+ which exposes the signature of "action" */
189
+ return (...args: any[]) => {
190
+ if (description)
191
+ return run(description, () => action(...args), oncatch, onfinally)
192
+ else
193
+ return run(() => action(...args), oncatch, onfinally)
194
+ }
195
+ }
196
+
197
+ /* import an object with parsing and strict error handling */
198
+ export function importObject<T>(name: string, arg: object | string, validator: Type<T, {}>): T {
199
+ const obj: object = typeof arg === "string" ?
200
+ run(`${name}: parsing JSON`, () => JSON.parse(arg)) :
201
+ arg
202
+ const result = validator(obj)
203
+ if (result instanceof type.errors)
204
+ throw new Error(`${name}: validation: ${result.summary}`)
205
+ return result as T
206
+ }
207
+
19
208
  /* calculate duration of an audio buffer */
20
209
  export function audioBufferDuration (
21
210
  buffer: Buffer,
@@ -139,6 +328,11 @@ export function createTransformStreamForReadableSide (type: "text" | "audio", ge
139
328
  decodeStrings: false,
140
329
  highWaterMark: (type === "audio" ? 19200 : 65536),
141
330
  transform (chunk: Buffer | string, encoding, callback) {
331
+ if (chunk === null) {
332
+ this.push(null)
333
+ callback()
334
+ return
335
+ }
142
336
  const timeZero = getTimeZero()
143
337
  const start = DateTime.now().diff(timeZero)
144
338
  let end = start
@@ -146,7 +340,8 @@ export function createTransformStreamForReadableSide (type: "text" | "audio", ge
146
340
  const duration = audioBufferDuration(chunk as Buffer)
147
341
  end = start.plus(duration * 1000)
148
342
  }
149
- const obj = new SpeechFlowChunk(start, end, "final", type, chunk)
343
+ const payload = ensureStreamChunk(type, chunk) as Buffer | string
344
+ const obj = new SpeechFlowChunk(start, end, "final", type, payload)
150
345
  this.push(obj)
151
346
  callback()
152
347
  },
@@ -34,6 +34,7 @@ import chalk from "chalk"
34
34
 
35
35
  /* internal dependencies */
36
36
  import SpeechFlowNode from "./speechflow-node"
37
+ import * as utils from "./speechflow-utils"
37
38
  import pkg from "../../package.json"
38
39
 
39
40
  /* central CLI context */
@@ -258,16 +259,7 @@ let debug = false
258
259
  throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
259
260
  const [ , id, file ] = m
260
261
  const yaml = await cli.input(file, { encoding: "utf8" })
261
- let obj: any
262
- try {
263
- obj = jsYAML.load(yaml)
264
- }
265
- catch (err) {
266
- if (err instanceof Error)
267
- throw new Error(`failed to parse YAML configuration: ${err.message}`)
268
- else
269
- throw new Error(`failed to parse YAML configuration: ${err}`)
270
- }
262
+ const obj: any = utils.run("parsing YAML configuration", () => jsYAML.load(yaml))
271
263
  if (obj[id] === undefined)
272
264
  throw new Error(`no such id "${id}" found in configuration file "${file}"`)
273
265
  config = obj[id] as string
@@ -290,15 +282,17 @@ let debug = false
290
282
  "./speechflow-node-a2a-speex.js",
291
283
  "./speechflow-node-a2a-vad.js",
292
284
  "./speechflow-node-a2a-wav.js",
293
- "./speechflow-node-a2t-awstranscribe.js",
285
+ "./speechflow-node-a2t-amazon.js",
294
286
  "./speechflow-node-a2t-deepgram.js",
295
- "./speechflow-node-a2t-openaitranscribe.js",
296
- "./speechflow-node-t2a-awspolly.js",
287
+ "./speechflow-node-a2t-openai.js",
288
+ "./speechflow-node-t2a-amazon.js",
297
289
  "./speechflow-node-t2a-elevenlabs.js",
298
290
  "./speechflow-node-t2a-kokoro.js",
299
- "./speechflow-node-t2t-awstranslate.js",
291
+ "./speechflow-node-t2t-amazon.js",
300
292
  "./speechflow-node-t2t-deepl.js",
301
293
  "./speechflow-node-t2t-format.js",
294
+ "./speechflow-node-t2t-google.js",
295
+ "./speechflow-node-t2t-modify.js",
302
296
  "./speechflow-node-t2t-ollama.js",
303
297
  "./speechflow-node-t2t-openai.js",
304
298
  "./speechflow-node-t2t-sentence.js",
@@ -448,11 +442,15 @@ let debug = false
448
442
  cli!.log("error", `creation of node <${id}> failed: ${err}`)
449
443
  process.exit(1)
450
444
  }
451
- const params = Object.keys(node.params)
452
- .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
453
- cli!.log("info", `create node <${node.id}> (${params})`)
454
- graphNodes.add(node)
455
- return node
445
+ const params = Object.keys(node!.params).map((key) => {
446
+ if (key.match(/key/))
447
+ return `${key}: [...]`
448
+ else
449
+ return `${key}: ${JSON.stringify(node.params[key])}`
450
+ }).join(", ")
451
+ cli!.log("info", `create node <${node!.id}> (${params})`)
452
+ graphNodes.add(node!)
453
+ return node!
456
454
  },
457
455
  connectNodes (node1: SpeechFlowNode, node2: SpeechFlowNode) {
458
456
  cli!.log("info", `connect node <${node1.id}> to node <${node2.id}>`)
@@ -681,7 +679,7 @@ let debug = false
681
679
  cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
682
680
  return consumeExternalRequest(req)
683
681
  .then(() => h.response({ response: "OK" }).code(200))
684
- .catch((err) => h.response({ response: "ERROR", data: err.message }).code(417))
682
+ .catch((error: unknown) => h.response({ response: "ERROR", data: utils.ensureError(error).message }).code(417))
685
683
  }
686
684
  })
687
685
  hapi.route({
@@ -712,6 +710,8 @@ let debug = false
712
710
  const peer = ctx.peer
713
711
  wsPeers.delete(peer)
714
712
  ws.removeAllListeners()
713
+ if (ws.readyState === WebSocket.OPEN)
714
+ ws.close()
715
715
  cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
716
716
  }
717
717
  }
@@ -834,8 +834,8 @@ let debug = false
834
834
  Promise.all(closePromises),
835
835
  new Promise((resolve, reject) =>
836
836
  setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
837
- ]).catch((err) => {
838
- cli!.log("warning", `HAPI: WebSockets failed to close: ${err}`)
837
+ ]).catch((error: unknown) => {
838
+ cli!.log("warning", `HAPI: WebSockets failed to close: ${utils.ensureError(error).message}`)
839
839
  })
840
840
  wsPeers.clear()
841
841
  }