speechflow 1.7.0 → 2.0.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.
- package/CHANGELOG.md +23 -0
- package/README.md +425 -146
- package/etc/claude.md +5 -5
- package/etc/speechflow.yaml +2 -2
- package/package.json +3 -3
- package/speechflow-cli/dst/speechflow-main-api.js +6 -5
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-main-graph.js +35 -13
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +3 -7
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +3 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +4 -2
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +4 -2
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +1 -2
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +32 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +1 -6
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +9 -9
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +320 -0
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +6 -4
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +6 -11
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +6 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +218 -0
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +2 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +19 -6
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +195 -0
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +608 -0
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/{speechflow-node-t2t-transformers.d.ts → speechflow-node-t2t-opus.d.ts} +1 -3
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +159 -0
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.d.ts +11 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +118 -0
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +220 -0
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-node-t2t-openai.d.ts → speechflow-node-t2t-spellcheck.d.ts} +2 -2
- package/speechflow-cli/dst/{speechflow-node-t2t-openai.js → speechflow-node-t2t-spellcheck.js} +47 -99
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +3 -6
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.d.ts +16 -0
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +241 -0
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-node-t2t-ollama.d.ts → speechflow-node-t2t-translate.d.ts} +2 -2
- package/speechflow-cli/dst/{speechflow-node-t2t-transformers.js → speechflow-node-t2t-translate.js} +53 -115
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +10 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +3 -3
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +223 -0
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-file.js +80 -67
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +2 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.d.ts +17 -0
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +330 -0
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +39 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +500 -0
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +2 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +5 -6
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +5 -7
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.d.ts +35 -0
- package/speechflow-cli/dst/speechflow-util-llm.js +363 -0
- package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.js +4 -4
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.js +3 -3
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +4 -2
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-util.js +1 -0
- package/speechflow-cli/dst/speechflow-util.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/package.json +34 -17
- package/speechflow-cli/src/lib.d.ts +5 -0
- package/speechflow-cli/src/speechflow-main-api.ts +6 -5
- package/speechflow-cli/src/speechflow-main-graph.ts +40 -13
- package/speechflow-cli/src/speechflow-main-status.ts +4 -8
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +4 -2
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +4 -2
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +1 -2
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +33 -6
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +6 -11
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +13 -12
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +322 -0
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +8 -4
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +7 -11
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +6 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +206 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +22 -6
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +179 -0
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +701 -0
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +136 -0
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +93 -0
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +201 -0
- package/speechflow-cli/src/{speechflow-node-t2t-openai.ts → speechflow-node-t2t-spellcheck.ts} +48 -107
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +3 -6
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +229 -0
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +181 -0
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +16 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +3 -3
- package/speechflow-cli/src/speechflow-node-xio-device.ts +4 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +210 -0
- package/speechflow-cli/src/speechflow-node-xio-file.ts +93 -80
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +3 -2
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +325 -0
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +533 -0
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +2 -1
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +4 -4
- package/speechflow-cli/src/speechflow-util-audio.ts +10 -10
- package/speechflow-cli/src/speechflow-util-error.ts +9 -7
- package/speechflow-cli/src/speechflow-util-llm.ts +367 -0
- package/speechflow-cli/src/speechflow-util-misc.ts +4 -4
- package/speechflow-cli/src/speechflow-util-queue.ts +4 -4
- package/speechflow-cli/src/speechflow-util-stream.ts +5 -3
- package/speechflow-cli/src/speechflow-util.ts +1 -0
- package/speechflow-ui-db/package.json +9 -9
- package/speechflow-ui-st/package.json +9 -9
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +0 -293
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +0 -1
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +0 -281
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +0 -247
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import Stream from "node:stream"
|
|
9
|
+
|
|
10
|
+
/* external dependencies */
|
|
11
|
+
import { execa, type Subprocess, type Options } from "execa"
|
|
12
|
+
import shellParser from "shell-parser"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import SpeechFlowNode from "./speechflow-node"
|
|
16
|
+
import * as util from "./speechflow-util"
|
|
17
|
+
|
|
18
|
+
/* SpeechFlow node for external command execution */
|
|
19
|
+
export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
|
|
20
|
+
/* declare official node name */
|
|
21
|
+
public static name = "xio-exec"
|
|
22
|
+
|
|
23
|
+
/* internal state */
|
|
24
|
+
private subprocess: Subprocess | null = null
|
|
25
|
+
|
|
26
|
+
/* construct node */
|
|
27
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
28
|
+
super(id, cfg, opts, args)
|
|
29
|
+
|
|
30
|
+
/* declare node configuration parameters */
|
|
31
|
+
this.configure({
|
|
32
|
+
command: { type: "string", pos: 0, val: "" },
|
|
33
|
+
mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
|
|
34
|
+
type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
|
|
35
|
+
chunkAudio: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
|
|
36
|
+
chunkText: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/* sanity check parameters */
|
|
40
|
+
if (this.params.command === "")
|
|
41
|
+
throw new Error("required parameter \"command\" has to be given")
|
|
42
|
+
|
|
43
|
+
/* declare node input/output format */
|
|
44
|
+
if (this.params.mode === "rw") {
|
|
45
|
+
this.input = this.params.type
|
|
46
|
+
this.output = this.params.type
|
|
47
|
+
}
|
|
48
|
+
else if (this.params.mode === "r") {
|
|
49
|
+
this.input = "none"
|
|
50
|
+
this.output = this.params.type
|
|
51
|
+
}
|
|
52
|
+
else if (this.params.mode === "w") {
|
|
53
|
+
this.input = this.params.type
|
|
54
|
+
this.output = "none"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* open node */
|
|
59
|
+
async open () {
|
|
60
|
+
/* determine how many bytes we need per chunk when
|
|
61
|
+
the chunk should be of the required duration/size */
|
|
62
|
+
const highWaterMarkAudio = (
|
|
63
|
+
this.config.audioSampleRate *
|
|
64
|
+
(this.config.audioBitDepth / 8)
|
|
65
|
+
) / (1000 / this.params.chunkAudio)
|
|
66
|
+
const highWaterMarkText = this.params.chunkText
|
|
67
|
+
|
|
68
|
+
/* parse command into executable and arguments
|
|
69
|
+
(SECURITY: caller must ensure command parameter is properly validated
|
|
70
|
+
and does not contain untrusted user input to prevent command injection) */
|
|
71
|
+
const cmdParts = shellParser(this.params.command)
|
|
72
|
+
if (cmdParts.length === 0)
|
|
73
|
+
throw new Error("failed to parse command: no executable found")
|
|
74
|
+
|
|
75
|
+
/* warn about potentially dangerous shell metacharacters */
|
|
76
|
+
if (/[;&|`$()<>]/.test(this.params.command))
|
|
77
|
+
this.log("warning", "command contains shell metacharacters -- ensure input is trusted")
|
|
78
|
+
const executable = cmdParts[0]
|
|
79
|
+
const args = cmdParts.slice(1)
|
|
80
|
+
|
|
81
|
+
/* determine subprocess options */
|
|
82
|
+
const encoding = (this.params.type === "text" ?
|
|
83
|
+
this.config.textEncoding : "buffer") as Options["encoding"]
|
|
84
|
+
|
|
85
|
+
/* spawn subprocess */
|
|
86
|
+
this.log("info", `executing command: ${this.params.command}`)
|
|
87
|
+
this.subprocess = execa(executable, args, {
|
|
88
|
+
buffer: false,
|
|
89
|
+
encoding,
|
|
90
|
+
...(this.params.mode === "rw" ? { stdin: "pipe", stdout: "pipe" } : {}),
|
|
91
|
+
...(this.params.mode === "r" ? { stdin: "ignore", stdout: "pipe" } : {}),
|
|
92
|
+
...(this.params.mode === "w" ? { stdin: "pipe", stdout: "ignore" } : {})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/* handle subprocess errors */
|
|
96
|
+
this.subprocess.on("error", (err) => {
|
|
97
|
+
this.log("error", `subprocess error: ${err.message}`)
|
|
98
|
+
this.emit("error", err)
|
|
99
|
+
if (this.stream !== null)
|
|
100
|
+
this.stream.emit("error", err)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/* handle subprocess exit */
|
|
104
|
+
this.subprocess.on("exit", (code, signal) => {
|
|
105
|
+
if (code !== 0 && code !== null)
|
|
106
|
+
this.log("warning", `subprocess exited with code ${code}`)
|
|
107
|
+
else if (signal)
|
|
108
|
+
this.log("warning", `subprocess terminated by signal ${signal}`)
|
|
109
|
+
else
|
|
110
|
+
this.log("info", "subprocess terminated gracefully")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/* determine high water mark based on type */
|
|
114
|
+
const highWaterMark = this.params.type === "audio" ? highWaterMarkAudio : highWaterMarkText
|
|
115
|
+
|
|
116
|
+
/* configure stream encoding */
|
|
117
|
+
if (this.subprocess.stdout && this.params.type === "text")
|
|
118
|
+
this.subprocess.stdout.setEncoding(this.config.textEncoding)
|
|
119
|
+
if (this.subprocess.stdin)
|
|
120
|
+
this.subprocess.stdin.setDefaultEncoding(this.params.type === "text" ?
|
|
121
|
+
this.config.textEncoding : "binary")
|
|
122
|
+
|
|
123
|
+
/* dispatch according to mode */
|
|
124
|
+
if (this.params.mode === "rw") {
|
|
125
|
+
/* bidirectional mode: both stdin and stdout */
|
|
126
|
+
this.stream = Stream.Duplex.from({
|
|
127
|
+
readable: this.subprocess.stdout,
|
|
128
|
+
writable: this.subprocess.stdin
|
|
129
|
+
})
|
|
130
|
+
const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, highWaterMark)
|
|
131
|
+
const wrapper2 = util.createTransformStreamForReadableSide(
|
|
132
|
+
this.params.type, () => this.timeZero, highWaterMark)
|
|
133
|
+
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
134
|
+
}
|
|
135
|
+
else if (this.params.mode === "r") {
|
|
136
|
+
/* read-only mode: stdout only */
|
|
137
|
+
const wrapper = util.createTransformStreamForReadableSide(
|
|
138
|
+
this.params.type, () => this.timeZero, highWaterMark)
|
|
139
|
+
this.stream = Stream.compose(this.subprocess.stdout!, wrapper)
|
|
140
|
+
}
|
|
141
|
+
else if (this.params.mode === "w") {
|
|
142
|
+
/* write-only mode: stdin only */
|
|
143
|
+
const wrapper = util.createTransformStreamForWritableSide(
|
|
144
|
+
this.params.type, highWaterMark)
|
|
145
|
+
this.stream = Stream.compose(wrapper, this.subprocess.stdin!)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* close node */
|
|
150
|
+
async close () {
|
|
151
|
+
/* terminate subprocess */
|
|
152
|
+
if (this.subprocess !== null) {
|
|
153
|
+
/* gracefully end stdin if in write or read/write mode */
|
|
154
|
+
if ((this.params.mode === "w" || this.params.mode === "rw") && this.subprocess.stdin &&
|
|
155
|
+
!this.subprocess.stdin.destroyed && !this.subprocess.stdin.writableEnded) {
|
|
156
|
+
await Promise.race([
|
|
157
|
+
new Promise<void>((resolve, reject) => {
|
|
158
|
+
this.subprocess!.stdin!.end((err?: Error) => {
|
|
159
|
+
if (err) reject(err)
|
|
160
|
+
else resolve()
|
|
161
|
+
})
|
|
162
|
+
}),
|
|
163
|
+
util.timeout(2000)
|
|
164
|
+
]).catch((err: unknown) => {
|
|
165
|
+
const error = util.ensureError(err)
|
|
166
|
+
this.log("warning", `failed to gracefully close stdin: ${error.message}`)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* wait for subprocess to exit gracefully */
|
|
171
|
+
await Promise.race([
|
|
172
|
+
this.subprocess,
|
|
173
|
+
util.timeout(5000, "subprocess exit timeout")
|
|
174
|
+
]).catch(async (err: unknown) => {
|
|
175
|
+
/* force kill with SIGTERM */
|
|
176
|
+
const error = util.ensureError(err)
|
|
177
|
+
if (error.message.includes("timeout")) {
|
|
178
|
+
this.log("warning", "subprocess did not exit gracefully, forcing termination")
|
|
179
|
+
this.subprocess!.kill("SIGTERM")
|
|
180
|
+
return Promise.race([
|
|
181
|
+
this.subprocess,
|
|
182
|
+
util.timeout(2000)
|
|
183
|
+
])
|
|
184
|
+
}
|
|
185
|
+
}).catch(async () => {
|
|
186
|
+
/* force kill with SIGKILL */
|
|
187
|
+
this.log("warning", "subprocess did not respond to SIGTERM, forcing SIGKILL")
|
|
188
|
+
this.subprocess!.kill("SIGKILL")
|
|
189
|
+
return Promise.race([
|
|
190
|
+
this.subprocess,
|
|
191
|
+
util.timeout(1000)
|
|
192
|
+
])
|
|
193
|
+
}).catch(() => {
|
|
194
|
+
this.log("error", "subprocess did not terminate even after SIGKILL")
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
/* remove event listeners to prevent memory leaks */
|
|
198
|
+
this.subprocess.removeAllListeners("error")
|
|
199
|
+
this.subprocess.removeAllListeners("exit")
|
|
200
|
+
|
|
201
|
+
this.subprocess = null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* shutdown stream */
|
|
205
|
+
if (this.stream !== null) {
|
|
206
|
+
await util.destroyStream(this.stream)
|
|
207
|
+
this.stream = null
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -9,37 +9,39 @@ import fs from "node:fs"
|
|
|
9
9
|
import Stream from "node:stream"
|
|
10
10
|
|
|
11
11
|
/* internal dependencies */
|
|
12
|
-
import SpeechFlowNode
|
|
13
|
-
import * as util
|
|
12
|
+
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
13
|
+
import * as util from "./speechflow-util"
|
|
14
14
|
|
|
15
15
|
/* SpeechFlow node for file access */
|
|
16
16
|
export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
17
17
|
/* declare official node name */
|
|
18
18
|
public static name = "xio-file"
|
|
19
19
|
|
|
20
|
+
/* file descriptor for seekable write mode */
|
|
21
|
+
private fd: number | null = null
|
|
22
|
+
|
|
20
23
|
/* construct node */
|
|
21
24
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
22
25
|
super(id, cfg, opts, args)
|
|
23
26
|
|
|
24
27
|
/* declare node configuration parameters */
|
|
25
28
|
this.configure({
|
|
26
|
-
path:
|
|
27
|
-
mode:
|
|
28
|
-
type:
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
path: { type: "string", pos: 0, val: "" },
|
|
30
|
+
mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w)$/ },
|
|
31
|
+
type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
|
|
32
|
+
seekable: { type: "boolean", val: false },
|
|
33
|
+
chunkAudio: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
|
|
34
|
+
chunkText: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
|
|
31
35
|
})
|
|
32
36
|
|
|
33
37
|
/* sanity check parameters */
|
|
34
38
|
if (this.params.path === "")
|
|
35
39
|
throw new Error("required parameter \"path\" has to be given")
|
|
40
|
+
if (this.params.seekable && this.params.path === "-")
|
|
41
|
+
throw new Error("parameter \"seekable\" cannot be used with standard I/O")
|
|
36
42
|
|
|
37
43
|
/* declare node input/output format */
|
|
38
|
-
if (this.params.mode === "
|
|
39
|
-
this.input = this.params.type
|
|
40
|
-
this.output = this.params.type
|
|
41
|
-
}
|
|
42
|
-
else if (this.params.mode === "r") {
|
|
44
|
+
if (this.params.mode === "r") {
|
|
43
45
|
this.input = "none"
|
|
44
46
|
this.output = this.params.type
|
|
45
47
|
}
|
|
@@ -56,8 +58,8 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
56
58
|
const highWaterMarkAudio = (
|
|
57
59
|
this.config.audioSampleRate *
|
|
58
60
|
(this.config.audioBitDepth / 8)
|
|
59
|
-
) / (1000 / this.params.
|
|
60
|
-
const highWaterMarkText = this.params.
|
|
61
|
+
) / (1000 / this.params.chunkAudio)
|
|
62
|
+
const highWaterMarkText = this.params.chunkText
|
|
61
63
|
|
|
62
64
|
/* utility function: create a writable stream as chunker that
|
|
63
65
|
writes to process.stdout but properly handles finish events.
|
|
@@ -81,59 +83,7 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
81
83
|
})
|
|
82
84
|
|
|
83
85
|
/* dispatch according to mode and path */
|
|
84
|
-
if (this.params.mode === "
|
|
85
|
-
if (this.params.path === "-") {
|
|
86
|
-
/* standard I/O */
|
|
87
|
-
if (this.params.type === "audio") {
|
|
88
|
-
process.stdin.setEncoding()
|
|
89
|
-
process.stdout.setEncoding()
|
|
90
|
-
const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
|
|
91
|
-
process.stdin.pipe(streamR)
|
|
92
|
-
const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
|
|
93
|
-
streamW.pipe(process.stdout)
|
|
94
|
-
this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
process.stdin.setEncoding(this.config.textEncoding)
|
|
98
|
-
process.stdout.setEncoding(this.config.textEncoding)
|
|
99
|
-
const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
100
|
-
process.stdin.pipe(streamR)
|
|
101
|
-
const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
102
|
-
streamW.pipe(process.stdout)
|
|
103
|
-
this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
/* file I/O */
|
|
108
|
-
if (this.params.type === "audio") {
|
|
109
|
-
this.stream = Stream.Duplex.from({
|
|
110
|
-
readable: fs.createReadStream(this.params.path,
|
|
111
|
-
{ highWaterMark: highWaterMarkAudio }),
|
|
112
|
-
writable: fs.createWriteStream(this.params.path,
|
|
113
|
-
{ highWaterMark: highWaterMarkAudio })
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
this.stream = Stream.Duplex.from({
|
|
118
|
-
readable: fs.createReadStream(this.params.path, {
|
|
119
|
-
highWaterMark: highWaterMarkText,
|
|
120
|
-
encoding: this.config.textEncoding
|
|
121
|
-
}),
|
|
122
|
-
writable: fs.createWriteStream(this.params.path, {
|
|
123
|
-
highWaterMark: highWaterMarkText,
|
|
124
|
-
encoding: this.config.textEncoding
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/* convert regular stream into object-mode stream */
|
|
131
|
-
const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, 1)
|
|
132
|
-
const wrapper2 = util.createTransformStreamForReadableSide(
|
|
133
|
-
this.params.type, () => this.timeZero)
|
|
134
|
-
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
135
|
-
}
|
|
136
|
-
else if (this.params.mode === "r") {
|
|
86
|
+
if (this.params.mode === "r") {
|
|
137
87
|
if (this.params.path === "-") {
|
|
138
88
|
/* standard I/O */
|
|
139
89
|
let chunker: Stream.PassThrough
|
|
@@ -176,15 +126,63 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
176
126
|
}
|
|
177
127
|
else {
|
|
178
128
|
/* file I/O */
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
writable =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
129
|
+
if (this.params.seekable) {
|
|
130
|
+
/* seekable file I/O with file descriptor */
|
|
131
|
+
this.fd = fs.openSync(this.params.path, "w")
|
|
132
|
+
let writePosition = 0
|
|
133
|
+
const self = this
|
|
134
|
+
const writable = new Stream.Writable({
|
|
135
|
+
objectMode: true,
|
|
136
|
+
decodeStrings: false,
|
|
137
|
+
highWaterMark: 1,
|
|
138
|
+
write (chunk: SpeechFlowChunk, encoding, callback) {
|
|
139
|
+
const payload = Buffer.isBuffer(chunk.payload) ?
|
|
140
|
+
chunk.payload : Buffer.from(chunk.payload)
|
|
141
|
+
const seekPosition = chunk.meta.get("chunk:seek") as number | undefined
|
|
142
|
+
if (seekPosition !== undefined) {
|
|
143
|
+
/* seek to specified position and write (overload) */
|
|
144
|
+
fs.write(self.fd!, payload, 0, payload.byteLength, seekPosition, callback)
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
/* append at current position */
|
|
148
|
+
fs.write(self.fd!, payload, 0, payload.byteLength, writePosition, (err) => {
|
|
149
|
+
if (err)
|
|
150
|
+
callback(err)
|
|
151
|
+
else {
|
|
152
|
+
writePosition += payload.byteLength
|
|
153
|
+
callback()
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
final (callback) {
|
|
159
|
+
callback()
|
|
160
|
+
},
|
|
161
|
+
destroy (err, callback) {
|
|
162
|
+
if (self.fd !== null) {
|
|
163
|
+
fs.close(self.fd, () => {
|
|
164
|
+
self.fd = null
|
|
165
|
+
callback(err)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
callback(err)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
this.stream = writable
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
/* non-seekable file I/O with stream */
|
|
176
|
+
let writable: Stream.Writable
|
|
177
|
+
if (this.params.type === "audio")
|
|
178
|
+
writable = fs.createWriteStream(this.params.path,
|
|
179
|
+
{ highWaterMark: highWaterMarkAudio })
|
|
180
|
+
else
|
|
181
|
+
writable = fs.createWriteStream(this.params.path,
|
|
182
|
+
{ highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
|
|
183
|
+
const wrapper = util.createTransformStreamForWritableSide(this.params.type, 1)
|
|
184
|
+
this.stream = Stream.compose(wrapper, writable)
|
|
185
|
+
}
|
|
188
186
|
}
|
|
189
187
|
}
|
|
190
188
|
else
|
|
@@ -202,20 +200,35 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
|
|
|
202
200
|
/* for stdio streams, just end without destroying */
|
|
203
201
|
const stream = this.stream
|
|
204
202
|
if ((stream instanceof Stream.Writable || stream instanceof Stream.Duplex) &&
|
|
205
|
-
(!stream.writableEnded && !stream.destroyed)
|
|
203
|
+
(!stream.writableEnded && !stream.destroyed)) {
|
|
206
204
|
await Promise.race([
|
|
207
205
|
new Promise<void>((resolve, reject) => {
|
|
208
206
|
stream.end((err?: Error) => {
|
|
209
|
-
if (err)
|
|
210
|
-
|
|
207
|
+
if (err)
|
|
208
|
+
reject(err)
|
|
209
|
+
else
|
|
210
|
+
resolve()
|
|
211
211
|
})
|
|
212
212
|
}),
|
|
213
|
-
util.
|
|
213
|
+
util.timeout(5000)
|
|
214
214
|
])
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
this.stream = null
|
|
218
218
|
}
|
|
219
|
+
|
|
220
|
+
/* ensure file descriptor is closed */
|
|
221
|
+
if (this.fd !== null) {
|
|
222
|
+
await new Promise<void>((resolve, reject) => {
|
|
223
|
+
fs.close(this.fd!, (err) => {
|
|
224
|
+
this.fd = null
|
|
225
|
+
if (err)
|
|
226
|
+
reject(err)
|
|
227
|
+
else
|
|
228
|
+
resolve()
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
}
|
|
219
232
|
}
|
|
220
233
|
}
|
|
221
234
|
|
|
@@ -97,7 +97,8 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
97
97
|
this.log("info", `connection re-opened to MQTT ${this.params.url}`)
|
|
98
98
|
})
|
|
99
99
|
this.broker.on("disconnect", (packet: MQTT.IDisconnectPacket) => {
|
|
100
|
-
|
|
100
|
+
const reasonCode = packet.reasonCode ?? 0
|
|
101
|
+
this.log("info", `connection closed to MQTT ${this.params.url} (reason code: ${reasonCode})`)
|
|
101
102
|
})
|
|
102
103
|
this.chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
|
|
103
104
|
this.broker.on("message", (topic: string, payload: Buffer, packet: MQTT.IPublishPacket) => {
|
|
@@ -107,7 +108,7 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
|
|
|
107
108
|
const chunk = util.streamChunkDecode(payload)
|
|
108
109
|
this.chunkQueue!.write(chunk)
|
|
109
110
|
}
|
|
110
|
-
catch (_err:
|
|
111
|
+
catch (_err: unknown) {
|
|
111
112
|
this.log("warning", `received invalid CBOR chunk from MQTT ${this.params.url}`)
|
|
112
113
|
}
|
|
113
114
|
})
|