speechflow 1.6.0 → 1.6.2

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 (192) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/etc/claude.md +9 -4
  4. package/etc/keyfiles.txt +19 -0
  5. package/etc/speechflow.yaml +4 -4
  6. package/package.json +3 -3
  7. package/speechflow-cli/dst/speechflow-main-api.d.ts +12 -0
  8. package/speechflow-cli/dst/speechflow-main-api.js +319 -0
  9. package/speechflow-cli/dst/speechflow-main-api.js.map +1 -0
  10. package/speechflow-cli/dst/speechflow-main-cli.d.ts +28 -0
  11. package/speechflow-cli/dst/speechflow-main-cli.js +271 -0
  12. package/speechflow-cli/dst/speechflow-main-cli.js.map +1 -0
  13. package/speechflow-cli/dst/speechflow-main-config.d.ts +9 -0
  14. package/speechflow-cli/dst/speechflow-main-config.js +27 -0
  15. package/speechflow-cli/dst/speechflow-main-config.js.map +1 -0
  16. package/speechflow-cli/dst/speechflow-main-graph.d.ts +34 -0
  17. package/speechflow-cli/dst/speechflow-main-graph.js +365 -0
  18. package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -0
  19. package/speechflow-cli/dst/speechflow-main-nodes.d.ts +10 -0
  20. package/speechflow-cli/dst/speechflow-main-nodes.js +60 -0
  21. package/speechflow-cli/dst/speechflow-main-nodes.js.map +1 -0
  22. package/speechflow-cli/dst/speechflow-main-status.d.ts +11 -0
  23. package/speechflow-cli/dst/speechflow-main-status.js +60 -0
  24. package/speechflow-cli/dst/speechflow-main-status.js.map +1 -0
  25. package/speechflow-cli/dst/speechflow-main.d.ts +7 -0
  26. package/speechflow-cli/dst/speechflow-main.js +127 -0
  27. package/speechflow-cli/dst/speechflow-main.js.map +1 -0
  28. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +4 -4
  29. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  30. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +5 -6
  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.js +5 -6
  35. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +5 -5
  37. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  38. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +3 -3
  39. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +2 -2
  41. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
  42. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +4 -4
  43. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  44. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +2 -2
  45. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  46. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +4 -4
  47. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  48. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +4 -4
  49. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  50. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +4 -4
  51. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +6 -6
  53. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
  54. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +4 -4
  55. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  56. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +4 -4
  57. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
  58. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +2 -2
  59. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
  60. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +2 -2
  61. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  62. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +2 -2
  63. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
  64. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +2 -2
  65. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  66. package/speechflow-cli/dst/speechflow-node-t2t-google.js +5 -5
  67. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
  68. package/speechflow-cli/dst/speechflow-node-t2t-modify.js +2 -2
  69. package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -1
  70. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +2 -2
  71. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  72. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +2 -2
  73. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  74. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +2 -2
  75. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +2 -2
  77. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  78. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +2 -2
  79. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  80. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +2 -2
  81. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  82. package/speechflow-cli/dst/speechflow-node-xio-device.js +5 -5
  83. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  84. package/speechflow-cli/dst/speechflow-node-xio-file.js +27 -27
  85. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  86. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +4 -4
  87. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  88. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +7 -7
  89. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  90. package/speechflow-cli/dst/{speechflow-utils-audio-wt.js → speechflow-util-audio-wt.js} +1 -1
  91. package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -0
  92. package/speechflow-cli/dst/speechflow-util-audio.d.ts +22 -0
  93. package/speechflow-cli/dst/speechflow-util-audio.js +251 -0
  94. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -0
  95. package/speechflow-cli/dst/speechflow-util-error.d.ts +14 -0
  96. package/speechflow-cli/dst/speechflow-util-error.js +131 -0
  97. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -0
  98. package/speechflow-cli/dst/speechflow-util-queue.d.ts +68 -0
  99. package/speechflow-cli/dst/speechflow-util-queue.js +338 -0
  100. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -0
  101. package/speechflow-cli/dst/speechflow-util-stream.d.ts +18 -0
  102. package/speechflow-cli/dst/speechflow-util-stream.js +219 -0
  103. package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -0
  104. package/speechflow-cli/dst/speechflow-util-webaudio-wt.js +124 -0
  105. package/speechflow-cli/dst/speechflow-util-webaudio-wt.js.map +1 -0
  106. package/speechflow-cli/dst/{speechflow-utils-audio.js → speechflow-util-webaudio.js} +2 -2
  107. package/speechflow-cli/dst/speechflow-util-webaudio.js.map +1 -0
  108. package/speechflow-cli/dst/speechflow-util.d.ts +4 -0
  109. package/speechflow-cli/dst/speechflow-util.js +26 -0
  110. package/speechflow-cli/dst/speechflow-util.js.map +1 -0
  111. package/speechflow-cli/dst/speechflow.js +3 -912
  112. package/speechflow-cli/dst/speechflow.js.map +1 -1
  113. package/speechflow-cli/etc/oxlint.jsonc +4 -1
  114. package/speechflow-cli/package.json +1 -0
  115. package/speechflow-cli/src/lib.d.ts +2 -0
  116. package/speechflow-cli/src/speechflow-main-api.ts +315 -0
  117. package/speechflow-cli/src/speechflow-main-cli.ts +259 -0
  118. package/speechflow-cli/src/speechflow-main-config.ts +17 -0
  119. package/speechflow-cli/src/speechflow-main-graph.ts +370 -0
  120. package/speechflow-cli/src/speechflow-main-nodes.ts +61 -0
  121. package/speechflow-cli/src/speechflow-main-status.ts +70 -0
  122. package/speechflow-cli/src/speechflow-main.ts +106 -0
  123. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -4
  124. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +5 -6
  125. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +5 -5
  126. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +5 -6
  127. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +5 -5
  128. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +4 -4
  129. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +2 -2
  130. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +4 -4
  131. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
  132. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +4 -4
  133. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +4 -4
  134. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +4 -4
  135. package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +7 -7
  136. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +5 -5
  137. package/speechflow-cli/src/speechflow-node-a2t-openai.ts +5 -5
  138. package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +2 -2
  139. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +2 -2
  140. package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -2
  141. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +2 -2
  142. package/speechflow-cli/src/speechflow-node-t2t-google.ts +5 -5
  143. package/speechflow-cli/src/speechflow-node-t2t-modify.ts +2 -2
  144. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +2 -2
  145. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +2 -2
  146. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +2 -2
  147. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +2 -2
  148. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +2 -2
  149. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -2
  150. package/speechflow-cli/src/speechflow-node-xio-device.ts +5 -5
  151. package/speechflow-cli/src/speechflow-node-xio-file.ts +9 -10
  152. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +5 -5
  153. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +7 -7
  154. package/speechflow-cli/src/{speechflow-utils-audio.ts → speechflow-util-audio.ts} +131 -1
  155. package/speechflow-cli/src/speechflow-util-error.ts +184 -0
  156. package/speechflow-cli/src/speechflow-util-queue.ts +320 -0
  157. package/speechflow-cli/src/speechflow-util-stream.ts +197 -0
  158. package/speechflow-cli/src/speechflow-util.ts +10 -0
  159. package/speechflow-cli/src/speechflow.ts +3 -953
  160. package/speechflow-ui-db/dst/index.js +1 -1
  161. package/speechflow-ui-db/etc/vite-client.mts +0 -2
  162. package/speechflow-ui-db/src/app.vue +20 -19
  163. package/speechflow-ui-st/dst/index.js +1 -1
  164. package/speechflow-ui-st/etc/vite-client.mts +0 -2
  165. package/speechflow-ui-st/src/app.vue +10 -6
  166. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +0 -208
  167. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +0 -1
  168. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +0 -15
  169. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +0 -312
  170. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +0 -1
  171. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +0 -18
  172. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +0 -312
  173. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +0 -1
  174. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +0 -19
  175. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +0 -351
  176. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +0 -1
  177. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +0 -16
  178. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +0 -204
  179. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +0 -1
  180. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +0 -13
  181. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +0 -175
  182. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +0 -1
  183. package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +0 -1
  184. package/speechflow-cli/dst/speechflow-utils-audio.js.map +0 -1
  185. package/speechflow-cli/dst/speechflow-utils.d.ts +0 -108
  186. package/speechflow-cli/dst/speechflow-utils.js +0 -746
  187. package/speechflow-cli/dst/speechflow-utils.js.map +0 -1
  188. package/speechflow-cli/src/speechflow-utils.ts +0 -810
  189. /package/speechflow-cli/dst/{speechflow-node-a2a-dynamics-wt.d.ts → speechflow-util-audio-wt.d.ts} +0 -0
  190. /package/speechflow-cli/dst/{speechflow-utils-audio-wt.d.ts → speechflow-util-webaudio-wt.d.ts} +0 -0
  191. /package/speechflow-cli/dst/{speechflow-utils-audio.d.ts → speechflow-util-webaudio.d.ts} +0 -0
  192. /package/speechflow-cli/src/{speechflow-utils-audio-wt.ts → speechflow-util-audio-wt.ts} +0 -0
