speechflow
Version:
Speech Processing Flow Graph
143 lines (127 loc) • 5.32 kB
text/typescript
/*
** 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>
*/
/* standard dependencies */
import Stream from "node:stream"
/* external dependencies */
import { Duration } from "luxon"
/* internal dependencies */
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
/* SpeechFlow node for data flow tracing */
export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
/* declare official node name */
public static name = "x2x-trace"
/* internal state */
private destroyed = false
/* construct node */
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
super(id, cfg, opts, args)
/* declare node configuration parameters */
this.configure({
type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
name: { type: "string", pos: 1, val: "trace" },
mode: { type: "string", pos: 2, val: "filter", match: /^(?:filter|sink)$/ },
dashboard: { type: "string", val: "" }
})
/* sanity check parameters */
if (this.params.dashboard !== "" && this.params.type === "audio")
throw new Error("only trace nodes of type \"text\" can export to dashboard")
/* declare node input/output format */
this.input = this.params.type
if (this.params.mode === "filter")
this.output = this.params.type
else if (this.params.mode === "sink")
this.output = "none"
}
/* open node */
async open () {
/* wrapper for local logging */
const log = (level: string, msg: string) => {
if (this.params.name !== undefined)
this.log(level, `[${this.params.name}]: ${msg}`)
else
this.log(level, msg)
}
/* clear destruction flag */
this.destroyed = false
/* helper functions for formatting */
const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
const fmtMeta = (meta: Map<string, any>) => {
if (meta.size === 0)
return "none"
else
return `{ ${Array.from(meta.entries())
.map(([ k, v ]) => `${k}: ${JSON.stringify(v)}`)
.join(", ")
} }`
}
const fmtChunkBase = (chunk: SpeechFlowChunk) =>
`chunk: type=${chunk.type} ` +
`kind=${chunk.kind} ` +
`start=${fmtTime(chunk.timestampStart)} ` +
`end=${fmtTime(chunk.timestampEnd)} `
/* provide Transform stream */
const self = this
this.stream = new Stream.Transform({
writableObjectMode: true,
readableObjectMode: true,
decodeStrings: false,
highWaterMark: 1,
transform (chunk: SpeechFlowChunk, encoding, callback) {
let error: Error | undefined
if (self.destroyed) {
callback(new Error("stream already destroyed"))
return
}
if (Buffer.isBuffer(chunk.payload)) {
if (self.params.type === "audio")
log("debug", fmtChunkBase(chunk) +
`payload-type=Buffer payload-length=${chunk.payload.byteLength} ` +
`meta=${fmtMeta(chunk.meta)}`)
else
error = new Error(`${self.params.type} chunk: seen Buffer instead of String chunk type`)
}
else {
if (self.params.type === "text") {
log("debug", fmtChunkBase(chunk) +
`payload-type=String payload-length=${chunk.payload.length} ` +
`payload-content="${chunk.payload.toString()}" ` +
`meta=${fmtMeta(chunk.meta)}`)
if (self.params.dashboard !== "")
self.sendDashboard("text", self.params.dashboard, chunk.kind, chunk.payload.toString())
}
else
error = new Error(`${self.params.type} chunk: seen String instead of Buffer chunk type`)
}
if (self.params.mode === "sink")
callback()
else if (error !== undefined)
callback(error)
else {
this.push(chunk, encoding)
callback()
}
},
final (callback) {
if (self.destroyed || self.params.mode === "sink") {
callback()
return
}
this.push(null)
callback()
}
})
}
/* close node */
async close () {
/* close stream */
if (this.stream !== null) {
this.stream.destroy()
this.stream = null
}
/* indicate destruction */
this.destroyed = true
}
}