speechflow
Version:
Speech Processing Flow Graph
365 lines • 17.2 kB
JavaScript
;
/*
** 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