speechflow 1.4.2 → 1.4.4

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