UNPKG

speechflow

Version:

Speech Processing Flow Graph

365 lines 17.2 kB
"use strict"; /* ** SpeechFlow - Speech Processing Flow Graph ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com> ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only> */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeGraph = void 0; /* standard dependencies */ const node_stream_1 = __importDefault(require("node:stream")); const node_events_1 = require("node:events"); /* external dependencies */ const luxon_1 = require("luxon"); const flowlink_1 = __importDefault(require("flowlink")); const object_path_1 = __importDefault(require("object-path")); const util = __importStar(require("./speechflow-util")); /* the SpeechFlow node graph management */ class NodeGraph { cli; debug; /* internal state */ graphNodes = new Set(); activeNodes = new Set(); finishEvents = new node_events_1.EventEmitter(); timeZero = null; shuttingDown = false; /* simple construction */ constructor(cli, debug = false) { this.cli = cli; this.debug = debug; } /* get all graph nodes */ getGraphNodes() { return this.graphNodes; } /* find particular graph node */ findGraphNode(name) { return Array.from(this.graphNodes).find((node) => node.id === name); } /* graph establishment: PASS 1: parse DSL and create and connect nodes */ async createAndConnectNodes(config, nodes, cfg, variables, accessBus) { const flowlink = new flowlink_1.default({ trace: (msg) => { this.cli.log("debug", msg); } }); const nodeNums = new Map(); let ast; try { ast = flowlink.compile(config); } catch (err) { const errorMsg = err instanceof Error && err.name === "FlowLinkError" ? err.toString() : (err instanceof Error ? err.message : "internal error"); this.cli.log("error", `failed to parse SpeechFlow configuration: ${errorMsg}`); process.exit(1); } try { flowlink.execute(ast, { resolveVariable: (id) => { if (!object_path_1.default.has(variables, id)) throw new Error(`failed to resolve variable "${id}"`); const value = object_path_1.default.get(variables, id); this.cli.log("info", `resolve variable: "${id}" -> "${value}"`); return value; }, createNode: (id, opts, args) => { if (nodes[id] === undefined) throw new Error(`unknown node <${id}>`); let node; try { const NodeClass = nodes[id]; let num = nodeNums.get(NodeClass) ?? 0; nodeNums.set(NodeClass, ++num); const name = num === 1 ? id : `${id}:${num}`; node = new NodeClass(name, cfg, opts, args); node._accessBus = accessBus; } catch (err) { /* fatal error */ if (err instanceof Error) this.cli.log("error", `creation of node <${id}> failed: ${err.message}`); else this.cli.log("error", `creation of node <${id}> failed: ${err}`); process.exit(1); } const params = Object.keys(node.params).map((key) => { if (key.match(/key/)) return `${key}: [...]`; else return `${key}: ${JSON.stringify(node.params[key])}`; }).join(", "); this.cli.log("info", `create node <${node.id}> (${params})`); this.graphNodes.add(node); return node; }, connectNodes: (node1, node2) => { this.cli.log("info", `connect node <${node1.id}> to node <${node2.id}>`); node1.connect(node2); } }); } catch (err) { const errorMsg = err instanceof Error && err.name === "FlowLinkError" ? err.toString() : (err instanceof Error ? err.message : "internal error"); this.cli.log("error", `failed to materialize SpeechFlow configuration: ${errorMsg}`); process.exit(1); } } /* graph establishment: PASS 2: prune connections of nodes */ async pruneConnections() { for (const node of this.graphNodes) { /* determine connections */ let connectionsIn = Array.from(node.connectionsIn); let connectionsOut = Array.from(node.connectionsOut); /* ensure necessary incoming links */ if (node.input !== "none" && connectionsIn.length === 0) throw new Error(`node <${node.id}> requires input but has no input nodes connected`); /* prune unnecessary incoming links */ if (node.input === "none" && connectionsIn.length > 0) connectionsIn.forEach((other) => { other.disconnect(node); }); /* ensure necessary outgoing links */ if (node.output !== "none" && connectionsOut.length === 0) throw new Error(`node <${node.id}> requires output but has no output nodes connected`); /* prune unnecessary outgoing links */ if (node.output === "none" && connectionsOut.length > 0) connectionsOut.forEach((other) => { node.disconnect(other); }); /* check for payload compatibility */ connectionsIn = Array.from(node.connectionsIn); connectionsOut = Array.from(node.connectionsOut); for (const other of connectionsOut) if (other.input !== node.output) throw new Error(`${node.output} output node <${node.id}> cannot be ` + `connected to ${other.input} input node <${other.id}> (payload is incompatible)`); } } /* graph establishment: PASS 3: open nodes */ async openNodes() { this.timeZero = luxon_1.DateTime.now(); for (const node of this.graphNodes) { /* connect node events */ node.on("log", (level, msg, data) => { let str = `<${node.id}>: ${msg}`; if (data !== undefined) str += ` (${JSON.stringify(data)})`; this.cli.log(level, str); }); /* open node */ this.cli.log("info", `open node <${node.id}>`); node.setTimeZero(this.timeZero); await Promise.race([ node.open(), new Promise((resolve, reject) => setTimeout(() => reject(new Error("timeout")), 30 * 1000)) ]).catch((err) => { this.cli.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`); throw new Error(`failed to open node <${node.id}>: ${err.message}`); }); } } /* graph establishment: PASS 4: connect node streams */ async connectStreams() { for (const node of this.graphNodes) { if (node.stream === null) throw new Error(`stream of node <${node.id}> still not initialized`); for (const other of Array.from(node.connectionsOut)) { if (other.stream === null) throw new Error(`stream of incoming node <${other.id}> still not initialized`); this.cli.log("info", `connect stream of node <${node.id}> to stream of node <${other.id}>`); if (!(node.stream instanceof node_stream_1.default.Readable || node.stream instanceof node_stream_1.default.Duplex)) throw new Error(`stream of output node <${node.id}> is neither of Readable nor Duplex type`); if (!(other.stream instanceof node_stream_1.default.Writable || other.stream instanceof node_stream_1.default.Duplex)) throw new Error(`stream of input node <${other.id}> is neither of Writable nor Duplex type`); node.stream.pipe(other.stream); } } } /* graph establishment: PASS 5: track stream finishing */ trackFinishing(args, api) { this.finishEvents.removeAllListeners(); this.finishEvents.setMaxListeners(this.graphNodes.size + 10); for (const node of this.graphNodes) { if (node.stream === null) throw new Error(`stream of node <${node.id}> still not initialized`); this.cli.log("info", `observe stream of node <${node.id}> for finish event`); this.activeNodes.add(node); const deactivateNode = (node, msg) => { if (this.activeNodes.has(node)) this.activeNodes.delete(node); this.cli.log("info", `${msg} (${this.activeNodes.size} active nodes remaining)`); if (this.activeNodes.size === 0) { const timeFinished = luxon_1.DateTime.now(); const duration = timeFinished.diff(this.timeZero); this.cli.log("info", "**** everything finished -- stream processing in SpeechFlow graph stops " + `(total duration: ${duration.toFormat("hh:mm:ss.SSS")}) ****`); this.finishEvents.emit("finished"); this.shutdown("finished", args, api); } }; node.stream.on("end", () => { deactivateNode(node, `readable stream side of node <${node.id}> raised "end" event`); }); node.stream.on("finish", () => { deactivateNode(node, `writable stream side of node <${node.id}> raised "finish" event`); }); } /* start of internal stream processing */ this.cli.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****"); } /* graph destruction: PASS 1: disconnect node streams */ async disconnectStreams() { for (const node of this.graphNodes) { if (node.stream === null) { this.cli.log("warning", `stream of node <${node.id}> no longer initialized`); continue; } for (const other of Array.from(node.connectionsOut)) { if (other.stream === null) { this.cli.log("warning", `stream of incoming node <${other.id}> no longer initialized`); continue; } if (!(node.stream instanceof node_stream_1.default.Readable || node.stream instanceof node_stream_1.default.Duplex)) { this.cli.log("warning", `stream of output node <${node.id}> is neither of Readable nor Duplex type`); continue; } if (!(other.stream instanceof node_stream_1.default.Writable || other.stream instanceof node_stream_1.default.Duplex)) { this.cli.log("warning", `stream of input node <${other.id}> is neither of Writable nor Duplex type`); continue; } this.cli.log("info", `disconnect stream of node <${node.id}> from stream of node <${other.id}>`); node.stream.unpipe(other.stream); } } } /* graph destruction: PASS 2: close nodes */ async closeNodes() { for (const node of this.graphNodes) { this.cli.log("info", `close node <${node.id}>`); await Promise.race([ node.close(), new Promise((resolve, reject) => setTimeout(() => reject(new Error("timeout")), 10 * 1000)) ]).catch((err) => { this.cli.log("warning", `node <${node.id}> failed to close: ${err.message}`); }); } } /* graph destruction: PASS 3: disconnect nodes */ disconnectNodes() { for (const node of this.graphNodes) { this.cli.log("info", `disconnect node <${node.id}>`); const connectionsIn = Array.from(node.connectionsIn); const connectionsOut = Array.from(node.connectionsOut); connectionsIn.forEach((other) => { other.disconnect(node); }); connectionsOut.forEach((other) => { node.disconnect(other); }); } } /* graph destruction: PASS 4: destroy nodes */ destroyNodes() { for (const node of this.graphNodes) { this.cli.log("info", `destroy node <${node.id}>`); this.graphNodes.delete(node); } } /* setup signal handling for shutdown */ setupSignalHandlers(args, api) { /* internal helper functions */ const shutdownHandler = (signal) => this.shutdown(signal, args, api); const logError = (error) => { if (this.debug) this.cli.log("error", `uncaught exception: ${error.message}\n${error.stack}`); else this.cli.log("error", `uncaught exception: ${error.message}`); }; /* hook into process signals */ process.on("SIGINT", () => { shutdownHandler("SIGINT"); }); process.on("SIGUSR1", () => { shutdownHandler("SIGUSR1"); }); process.on("SIGUSR2", () => { shutdownHandler("SIGUSR2"); }); process.on("SIGTERM", () => { shutdownHandler("SIGTERM"); }); /* re-hook into uncaught exception handler */ process.removeAllListeners("uncaughtException"); process.on("uncaughtException", (err) => { const error = util.ensureError(err, "uncaught exception"); logError(error); shutdownHandler("exception"); }); /* re-hook into unhandled promise rejection handler */ process.removeAllListeners("unhandledRejection"); process.on("unhandledRejection", (reason) => { const error = util.ensureError(reason, "unhandled promise rejection"); logError(error); shutdownHandler("exception"); }); } /* shutdown procedure */ async shutdown(signal, args, api) { if (this.shuttingDown) return; this.shuttingDown = true; if (signal === "exception") this.cli.log("warning", "**** exception occurred -- shutting down service ****"); else if (signal !== "finished") this.cli.log("warning", `**** received signal ${signal} -- shutting down service ****`); /* shutdown API service */ await api.stop(args); /* disconnect, close and destroy nodes */ await this.disconnectStreams(); await this.closeNodes(); this.disconnectNodes(); this.destroyNodes(); /* clear event emitters */ this.finishEvents.removeAllListeners(); /* clear active nodes */ this.activeNodes.clear(); /* terminate process */ if (signal === "finished") { this.cli.log("info", "terminate process (exit code 0)"); process.exit(0); } else { this.cli.log("info", "terminate process (exit code 1)"); process.exit(1); } } } exports.NodeGraph = NodeGraph; //# sourceMappingURL=speechflow-main-graph.js.map