@@ -0,0 +1,259 @@
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 path from "node:path"
9
+
10
+ /* external dependencies */
11
+ import CLIio from "cli-io"
12
+ import yargs from "yargs"
13
+ import { hideBin } from "yargs/helpers"
14
+ import jsYAML from "js-yaml"
15
+ import dotenvx from "@dotenvx/dotenvx"
16
+ import syspath from "syspath"
17
+ import chalk from "chalk"
18
+
19
+ /* internal dependencies */
20
+ import * as util from "./speechflow-util"
21
+ import pkg from "../../package.json"
22
+
23
+ /* command-line options */
24
+ export interface CLIOptions {
25
+ V: boolean
26
+ S: boolean
27
+ v: string
28
+ a: string
29
+ p: number
30
+ C: string
31
+ d: string
32
+ o: string
33
+ e: string
34
+ f: string
35
+ c: string
36
+ _: (string | number)[]
37
+ }
38
+
39
+ export class CLIContext {
40
+ public cli: CLIio | null = null
41
+ public args: CLIOptions | null = null
42
+ public config: string | null = null
43
+ public debug = false
44
+
45
+ /* type guard for initialization */
46
+ isInitialized (): this is CLIContext & { cli: CLIio; args: CLIOptions; config: string } {
47
+ return this.cli !== null && this.args !== null && this.config !== null
48
+ }
49
+
50
+ /* initialization of CLI */
51
+ async init (): Promise<void> {
52
+ /* determine system paths */
53
+ const { dataDir } = syspath({
54
+ appName: "speechflow",
55
+ dataDirAutoCreate: true
56
+ })
57
+
58
+ /* parse command-line arguments */
59
+ const coerce = (arg: string) => Array.isArray(arg) ? arg[arg.length - 1] : arg
60
+ this.args = await yargs()
61
+ /* eslint @stylistic/indent: off */
62
+ .usage(
63
+ "Usage: $0 " +
64
+ "[-h|--help] " +
65
+ "[-V|--version] " +
66
+ "[-S|--status] " +
67
+ "[-v|--verbose <level>] " +
68
+ "[-a|--address <ip-address>] " +
69
+ "[-p|--port <tcp-port>] " +
70
+ "[-C|--cache <directory>] " +
71
+ "[-d|--dashboard <type>:<id>:<name>[,...]] " +
72
+ "[-o|--osc <ip-address>:<udp-port> " +
73
+ "[-e|--expression <expression>] " +
74
+ "[-f|--file <file>] " +
75
+ "[-c|--config <id>@<yaml-config-file>] " +
76
+ "[<argument> [...]]"
77
+ )
78
+ .version(false)
79
+ .option("V", {
80
+ alias: "version",
81
+ type: "boolean",
82
+ array: false,
83
+ coerce,
84
+ default: false,
85
+ describe: "show program version information"
86
+ })
87
+ .option("S", {
88
+ alias: "status",
89
+ type: "boolean",
90
+ array: false,
91
+ coerce,
92
+ default: false,
93
+ describe: "show one-time status of nodes"
94
+ })
95
+ .option("v", {
96
+ alias: "log-level",
97
+ type: "string",
98
+ array: false,
99
+ coerce,
100
+ nargs: 1,
101
+ default: "warning",
102
+ describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
103
+ })
104
+ .option("a", {
105
+ alias: "address",
106
+ type: "string",
107
+ array: false,
108
+ coerce,
109
+ nargs: 1,
110
+ default: "0.0.0.0",
111
+ describe: "IP address for REST/WebSocket API"
112
+ })
113
+ .option("p", {
114
+ alias: "port",
115
+ type: "number",
116
+ array: false,
117
+ coerce,
118
+ nargs: 1,
119
+ default: 8484,
120
+ describe: "TCP port for REST/WebSocket API"
121
+ })
122
+ .option("C", {
123
+ alias: "cache",
124
+ type: "string",
125
+ array: false,
126
+ coerce,
127
+ nargs: 1,
128
+ default: path.join(dataDir, "cache"),
129
+ describe: "directory for cached files (primarily AI model files)"
130
+ })
131
+ .option("d", {
132
+ alias: "dashboard",
133
+ type: "string",
134
+ array: false,
135
+ coerce,
136
+ nargs: 1,
137
+ default: "",
138
+ describe: "list of dashboard block types and names"
139
+ })
140
+ .option("o", {
141
+ alias: "osc",
142
+ type: "string",
143
+ array: false,
144
+ coerce,
145
+ nargs: 1,
146
+ default: "",
147
+ describe: "OSC/UDP endpoint to send dashboard information"
148
+ })
149
+ .option("e", {
150
+ alias: "expression",
151
+ type: "string",
152
+ array: false,
153
+ coerce,
154
+ nargs: 1,
155
+ default: "",
156
+ describe: "FlowLink expression string"
157
+ })
158
+ .option("f", {
159
+ alias: "file",
160
+ type: "string",
161
+ array: false,
162
+ coerce,
163
+ nargs: 1,
164
+ default: "",
165
+ describe: "FlowLink expression file"
166
+ })
167
+ .option("c", {
168
+ alias: "config",
169
+ type: "string",
170
+ array: false,
171
+ coerce,
172
+ nargs: 1,
173
+ default: "",
174
+ describe: "FlowLink expression reference into YAML file (in format <id>@<file>)"
175
+ })
176
+ .help("h", "show usage help")
177
+ .alias("h", "help")
178
+ .showHelpOnFail(true)
179
+ .strict()
180
+ .demand(0)
181
+ .parse(hideBin(process.argv)) as CLIOptions
182
+
183
+ /* short-circuit version request */
184
+ if (this.args.V) {
185
+ process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
186
+ process.stderr.write(`${pkg.description}\n`)
187
+ process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
188
+ process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
189
+ process.exit(0)
190
+ }
191
+
192
+ /* establish CLI environment */
193
+ this.cli = new CLIio({
194
+ encoding: "utf8",
195
+ logLevel: this.args.v,
196
+ logTime: true,
197
+ logPrefix: pkg.name
198
+ })
199
+ if (this.args.v.match(/^(?:info|debug)$/))
200
+ this.debug = true
201
+
202
+ /* provide startup information */
203
+ this.cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
204
+
205
+ /* load .env files */
206
+ const result = dotenvx.config({
207
+ encoding: "utf8",
208
+ ignore: [ "MISSING_ENV_FILE" ],
209
+ quiet: true
210
+ })
211
+ if (result?.parsed !== undefined)
212
+ for (const key of Object.keys(result.parsed))
213
+ this.cli.log("info", `loaded environment variable "${key}" from ".env" files`)
214
+
215
+ /* sanity check configuration situation */
216
+ let n = 0
217
+ if (typeof this.args.e === "string" && this.args.e !== "") n++
218
+ if (typeof this.args.f === "string" && this.args.f !== "") n++
219
+ if (typeof this.args.c === "string" && this.args.c !== "") n++
220
+ if (n === 0)
221
+ throw new Error("need at least one FlowLink specification source (use one of the options -e, -f or -c)")
222
+ else if (n !== 1)
223
+ throw new Error("cannot use more than one FlowLink specification source (use only one of the options -e, -f or -c)")
224
+
225
+ /* read configuration */
226
+ if (typeof this.args.e === "string" && this.args.e !== "")
227
+ this.config = this.args.e
228
+ else if (typeof this.args.f === "string" && this.args.f !== "")
229
+ this.config = await this.cli.input(this.args.f, { encoding: "utf8" })
230
+ else if (typeof this.args.c === "string" && this.args.c !== "") {
231
+ const m = this.args.c.match(/^(.+?)@(.+)$/)
232
+ if (m === null)
233
+ throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
234
+ const [ , id, file ] = m
235
+ const yaml = await this.cli.input(file, { encoding: "utf8" })
236
+ const obj: any = util.run("parsing YAML configuration", () => jsYAML.load(yaml))
237
+ if (obj[id] === undefined)
238
+ throw new Error(`no such id "${id}" found in configuration file "${file}"`)
239
+ this.config = obj[id] as string
240
+ }
241
+ }
242
+
243
+ /* utility function for handling a top-level error */
244
+ handleTopLevelError (err: Error): never {
245
+ if (this.cli !== null) {
246
+ if (this.debug)
247
+ this.cli.log("error", `${err.message}\n${err.stack}`)
248
+ else
249
+ this.cli.log("error", `${err.message}`)
250
+ }
251
+ else {
252
+ if (this.debug)
253
+ process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
254
+ else
255
+ process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}`)
256
+ }
257
+ process.exit(1)
258
+ }
259
+ }
@@ -0,0 +1,17 @@
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
+ /* the node configuration */
8
+ export class NodeConfig {
9
+ constructor (
10
+ public readonly audioChannels = 1,
11
+ public readonly audioBitDepth = 16,
12
+ public readonly audioLittleEndian = true,
13
+ public readonly audioSampleRate = 48000,
14
+ public readonly textEncoding = "utf8",
15
+ public cacheDir = ""
16
+ ) {}
17
+ }
@@ -0,0 +1,370 @@
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
+ import { EventEmitter } from "node:events"
10
+
11
+ /* external dependencies */
12
+ import { DateTime } from "luxon"
13
+ import CLIio from "cli-io"
14
+ import FlowLink from "flowlink"
15
+ import objectPath from "object-path"
16
+
17
+ /* internal dependencies */
18
+ import SpeechFlowNode from "./speechflow-node"
19
+ import { NodeConfig } from "./speechflow-main-config"
20
+ import { CLIOptions } from "./speechflow-main-cli"
21
+ import { APIServer } from "./speechflow-main-api"
22
+ import * as util from "./speechflow-util"
23
+
24
+ /* the SpeechFlow node graph management */
25
+ export class NodeGraph {
26
+ /* internal state */
27
+ private graphNodes = new Set<SpeechFlowNode>()
28
+ private activeNodes = new Set<SpeechFlowNode>()
29
+ private finishEvents = new EventEmitter()
30
+ private timeZero: DateTime | null = null
31
+ private shuttingDown = false
32
+
33
+ /* simple construction */
34
+ constructor (
35
+ private cli: CLIio,
36
+ private debug = false
37
+ ) {}
38
+
39
+ /* get all graph nodes */
40
+ getGraphNodes (): Set<SpeechFlowNode> {
41
+ return this.graphNodes
42
+ }
43
+
44
+ /* find particular graph node */
45
+ findGraphNode (name: string): SpeechFlowNode | undefined {
46
+ return Array.from(this.graphNodes).find((node) => node.id === name)
47
+ }
48
+
49
+ /* graph establishment: PASS 1: parse DSL and create and connect nodes */
50
+ async createAndConnectNodes (
51
+ config: string,
52
+ nodes: { [ id: string ]: typeof SpeechFlowNode },
53
+ cfg: NodeConfig,
54
+ variables: { argv: any[], env: any },
55
+ accessBus: (name: string) => EventEmitter
56
+ ): Promise<void> {
57
+ const flowlink = new FlowLink<SpeechFlowNode>({
58
+ trace: (msg: string) => {
59
+ this.cli.log("debug", msg)
60
+ }
61
+ })
62
+ const nodeNums = new Map<typeof SpeechFlowNode, number>()
63
+ let ast: unknown
64
+ try {
65
+ ast = flowlink.compile(config)
66
+ }
67
+ catch (err) {
68
+ const errorMsg = err instanceof Error && err.name === "FlowLinkError"
69
+ ? err.toString() : (err instanceof Error ? err.message : "internal error")
70
+ this.cli.log("error", `failed to parse SpeechFlow configuration: ${errorMsg}`)
71
+ process.exit(1)
72
+ }
73
+ try {
74
+ flowlink.execute(ast, {
75
+ resolveVariable: (id: string) => {
76
+ if (!objectPath.has(variables, id))
77
+ throw new Error(`failed to resolve variable "${id}"`)
78
+ const value = objectPath.get(variables, id)
79
+ this.cli.log("info", `resolve variable: "${id}" -> "${value}"`)
80
+ return value
81
+ },
82
+ createNode: (id: string, opts: { [ id: string ]: any }, args: any[]) => {
83
+ if (nodes[id] === undefined)
84
+ throw new Error(`unknown node <${id}>`)
85
+ let node: SpeechFlowNode
86
+ try {
87
+ const NodeClass = nodes[id]
88
+ let num = nodeNums.get(NodeClass) ?? 0
89
+ nodeNums.set(NodeClass, ++num)
90
+ const name = num === 1 ? id : `${id}:${num}`
91
+ node = new NodeClass(name, cfg, opts, args)
92
+ node._accessBus = accessBus
93
+ }
94
+ catch (err) {
95
+ /* fatal error */
96
+ if (err instanceof Error)
97
+ this.cli.log("error", `creation of node <${id}> failed: ${err.message}`)
98
+ else
99
+ this.cli.log("error", `creation of node <${id}> failed: ${err}`)
100
+ process.exit(1)
101
+ }
102
+ const params = Object.keys(node.params).map((key) => {
103
+ if (key.match(/key/))
104
+ return `${key}: [...]`
105
+ else
106
+ return `${key}: ${JSON.stringify(node.params[key])}`
107
+ }).join(", ")
108
+ this.cli.log("info", `create node <${node.id}> (${params})`)
109
+ this.graphNodes.add(node)
110
+ return node
111
+ },
112
+ connectNodes: (node1: SpeechFlowNode, node2: SpeechFlowNode) => {
113
+ this.cli.log("info", `connect node <${node1.id}> to node <${node2.id}>`)
114
+ node1.connect(node2)
115
+ }
116
+ })
117
+ }
118
+ catch (err) {
119
+ const errorMsg = err instanceof Error && err.name === "FlowLinkError"
120
+ ? err.toString() : (err instanceof Error ? err.message : "internal error")
121
+ this.cli.log("error", `failed to materialize SpeechFlow configuration: ${errorMsg}`)
122
+ process.exit(1)
123
+ }
124
+ }
125
+
126
+ /* graph establishment: PASS 2: prune connections of nodes */
127
+ async pruneConnections () {
128
+ for (const node of this.graphNodes) {
129
+ /* determine connections */
130
+ let connectionsIn = Array.from(node.connectionsIn)
131
+ let connectionsOut = Array.from(node.connectionsOut)
132
+
133
+ /* ensure necessary incoming links */
134
+ if (node.input !== "none" && connectionsIn.length === 0)
135
+ throw new Error(`node <${node.id}> requires input but has no input nodes connected`)
136
+
137
+ /* prune unnecessary incoming links */
138
+ if (node.input === "none" && connectionsIn.length > 0)
139
+ connectionsIn.forEach((other) => { other.disconnect(node) })
140
+
141
+ /* ensure necessary outgoing links */
142
+ if (node.output !== "none" && connectionsOut.length === 0)
143
+ throw new Error(`node <${node.id}> requires output but has no output nodes connected`)
144
+
145
+ /* prune unnecessary outgoing links */
146
+ if (node.output === "none" && connectionsOut.length > 0)
147
+ connectionsOut.forEach((other) => { node.disconnect(other) })
148
+
149
+ /* check for payload compatibility */
150
+ connectionsIn = Array.from(node.connectionsIn)
151
+ connectionsOut = Array.from(node.connectionsOut)
152
+ for (const other of connectionsOut)
153
+ if (other.input !== node.output)
154
+ throw new Error(`${node.output} output node <${node.id}> cannot be ` +
155
+ `connected to ${other.input} input node <${other.id}> (payload is incompatible)`)
156
+ }
157
+ }
158
+
159
+ /* graph establishment: PASS 3: open nodes */
160
+ async openNodes(): Promise<void> {
161
+ this.timeZero = DateTime.now()
162
+ for (const node of this.graphNodes) {
163
+ /* connect node events */
164
+ node.on("log", (level: string, msg: string, data?: any) => {
165
+ let str = `<${node.id}>: ${msg}`
166
+ if (data !== undefined)
167
+ str += ` (${JSON.stringify(data)})`
168
+ this.cli.log(level, str)
169
+ })
170
+
171
+ /* open node */
172
+ this.cli.log("info", `open node <${node.id}>`)
173
+ node.setTimeZero(this.timeZero)
174
+ await Promise.race<void>([
175
+ node.open(),
176
+ new Promise<never>((resolve, reject) => setTimeout(() =>
177
+ reject(new Error("timeout")), 10 * 1000))
178
+ ]).catch((err: Error) => {
179
+ this.cli.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`)
180
+ throw new Error(`failed to open node <${node.id}>: ${err.message}`)
181
+ })
182
+ }
183
+ }
184
+
185
+ /* graph establishment: PASS 4: connect node streams */
186
+ async connectStreams() {
187
+ for (const node of this.graphNodes) {
188
+ if (node.stream === null)
189
+ throw new Error(`stream of node <${node.id}> still not initialized`)
190
+ for (const other of Array.from(node.connectionsOut)) {
191
+ if (other.stream === null)
192
+ throw new Error(`stream of incoming node <${other.id}> still not initialized`)
193
+ this.cli.log("info", `connect stream of node <${node.id}> to stream of node <${other.id}>`)
194
+ if (!( node.stream instanceof Stream.Readable
195
+ || node.stream instanceof Stream.Duplex ))
196
+ throw new Error(`stream of output node <${node.id}> is neither of Readable nor Duplex type`)
197
+ if (!( other.stream instanceof Stream.Writable
198
+ || other.stream instanceof Stream.Duplex ))
199
+ throw new Error(`stream of input node <${other.id}> is neither of Writable nor Duplex type`)
200
+ node.stream.pipe(other.stream)
201
+ }
202
+ }
203
+ }
204
+
205
+ /* graph establishment: PASS 5: track stream finishing */
206
+ trackFinishing(args: CLIOptions, api: APIServer): void {
207
+ this.finishEvents.removeAllListeners()
208
+ this.finishEvents.setMaxListeners(this.graphNodes.size + 10)
209
+ for (const node of this.graphNodes) {
210
+ if (node.stream === null)
211
+ throw new Error(`stream of node <${node.id}> still not initialized`)
212
+ this.cli.log("info", `observe stream of node <${node.id}> for finish event`)
213
+ this.activeNodes.add(node)
214
+ const deactivateNode = (node: SpeechFlowNode, msg: string) => {
215
+ if (this.activeNodes.has(node))
216
+ this.activeNodes.delete(node)
217
+ this.cli.log("info", `${msg} (${this.activeNodes.size} active nodes remaining)`)
218
+ if (this.activeNodes.size === 0) {
219
+ const timeFinished = DateTime.now()
220
+ const duration = timeFinished.diff(this.timeZero!)
221
+ this.cli.log("info", "**** everything finished -- stream processing in SpeechFlow graph stops " +
222
+ `(total duration: ${duration.toFormat("hh:mm:ss.SSS")}) ****`)
223
+ this.finishEvents.emit("finished")
224
+ this.shutdown("finished", args, api)
225
+ }
226
+ }
227
+ node.stream.on("end", () => {
228
+ deactivateNode(node, `readable stream side of node <${node.id}> raised "end" event`)
229
+ })
230
+ node.stream.on("finish", () => {
231
+ deactivateNode(node, `writable stream side of node <${node.id}> raised "finish" event`)
232
+ })
233
+ }
234
+
235
+ /* start of internal stream processing */
236
+ this.cli.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
237
+ }
238
+
239
+ /* graph destruction: PASS 1: disconnect node streams */
240
+ async disconnectStreams(): Promise<void> {
241
+ for (const node of this.graphNodes) {
242
+ if (node.stream === null) {
243
+ this.cli.log("warning", `stream of node <${node.id}> no longer initialized`)
244
+ continue
245
+ }
246
+ for (const other of Array.from(node.connectionsOut)) {
247
+ if (other.stream === null) {
248
+ this.cli.log("warning", `stream of incoming node <${other.id}> no longer initialized`)
249
+ continue
250
+ }
251
+ if (!( node.stream instanceof Stream.Readable
252
+ || node.stream instanceof Stream.Duplex )) {
253
+ this.cli.log("warning", `stream of output node <${node.id}> is neither of Readable nor Duplex type`)
254
+ continue
255
+ }
256
+ if (!( other.stream instanceof Stream.Writable
257
+ || other.stream instanceof Stream.Duplex )) {
258
+ this.cli.log("warning", `stream of input node <${other.id}> is neither of Writable nor Duplex type`)
259
+ continue
260
+ }
261
+ this.cli.log("info", `disconnect stream of node <${node.id}> from stream of node <${other.id}>`)
262
+ node.stream.unpipe(other.stream)
263
+ }
264
+ }
265
+ }
266
+
267
+ /* graph destruction: PASS 2: close nodes */
268
+ async closeNodes(): Promise<void> {
269
+ for (const node of this.graphNodes) {
270
+ this.cli.log("info", `close node <${node.id}>`)
271
+ await Promise.race<void>([
272
+ node.close(),
273
+ new Promise<never>((resolve, reject) => setTimeout(() =>
274
+ reject(new Error("timeout")), 10 * 1000))
275
+ ]).catch((err: Error) => {
276
+ this.cli.log("warning", `node <${node.id}> failed to close: ${err.message}`)
277
+ })
278
+ }
279
+ }
280
+
281
+ /* graph destruction: PASS 3: disconnect nodes */
282
+ disconnectNodes(): void {
283
+ for (const node of this.graphNodes) {
284
+ this.cli.log("info", `disconnect node <${node.id}>`)
285
+ const connectionsIn = Array.from(node.connectionsIn)
286
+ const connectionsOut = Array.from(node.connectionsOut)
287
+ connectionsIn.forEach((other) => { other.disconnect(node) })
288
+ connectionsOut.forEach((other) => { node.disconnect(other) })
289
+ }
290
+ }
291
+
292
+ /* graph destruction: PASS 4: destroy nodes */
293
+ destroyNodes(): void {
294
+ for (const node of this.graphNodes) {
295
+ this.cli.log("info", `destroy node <${node.id}>`)
296
+ this.graphNodes.delete(node)
297
+ }
298
+ }
299
+
300
+ /* setup signal handling for shutdown */
301
+ setupSignalHandlers(args: CLIOptions, api: APIServer): void {
302
+ /* internal helper functions */
303
+ const shutdownHandler = (signal: string) =>
304
+ this.shutdown(signal, args, api)
305
+ const logError = (error: Error) => {
306
+ if (this.debug)
307
+ this.cli.log("error", `uncaught exception: ${error.message}\n${error.stack}`)
308
+ else
309
+ this.cli.log("error", `uncaught exception: ${error.message}`)
310
+ }
311
+
312
+ /* hook into process signals */
313
+ process.on("SIGINT", () => { shutdownHandler("SIGINT") })
314
+ process.on("SIGUSR1", () => { shutdownHandler("SIGUSR1") })
315
+ process.on("SIGUSR2", () => { shutdownHandler("SIGUSR2") })
316
+ process.on("SIGTERM", () => { shutdownHandler("SIGTERM") })
317
+
318
+ /* re-hook into uncaught exception handler */
319
+ process.removeAllListeners("uncaughtException")
320
+ process.on("uncaughtException", (err) => {
321
+ const error = util.ensureError(err, "uncaught exception")
322
+ logError(error)
323
+ shutdownHandler("exception")
324
+ })
325
+
326
+ /* re-hook into unhandled promise rejection handler */
327
+ process.removeAllListeners("unhandledRejection")
328
+ process.on("unhandledRejection", (reason) => {
329
+ const error = util.ensureError(reason, "unhandled promise rejection")
330
+ logError(error)
331
+ shutdownHandler("exception")
332
+ })
333
+ }
334
+
335
+ /* shutdown procedure */
336
+ async shutdown(signal: string, args: CLIOptions, api: APIServer): Promise<void> {
337
+ if (this.shuttingDown)
338
+ return
339
+ this.shuttingDown = true
340
+ if (signal === "exception")
341
+ this.cli.log("warning", "**** exception occurred -- shutting down service ****")
342
+ else if (signal !== "finished")
343
+ this.cli.log("warning", `**** received signal ${signal} -- shutting down service ****`)
344
+
345
+ /* shutdown API service */
346
+ await api.stop(args)
347
+
348
+ /* disconnect, close and destroy nodes */
349
+ await this.disconnectStreams()
350
+ await this.closeNodes()
351
+ this.disconnectNodes()
352
+ this.destroyNodes()
353
+
354
+ /* clear event emitters */
355
+ this.finishEvents.removeAllListeners()
356
+
357
+ /* clear active nodes */
358
+ this.activeNodes.clear()
359
+
360
+ /* terminate process */
361
+ if (signal === "finished") {
362
+ this.cli.log("info", "terminate process (exit code 0)")
363
+ process.exit(0)
364
+ }
365
+ else {
366
+ this.cli.log("info", "terminate process (exit code 1)")
367
+ process.exit(1)
368
+ }
369
+ }
370
+ }