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
@@ -5,957 +5,7 @@
5
5
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
6
  */
7
7
 
8
- /* standard dependencies */
9
- import path from "node:path"
10
- import Stream from "node:stream"
11
- import { EventEmitter } from "node:events"
12
- import http from "node:http"
13
- import * as HAPI from "@hapi/hapi"
14
- import Inert from "@hapi/inert"
15
- import WebSocket from "ws"
16
- import HAPIWebSocket from "hapi-plugin-websocket"
17
- import HAPIHeader from "hapi-plugin-header"
18
- import OSC from "osc-js"
19
-
20
- /* external dependencies */
21
- import { DateTime } from "luxon"
22
- import CLIio from "cli-io"
23
- import yargs from "yargs"
24
- import { hideBin } from "yargs/helpers"
25
- import jsYAML from "js-yaml"
26
- import FlowLink from "flowlink"
27
- import objectPath from "object-path"
28
- import installedPackages from "installed-packages"
29
- import dotenvx from "@dotenvx/dotenvx"
30
- import syspath from "syspath"
31
- import * as arktype from "arktype"
32
- import Table from "cli-table3"
33
- import chalk from "chalk"
34
-
35
- /* internal dependencies */
36
- import SpeechFlowNode from "./speechflow-node"
37
- import * as utils from "./speechflow-utils"
38
- import pkg from "../../package.json"
39
-
40
- /* central CLI context */
41
- let cli: CLIio | null = null
42
-
43
- type wsPeerCtx = {
44
- peer: string
45
- }
46
- type wsPeerInfo = {
47
- ctx: wsPeerCtx
48
- ws: WebSocket
49
- req: http.IncomingMessage
50
- }
51
-
52
- /* establish asynchronous environment */
53
- let debug = false
54
- ;(async () => {
55
- /* determine system paths */
56
- const { dataDir } = syspath({
57
- appName: "speechflow",
58
- dataDirAutoCreate: true
59
- })
60
-
61
- /* parse command-line arguments */
62
- const coerce = (arg: string) => Array.isArray(arg) ? arg[arg.length - 1] : arg
63
- const args = await yargs()
64
- /* eslint @stylistic/indent: off */
65
- .usage(
66
- "Usage: $0 " +
67
- "[-h|--help] " +
68
- "[-V|--version] " +
69
- "[-S|--status] " +
70
- "[-v|--verbose <level>] " +
71
- "[-a|--address <ip-address>] " +
72
- "[-p|--port <tcp-port>] " +
73
- "[-C|--cache <directory>] " +
74
- "[-d|--dashboard <type>:<id>:<name>[,...]] " +
75
- "[-o|--osc <ip-address>:<udp-port> " +
76
- "[-e|--expression <expression>] " +
77
- "[-f|--file <file>] " +
78
- "[-c|--config <id>@<yaml-config-file>] " +
79
- "[<argument> [...]]"
80
- )
81
- .version(false)
82
- .option("V", {
83
- alias: "version",
84
- type: "boolean",
85
- array: false,
86
- coerce,
87
- default: false,
88
- describe: "show program version information"
89
- })
90
- .option("S", {
91
- alias: "status",
92
- type: "boolean",
93
- array: false,
94
- coerce,
95
- default: false,
96
- describe: "show one-time status of nodes"
97
- })
98
- .option("v", {
99
- alias: "log-level",
100
- type: "string",
101
- array: false,
102
- coerce,
103
- nargs: 1,
104
- default: "warning",
105
- describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
106
- })
107
- .option("a", {
108
- alias: "address",
109
- type: "string",
110
- array: false,
111
- coerce,
112
- nargs: 1,
113
- default: "0.0.0.0",
114
- describe: "IP address for REST/WebSocket API"
115
- })
116
- .option("p", {
117
- alias: "port",
118
- type: "number",
119
- array: false,
120
- coerce,
121
- nargs: 1,
122
- default: 8484,
123
- describe: "TCP port for REST/WebSocket API"
124
- })
125
- .option("C", {
126
- alias: "cache",
127
- type: "string",
128
- array: false,
129
- coerce,
130
- nargs: 1,
131
- default: path.join(dataDir, "cache"),
132
- describe: "directory for cached files (primarily AI model files)"
133
- })
134
- .option("d", {
135
- alias: "dashboard",
136
- type: "string",
137
- array: false,
138
- coerce,
139
- nargs: 1,
140
- default: "",
141
- describe: "list of dashboard block types and names"
142
- })
143
- .option("o", {
144
- alias: "osc",
145
- type: "string",
146
- array: false,
147
- coerce,
148
- nargs: 1,
149
- default: "",
150
- describe: "OSC/UDP endpoint to send dashboard information"
151
- })
152
- .option("e", {
153
- alias: "expression",
154
- type: "string",
155
- array: false,
156
- coerce,
157
- nargs: 1,
158
- default: "",
159
- describe: "FlowLink expression string"
160
- })
161
- .option("f", {
162
- alias: "file",
163
- type: "string",
164
- array: false,
165
- coerce,
166
- nargs: 1,
167
- default: "",
168
- describe: "FlowLink expression file"
169
- })
170
- .option("c", {
171
- alias: "config",
172
- type: "string",
173
- array: false,
174
- coerce,
175
- nargs: 1,
176
- default: "",
177
- describe: "FlowLink expression reference into YAML file (in format <id>@<file>)"
178
- })
179
- .help("h", "show usage help")
180
- .alias("h", "help")
181
- .showHelpOnFail(true)
182
- .strict()
183
- .demand(0)
184
- .parse(hideBin(process.argv))
185
-
186
- /* short-circuit version request */
187
- if (args.V) {
188
- process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
189
- process.stderr.write(`${pkg.description}\n`)
190
- process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
191
- process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
192
- process.exit(0)
193
- }
194
-
195
- /* establish CLI environment */
196
- cli = new CLIio({
197
- encoding: "utf8",
198
- logLevel: args.v,
199
- logTime: true,
200
- logPrefix: pkg.name
201
- })
202
- if (args.v.match(/^(?:info|debug)$/))
203
- debug = true
204
-
205
- /* catch uncaught exceptions */
206
- process.on("uncaughtException", (err) => {
207
- if (debug)
208
- cli!.log("error", `uncaught exception: ${err.message}\n${err.stack}`)
209
- else
210
- cli!.log("error", `uncaught exception: ${err.message}`)
211
- process.exit(1)
212
- })
213
-
214
- /* catch unhandled promise rejections */
215
- process.on("unhandledRejection", (reason) => {
216
- if (reason instanceof Error) {
217
- if (debug)
218
- cli!.log("error", `unhandled rejection: ${reason.message}\n${reason.stack}`)
219
- else
220
- cli!.log("error", `unhandled rejection: ${reason.message}`)
221
- }
222
- else
223
- cli!.log("error", `unhandled rejection: ${reason}`)
224
- process.exit(1)
225
- })
226
-
227
- /* provide startup information */
228
- cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
229
-
230
- /* load .env files */
231
- const result = dotenvx.config({
232
- encoding: "utf8",
233
- ignore: [ "MISSING_ENV_FILE" ],
234
- quiet: true
235
- })
236
- if (result?.parsed !== undefined)
237
- for (const key of Object.keys(result.parsed))
238
- cli.log("info", `loaded environment variable "${key}" from ".env" files`)
239
-
240
- /* sanity check usage */
241
- let n = 0
242
- if (typeof args.e === "string" && args.e !== "") n++
243
- if (typeof args.f === "string" && args.f !== "") n++
244
- if (typeof args.c === "string" && args.c !== "") n++
245
- if (n === 0)
246
- throw new Error("need at least one FlowLink specification source (use one of the options -e, -f or -c)")
247
- else if (n !== 1)
248
- throw new Error("cannot use more than one FlowLink specification source (use only one of the options -e, -f or -c)")
249
-
250
- /* read configuration */
251
- let config = ""
252
- if (typeof args.e === "string" && args.e !== "")
253
- config = args.e
254
- else if (typeof args.f === "string" && args.f !== "")
255
- config = await cli.input(args.f, { encoding: "utf8" })
256
- else if (typeof args.c === "string" && args.c !== "") {
257
- const m = args.c.match(/^(.+?)@(.+)$/)
258
- if (m === null)
259
- throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
260
- const [ , id, file ] = m
261
- const yaml = await cli.input(file, { encoding: "utf8" })
262
- const obj: any = utils.run("parsing YAML configuration", () => jsYAML.load(yaml))
263
- if (obj[id] === undefined)
264
- throw new Error(`no such id "${id}" found in configuration file "${file}"`)
265
- config = obj[id] as string
266
- }
267
-
268
- /* track the available SpeechFlow nodes */
269
- const nodes: { [ id: string ]: typeof SpeechFlowNode } = {}
270
-
271
- /* load internal SpeechFlow nodes */
272
- const pkgsI = [
273
- "./speechflow-node-a2a-compressor.js",
274
- "./speechflow-node-a2a-expander.js",
275
- "./speechflow-node-a2a-ffmpeg.js",
276
- "./speechflow-node-a2a-filler.js",
277
- "./speechflow-node-a2a-gain.js",
278
- "./speechflow-node-a2a-gender.js",
279
- "./speechflow-node-a2a-meter.js",
280
- "./speechflow-node-a2a-mute.js",
281
- "./speechflow-node-a2a-rnnoise.js",
282
- "./speechflow-node-a2a-speex.js",
283
- "./speechflow-node-a2a-vad.js",
284
- "./speechflow-node-a2a-wav.js",
285
- "./speechflow-node-a2t-amazon.js",
286
- "./speechflow-node-a2t-deepgram.js",
287
- "./speechflow-node-a2t-openai.js",
288
- "./speechflow-node-t2a-amazon.js",
289
- "./speechflow-node-t2a-elevenlabs.js",
290
- "./speechflow-node-t2a-kokoro.js",
291
- "./speechflow-node-t2t-amazon.js",
292
- "./speechflow-node-t2t-deepl.js",
293
- "./speechflow-node-t2t-format.js",
294
- "./speechflow-node-t2t-google.js",
295
- "./speechflow-node-t2t-modify.js",
296
- "./speechflow-node-t2t-ollama.js",
297
- "./speechflow-node-t2t-openai.js",
298
- "./speechflow-node-t2t-sentence.js",
299
- "./speechflow-node-t2t-subtitle.js",
300
- "./speechflow-node-t2t-transformers.js",
301
- "./speechflow-node-x2x-filter.js",
302
- "./speechflow-node-x2x-trace.js",
303
- "./speechflow-node-xio-device.js",
304
- "./speechflow-node-xio-file.js",
305
- "./speechflow-node-xio-mqtt.js",
306
- "./speechflow-node-xio-websocket.js"
307
- ]
308
- for (const pkg of pkgsI) {
309
- let node: any = await import(pkg)
310
- while (node.default !== undefined)
311
- node = node.default
312
- if (typeof node === "function" && typeof node.name === "string") {
313
- cli.log("info", `loading SpeechFlow node <${node.name}> from internal module`)
314
- nodes[node.name] = node as typeof SpeechFlowNode
315
- }
316
- }
317
-
318
- /* load external SpeechFlow nodes */
319
- const pkgsE = await installedPackages()
320
- for (const pkg of pkgsE) {
321
- if (pkg.match(/^(?:@[^/]+\/)?speechflow-node-.+$/)) {
322
- let node: any = await import(pkg)
323
- while (node.default !== undefined)
324
- node = node.default
325
- if (typeof node === "function" && typeof node.name === "string") {
326
- if (nodes[node.name] !== undefined) {
327
- cli.log("warning", `failed loading SpeechFlow node <${node.name}> ` +
328
- `from external module "${pkg}" -- node already exists`)
329
- continue
330
- }
331
- cli.log("info", `loading SpeechFlow node <${node.name}> from external module "${pkg}"`)
332
- nodes[node.name] = node as typeof SpeechFlowNode
333
- }
334
- }
335
- }
336
-
337
- /* static configuration */
338
- const cfg = {
339
- audioChannels: 1,
340
- audioBitDepth: 16,
341
- audioLittleEndian: true,
342
- audioSampleRate: 48000,
343
- textEncoding: "utf8",
344
- cacheDir: args.C
345
- }
346
-
347
- /* provide access to internal communication busses */
348
- const busses = new Map<string, EventEmitter>()
349
- const accessBus = (name: string): EventEmitter => {
350
- let bus: EventEmitter
351
- if (busses.has(name))
352
- bus = busses.get(name)!
353
- else {
354
- bus = new EventEmitter()
355
- busses.set(name, bus)
356
- }
357
- return bus
358
- }
359
-
360
- /* handle one-time status query of nodes */
361
- if (args.S) {
362
- const table = new Table({
363
- head: [
364
- chalk.reset.bold("NODE"),
365
- chalk.reset.bold("PROPERTY"),
366
- chalk.reset.bold("VALUE")
367
- ],
368
- colWidths: [ 15, 15, 50 - (2 * 2 + 2 * 3) ],
369
- style: { "padding-left": 1, "padding-right": 1, border: [ "grey" ], compact: true },
370
- chars: { "left-mid": "", mid: "", "mid-mid": "", "right-mid": "" }
371
- })
372
- for (const name of Object.keys(nodes)) {
373
- cli!.log("info", `gathering status of node <${name}>`)
374
- const node = new nodes[name](name, cfg, {}, [])
375
- node._accessBus = accessBus
376
- const status = await Promise.race<{ [ key: string ]: string | number }>([
377
- node.status(),
378
- new Promise<never>((resolve, reject) => setTimeout(() =>
379
- reject(new Error("timeout")), 10 * 1000))
380
- ]).catch((err: Error) => {
381
- cli!.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
382
- return {} as { [ key: string ]: string | number }
383
- })
384
- if (Object.keys(status).length > 0) {
385
- let first = true
386
- for (const key of Object.keys(status)) {
387
- table.push([ first ? chalk.bold(name) : "", key, chalk.blue(status[key]) ])
388
- first = false
389
- }
390
- }
391
- }
392
- const output = table.toString()
393
- process.stdout.write(output + "\n")
394
- process.exit(0)
395
- }
396
-
397
- /* graph processing: PASS 1: parse DSL and create and connect nodes */
398
- const flowlink = new FlowLink<SpeechFlowNode>({
399
- trace: (msg: string) => {
400
- cli!.log("debug", msg)
401
- }
402
- })
403
- const variables = { argv: args._, env: process.env }
404
- const graphNodes = new Set<SpeechFlowNode>()
405
- const nodeNums = new Map<typeof SpeechFlowNode, number>()
406
- let ast: unknown
407
- try {
408
- ast = flowlink.compile(config)
409
- }
410
- catch (err) {
411
- const errorMsg = err instanceof Error && err.name === "FlowLinkError"
412
- ? err.toString() : (err instanceof Error ? err.message : "internal error")
413
- cli!.log("error", `failed to parse SpeechFlow configuration: ${errorMsg}`)
414
- process.exit(1)
415
- }
416
- try {
417
- flowlink.execute(ast, {
418
- resolveVariable (id: string) {
419
- if (!objectPath.has(variables, id))
420
- throw new Error(`failed to resolve variable "${id}"`)
421
- const value = objectPath.get(variables, id)
422
- cli!.log("info", `resolve variable: "${id}" -> "${value}"`)
423
- return value
424
- },
425
- createNode (id: string, opts: { [ id: string ]: any }, args: any[]) {
426
- if (nodes[id] === undefined)
427
- throw new Error(`unknown node <${id}>`)
428
- let node: SpeechFlowNode
429
- try {
430
- const NodeClass = nodes[id]
431
- let num = nodeNums.get(NodeClass) ?? 0
432
- nodeNums.set(NodeClass, ++num)
433
- const name = num === 1 ? id : `${id}:${num}`
434
- node = new NodeClass(name, cfg, opts, args)
435
- node._accessBus = accessBus
436
- }
437
- catch (err) {
438
- /* fatal error */
439
- if (err instanceof Error)
440
- cli!.log("error", `creation of node <${id}> failed: ${err.message}`)
441
- else
442
- cli!.log("error", `creation of node <${id}> failed: ${err}`)
443
- process.exit(1)
444
- }
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!
454
- },
455
- connectNodes (node1: SpeechFlowNode, node2: SpeechFlowNode) {
456
- cli!.log("info", `connect node <${node1.id}> to node <${node2.id}>`)
457
- node1.connect(node2)
458
- }
459
- })
460
- }
461
- catch (err) {
462
- const errorMsg = err instanceof Error && err.name === "FlowLinkError"
463
- ? err.toString() : (err instanceof Error ? err.message : "internal error")
464
- cli!.log("error", `failed to materialize SpeechFlow configuration: ${errorMsg}`)
465
- process.exit(1)
466
- }
467
-
468
- /* graph processing: PASS 2: prune connections of nodes */
469
- for (const node of graphNodes) {
470
- /* determine connections */
471
- let connectionsIn = Array.from(node.connectionsIn)
472
- let connectionsOut = Array.from(node.connectionsOut)
473
-
474
- /* ensure necessary incoming links */
475
- if (node.input !== "none" && connectionsIn.length === 0)
476
- throw new Error(`node <${node.id}> requires input but has no input nodes connected`)
477
-
478
- /* prune unnecessary incoming links */
479
- if (node.input === "none" && connectionsIn.length > 0)
480
- connectionsIn.forEach((other) => { other.disconnect(node) })
481
-
482
- /* ensure necessary outgoing links */
483
- if (node.output !== "none" && connectionsOut.length === 0)
484
- throw new Error(`node <${node.id}> requires output but has no output nodes connected`)
485
-
486
- /* prune unnecessary outgoing links */
487
- if (node.output === "none" && connectionsOut.length > 0)
488
- connectionsOut.forEach((other) => { node.disconnect(other) })
489
-
490
- /* check for payload compatibility */
491
- connectionsIn = Array.from(node.connectionsIn)
492
- connectionsOut = Array.from(node.connectionsOut)
493
- for (const other of connectionsOut)
494
- if (other.input !== node.output)
495
- throw new Error(`${node.output} output node <${node.id}> cannot be ` +
496
- `connected to ${other.input} input node <${other.id}> (payload is incompatible)`)
497
- }
498
-
499
- /* graph processing: PASS 3: open nodes */
500
- const timeZero = DateTime.now()
501
- for (const node of graphNodes) {
502
- /* connect node events */
503
- node.on("log", (level: string, msg: string, data?: any) => {
504
- let str = `<${node.id}>: ${msg}`
505
- if (data !== undefined)
506
- str += ` (${JSON.stringify(data)})`
507
- cli!.log(level, str)
508
- })
509
-
510
- /* open node */
511
- cli!.log("info", `open node <${node.id}>`)
512
- node.setTimeZero(timeZero)
513
- await Promise.race<void>([
514
- node.open(),
515
- new Promise<never>((resolve, reject) => setTimeout(() =>
516
- reject(new Error("timeout")), 10 * 1000))
517
- ]).catch((err: Error) => {
518
- cli!.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`)
519
- throw new Error(`failed to open node <${node.id}>: ${err.message}`)
520
- })
521
- }
522
-
523
- /* graph processing: PASS 4: connect node streams */
524
- for (const node of graphNodes) {
525
- if (node.stream === null)
526
- throw new Error(`stream of node <${node.id}> still not initialized`)
527
- for (const other of Array.from(node.connectionsOut)) {
528
- if (other.stream === null)
529
- throw new Error(`stream of incoming node <${other.id}> still not initialized`)
530
- cli!.log("info", `connect stream of node <${node.id}> to stream of node <${other.id}>`)
531
- if (!( node.stream instanceof Stream.Readable
532
- || node.stream instanceof Stream.Duplex ))
533
- throw new Error(`stream of output node <${node.id}> is neither of Readable nor Duplex type`)
534
- if (!( other.stream instanceof Stream.Writable
535
- || other.stream instanceof Stream.Duplex ))
536
- throw new Error(`stream of input node <${other.id}> is neither of Writable nor Duplex type`)
537
- node.stream.pipe(other.stream)
538
- }
539
- }
540
-
541
- /* graph processing: PASS 5: track stream finishing */
542
- const activeNodes = new Set<SpeechFlowNode>()
543
- const finishEvents = new EventEmitter()
544
- finishEvents.setMaxListeners(graphNodes.size + 10)
545
- for (const node of graphNodes) {
546
- if (node.stream === null)
547
- throw new Error(`stream of node <${node.id}> still not initialized`)
548
- cli!.log("info", `observe stream of node <${node.id}> for finish event`)
549
- activeNodes.add(node)
550
- const deactivateNode = (node: SpeechFlowNode, msg: string) => {
551
- if (activeNodes.has(node))
552
- activeNodes.delete(node)
553
- cli!.log("info", `${msg} (${activeNodes.size} active nodes remaining)`)
554
- if (activeNodes.size === 0) {
555
- const timeFinished = DateTime.now()
556
- const duration = timeFinished.diff(timeZero)
557
- cli!.log("info", "**** everything finished -- stream processing in SpeechFlow graph stops " +
558
- `(total duration: ${duration.toFormat("hh:mm:ss.SSS")}) ****`)
559
- finishEvents.emit("finished")
560
- }
561
- }
562
- node.stream.on("end", () => {
563
- deactivateNode(node, `readable stream side of node <${node.id}> raised "end" event`)
564
- })
565
- node.stream.on("finish", () => {
566
- deactivateNode(node, `writable stream side of node <${node.id}> raised "finish" event`)
567
- })
568
- }
569
-
570
- /* define external request/response structure */
571
- const requestValidator = arktype.type({
572
- request: "string",
573
- node: "string",
574
- args: "unknown[]"
575
- })
576
-
577
- /* forward external request to target node in graph */
578
- const consumeExternalRequest = async (_req: any) => {
579
- const req = requestValidator(_req)
580
- if (req instanceof arktype.type.errors)
581
- throw new Error(`invalid request: ${req.summary}`)
582
- if (req.request !== "COMMAND")
583
- throw new Error("invalid external request (command expected)")
584
- const name = req.node as string
585
- const args = req.args as any[]
586
- const foundNode = Array.from(graphNodes).find((node) => node.id === name)
587
- if (foundNode === undefined) {
588
- cli!.log("warning", `external request failed: no such node <${name}>`)
589
- throw new Error(`external request failed: no such node <${name}>`)
590
- }
591
- else {
592
- await Promise.race<void>([
593
- foundNode.receiveRequest(args),
594
- new Promise<never>((resolve, reject) => setTimeout(() =>
595
- reject(new Error("timeout")), 10 * 1000))
596
- ]).catch((err: Error) => {
597
- cli!.log("warning", `external request to node <${name}> failed: ${err.message}`)
598
- })
599
- }
600
- }
601
-
602
- /* establish REST/WebSocket API */
603
- const wsPeers = new Map<string, wsPeerInfo>()
604
- const hapi = new HAPI.Server({
605
- address: args.a,
606
- port: args.p
607
- })
608
- await hapi.register({ plugin: Inert })
609
- await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
610
- await hapi.register({ plugin: HAPIWebSocket })
611
- hapi.events.on("response", (request: HAPI.Request) => {
612
- let protocol = `HTTP/${request.raw.req.httpVersion}`
613
- const ws = request.websocket()
614
- if (ws.mode === "websocket") {
615
- const wsVersion = (ws.ws as any).protocolVersion ??
616
- request.headers["sec-websocket-version"] ?? "13?"
617
- protocol = `WebSocket/${wsVersion}+${protocol}`
618
- }
619
- const msg =
620
- "remote=" + request.info.remoteAddress + ", " +
621
- "method=" + request.method.toUpperCase() + ", " +
622
- "url=" + request.url.pathname + ", " +
623
- "protocol=" + protocol + ", " +
624
- "response=" + ("statusCode" in request.response ? request.response.statusCode : "<unknown>")
625
- cli!.log("info", `HAPI: request: ${msg}`)
626
- })
627
- hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
628
- if (event.error instanceof Error)
629
- cli!.log("error", `HAPI: request-error: ${event.error.message}`)
630
- else
631
- cli!.log("error", `HAPI: request-error: ${event.error}`)
632
- })
633
- hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
634
- if (tags.error) {
635
- const err = event.error
636
- if (err instanceof Error)
637
- cli!.log("error", `HAPI: log: ${err.message}`)
638
- else
639
- cli!.log("error", `HAPI: log: ${err}`)
640
- }
641
- })
642
- hapi.route({
643
- method: "GET",
644
- path: "/{param*}",
645
- handler: {
646
- directory: {
647
- path: path.join(__dirname, "../../speechflow-ui-db/dst"),
648
- redirectToSlash: true,
649
- index: true
650
- }
651
- }
652
- })
653
- hapi.route({
654
- method: "GET",
655
- path: "/api/dashboard",
656
- handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
657
- const config = []
658
- for (const block of args.d.split(",")) {
659
- const [ type, id, name ] = block.split(":")
660
- config.push({ type, id, name })
661
- }
662
- return h.response(config).code(200)
663
- }
664
- })
665
- hapi.route({
666
- method: "GET",
667
- path: "/api/{req}/{node}/{params*}",
668
- options: {},
669
- handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
670
- const peer = request.info.remoteAddress
671
- const params = request.params.params as string ?? ""
672
- if (params.length > 1000)
673
- return h.response({ response: "ERROR", data: "parameters too long" }).code(400)
674
- const req = {
675
- request: request.params.req,
676
- node: request.params.node,
677
- args: params.split("/").filter((seg) => seg !== "")
678
- }
679
- cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
680
- return consumeExternalRequest(req)
681
- .then(() => h.response({ response: "OK" }).code(200))
682
- .catch((error: unknown) => h.response({ response: "ERROR", data: utils.ensureError(error).message }).code(417))
683
- }
684
- })
685
- hapi.route({
686
- method: "POST",
687
- path: "/api",
688
- options: {
689
- payload: {
690
- output: "data",
691
- parse: true,
692
- allow: "application/json",
693
- maxBytes: 1 * 1024 * 1024
694
- },
695
- plugins: {
696
- websocket: {
697
- autoping: 30 * 1000,
698
- connect: (args: any) => {
699
- const ctx: wsPeerCtx = args.ctx
700
- const ws: WebSocket = args.ws
701
- const req: http.IncomingMessage = args.req
702
- const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
703
- ctx.peer = peer
704
- wsPeers.set(peer, { ctx, ws, req })
705
- cli!.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
706
- },
707
- disconnect: (args: any) => {
708
- const ctx: wsPeerCtx = args.ctx
709
- const ws: WebSocket = args.ws
710
- const peer = ctx.peer
711
- wsPeers.delete(peer)
712
- ws.removeAllListeners()
713
- if (ws.readyState === WebSocket.OPEN)
714
- ws.close()
715
- cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
716
- }
717
- }
718
- }
719
- },
720
- handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
721
- /* on WebSocket message transfer */
722
- const peer = request.info.remoteAddress
723
- const req = requestValidator(request.payload)
724
- if (req instanceof arktype.type.errors)
725
- return h.response({ response: "ERROR", data: `invalid request: ${req.summary}` }).code(417)
726
- cli!.log("info", `HAPI: peer ${peer}: POST: ${JSON.stringify(req)}`)
727
- return consumeExternalRequest(req)
728
- .then(() => h.response({ response: "OK" }).code(200))
729
- .catch((err: Error) => h.response({ response: "ERROR", data: err.message }).code(417))
730
- }
731
- })
732
- await hapi.start()
733
- cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.a}:${args.p}`)
734
-
735
- /* hook for sendResponse method of nodes */
736
- for (const node of graphNodes) {
737
- node.on("send-response", (args: any[]) => {
738
- const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
739
- for (const [ peer, info ] of wsPeers.entries()) {
740
- cli!.log("debug", `HAPI: remote peer ${peer}: sending ${data}`)
741
- if (info.ws.readyState === WebSocket.OPEN)
742
- info.ws.send(data)
743
- }
744
- })
745
- }
746
-
747
- /* establish OSC event emission */
748
- let sendOSC: (url: string, ...args: any[]) => void
749
- if (args.o !== "") {
750
- const osc = new OSC({ plugin: new OSC.DatagramPlugin({ type: "udp4" }) })
751
- const m = args.o.match(/^(.+?):(\d+)$/)
752
- if (m === null)
753
- throw new Error("invalid OSC/UDP endpoint (expected <ip-adress>:<udp-port>)")
754
- const host = m[1]
755
- const port = m[2]
756
- sendOSC = (url: string, ...args: any[]) => {
757
- const msg = new OSC.Message(url, ...args)
758
- osc.send(msg, { host, port })
759
- }
760
- }
761
-
762
- /* hook for send-dashboard method of nodes */
763
- for (const node of graphNodes) {
764
- node.on("send-dashboard", (info: {
765
- type: "audio" | "text",
766
- id: string,
767
- kind: "final" | "intermediate",
768
- value: string | number
769
- }) => {
770
- const data = JSON.stringify({
771
- response: "DASHBOARD",
772
- node: "",
773
- args: [ info.type, info.id, info.kind, info.value ]
774
- })
775
- for (const [ peer, info ] of wsPeers.entries()) {
776
- cli!.log("debug", `HAPI: dashboard peer ${peer}: send ${data}`)
777
- info.ws.send(data)
778
- }
779
- for (const node of graphNodes) {
780
- Promise.race<void>([
781
- node.receiveDashboard(info.type, info.id, info.kind, info.value),
782
- new Promise<never>((resolve, reject) => setTimeout(() =>
783
- reject(new Error("timeout")), 10 * 1000))
784
- ]).catch((err: Error) => {
785
- cli!.log("warning", `sending dashboard info to node <${node.id}> failed: ${err.message}`)
786
- })
787
- }
788
- if (args.o !== "")
789
- sendOSC("/speechflow/dashboard", info.type, info.id, info.kind, info.value)
790
- })
791
- }
792
-
793
- /* start of internal stream processing */
794
- cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
795
-
796
- /* gracefully shutdown process */
797
- let shuttingDown = false
798
- const shutdown = async (signal: string) => {
799
- if (shuttingDown)
800
- return
801
- shuttingDown = true
802
- if (signal === "finished")
803
- cli!.log("info", "**** streams of all nodes finished -- shutting down service ****")
804
- else if (signal === "exception")
805
- cli!.log("warning", "**** exception occurred -- shutting down service ****")
806
- else
807
- cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
808
-
809
- /* shutdown HAPI service */
810
- cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.a}:${args.p}`)
811
- await hapi.stop({ timeout: 2000 })
812
-
813
- /* clear WebSocket connections */
814
- if (wsPeers.size > 0) {
815
- cli!.log("info", "HAPI: closing WebSocket connections")
816
- const closePromises: Promise<void>[] = []
817
- for (const [ peer, info ] of wsPeers.entries()) {
818
- closePromises.push(new Promise<void>((resolve, reject) => {
819
- if (info.ws.readyState !== WebSocket.OPEN)
820
- resolve()
821
- else {
822
- const timeout = setTimeout(() => {
823
- reject(new Error(`timeout for peer ${peer}`))
824
- }, 2 * 1000)
825
- info.ws.once("close", () => {
826
- clearTimeout(timeout)
827
- resolve()
828
- })
829
- info.ws.close()
830
- }
831
- }))
832
- }
833
- await Promise.race([
834
- Promise.all(closePromises),
835
- new Promise((resolve, reject) =>
836
- setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
837
- ]).catch((error: unknown) => {
838
- cli!.log("warning", `HAPI: WebSockets failed to close: ${utils.ensureError(error).message}`)
839
- })
840
- wsPeers.clear()
841
- }
842
-
843
- /* graph processing: PASS 1: disconnect node streams */
844
- for (const node of graphNodes) {
845
- if (node.stream === null) {
846
- cli!.log("warning", `stream of node <${node.id}> no longer initialized`)
847
- continue
848
- }
849
- for (const other of Array.from(node.connectionsOut)) {
850
- if (other.stream === null) {
851
- cli!.log("warning", `stream of incoming node <${other.id}> no longer initialized`)
852
- continue
853
- }
854
- if (!( node.stream instanceof Stream.Readable
855
- || node.stream instanceof Stream.Duplex )) {
856
- cli!.log("warning", `stream of output node <${node.id}> is neither of Readable nor Duplex type`)
857
- continue
858
- }
859
- if (!( other.stream instanceof Stream.Writable
860
- || other.stream instanceof Stream.Duplex )) {
861
- cli!.log("warning", `stream of input node <${other.id}> is neither of Writable nor Duplex type`)
862
- continue
863
- }
864
- cli!.log("info", `disconnect stream of node <${node.id}> from stream of node <${other.id}>`)
865
- node.stream.unpipe(other.stream)
866
- }
867
- }
868
-
869
- /* graph processing: PASS 2: close nodes */
870
- for (const node of graphNodes) {
871
- cli!.log("info", `close node <${node.id}>`)
872
- await Promise.race<void>([
873
- node.close(),
874
- new Promise<never>((resolve, reject) => setTimeout(() =>
875
- reject(new Error("timeout")), 10 * 1000))
876
- ]).catch((err: Error) => {
877
- cli!.log("warning", `node <${node.id}> failed to close: ${err.message}`)
878
- })
879
- }
880
-
881
- /* graph processing: PASS 3: disconnect nodes */
882
- for (const node of graphNodes) {
883
- cli!.log("info", `disconnect node <${node.id}>`)
884
- const connectionsIn = Array.from(node.connectionsIn)
885
- const connectionsOut = Array.from(node.connectionsOut)
886
- connectionsIn.forEach((other) => { other.disconnect(node) })
887
- connectionsOut.forEach((other) => { node.disconnect(other) })
888
- }
889
-
890
- /* graph processing: PASS 4: shutdown nodes */
891
- for (const node of graphNodes) {
892
- cli!.log("info", `destroy node <${node.id}>`)
893
- graphNodes.delete(node)
894
- }
895
-
896
- /* clear event emitters */
897
- finishEvents.removeAllListeners()
898
-
899
- /* clear active nodes */
900
- activeNodes.clear()
901
-
902
- /* terminate process */
903
- if (signal === "finished") {
904
- cli!.log("info", "terminate process (exit code 0)")
905
- process.exit(0)
906
- }
907
- else {
908
- cli!.log("info", "terminate process (exit code 1)")
909
- process.exit(1)
910
- }
911
- }
912
-
913
- /* hook into regular finish */
914
- finishEvents.on("finished", () => { shutdown("finished") })
915
-
916
- /* hook into process signals */
917
- process.on("SIGINT", () => { shutdown("SIGINT") })
918
- process.on("SIGUSR1", () => { shutdown("SIGUSR1") })
919
- process.on("SIGUSR2", () => { shutdown("SIGUSR2") })
920
- process.on("SIGTERM", () => { shutdown("SIGTERM") })
921
-
922
- /* re-hook into uncaught exception handler */
923
- process.removeAllListeners("uncaughtException")
924
- process.on("uncaughtException", (err) => {
925
- if (debug)
926
- cli!.log("error", `uncaught exception: ${err.message}\n${err.stack}`)
927
- else
928
- cli!.log("error", `uncaught exception: ${err.message}`)
929
- shutdown("exception")
930
- })
931
-
932
- /* re-hook into unhandled promise rejection handler */
933
- process.removeAllListeners("unhandledRejection")
934
- process.on("unhandledRejection", (reason) => {
935
- if (reason instanceof Error) {
936
- if (debug)
937
- cli!.log("error", `unhandled rejection: ${reason.message}\n${reason.stack}`)
938
- else
939
- cli!.log("error", `unhandled rejection: ${reason.message}`)
940
- }
941
- else
942
- cli!.log("error", `unhandled rejection: ${reason}`)
943
- shutdown("exception")
944
- })
945
- })().catch((err: Error) => {
946
- /* top-level exception handling */
947
- if (cli !== null) {
948
- if (debug)
949
- cli.log("error", `${err.message}\n${err.stack}`)
950
- else
951
- cli.log("error", `${err.message}`)
952
- }
953
- else {
954
- if (debug)
955
- process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
956
- else
957
- process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}`)
958
- }
959
- process.exit(1)
960
- })
8
+ /* pass-through control to the main module */
9
+ import Main from "./speechflow-main"
10
+ Main.main()
961
11