speechflow
Version:
Speech Processing Flow Graph
250 lines • 10.7 kB
JavaScript
"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 });
/* standard dependencies */
const node_stream_1 = __importDefault(require("node:stream"));
/* external dependencies */
const luxon_1 = require("luxon");
/* internal dependencies */
const speechflow_node_1 = __importDefault(require("./speechflow-node"));
const util = __importStar(require("./speechflow-util"));
/* SpeechFlow node for sentence splitting */
class SpeechFlowNodeT2TSentence extends speechflow_node_1.default {
/* declare official node name */
static name = "t2t-sentence";
/* internal state */
queue = new util.Queue();
queueRecv = this.queue.pointerUse("recv");
queueSplit = this.queue.pointerUse("split");
queueSend = this.queue.pointerUse("send");
destroyed = false;
workingOffTimer = null;
/* construct node */
constructor(id, cfg, opts, args) {
super(id, cfg, opts, args);
/* declare node configuration parameters */
this.configure({});
/* declare node input/output format */
this.input = "text";
this.output = "text";
}
/* open node */
async open() {
/* clear destruction flag */
this.destroyed = false;
/* work off queued text frames */
let workingOff = false;
const workOffQueue = async () => {
if (this.destroyed)
return;
/* control working off round */
if (workingOff)
return;
workingOff = true;
if (this.workingOffTimer !== null) {
clearTimeout(this.workingOffTimer);
this.workingOffTimer = null;
}
this.queue.off("write", workOffQueue);
/* try to work off one or more chunks */
while (!this.destroyed) {
const element = this.queueSplit.peek();
if (element === undefined)
break;
if (element.type === "text-eof") {
this.queueSplit.walk(+1);
break;
}
const chunk = element.chunk;
const payload = chunk.payload;
const m = payload.match(/^((?:.|\r?\n)+?[.;?!])\s*((?:.|\r?\n)*)$/);
if (m !== null) {
/* contains a sentence */
const [, sentence, rest] = m;
if (rest !== "") {
/* contains more than a sentence */
const chunk2 = chunk.clone();
const duration = luxon_1.Duration.fromMillis(chunk.timestampEnd.minus(chunk.timestampStart).toMillis() *
(sentence.length / payload.length));
chunk2.timestampStart = chunk.timestampStart.plus(duration);
chunk.timestampEnd = chunk2.timestampStart;
chunk.payload = sentence;
chunk2.payload = rest;
element.complete = true;
this.queueSplit.touch();
this.queueSplit.walk(+1);
this.queueSplit.insert({ type: "text-frame", chunk: chunk2 });
}
else {
/* contains just the sentence */
element.complete = true;
this.queueSplit.touch();
this.queueSplit.walk(+1);
}
}
else {
/* contains less than a sentence */
const position = this.queueSplit.position();
if (position < this.queueSplit.maxPosition() - 1) {
/* merge into following chunk */
const element2 = this.queueSplit.peek(position + 1);
if (element2 === undefined)
break;
if (element2.type === "text-eof") {
element.complete = true;
this.queueSplit.touch();
this.queueSplit.walk(+1);
break;
}
element2.chunk.timestampStart = element.chunk.timestampStart;
element2.chunk.payload =
element.chunk.payload + " " +
element2.chunk.payload;
this.queueSplit.delete();
this.queueSplit.touch();
}
else
break;
}
}
/* re-initiate working off round (if still not destroyed) */
workingOff = false;
if (!this.destroyed) {
this.workingOffTimer = setTimeout(workOffQueue, 100);
this.queue.once("write", workOffQueue);
}
};
this.queue.once("write", workOffQueue);
/* provide Duplex stream and internally attach to classifier */
const self = this;
this.stream = new node_stream_1.default.Duplex({
writableObjectMode: true,
readableObjectMode: true,
decodeStrings: false,
highWaterMark: 1,
/* receive text chunk (writable side of stream) */
write(chunk, encoding, callback) {
if (self.destroyed)
callback(new Error("stream already destroyed"));
else if (Buffer.isBuffer(chunk.payload))
callback(new Error("expected text input as string chunks"));
else if (chunk.payload.length === 0)
callback();
else {
self.log("info", `received text: ${JSON.stringify(chunk.payload)}`);
self.queueRecv.append({ type: "text-frame", chunk });
callback();
}
},
/* receive no more text chunks (writable side of stream) */
final(callback) {
if (self.destroyed) {
callback();
return;
}
/* signal end of file */
self.queueRecv.append({ type: "text-eof" });
callback();
},
/* send text chunk(s) (readable side of stream) */
read(_size) {
/* flush pending text chunks */
const flushPendingChunks = () => {
if (self.destroyed) {
this.push(null);
return;
}
const element = self.queueSend.peek();
if (element !== undefined
&& element.type === "text-eof") {
this.push(null);
self.queueSend.walk(+1);
}
else if (element !== undefined
&& element.type === "text-frame"
&& element.complete === true) {
while (true) {
const nextElement = self.queueSend.peek();
if (nextElement === undefined)
break;
else if (nextElement.type === "text-eof") {
this.push(null);
self.queueSend.walk(+1);
break;
}
else if (nextElement.type === "text-frame"
&& nextElement.complete !== true)
break;
self.log("info", `send text: ${JSON.stringify(nextElement.chunk.payload)}`);
this.push(nextElement.chunk);
self.queueSend.walk(+1);
self.queue.trim();
}
}
else if (!self.destroyed)
self.queue.once("write", flushPendingChunks);
};
flushPendingChunks();
}
});
}
/* close node */
async close() {
/* indicate destruction */
this.destroyed = true;
/* clean up timer */
if (this.workingOffTimer !== null) {
clearTimeout(this.workingOffTimer);
this.workingOffTimer = null;
}
/* remove any pending event listeners */
this.queue.removeAllListeners("write");
/* close stream */
if (this.stream !== null) {
this.stream.destroy();
this.stream = null;
}
}
}
exports.default = SpeechFlowNodeT2TSentence;
//# sourceMappingURL=speechflow-node-t2t-sentence.js.map