speechflow 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /*
3
+ ** SpeechFlow - Speech Processing Flow Graph
4
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const node_stream_1 = __importDefault(require("node:stream"));
45
+ const node_events_1 = require("node:events");
46
+ const ElevenLabs = __importStar(require("elevenlabs"));
47
+ const get_stream_1 = require("get-stream");
48
+ const speechflow_node_1 = __importDefault(require("./speechflow-node"));
49
+ /*
50
+ const elevenlabsVoices = {
51
+ "drew": { name: "Drew", model: "eleven_multilingual_v2", lang: [ "en", "de" ] },
52
+ "george": { name: "George", model: "eleven_multilingual_v2", lang: [ "en", "de" ] },
53
+ "bill": { name: "Bill", model: "eleven_multilingual_v2", lang: [ "en", "de" ] },
54
+ "daniel": { name: "Daniel", model: "eleven_multilingual_v1", lang: [ "en", "de" ] },
55
+ "brian": { name: "Brian", model: "eleven_turbo_v2", lang: [ "en" ] },
56
+ "sarah": { name: "Sarah", model: "eleven_multilingual_v2", lang: [ "en", "de" ] },
57
+ "racel": { name: "Racel", model: "eleven_multilingual_v2", lang: [ "en", "de" ] },
58
+ "grace": { name: "Grace", model: "eleven_multilingual_v1", lang: [ "en", "de" ] },
59
+ "matilda": { name: "Matilda", model: "eleven_multilingual_v1", lang: [ "en", "de" ] },
60
+ "alice": { name: "Alice", model: "eleven_turbo_v2", lang: [ "en" ] }
61
+ }
62
+ */
63
+ class SpeechFlowNodeElevenlabs extends speechflow_node_1.default {
64
+ elevenlabs = null;
65
+ constructor(id, opts, args) {
66
+ super(id, opts, args);
67
+ this.configure({
68
+ key: { type: "string", val: process.env.SPEECHFLOW_KEY_ELEVENLABS },
69
+ voice: { type: "string", val: "Brian", pos: 0 },
70
+ language: { type: "string", val: "de", pos: 1 }
71
+ });
72
+ }
73
+ async open() {
74
+ this.input = "text";
75
+ this.output = "audio";
76
+ this.elevenlabs = new ElevenLabs.ElevenLabsClient({
77
+ apiKey: this.params.key
78
+ });
79
+ const voices = await this.elevenlabs.voices.getAll();
80
+ const voice = voices.voices.find((voice) => voice.name === this.params.voice);
81
+ if (voice === undefined)
82
+ throw new Error(`invalid ElevenLabs voice "${this.params.voice}"`);
83
+ const speechStream = (text) => {
84
+ return this.elevenlabs.textToSpeech.convert(voice.voice_id, {
85
+ text,
86
+ optimize_streaming_latency: 2,
87
+ output_format: "pcm_16000", // S16LE
88
+ model_id: "eleven_flash_v2_5",
89
+ /*
90
+ voice_settings: {
91
+ stability: 0,
92
+ similarity_boost: 0
93
+ }
94
+ */
95
+ }, {
96
+ timeoutInSeconds: 30,
97
+ maxRetries: 10
98
+ });
99
+ };
100
+ const queue = new node_events_1.EventEmitter();
101
+ this.stream = new node_stream_1.default.Duplex({
102
+ write(chunk, encoding, callback) {
103
+ if (encoding !== "utf8" && encoding !== "utf-8")
104
+ callback(new Error("only text input supported by Elevenlabs node"));
105
+ const data = chunk.toString();
106
+ speechStream(data).then((stream) => {
107
+ (0, get_stream_1.getStreamAsBuffer)(stream).then((buffer) => {
108
+ queue.emit("audio", buffer);
109
+ callback();
110
+ }).catch((error) => {
111
+ callback(error);
112
+ });
113
+ }).catch((error) => {
114
+ callback(error);
115
+ });
116
+ },
117
+ read(size) {
118
+ queue.once("audio", (buffer) => {
119
+ this.push(buffer, "binary");
120
+ });
121
+ }
122
+ });
123
+ }
124
+ async close() {
125
+ if (this.stream !== null) {
126
+ this.stream.destroy();
127
+ this.stream = null;
128
+ }
129
+ }
130
+ }
131
+ exports.default = SpeechFlowNodeElevenlabs;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /*
3
+ ** SpeechFlow - Speech Processing Flow Graph
4
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const speechflow_node_1 = __importDefault(require("./speechflow-node"));
13
+ class SpeechFlowNodeDevice extends speechflow_node_1.default {
14
+ constructor(id, opts, args) {
15
+ super(id, opts, args);
16
+ this.configure({
17
+ path: { type: "string", pos: 0 },
18
+ mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
19
+ type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ }
20
+ });
21
+ }
22
+ async open() {
23
+ if (this.params.mode === "r") {
24
+ this.output = this.params.type;
25
+ if (this.params.path === "-")
26
+ this.stream = process.stdin;
27
+ else
28
+ this.stream = node_fs_1.default.createReadStream(this.params.path, { encoding: this.params.type === "text" ? this.config.textEncoding : "binary" });
29
+ }
30
+ else if (this.params.mode === "w") {
31
+ this.input = this.params.type;
32
+ if (this.params.path === "-")
33
+ this.stream = process.stdout;
34
+ else
35
+ this.stream = node_fs_1.default.createWriteStream(this.params.path, { encoding: this.params.type === "text" ? this.config.textEncoding : "binary" });
36
+ }
37
+ else
38
+ throw new Error(`invalid file mode "${this.params.mode}"`);
39
+ }
40
+ async close() {
41
+ if (this.stream !== null && this.params.path !== "-") {
42
+ this.stream.destroy();
43
+ this.stream = null;
44
+ }
45
+ }
46
+ }
47
+ exports.default = SpeechFlowNodeDevice;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /*
3
+ ** SpeechFlow - Speech Processing Flow Graph
4
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const node_stream_1 = __importDefault(require("node:stream"));
12
+ const ws_1 = __importDefault(require("ws"));
13
+ const reconnecting_websocket_1 = __importDefault(require("@opensumi/reconnecting-websocket"));
14
+ const speechflow_node_1 = __importDefault(require("./speechflow-node"));
15
+ class SpeechFlowNodeWebsocket extends speechflow_node_1.default {
16
+ server = null;
17
+ client = null;
18
+ constructor(id, opts, args) {
19
+ super(id, opts, args);
20
+ this.configure({
21
+ listen: { type: "string", val: "", match: /^(?:|ws:\/\/(.+?):(\d+))$/ },
22
+ connect: { type: "string", val: "", match: /^(?:|ws:\/\/(.+?):(\d+)(?:\/.*)?)$/ },
23
+ type: { type: "string", val: "text", match: /^(?:audio|text)$/ }
24
+ });
25
+ }
26
+ async open() {
27
+ this.input = this.params.type;
28
+ this.output = this.params.type;
29
+ if (this.params.listen !== "") {
30
+ const url = new URL(this.params.listen);
31
+ let websocket = null;
32
+ const server = new ws_1.default.WebSocketServer({
33
+ host: url.hostname,
34
+ port: Number.parseInt(url.port),
35
+ path: url.pathname
36
+ });
37
+ server.on("listening", () => {
38
+ this.log("info", `listening on URL ${this.params.listen}`);
39
+ });
40
+ server.on("connection", (ws, request) => {
41
+ this.log("info", `connection opened on URL ${this.params.listen}`);
42
+ websocket = ws;
43
+ });
44
+ server.on("close", () => {
45
+ this.log("info", `connection closed on URL ${this.params.listen}`);
46
+ websocket = null;
47
+ });
48
+ server.on("error", (error) => {
49
+ this.log("error", `error on URL ${this.params.listen}: ${error.message}`);
50
+ websocket = null;
51
+ });
52
+ this.stream = new node_stream_1.default.Duplex({
53
+ write(chunk, encoding, callback) {
54
+ const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
55
+ if (websocket !== null) {
56
+ websocket.send(data, (error) => {
57
+ if (error)
58
+ callback(error);
59
+ else
60
+ callback();
61
+ });
62
+ }
63
+ else
64
+ callback(new Error("still no Websocket connection available"));
65
+ },
66
+ read(size) {
67
+ if (websocket !== null) {
68
+ websocket.once("message", (data, isBinary) => {
69
+ this.push(data, isBinary ? "binary" : "utf8");
70
+ });
71
+ }
72
+ else
73
+ throw new Error("still no Websocket connection available");
74
+ }
75
+ });
76
+ }
77
+ else if (this.params.connect !== "") {
78
+ this.client = new reconnecting_websocket_1.default(this.params.connect, [], {
79
+ WebSocket: ws_1.default,
80
+ WebSocketOptions: {},
81
+ reconnectionDelayGrowFactor: 1.3,
82
+ maxReconnectionDelay: 4000,
83
+ minReconnectionDelay: 1000,
84
+ connectionTimeout: 4000,
85
+ minUptime: 5000
86
+ });
87
+ this.client.addEventListener("open", (ev) => {
88
+ this.log("info", `connection opened on URL ${this.params.connect}`);
89
+ });
90
+ this.client.addEventListener("close", (ev) => {
91
+ this.log("info", `connection closed on URL ${this.params.connect}`);
92
+ });
93
+ this.client.addEventListener("error", (ev) => {
94
+ this.log("error", `error on URL ${this.params.connect}: ${ev.error.message}`);
95
+ });
96
+ const client = this.client;
97
+ client.binaryType = "arraybuffer";
98
+ this.stream = new node_stream_1.default.Duplex({
99
+ write(chunk, encoding, callback) {
100
+ const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
101
+ if (client.OPEN) {
102
+ client.send(data);
103
+ callback();
104
+ }
105
+ else
106
+ callback(new Error("still no Websocket connection available"));
107
+ },
108
+ read(size) {
109
+ if (client.OPEN) {
110
+ client.addEventListener("message", (ev) => {
111
+ if (ev.data instanceof ArrayBuffer)
112
+ this.push(ev.data, "binary");
113
+ else
114
+ this.push(ev.data, "utf8");
115
+ }, { once: true });
116
+ }
117
+ else
118
+ throw new Error("still no Websocket connection available");
119
+ }
120
+ });
121
+ }
122
+ else
123
+ throw new Error("neither listen nor connect mode requested");
124
+ }
125
+ async close() {
126
+ if (this.server !== null) {
127
+ await new Promise((resolve, reject) => {
128
+ this.server.close((error) => {
129
+ if (error)
130
+ reject(error);
131
+ else
132
+ resolve();
133
+ });
134
+ });
135
+ this.server = null;
136
+ }
137
+ if (this.client !== null) {
138
+ this.client.close();
139
+ this.client = null;
140
+ }
141
+ if (this.stream !== null) {
142
+ this.stream.destroy();
143
+ this.stream = null;
144
+ }
145
+ }
146
+ }
147
+ exports.default = SpeechFlowNodeWebsocket;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /*
3
+ ** SpeechFlow - Speech Processing Flow Graph
4
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const node_events_1 = __importDefault(require("node:events"));
12
+ class SpeechFlowNode extends node_events_1.default.EventEmitter {
13
+ id;
14
+ opts;
15
+ args;
16
+ config = {
17
+ audioChannels: 1, /* audio mono channel */
18
+ audioBitDepth: 16, /* audio PCM 16-bit integer */
19
+ audioLittleEndian: true, /* audio PCM little-endian */
20
+ audioSampleRate: 48000, /* audio 48kHz sample rate */
21
+ textEncoding: "utf8" /* UTF-8 text encoding */
22
+ };
23
+ input = "none";
24
+ output = "none";
25
+ params = {};
26
+ stream = null;
27
+ connectionsIn = new Set();
28
+ connectionsOut = new Set();
29
+ constructor(id, opts, args) {
30
+ super();
31
+ this.id = id;
32
+ this.opts = opts;
33
+ this.args = args;
34
+ }
35
+ configure(spec) {
36
+ for (const name of Object.keys(spec)) {
37
+ if (this.opts[name] !== undefined) {
38
+ if (typeof this.opts[name] !== spec[name].type)
39
+ throw new Error(`invalid type of option "${name}"`);
40
+ if ("match" in spec[name] && this.opts[name].match(spec[name].match) === null)
41
+ throw new Error(`invalid value of option "${name}" (has to match ${spec[name].match})`);
42
+ this.params[name] = this.opts[name];
43
+ }
44
+ else if (this.opts[name] === undefined
45
+ && "pos" in spec[name]
46
+ && spec[name].pos < this.args.length) {
47
+ if (typeof this.args[spec[name].pos] !== spec[name].type)
48
+ throw new Error(`invalid type of argument "${name}"`);
49
+ if ("match" in spec[name] && this.args[spec[name].pos].match(spec[name].match) === null)
50
+ throw new Error(`invalid value of option "${name}" (has to match ${spec[name].match})`);
51
+ this.params[name] = this.args[spec[name].pos];
52
+ }
53
+ else if ("val" in spec[name] && spec[name].val !== undefined)
54
+ this.params[name] = spec[name].val;
55
+ else
56
+ throw new Error(`required parameter "${name}" not given`);
57
+ }
58
+ }
59
+ connect(other) {
60
+ this.connectionsOut.add(other);
61
+ other.connectionsIn.add(this);
62
+ }
63
+ disconnect(other) {
64
+ if (!this.connectionsOut.has(other))
65
+ throw new Error("invalid node: not connected to this node");
66
+ this.connectionsOut.delete(other);
67
+ other.connectionsIn.delete(this);
68
+ }
69
+ log(level, msg, data) {
70
+ this.emit("log", level, msg, data);
71
+ }
72
+ async open() {
73
+ }
74
+ async close() {
75
+ }
76
+ }
77
+ exports.default = SpeechFlowNode;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /*
3
+ ** SpeechFlow - Speech Processing Flow Graph
4
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const naudiodon_1 = __importDefault(require("@gpeng/naudiodon"));
12
+ class SpeechFlowUtil {
13
+ static audioDeviceFromURL(mode, url) {
14
+ const m = url.match(/^(.+?):(.+)$/);
15
+ if (m === null)
16
+ throw new Error(`invalid audio device URL "${url}"`);
17
+ const [, type, name] = m;
18
+ const apis = naudiodon_1.default.getHostAPIs();
19
+ const api = apis.HostAPIs.find((api) => api.type.toLowerCase() === type.toLowerCase());
20
+ if (!api)
21
+ throw new Error(`invalid audio device type "${type}"`);
22
+ const devices = naudiodon_1.default.getDevices();
23
+ console.log(devices);
24
+ const device = devices.find((device) => {
25
+ return (((mode === "r" && device.maxInputChannels > 0)
26
+ || (mode === "w" && device.maxOutputChannels > 0)
27
+ || (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
28
+ || (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
29
+ && device.name.match(name)
30
+ && device.hostAPIName === api.name);
31
+ });
32
+ if (!device)
33
+ throw new Error(`invalid audio device name "${name}" (of audio type "${type}")`);
34
+ return device;
35
+ }
36
+ }
37
+ exports.default = SpeechFlowUtil;
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ /*
3
+ ** SpeechFlow - Speech Processing Flow Graph
4
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const cli_io_1 = __importDefault(require("cli-io"));
12
+ const yargs_1 = __importDefault(require("yargs"));
13
+ const js_yaml_1 = __importDefault(require("js-yaml"));
14
+ const flowlink_1 = __importDefault(require("flowlink"));
15
+ const object_path_1 = __importDefault(require("object-path"));
16
+ const speechflow_node_file_1 = __importDefault(require("./speechflow-node-file"));
17
+ const speechflow_node_device_1 = __importDefault(require("./speechflow-node-device"));
18
+ const speechflow_node_websocket_1 = __importDefault(require("./speechflow-node-websocket"));
19
+ const speechflow_node_deepgram_1 = __importDefault(require("./speechflow-node-deepgram"));
20
+ const speechflow_node_deepl_1 = __importDefault(require("./speechflow-node-deepl"));
21
+ const speechflow_node_elevenlabs_1 = __importDefault(require("./speechflow-node-elevenlabs"));
22
+ const package_json_1 = __importDefault(require("../package.json"));
23
+ let cli = null;
24
+ (async () => {
25
+ /* parse command-line arguments */
26
+ const args = await (0, yargs_1.default)()
27
+ /* eslint @stylistic/indent: off */
28
+ .usage("Usage: $0 " +
29
+ "[-h|--help] " +
30
+ "[-V|--version] " +
31
+ "[-v|--verbose <level>] " +
32
+ "[-e|--expression <expression>] " +
33
+ "[-f|--expression-file <expression-file>] " +
34
+ "[-c|--config <key>@<yaml-config-file>] " +
35
+ "[<argument> [...]]")
36
+ .help("h").alias("h", "help").default("h", false)
37
+ .describe("h", "show usage help")
38
+ .boolean("V").alias("V", "version").default("V", false)
39
+ .describe("V", "show program version information")
40
+ .string("v").nargs("v", 1).alias("v", "log-level").default("v", "warning")
41
+ .describe("v", "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')")
42
+ .string("e").nargs("e", 1).alias("e", "expression").default("e", "")
43
+ .describe("e", "FlowLink expression")
44
+ .string("f").nargs("f", 1).alias("f", "expression-file").default("f", "")
45
+ .describe("f", "FlowLink expression file")
46
+ .string("c").nargs("c", 1).alias("c", "config-file").default("c", "")
47
+ .describe("c", "configuration in format <id>@<file>")
48
+ .version(false)
49
+ .strict()
50
+ .showHelpOnFail(true)
51
+ .demand(0)
52
+ .parse(process.argv.slice(2));
53
+ /* short-circuit version request */
54
+ if (args.version) {
55
+ process.stderr.write(`${package_json_1.default.name} ${package_json_1.default.version} <${package_json_1.default.homepage}>\n`);
56
+ process.stderr.write(`${package_json_1.default.description}\n`);
57
+ process.stderr.write(`Copyright (c) 2024-2025 ${package_json_1.default.author.name} <${package_json_1.default.author.url}>\n`);
58
+ process.stderr.write(`Licensed under ${package_json_1.default.license} <http://spdx.org/licenses/${package_json_1.default.license}.html>\n`);
59
+ process.exit(0);
60
+ }
61
+ /* establish CLI environment */
62
+ cli = new cli_io_1.default({
63
+ encoding: "utf8",
64
+ logLevel: args.logLevel,
65
+ logTime: true,
66
+ logPrefix: package_json_1.default.name
67
+ });
68
+ /* handle uncaught exceptions */
69
+ process.on("uncaughtException", async (err) => {
70
+ cli.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`);
71
+ process.exit(1);
72
+ });
73
+ /* handle unhandled promise rejections */
74
+ process.on("unhandledRejection", async (reason, promise) => {
75
+ if (reason instanceof Error)
76
+ cli.log("error", `promise rejection not handled: ${reason.message}: ${reason.stack}`);
77
+ else
78
+ cli.log("error", `promise rejection not handled: ${reason}`);
79
+ process.exit(1);
80
+ });
81
+ /* sanity check usage */
82
+ let n = 0;
83
+ if (typeof args.expression === "string" && args.expression !== "")
84
+ n++;
85
+ if (typeof args.expressionFile === "string" && args.expressionFile !== "")
86
+ n++;
87
+ if (typeof args.configFile === "string" && args.configFile !== "")
88
+ n++;
89
+ if (n !== 1)
90
+ throw new Error("cannot use more than one FlowLink specification source (either option -e, -f or -c)");
91
+ /* read configuration */
92
+ let config = "";
93
+ if (typeof args.expression === "string" && args.expression !== "")
94
+ config = args.expression;
95
+ else if (typeof args.expressionFile === "string" && args.expressionFile !== "")
96
+ config = await cli.input(args.expressionFile, { encoding: "utf8" });
97
+ else if (typeof args.configFile === "string" && args.configFile !== "") {
98
+ const m = args.configFile.match(/^(.+?)@(.+)$/);
99
+ if (m === null)
100
+ throw new Error("invalid configuration file specification (expected \"<key>@<yaml-config-file>\")");
101
+ const [, key, file] = m;
102
+ const yaml = await cli.input(file, { encoding: "utf8" });
103
+ const obj = js_yaml_1.default.load(yaml);
104
+ if (obj[key] === undefined)
105
+ throw new Error(`no such key "${key}" found in configuration file`);
106
+ config = obj[key];
107
+ }
108
+ /* configuration of nodes */
109
+ const nodes = {
110
+ "file": speechflow_node_file_1.default,
111
+ "device": speechflow_node_device_1.default,
112
+ "websocket": speechflow_node_websocket_1.default,
113
+ "deepgram": speechflow_node_deepgram_1.default,
114
+ "deepl": speechflow_node_deepl_1.default,
115
+ "elevenlabs": speechflow_node_elevenlabs_1.default
116
+ };
117
+ /* parse configuration into node graph */
118
+ const flowlink = new flowlink_1.default({
119
+ trace: (msg) => {
120
+ cli.log("debug", msg);
121
+ }
122
+ });
123
+ let nodenum = 1;
124
+ const variables = { argv: args._, env: process.env };
125
+ const graphNodes = new Set();
126
+ flowlink.evaluate(config, {
127
+ resolveVariable(id) {
128
+ if (!object_path_1.default.has(variables, id))
129
+ throw new Error(`failed to resolve variable "${id}"`);
130
+ const value = object_path_1.default.get(variables, id);
131
+ cli.log("info", `resolve variable: "${id}" -> "${value}"`);
132
+ return value;
133
+ },
134
+ createNode(id, opts, args) {
135
+ if (nodes[id] === undefined)
136
+ throw new Error(`unknown SpeechFlow node "${id}"`);
137
+ const node = new nodes[id](`${id}[${nodenum++}]`, opts, args);
138
+ graphNodes.add(node);
139
+ const params = Object.keys(node.params)
140
+ .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ");
141
+ cli.log("info", `created SpeechFlow node "${node.id}" (${params})`);
142
+ return node;
143
+ },
144
+ connectNode(node1, node2) {
145
+ cli.log("info", `connect SpeechFlow node "${node1.id}" to node "${node2.id}"`);
146
+ node1.connect(node2);
147
+ }
148
+ });
149
+ /* graph processing: PASS 1: activate and sanity check nodes */
150
+ for (const node of graphNodes) {
151
+ /* connect node events */
152
+ node.on("log", (level, msg, data) => {
153
+ let str = `[${node.id}]: ${msg}`;
154
+ if (data !== undefined)
155
+ str += ` (${JSON.stringify(data)})`;
156
+ cli.log(level, str);
157
+ });
158
+ /* open node */
159
+ cli.log("info", `opening node "${node.id}"`);
160
+ await node.open().catch((err) => {
161
+ cli.log("error", `[${node.id}]: ${err.message}`);
162
+ throw new Error(`failed to open node "${node.id}"`);
163
+ });
164
+ /* determine connections */
165
+ const connectionsIn = Array.from(node.connectionsIn);
166
+ const connectionsOut = Array.from(node.connectionsOut);
167
+ /* ensure necessary incoming links */
168
+ if (node.input !== "none" && connectionsIn.length === 0)
169
+ throw new Error(`node "${node.id}" requires input but has no input nodes connected`);
170
+ /* prune unnecessary incoming links */
171
+ if (node.input === "none" && connectionsIn.length > 0)
172
+ connectionsIn.forEach((other) => { other.disconnect(node); });
173
+ /* ensure necessary outgoing links */
174
+ if (node.output !== "none" && connectionsOut.length === 0)
175
+ throw new Error(`node "${node.id}" requires output but has no output nodes connected`);
176
+ /* prune unnecessary outgoing links */
177
+ if (node.output === "none" && connectionsOut.length > 0)
178
+ connectionsOut.forEach((other) => { node.disconnect(other); });
179
+ }
180
+ /* graph processing: PASS 2: activate streams */
181
+ for (const node of graphNodes) {
182
+ if (node.stream === null)
183
+ throw new Error(`stream of outgoing node "${node.id}" still not initialized`);
184
+ for (const other of Array.from(node.connectionsOut)) {
185
+ if (other.stream === null)
186
+ throw new Error(`stream of incoming node "${other.id}" still not initialized`);
187
+ if (node.output !== other.input)
188
+ throw new Error(`${node.output} output node "${node.id}" cannot be " +
189
+ "connected to ${other.input} input node "${other.id}" (payload is incompatible)`);
190
+ cli.log("info", `connecting stream of node "${node.id}" to stream of node "${other.id}"`);
191
+ node.stream.pipe(other.stream);
192
+ }
193
+ }
194
+ /* gracefully shutdown process */
195
+ let shuttingDown = false;
196
+ const shutdown = async (signal) => {
197
+ if (shuttingDown)
198
+ return;
199
+ shuttingDown = true;
200
+ cli.log("warning", `received signal ${signal} -- shutting down service`);
201
+ for (const node of graphNodes) {
202
+ cli.log("info", `closing node "${node.id}"`);
203
+ const connectionsIn = Array.from(node.connectionsIn);
204
+ const connectionsOut = Array.from(node.connectionsOut);
205
+ connectionsIn.forEach((other) => { other.disconnect(node); });
206
+ connectionsOut.forEach((other) => { node.disconnect(other); });
207
+ await node.close();
208
+ }
209
+ process.exit(1);
210
+ };
211
+ process.on("SIGINT", () => {
212
+ shutdown("SIGINT");
213
+ });
214
+ process.on("SIGTERM", () => {
215
+ shutdown("SIGTERM");
216
+ });
217
+ })().catch((err) => {
218
+ if (cli !== null)
219
+ cli.log("error", err.message);
220
+ else
221
+ process.stderr.write(`${package_json_1.default.name}: ERROR: ${err.message}\n`);
222
+ process.exit(1);
223
+ });