speechflow 0.9.0 → 0.9.2
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/README.md +30 -0
- package/dst/speechflow-node-deepgram.d.ts +10 -0
- package/dst/speechflow-node-deepgram.js +44 -23
- package/dst/speechflow-node-deepl.d.ts +10 -0
- package/dst/speechflow-node-deepl.js +30 -12
- package/dst/speechflow-node-device.d.ts +11 -0
- package/dst/speechflow-node-device.js +73 -14
- package/dst/speechflow-node-elevenlabs.d.ts +10 -0
- package/dst/speechflow-node-elevenlabs.js +14 -2
- package/dst/speechflow-node-ffmpeg.d.ts +11 -0
- package/dst/speechflow-node-ffmpeg.js +114 -0
- package/dst/speechflow-node-file.d.ts +9 -0
- package/dst/speechflow-node-file.js +71 -13
- package/dst/speechflow-node-gemma.d.ts +11 -0
- package/dst/speechflow-node-gemma.js +152 -0
- package/dst/speechflow-node-websocket.d.ts +11 -0
- package/dst/speechflow-node-websocket.js +34 -6
- package/dst/speechflow-node.d.ts +38 -0
- package/dst/speechflow-node.js +28 -10
- package/dst/speechflow.d.ts +1 -0
- package/dst/speechflow.js +128 -43
- package/etc/tsconfig.json +2 -0
- package/package.json +25 -11
- package/src/speechflow-node-deepgram.ts +55 -24
- package/src/speechflow-node-deepl.ts +38 -16
- package/src/speechflow-node-device.ts +88 -14
- package/src/speechflow-node-elevenlabs.ts +19 -2
- package/src/speechflow-node-ffmpeg.ts +122 -0
- package/src/speechflow-node-file.ts +76 -14
- package/src/speechflow-node-gemma.ts +169 -0
- package/src/speechflow-node-websocket.ts +52 -13
- package/src/speechflow-node.ts +43 -21
- package/src/speechflow.ts +144 -47
- package/dst/speechflow-util.js +0 -37
- package/src/speechflow-util.ts +0 -36
|
@@ -4,26 +4,57 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import ReconnWebsocket, { ErrorEvent } from "@opensumi/reconnecting-websocket"
|
|
10
|
-
import SpeechFlowNode from "./speechflow-node"
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import Stream from "node:stream"
|
|
11
9
|
|
|
10
|
+
/* external dependencies */
|
|
11
|
+
import ws from "ws"
|
|
12
|
+
import ReconnWebsocket, { ErrorEvent } from "@opensumi/reconnecting-websocket"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import SpeechFlowNode from "./speechflow-node"
|
|
16
|
+
|
|
17
|
+
/* SpeechFlow node for Websocket networking */
|
|
12
18
|
export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
19
|
+
/* declare official node name */
|
|
20
|
+
public static name = "websocket"
|
|
21
|
+
|
|
22
|
+
/* internal state */
|
|
13
23
|
private server: ws.WebSocketServer | null = null
|
|
14
|
-
private client: WebSocket
|
|
24
|
+
private client: WebSocket | null = null
|
|
25
|
+
|
|
26
|
+
/* construct node */
|
|
15
27
|
constructor (id: string, opts: { [ id: string ]: any }, args: any[]) {
|
|
16
28
|
super(id, opts, args)
|
|
29
|
+
|
|
30
|
+
/* declare node configuration parameters */
|
|
17
31
|
this.configure({
|
|
18
32
|
listen: { type: "string", val: "", match: /^(?:|ws:\/\/(.+?):(\d+))$/ },
|
|
19
33
|
connect: { type: "string", val: "", match: /^(?:|ws:\/\/(.+?):(\d+)(?:\/.*)?)$/ },
|
|
20
34
|
type: { type: "string", val: "text", match: /^(?:audio|text)$/ }
|
|
21
35
|
})
|
|
36
|
+
|
|
37
|
+
/* sanity check usage */
|
|
38
|
+
if (this.params.listen !== "" && this.params.connect !== "")
|
|
39
|
+
throw new Error("Websocket node cannot listen and connect at the same time")
|
|
40
|
+
else if (this.params.listen === "" && this.params.connect === "")
|
|
41
|
+
throw new Error("Websocket node requires either listen or connect mode")
|
|
42
|
+
|
|
43
|
+
/* declare node input/output format */
|
|
44
|
+
if (this.params.listen !== "") {
|
|
45
|
+
this.input = "none"
|
|
46
|
+
this.output = this.params.type
|
|
47
|
+
}
|
|
48
|
+
else if (this.params.connect !== "") {
|
|
49
|
+
this.input = this.params.type
|
|
50
|
+
this.output = "none"
|
|
51
|
+
}
|
|
22
52
|
}
|
|
53
|
+
|
|
54
|
+
/* open node */
|
|
23
55
|
async open () {
|
|
24
|
-
this.input = this.params.type
|
|
25
|
-
this.output = this.params.type
|
|
26
56
|
if (this.params.listen !== "") {
|
|
57
|
+
/* listen locally on a Websocket port */
|
|
27
58
|
const url = new URL(this.params.listen)
|
|
28
59
|
let websocket: ws.WebSocket | null = null
|
|
29
60
|
const server = new ws.WebSocketServer({
|
|
@@ -46,8 +77,9 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
46
77
|
this.log("error", `error on URL ${this.params.listen}: ${error.message}`)
|
|
47
78
|
websocket = null
|
|
48
79
|
})
|
|
80
|
+
const textEncoding = this.config.textEncoding
|
|
49
81
|
this.stream = new Stream.Duplex({
|
|
50
|
-
write (chunk: Buffer, encoding
|
|
82
|
+
write (chunk: Buffer, encoding, callback) {
|
|
51
83
|
const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
|
|
52
84
|
if (websocket !== null) {
|
|
53
85
|
websocket.send(data, (error) => {
|
|
@@ -61,7 +93,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
61
93
|
read (size: number) {
|
|
62
94
|
if (websocket !== null) {
|
|
63
95
|
websocket.once("message", (data, isBinary) => {
|
|
64
|
-
this.push(data, isBinary ? "binary" :
|
|
96
|
+
this.push(data, isBinary ? "binary" : textEncoding)
|
|
65
97
|
})
|
|
66
98
|
}
|
|
67
99
|
else
|
|
@@ -70,6 +102,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
70
102
|
})
|
|
71
103
|
}
|
|
72
104
|
else if (this.params.connect !== "") {
|
|
105
|
+
/* connect remotely to a Websocket port */
|
|
73
106
|
this.client = new ReconnWebsocket(this.params.connect, [], {
|
|
74
107
|
WebSocket: ws,
|
|
75
108
|
WebSocketOptions: {},
|
|
@@ -90,8 +123,9 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
90
123
|
})
|
|
91
124
|
const client = this.client
|
|
92
125
|
client.binaryType = "arraybuffer"
|
|
126
|
+
const textEncoding = this.config.textEncoding
|
|
93
127
|
this.stream = new Stream.Duplex({
|
|
94
|
-
write (chunk: Buffer, encoding
|
|
128
|
+
write (chunk: Buffer, encoding, callback) {
|
|
95
129
|
const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
|
|
96
130
|
if (client.OPEN) {
|
|
97
131
|
client.send(data)
|
|
@@ -106,7 +140,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
106
140
|
if (ev.data instanceof ArrayBuffer)
|
|
107
141
|
this.push(ev.data, "binary")
|
|
108
142
|
else
|
|
109
|
-
this.push(ev.data,
|
|
143
|
+
this.push(ev.data, textEncoding)
|
|
110
144
|
}, { once: true })
|
|
111
145
|
}
|
|
112
146
|
else
|
|
@@ -114,10 +148,11 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
114
148
|
}
|
|
115
149
|
})
|
|
116
150
|
}
|
|
117
|
-
else
|
|
118
|
-
throw new Error("neither listen nor connect mode requested")
|
|
119
151
|
}
|
|
152
|
+
|
|
153
|
+
/* close node */
|
|
120
154
|
async close () {
|
|
155
|
+
/* close Websocket server */
|
|
121
156
|
if (this.server !== null) {
|
|
122
157
|
await new Promise<void>((resolve, reject) => {
|
|
123
158
|
this.server!.close((error) => {
|
|
@@ -127,10 +162,14 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
127
162
|
})
|
|
128
163
|
this.server = null
|
|
129
164
|
}
|
|
165
|
+
|
|
166
|
+
/* close Websocket client */
|
|
130
167
|
if (this.client !== null) {
|
|
131
168
|
this.client!.close()
|
|
132
169
|
this.client = null
|
|
133
170
|
}
|
|
171
|
+
|
|
172
|
+
/* close stream */
|
|
134
173
|
if (this.stream !== null) {
|
|
135
174
|
this.stream.destroy()
|
|
136
175
|
this.stream = null
|
package/src/speechflow-node.ts
CHANGED
|
@@ -4,73 +4,95 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import Events from "node:events"
|
|
9
|
+
import Stream from "node:stream"
|
|
9
10
|
|
|
11
|
+
/* the base class for all SpeechFlow nodes */
|
|
10
12
|
export default class SpeechFlowNode extends Events.EventEmitter {
|
|
11
|
-
|
|
13
|
+
/* general constant configuration (for reference) */
|
|
14
|
+
config = {
|
|
12
15
|
audioChannels: 1, /* audio mono channel */
|
|
13
16
|
audioBitDepth: 16, /* audio PCM 16-bit integer */
|
|
14
17
|
audioLittleEndian: true, /* audio PCM little-endian */
|
|
15
18
|
audioSampleRate: 48000, /* audio 48kHz sample rate */
|
|
16
19
|
textEncoding: "utf8" /* UTF-8 text encoding */
|
|
17
20
|
} as const
|
|
18
|
-
public input = "none"
|
|
19
|
-
public output = "none"
|
|
20
|
-
public params = {} as { [ id: string ]: any }
|
|
21
|
-
public stream: Stream.Writable | Stream.Readable | Stream.Duplex | null = null
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/* announced information */
|
|
23
|
+
input = "none"
|
|
24
|
+
output = "none"
|
|
25
|
+
params: { [ id: string ]: any } = {}
|
|
26
|
+
stream: Stream.Writable | Stream.Readable | Stream.Duplex | null = null
|
|
27
|
+
connectionsIn = new Set<SpeechFlowNode>()
|
|
28
|
+
connectionsOut = new Set<SpeechFlowNode>()
|
|
25
29
|
|
|
30
|
+
/* the default constructor */
|
|
26
31
|
constructor (
|
|
27
|
-
public
|
|
32
|
+
public id: string,
|
|
28
33
|
private opts: { [ id: string ]: any },
|
|
29
34
|
private args: any[]
|
|
30
35
|
) {
|
|
31
36
|
super()
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
/* INTERNAL: utility function: create "params" attribute from constructor of sub-classes */
|
|
34
40
|
configure (spec: { [ id: string ]: { type: string, pos?: number, val?: any, match?: RegExp } }) {
|
|
35
41
|
for (const name of Object.keys(spec)) {
|
|
36
42
|
if (this.opts[name] !== undefined) {
|
|
43
|
+
/* named parameter */
|
|
37
44
|
if (typeof this.opts[name] !== spec[name].type)
|
|
38
|
-
throw new Error(`invalid type of
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
throw new Error(`invalid type of named parameter "${name}" ` +
|
|
46
|
+
`(has to be ${spec[name].type})`)
|
|
47
|
+
if ("match" in spec[name]
|
|
48
|
+
&& this.opts[name].match(spec[name].match) === null)
|
|
49
|
+
throw new Error(`invalid value of named parameter "${name}" ` +
|
|
50
|
+
`(has to match ${spec[name].match})`)
|
|
41
51
|
this.params[name] = this.opts[name]
|
|
42
52
|
}
|
|
43
53
|
else if (this.opts[name] === undefined
|
|
44
54
|
&& "pos" in spec[name]
|
|
45
|
-
&& spec[name].pos
|
|
55
|
+
&& typeof spec[name].pos === "number"
|
|
56
|
+
&& spec[name].pos < this.args.length) {
|
|
57
|
+
/* positional argument */
|
|
46
58
|
if (typeof this.args[spec[name].pos!] !== spec[name].type)
|
|
47
|
-
throw new Error(`invalid type of
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
throw new Error(`invalid type of positional parameter "${name}" ` +
|
|
60
|
+
`(has to be ${spec[name].type})`)
|
|
61
|
+
if ("match" in spec[name]
|
|
62
|
+
&& this.args[spec[name].pos!].match(spec[name].match) === null)
|
|
63
|
+
throw new Error(`invalid value of positional parameter "${name}" ` +
|
|
64
|
+
`(has to match ${spec[name].match})`)
|
|
50
65
|
this.params[name] = this.args[spec[name].pos!]
|
|
51
66
|
}
|
|
52
67
|
else if ("val" in spec[name] && spec[name].val !== undefined)
|
|
68
|
+
/* default argument */
|
|
53
69
|
this.params[name] = spec[name].val
|
|
54
70
|
else
|
|
55
71
|
throw new Error(`required parameter "${name}" not given`)
|
|
56
72
|
}
|
|
57
73
|
}
|
|
74
|
+
|
|
75
|
+
/* connect node to another one */
|
|
58
76
|
connect (other: SpeechFlowNode) {
|
|
59
77
|
this.connectionsOut.add(other)
|
|
60
78
|
other.connectionsIn.add(this)
|
|
61
79
|
}
|
|
80
|
+
|
|
81
|
+
/* disconnect node from another one */
|
|
62
82
|
disconnect (other: SpeechFlowNode) {
|
|
63
83
|
if (!this.connectionsOut.has(other))
|
|
64
84
|
throw new Error("invalid node: not connected to this node")
|
|
65
85
|
this.connectionsOut.delete(other)
|
|
66
86
|
other.connectionsIn.delete(this)
|
|
67
87
|
}
|
|
88
|
+
|
|
89
|
+
/* internal log function */
|
|
68
90
|
log (level: string, msg: string, data?: any) {
|
|
69
91
|
this.emit("log", level, msg, data)
|
|
70
92
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
async
|
|
74
|
-
}
|
|
93
|
+
|
|
94
|
+
/* default implementation for open/close operations */
|
|
95
|
+
async open () {}
|
|
96
|
+
async close () {}
|
|
75
97
|
}
|
|
76
98
|
|
package/src/speechflow.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*!
|
|
2
3
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
4
|
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
5
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
/* standard dependencies */
|
|
7
9
|
import Stream from "node:stream"
|
|
8
10
|
|
|
11
|
+
/* external dependencies */
|
|
9
12
|
import CLIio from "cli-io"
|
|
10
13
|
import yargs from "yargs"
|
|
11
14
|
import jsYAML from "js-yaml"
|
|
12
15
|
import FlowLink from "flowlink"
|
|
13
16
|
import objectPath from "object-path"
|
|
17
|
+
import installedPackages from "installed-packages"
|
|
18
|
+
import dotenvx from "@dotenvx/dotenvx"
|
|
14
19
|
|
|
20
|
+
/* internal dependencies */
|
|
15
21
|
import SpeechFlowNode from "./speechflow-node"
|
|
16
|
-
import SpeechFlowNodeFile from "./speechflow-node-file"
|
|
17
|
-
import SpeechFlowNodeDevice from "./speechflow-node-device"
|
|
18
|
-
import SpeechFlowNodeWebsocket from "./speechflow-node-websocket"
|
|
19
|
-
import SpeechFlowNodeDeepgram from "./speechflow-node-deepgram"
|
|
20
|
-
import SpeechFlowNodeDeepL from "./speechflow-node-deepl"
|
|
21
|
-
import SpeechFlowNodeElevenLabs from "./speechflow-node-elevenlabs"
|
|
22
|
-
|
|
23
22
|
import pkg from "../package.json"
|
|
24
23
|
|
|
24
|
+
/* central CLI context */
|
|
25
25
|
let cli: CLIio | null = null
|
|
26
|
+
|
|
27
|
+
/* establish asynchronous environment */
|
|
26
28
|
;(async () => {
|
|
27
29
|
/* parse command-line arguments */
|
|
28
30
|
const args = await yargs()
|
|
@@ -57,7 +59,7 @@ let cli: CLIio | null = null
|
|
|
57
59
|
|
|
58
60
|
/* short-circuit version request */
|
|
59
61
|
if (args.version) {
|
|
60
|
-
process.stderr.write(
|
|
62
|
+
process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
|
|
61
63
|
process.stderr.write(`${pkg.description}\n`)
|
|
62
64
|
process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
|
|
63
65
|
process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
|
|
@@ -72,6 +74,15 @@ let cli: CLIio | null = null
|
|
|
72
74
|
logPrefix: pkg.name
|
|
73
75
|
})
|
|
74
76
|
|
|
77
|
+
/* provide startup information */
|
|
78
|
+
cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
|
|
79
|
+
|
|
80
|
+
/* load .env files */
|
|
81
|
+
const result = dotenvx.config({ encoding: "utf8", quiet: true })
|
|
82
|
+
if (result?.parsed !== undefined)
|
|
83
|
+
for (const key of Object.keys(result.parsed))
|
|
84
|
+
cli.log("info", `loaded environment variable "${key}" from ".env" files`)
|
|
85
|
+
|
|
75
86
|
/* handle uncaught exceptions */
|
|
76
87
|
process.on("uncaughtException", async (err: Error) => {
|
|
77
88
|
cli!.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`)
|
|
@@ -113,17 +124,50 @@ let cli: CLIio | null = null
|
|
|
113
124
|
config = obj[key] as string
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
/*
|
|
117
|
-
const nodes: { [ id: string ]: typeof SpeechFlowNode } = {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
127
|
+
/* track the available SpeechFlow nodes */
|
|
128
|
+
const nodes: { [ id: string ]: typeof SpeechFlowNode } = {}
|
|
129
|
+
|
|
130
|
+
/* load internal SpeechFlow nodes */
|
|
131
|
+
const pkgsI = [
|
|
132
|
+
"./speechflow-node-file.js",
|
|
133
|
+
"./speechflow-node-device.js",
|
|
134
|
+
"./speechflow-node-websocket.js",
|
|
135
|
+
"./speechflow-node-ffmpeg.js",
|
|
136
|
+
"./speechflow-node-deepgram.js",
|
|
137
|
+
"./speechflow-node-deepl.js",
|
|
138
|
+
"./speechflow-node-elevenlabs.js",
|
|
139
|
+
"./speechflow-node-gemma.js",
|
|
140
|
+
]
|
|
141
|
+
for (const pkg of pkgsI) {
|
|
142
|
+
let node: any = await import(pkg)
|
|
143
|
+
while (node.default !== undefined)
|
|
144
|
+
node = node.default
|
|
145
|
+
if (typeof node === "function" && typeof node.name === "string") {
|
|
146
|
+
cli.log("info", `loading SpeechFlow node "${node.name}" from internal module`)
|
|
147
|
+
nodes[node.name] = node as typeof SpeechFlowNode
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* load external SpeechFlow nodes */
|
|
152
|
+
const pkgsE = await installedPackages()
|
|
153
|
+
for (const pkg of pkgsE) {
|
|
154
|
+
if (pkg.match(/^(?:@[^/]+\/)?speechflow-node-.+$/)) {
|
|
155
|
+
let node: any = await import(pkg)
|
|
156
|
+
while (node.default !== undefined)
|
|
157
|
+
node = node.default
|
|
158
|
+
if (typeof node === "function" && typeof node.name === "string") {
|
|
159
|
+
if (nodes[node.name] !== undefined) {
|
|
160
|
+
cli.log("warning", `failed loading SpeechFlow node "${node.name}" ` +
|
|
161
|
+
`from external module "${pkg}" -- node already exists`)
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
cli.log("info", `loading SpeechFlow node "${node.name}" from external module "${pkg}"`)
|
|
165
|
+
nodes[node.name] = node as typeof SpeechFlowNode
|
|
166
|
+
}
|
|
167
|
+
}
|
|
124
168
|
}
|
|
125
169
|
|
|
126
|
-
/* parse
|
|
170
|
+
/* graph processing: PASS 1: parse DSL and create and connect nodes */
|
|
127
171
|
const flowlink = new FlowLink<SpeechFlowNode>({
|
|
128
172
|
trace: (msg: string) => {
|
|
129
173
|
cli!.log("debug", msg)
|
|
@@ -142,37 +186,22 @@ let cli: CLIio | null = null
|
|
|
142
186
|
},
|
|
143
187
|
createNode (id: string, opts: { [ id: string ]: any }, args: any[]) {
|
|
144
188
|
if (nodes[id] === undefined)
|
|
145
|
-
throw new Error(`unknown
|
|
189
|
+
throw new Error(`unknown node "${id}"`)
|
|
146
190
|
const node = new nodes[id](`${id}[${nodenum++}]`, opts, args)
|
|
147
|
-
graphNodes.add(node)
|
|
148
191
|
const params = Object.keys(node.params)
|
|
149
192
|
.map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
|
|
150
|
-
cli!.log("info", `
|
|
193
|
+
cli!.log("info", `create node "${node.id}" (${params})`)
|
|
194
|
+
graphNodes.add(node)
|
|
151
195
|
return node
|
|
152
196
|
},
|
|
153
197
|
connectNode (node1: SpeechFlowNode, node2: SpeechFlowNode) {
|
|
154
|
-
cli!.log("info", `connect
|
|
198
|
+
cli!.log("info", `connect node "${node1.id}" to node "${node2.id}"`)
|
|
155
199
|
node1.connect(node2)
|
|
156
200
|
}
|
|
157
201
|
})
|
|
158
202
|
|
|
159
|
-
/* graph processing: PASS
|
|
203
|
+
/* graph processing: PASS 2: prune connections of nodes */
|
|
160
204
|
for (const node of graphNodes) {
|
|
161
|
-
/* connect node events */
|
|
162
|
-
node.on("log", (level: string, msg: string, data?: any) => {
|
|
163
|
-
let str = `[${node.id}]: ${msg}`
|
|
164
|
-
if (data !== undefined)
|
|
165
|
-
str += ` (${JSON.stringify(data)})`
|
|
166
|
-
cli!.log(level, str)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
/* open node */
|
|
170
|
-
cli!.log("info", `opening node "${node.id}"`)
|
|
171
|
-
await node.open().catch((err: Error) => {
|
|
172
|
-
cli!.log("error", `[${node.id}]: ${err.message}`)
|
|
173
|
-
throw new Error(`failed to open node "${node.id}"`)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
205
|
/* determine connections */
|
|
177
206
|
const connectionsIn = Array.from(node.connectionsIn)
|
|
178
207
|
const connectionsOut = Array.from(node.connectionsOut)
|
|
@@ -192,20 +221,47 @@ let cli: CLIio | null = null
|
|
|
192
221
|
/* prune unnecessary outgoing links */
|
|
193
222
|
if (node.output === "none" && connectionsOut.length > 0)
|
|
194
223
|
connectionsOut.forEach((other) => { node.disconnect(other) })
|
|
224
|
+
|
|
225
|
+
/* check for payload compatibility */
|
|
226
|
+
for (const other of connectionsOut)
|
|
227
|
+
if (other.input !== node.output)
|
|
228
|
+
throw new Error(`${node.output} output node "${node.id}" cannot be ` +
|
|
229
|
+
`connected to ${other.input} input node "${other.id}" (payload is incompatible)`)
|
|
195
230
|
}
|
|
196
231
|
|
|
197
|
-
/* graph processing: PASS
|
|
232
|
+
/* graph processing: PASS 3: open nodes */
|
|
233
|
+
for (const node of graphNodes) {
|
|
234
|
+
/* connect node events */
|
|
235
|
+
node.on("log", (level: string, msg: string, data?: any) => {
|
|
236
|
+
let str = `[${node.id}]: ${msg}`
|
|
237
|
+
if (data !== undefined)
|
|
238
|
+
str += ` (${JSON.stringify(data)})`
|
|
239
|
+
cli!.log(level, str)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
/* open node */
|
|
243
|
+
cli!.log("info", `open node "${node.id}"`)
|
|
244
|
+
await node.open().catch((err: Error) => {
|
|
245
|
+
cli!.log("error", `[${node.id}]: ${err.message}`)
|
|
246
|
+
throw new Error(`failed to open node "${node.id}"`)
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* graph processing: PASS 4: connect node streams */
|
|
198
251
|
for (const node of graphNodes) {
|
|
199
252
|
if (node.stream === null)
|
|
200
|
-
throw new Error(`stream of
|
|
253
|
+
throw new Error(`stream of node "${node.id}" still not initialized`)
|
|
201
254
|
for (const other of Array.from(node.connectionsOut)) {
|
|
202
255
|
if (other.stream === null)
|
|
203
256
|
throw new Error(`stream of incoming node "${other.id}" still not initialized`)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
257
|
+
cli!.log("info", `connect stream of node "${node.id}" to stream of node "${other.id}"`)
|
|
258
|
+
if (!( node.stream instanceof Stream.Readable
|
|
259
|
+
|| node.stream instanceof Stream.Duplex ))
|
|
260
|
+
throw new Error(`stream of output node "${node.id}" is neither of Readable nor Duplex type`)
|
|
261
|
+
if (!( other.stream instanceof Stream.Writable
|
|
262
|
+
|| other.stream instanceof Stream.Duplex ))
|
|
263
|
+
throw new Error(`stream of input node "${other.id}" is neither of Writable nor Duplex type`)
|
|
264
|
+
node.stream.pipe(other.stream)
|
|
209
265
|
}
|
|
210
266
|
}
|
|
211
267
|
|
|
@@ -216,14 +272,55 @@ let cli: CLIio | null = null
|
|
|
216
272
|
return
|
|
217
273
|
shuttingDown = true
|
|
218
274
|
cli!.log("warning", `received signal ${signal} -- shutting down service`)
|
|
275
|
+
|
|
276
|
+
/* graph processing: PASS 1: disconnect node streams */
|
|
277
|
+
for (const node of graphNodes) {
|
|
278
|
+
if (node.stream === null) {
|
|
279
|
+
cli!.log("warning", `stream of node "${node.id}" no longer initialized`)
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
for (const other of Array.from(node.connectionsOut)) {
|
|
283
|
+
if (other.stream === null) {
|
|
284
|
+
cli!.log("warning", `stream of incoming node "${other.id}" no longer initialized`)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
if (!( node.stream instanceof Stream.Readable
|
|
288
|
+
|| node.stream instanceof Stream.Duplex )) {
|
|
289
|
+
cli!.log("warning", `stream of output node "${node.id}" is neither of Readable nor Duplex type`)
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
if (!( other.stream instanceof Stream.Writable
|
|
293
|
+
|| other.stream instanceof Stream.Duplex )) {
|
|
294
|
+
cli!.log("warning", `stream of input node "${other.id}" is neither of Writable nor Duplex type`)
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
cli!.log("info", `disconnect stream of node "${node.id}" from stream of node "${other.id}"`)
|
|
298
|
+
node.stream.unpipe(other.stream)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* graph processing: PASS 2: close nodes */
|
|
219
303
|
for (const node of graphNodes) {
|
|
220
|
-
cli!.log("info", `
|
|
304
|
+
cli!.log("info", `close node "${node.id}"`)
|
|
305
|
+
await node.close()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* graph processing: PASS 3: disconnect nodes */
|
|
309
|
+
for (const node of graphNodes) {
|
|
310
|
+
cli!.log("info", `disconnect node "${node.id}"`)
|
|
221
311
|
const connectionsIn = Array.from(node.connectionsIn)
|
|
222
312
|
const connectionsOut = Array.from(node.connectionsOut)
|
|
223
313
|
connectionsIn.forEach((other) => { other.disconnect(node) })
|
|
224
314
|
connectionsOut.forEach((other) => { node.disconnect(other) })
|
|
225
|
-
await node.close()
|
|
226
315
|
}
|
|
316
|
+
|
|
317
|
+
/* graph processing: PASS 4: shutdown nodes */
|
|
318
|
+
for (const node of graphNodes) {
|
|
319
|
+
cli!.log("info", `destroy node "${node.id}"`)
|
|
320
|
+
graphNodes.delete(node)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* terminate process */
|
|
227
324
|
process.exit(1)
|
|
228
325
|
}
|
|
229
326
|
process.on("SIGINT", () => {
|
package/dst/speechflow-util.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
** SpeechFlow - Speech Processing Flow Graph
|
|
4
|
-
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
5
|
-
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
6
|
-
*/
|
|
7
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
-
};
|
|
10
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
const naudiodon_1 = __importDefault(require("@gpeng/naudiodon"));
|
|
12
|
-
class SpeechFlowUtil {
|
|
13
|
-
static audioDeviceFromURL(mode, url) {
|
|
14
|
-
const m = url.match(/^(.+?):(.+)$/);
|
|
15
|
-
if (m === null)
|
|
16
|
-
throw new Error(`invalid audio device URL "${url}"`);
|
|
17
|
-
const [, type, name] = m;
|
|
18
|
-
const apis = naudiodon_1.default.getHostAPIs();
|
|
19
|
-
const api = apis.HostAPIs.find((api) => api.type.toLowerCase() === type.toLowerCase());
|
|
20
|
-
if (!api)
|
|
21
|
-
throw new Error(`invalid audio device type "${type}"`);
|
|
22
|
-
const devices = naudiodon_1.default.getDevices();
|
|
23
|
-
console.log(devices);
|
|
24
|
-
const device = devices.find((device) => {
|
|
25
|
-
return (((mode === "r" && device.maxInputChannels > 0)
|
|
26
|
-
|| (mode === "w" && device.maxOutputChannels > 0)
|
|
27
|
-
|| (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
|
|
28
|
-
|| (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
|
|
29
|
-
&& device.name.match(name)
|
|
30
|
-
&& device.hostAPIName === api.name);
|
|
31
|
-
});
|
|
32
|
-
if (!device)
|
|
33
|
-
throw new Error(`invalid audio device name "${name}" (of audio type "${type}")`);
|
|
34
|
-
return device;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
exports.default = SpeechFlowUtil;
|
package/src/speechflow-util.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
import PortAudio from "@gpeng/naudiodon"
|
|
8
|
-
|
|
9
|
-
export default class SpeechFlowUtil {
|
|
10
|
-
static audioDeviceFromURL (mode: "any" | "r" | "w" | "rw", url: string) {
|
|
11
|
-
const m = url.match(/^(.+?):(.+)$/)
|
|
12
|
-
if (m === null)
|
|
13
|
-
throw new Error(`invalid audio device URL "${url}"`)
|
|
14
|
-
const [ , type, name ] = m
|
|
15
|
-
const apis = PortAudio.getHostAPIs()
|
|
16
|
-
const api = apis.HostAPIs.find((api) => api.type.toLowerCase() === type.toLowerCase())
|
|
17
|
-
if (!api)
|
|
18
|
-
throw new Error(`invalid audio device type "${type}"`)
|
|
19
|
-
const devices = PortAudio.getDevices()
|
|
20
|
-
console.log(devices)
|
|
21
|
-
const device = devices.find((device) => {
|
|
22
|
-
return (
|
|
23
|
-
( ( mode === "r" && device.maxInputChannels > 0)
|
|
24
|
-
|| (mode === "w" && device.maxOutputChannels > 0)
|
|
25
|
-
|| (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
|
|
26
|
-
|| (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
|
|
27
|
-
&& device.name.match(name)
|
|
28
|
-
&& device.hostAPIName === api.name
|
|
29
|
-
)
|
|
30
|
-
})
|
|
31
|
-
if (!device)
|
|
32
|
-
throw new Error(`invalid audio device name "${name}" (of audio type "${type}")`)
|
|
33
|
-
return device
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|