speechflow 0.9.0 → 0.9.1
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 +24 -10
- 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 +142 -46
- package/dst/speechflow-util.js +0 -37
- package/src/speechflow-util.ts +0 -36
package/dst/speechflow.js
CHANGED
|
@@ -8,18 +8,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
8
8
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
9
|
};
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
/* standard dependencies */
|
|
12
|
+
const node_stream_1 = __importDefault(require("node:stream"));
|
|
13
|
+
/* external dependencies */
|
|
11
14
|
const cli_io_1 = __importDefault(require("cli-io"));
|
|
12
15
|
const yargs_1 = __importDefault(require("yargs"));
|
|
13
16
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
14
17
|
const flowlink_1 = __importDefault(require("flowlink"));
|
|
15
18
|
const object_path_1 = __importDefault(require("object-path"));
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const speechflow_node_websocket_1 = __importDefault(require("./speechflow-node-websocket"));
|
|
19
|
-
const speechflow_node_deepgram_1 = __importDefault(require("./speechflow-node-deepgram"));
|
|
20
|
-
const speechflow_node_deepl_1 = __importDefault(require("./speechflow-node-deepl"));
|
|
21
|
-
const speechflow_node_elevenlabs_1 = __importDefault(require("./speechflow-node-elevenlabs"));
|
|
19
|
+
const installed_packages_1 = __importDefault(require("installed-packages"));
|
|
20
|
+
const dotenvx_1 = __importDefault(require("@dotenvx/dotenvx"));
|
|
22
21
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
22
|
+
/* central CLI context */
|
|
23
23
|
let cli = null;
|
|
24
24
|
(async () => {
|
|
25
25
|
/* parse command-line arguments */
|
|
@@ -52,7 +52,7 @@ let cli = null;
|
|
|
52
52
|
.parse(process.argv.slice(2));
|
|
53
53
|
/* short-circuit version request */
|
|
54
54
|
if (args.version) {
|
|
55
|
-
process.stderr.write(
|
|
55
|
+
process.stderr.write(`SpeechFlow ${package_json_1.default["x-stdver"]} (${package_json_1.default["x-release"]}) <${package_json_1.default.homepage}>\n`);
|
|
56
56
|
process.stderr.write(`${package_json_1.default.description}\n`);
|
|
57
57
|
process.stderr.write(`Copyright (c) 2024-2025 ${package_json_1.default.author.name} <${package_json_1.default.author.url}>\n`);
|
|
58
58
|
process.stderr.write(`Licensed under ${package_json_1.default.license} <http://spdx.org/licenses/${package_json_1.default.license}.html>\n`);
|
|
@@ -65,6 +65,13 @@ let cli = null;
|
|
|
65
65
|
logTime: true,
|
|
66
66
|
logPrefix: package_json_1.default.name
|
|
67
67
|
});
|
|
68
|
+
/* provide startup information */
|
|
69
|
+
cli.log("info", `starting SpeechFlow ${package_json_1.default["x-stdver"]} (${package_json_1.default["x-release"]})`);
|
|
70
|
+
/* load .env files */
|
|
71
|
+
const result = dotenvx_1.default.config({ encoding: "utf8", quiet: true });
|
|
72
|
+
if (result?.parsed !== undefined)
|
|
73
|
+
for (const key of Object.keys(result.parsed))
|
|
74
|
+
cli.log("info", `loaded environment variable "${key}" from ".env" files`);
|
|
68
75
|
/* handle uncaught exceptions */
|
|
69
76
|
process.on("uncaughtException", async (err) => {
|
|
70
77
|
cli.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`);
|
|
@@ -105,16 +112,47 @@ let cli = null;
|
|
|
105
112
|
throw new Error(`no such key "${key}" found in configuration file`);
|
|
106
113
|
config = obj[key];
|
|
107
114
|
}
|
|
108
|
-
/*
|
|
109
|
-
const nodes = {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
/* track the available SpeechFlow nodes */
|
|
116
|
+
const nodes = {};
|
|
117
|
+
/* load internal SpeechFlow nodes */
|
|
118
|
+
const pkgsI = [
|
|
119
|
+
"./speechflow-node-file.js",
|
|
120
|
+
"./speechflow-node-device.js",
|
|
121
|
+
"./speechflow-node-websocket.js",
|
|
122
|
+
"./speechflow-node-ffmpeg.js",
|
|
123
|
+
"./speechflow-node-deepgram.js",
|
|
124
|
+
"./speechflow-node-deepl.js",
|
|
125
|
+
"./speechflow-node-elevenlabs.js",
|
|
126
|
+
"./speechflow-node-gemma.js",
|
|
127
|
+
];
|
|
128
|
+
for (const pkg of pkgsI) {
|
|
129
|
+
let node = await import(pkg);
|
|
130
|
+
while (node.default !== undefined)
|
|
131
|
+
node = node.default;
|
|
132
|
+
if (typeof node === "function" && typeof node.name === "string") {
|
|
133
|
+
cli.log("info", `loading SpeechFlow node "${node.name}" from internal module`);
|
|
134
|
+
nodes[node.name] = node;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/* load external SpeechFlow nodes */
|
|
138
|
+
const pkgsE = await (0, installed_packages_1.default)();
|
|
139
|
+
for (const pkg of pkgsE) {
|
|
140
|
+
if (pkg.match(/^(?:@[^/]+\/)?speechflow-node-.+$/)) {
|
|
141
|
+
let node = await import(pkg);
|
|
142
|
+
while (node.default !== undefined)
|
|
143
|
+
node = node.default;
|
|
144
|
+
if (typeof node === "function" && typeof node.name === "string") {
|
|
145
|
+
if (nodes[node.name] !== undefined) {
|
|
146
|
+
cli.log("warning", `failed loading SpeechFlow node "${node.name}" ` +
|
|
147
|
+
`from external module "${pkg}" -- node already exists`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
cli.log("info", `loading SpeechFlow node "${node.name}" from external module "${pkg}"`);
|
|
151
|
+
nodes[node.name] = node;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/* graph processing: PASS 1: parse DSL and create and connect nodes */
|
|
118
156
|
const flowlink = new flowlink_1.default({
|
|
119
157
|
trace: (msg) => {
|
|
120
158
|
cli.log("debug", msg);
|
|
@@ -133,34 +171,21 @@ let cli = null;
|
|
|
133
171
|
},
|
|
134
172
|
createNode(id, opts, args) {
|
|
135
173
|
if (nodes[id] === undefined)
|
|
136
|
-
throw new Error(`unknown
|
|
174
|
+
throw new Error(`unknown node "${id}"`);
|
|
137
175
|
const node = new nodes[id](`${id}[${nodenum++}]`, opts, args);
|
|
138
|
-
graphNodes.add(node);
|
|
139
176
|
const params = Object.keys(node.params)
|
|
140
177
|
.map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ");
|
|
141
|
-
cli.log("info", `
|
|
178
|
+
cli.log("info", `create node "${node.id}" (${params})`);
|
|
179
|
+
graphNodes.add(node);
|
|
142
180
|
return node;
|
|
143
181
|
},
|
|
144
182
|
connectNode(node1, node2) {
|
|
145
|
-
cli.log("info", `connect
|
|
183
|
+
cli.log("info", `connect node "${node1.id}" to node "${node2.id}"`);
|
|
146
184
|
node1.connect(node2);
|
|
147
185
|
}
|
|
148
186
|
});
|
|
149
|
-
/* graph processing: PASS
|
|
187
|
+
/* graph processing: PASS 2: prune connections of nodes */
|
|
150
188
|
for (const node of graphNodes) {
|
|
151
|
-
/* connect node events */
|
|
152
|
-
node.on("log", (level, msg, data) => {
|
|
153
|
-
let str = `[${node.id}]: ${msg}`;
|
|
154
|
-
if (data !== undefined)
|
|
155
|
-
str += ` (${JSON.stringify(data)})`;
|
|
156
|
-
cli.log(level, str);
|
|
157
|
-
});
|
|
158
|
-
/* open node */
|
|
159
|
-
cli.log("info", `opening node "${node.id}"`);
|
|
160
|
-
await node.open().catch((err) => {
|
|
161
|
-
cli.log("error", `[${node.id}]: ${err.message}`);
|
|
162
|
-
throw new Error(`failed to open node "${node.id}"`);
|
|
163
|
-
});
|
|
164
189
|
/* determine connections */
|
|
165
190
|
const connectionsIn = Array.from(node.connectionsIn);
|
|
166
191
|
const connectionsOut = Array.from(node.connectionsOut);
|
|
@@ -176,18 +201,42 @@ let cli = null;
|
|
|
176
201
|
/* prune unnecessary outgoing links */
|
|
177
202
|
if (node.output === "none" && connectionsOut.length > 0)
|
|
178
203
|
connectionsOut.forEach((other) => { node.disconnect(other); });
|
|
204
|
+
/* check for payload compatibility */
|
|
205
|
+
for (const other of connectionsOut)
|
|
206
|
+
if (other.input !== node.output)
|
|
207
|
+
throw new Error(`${node.output} output node "${node.id}" cannot be ` +
|
|
208
|
+
`connected to ${other.input} input node "${other.id}" (payload is incompatible)`);
|
|
179
209
|
}
|
|
180
|
-
/* graph processing: PASS
|
|
210
|
+
/* graph processing: PASS 3: open nodes */
|
|
211
|
+
for (const node of graphNodes) {
|
|
212
|
+
/* connect node events */
|
|
213
|
+
node.on("log", (level, msg, data) => {
|
|
214
|
+
let str = `[${node.id}]: ${msg}`;
|
|
215
|
+
if (data !== undefined)
|
|
216
|
+
str += ` (${JSON.stringify(data)})`;
|
|
217
|
+
cli.log(level, str);
|
|
218
|
+
});
|
|
219
|
+
/* open node */
|
|
220
|
+
cli.log("info", `open node "${node.id}"`);
|
|
221
|
+
await node.open().catch((err) => {
|
|
222
|
+
cli.log("error", `[${node.id}]: ${err.message}`);
|
|
223
|
+
throw new Error(`failed to open node "${node.id}"`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/* graph processing: PASS 4: connect node streams */
|
|
181
227
|
for (const node of graphNodes) {
|
|
182
228
|
if (node.stream === null)
|
|
183
|
-
throw new Error(`stream of
|
|
229
|
+
throw new Error(`stream of node "${node.id}" still not initialized`);
|
|
184
230
|
for (const other of Array.from(node.connectionsOut)) {
|
|
185
231
|
if (other.stream === null)
|
|
186
232
|
throw new Error(`stream of incoming node "${other.id}" still not initialized`);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
233
|
+
cli.log("info", `connect stream of node "${node.id}" to stream of node "${other.id}"`);
|
|
234
|
+
if (!(node.stream instanceof node_stream_1.default.Readable
|
|
235
|
+
|| node.stream instanceof node_stream_1.default.Duplex))
|
|
236
|
+
throw new Error(`stream of output node "${node.id}" is neither of Readable nor Duplex type`);
|
|
237
|
+
if (!(other.stream instanceof node_stream_1.default.Writable
|
|
238
|
+
|| other.stream instanceof node_stream_1.default.Duplex))
|
|
239
|
+
throw new Error(`stream of input node "${other.id}" is neither of Writable nor Duplex type`);
|
|
191
240
|
node.stream.pipe(other.stream);
|
|
192
241
|
}
|
|
193
242
|
}
|
|
@@ -198,14 +247,50 @@ let cli = null;
|
|
|
198
247
|
return;
|
|
199
248
|
shuttingDown = true;
|
|
200
249
|
cli.log("warning", `received signal ${signal} -- shutting down service`);
|
|
250
|
+
/* graph processing: PASS 1: disconnect node streams */
|
|
251
|
+
for (const node of graphNodes) {
|
|
252
|
+
if (node.stream === null) {
|
|
253
|
+
cli.log("warning", `stream of node "${node.id}" no longer initialized`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const other of Array.from(node.connectionsOut)) {
|
|
257
|
+
if (other.stream === null) {
|
|
258
|
+
cli.log("warning", `stream of incoming node "${other.id}" no longer initialized`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!(node.stream instanceof node_stream_1.default.Readable
|
|
262
|
+
|| node.stream instanceof node_stream_1.default.Duplex)) {
|
|
263
|
+
cli.log("warning", `stream of output node "${node.id}" is neither of Readable nor Duplex type`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!(other.stream instanceof node_stream_1.default.Writable
|
|
267
|
+
|| other.stream instanceof node_stream_1.default.Duplex)) {
|
|
268
|
+
cli.log("warning", `stream of input node "${other.id}" is neither of Writable nor Duplex type`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
cli.log("info", `disconnect stream of node "${node.id}" from stream of node "${other.id}"`);
|
|
272
|
+
node.stream.unpipe(other.stream);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/* graph processing: PASS 2: close nodes */
|
|
276
|
+
for (const node of graphNodes) {
|
|
277
|
+
cli.log("info", `close node "${node.id}"`);
|
|
278
|
+
await node.close();
|
|
279
|
+
}
|
|
280
|
+
/* graph processing: PASS 3: disconnect nodes */
|
|
201
281
|
for (const node of graphNodes) {
|
|
202
|
-
cli.log("info", `
|
|
282
|
+
cli.log("info", `disconnect node "${node.id}"`);
|
|
203
283
|
const connectionsIn = Array.from(node.connectionsIn);
|
|
204
284
|
const connectionsOut = Array.from(node.connectionsOut);
|
|
205
285
|
connectionsIn.forEach((other) => { other.disconnect(node); });
|
|
206
286
|
connectionsOut.forEach((other) => { node.disconnect(other); });
|
|
207
|
-
await node.close();
|
|
208
287
|
}
|
|
288
|
+
/* graph processing: PASS 4: shutdown nodes */
|
|
289
|
+
for (const node of graphNodes) {
|
|
290
|
+
cli.log("info", `destroy node "${node.id}"`);
|
|
291
|
+
graphNodes.delete(node);
|
|
292
|
+
}
|
|
293
|
+
/* terminate process */
|
|
209
294
|
process.exit(1);
|
|
210
295
|
};
|
|
211
296
|
process.on("SIGINT", () => {
|
package/etc/tsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speechflow",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"x-stdver": "0.9.
|
|
5
|
-
"x-release": "2025-04-
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"x-stdver": "0.9.1-EA",
|
|
5
|
+
"x-release": "2025-04-27",
|
|
6
6
|
"homepage": "https://github.com/rse/speechflow",
|
|
7
7
|
"description": "Speech Processing Flow Graph",
|
|
8
8
|
"license": "GPL-3.0-only",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"deepl-node": "1.17.3",
|
|
26
26
|
"elevenlabs": "1.57.0",
|
|
27
27
|
"stream-transform": "3.3.3",
|
|
28
|
-
"get-stream": "
|
|
28
|
+
"get-stream": "9.0.1",
|
|
29
|
+
"@dotenvx/dotenvx": "1.41.0",
|
|
29
30
|
"speex-resampler": "3.0.1",
|
|
30
31
|
"pcm-convert": "1.6.5",
|
|
31
32
|
"object-path": "0.11.8",
|
|
@@ -33,7 +34,10 @@
|
|
|
33
34
|
"bufferutil": "4.0.9",
|
|
34
35
|
"utf-8-validate": "6.0.5",
|
|
35
36
|
"@opensumi/reconnecting-websocket": "4.4.0",
|
|
36
|
-
"
|
|
37
|
+
"ollama": "0.5.15",
|
|
38
|
+
"@rse/ffmpeg": "1.4.2",
|
|
39
|
+
"ffmpeg-stream": "1.0.0",
|
|
40
|
+
"installed-packages": "1.0.13"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"eslint": "9.25.1",
|
|
@@ -44,8 +48,8 @@
|
|
|
44
48
|
"eslint-plugin-node": "11.1.0",
|
|
45
49
|
"@typescript-eslint/eslint-plugin": "8.31.0",
|
|
46
50
|
"@typescript-eslint/parser": "8.31.0",
|
|
47
|
-
"oxlint": "0.16.
|
|
48
|
-
"eslint-plugin-oxlint": "0.16.
|
|
51
|
+
"oxlint": "0.16.8",
|
|
52
|
+
"eslint-plugin-oxlint": "0.16.8",
|
|
49
53
|
"@biomejs/biome": "1.9.4",
|
|
50
54
|
"eslint-config-biome": "1.9.4",
|
|
51
55
|
|
|
@@ -54,6 +58,7 @@
|
|
|
54
58
|
"@types/js-yaml": "4.0.9",
|
|
55
59
|
"@types/object-path": "0.11.4",
|
|
56
60
|
"@types/ws": "8.18.1",
|
|
61
|
+
"@types/resolve": "1.20.6",
|
|
57
62
|
|
|
58
63
|
"ts-node": "10.9.2",
|
|
59
64
|
"stmux": "1.8.10",
|
|
@@ -65,11 +70,20 @@
|
|
|
65
70
|
"nps": "5.10.0",
|
|
66
71
|
"cross-env": "7.0.3"
|
|
67
72
|
},
|
|
68
|
-
"upd": [
|
|
73
|
+
"upd": [],
|
|
69
74
|
"engines": {
|
|
70
|
-
"node": ">=
|
|
75
|
+
"node": ">=22.0.0"
|
|
76
|
+
},
|
|
77
|
+
"bin": { "speechflow": "./dst/speechflow.js" },
|
|
78
|
+
"types": "./dst/speechflow-node.d.ts",
|
|
79
|
+
"module": "./dst/speechflow-node.js",
|
|
80
|
+
"main": "./dst/speechflow-node.js",
|
|
81
|
+
"exports": {
|
|
82
|
+
".": {
|
|
83
|
+
"import": { "types": "./dst/speechflow-node.d.ts", "default": "./dst/speechflow-node.js" },
|
|
84
|
+
"require": { "types": "./dst/speechflow-node.d.ts", "default": "./dst/speechflow-node.js" }
|
|
85
|
+
}
|
|
71
86
|
},
|
|
72
|
-
"main": "dst/speechflow.js",
|
|
73
87
|
"scripts": {
|
|
74
88
|
"start": "nps -c etc/nps.yaml"
|
|
75
89
|
}
|
|
@@ -4,33 +4,51 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/* standard dependencies */
|
|
7
8
|
import { EventEmitter } from "node:events"
|
|
9
|
+
|
|
10
|
+
/* external dependencies */
|
|
8
11
|
import Stream from "node:stream"
|
|
9
12
|
import * as Deepgram from "@deepgram/sdk"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
10
15
|
import SpeechFlowNode from "./speechflow-node"
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
/* SpeechFlow node for Deepgram speech-to-text conversion */
|
|
18
|
+
export default class SpeechFlowNodeDeepgram extends SpeechFlowNode {
|
|
19
|
+
/* declare official node name */
|
|
20
|
+
public static name = "deepgram"
|
|
21
|
+
|
|
22
|
+
/* internal state */
|
|
13
23
|
private dg: Deepgram.LiveClient | null = null
|
|
24
|
+
|
|
25
|
+
/* construct node */
|
|
14
26
|
constructor (id: string, opts: { [ id: string ]: any }, args: any[]) {
|
|
15
27
|
super(id, opts, args)
|
|
28
|
+
|
|
29
|
+
/* declare node configuration parameters */
|
|
16
30
|
this.configure({
|
|
17
31
|
key: { type: "string", val: process.env.SPEECHFLOW_KEY_DEEPGRAM },
|
|
18
|
-
model: { type: "string", val: "nova-
|
|
32
|
+
model: { type: "string", val: "nova-3", pos: 0 },
|
|
19
33
|
version: { type: "string", val: "latest", pos: 1 },
|
|
20
|
-
language: { type: "string", val: "
|
|
34
|
+
language: { type: "string", val: "multi", pos: 2 }
|
|
21
35
|
})
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
|
|
37
|
+
/* declare node input/output format */
|
|
24
38
|
this.input = "audio"
|
|
25
39
|
this.output = "text"
|
|
26
|
-
|
|
40
|
+
}
|
|
27
41
|
|
|
42
|
+
/* open node */
|
|
43
|
+
async open () {
|
|
28
44
|
/* sanity check situation */
|
|
29
45
|
if (this.config.audioBitDepth !== 16 || !this.config.audioLittleEndian)
|
|
30
46
|
throw new Error("Deepgram node currently supports PCM-S16LE audio only")
|
|
31
47
|
|
|
32
|
-
/*
|
|
48
|
+
/* create queue for results */
|
|
33
49
|
const queue = new EventEmitter()
|
|
50
|
+
|
|
51
|
+
/* connect to Deepgram API */
|
|
34
52
|
const deepgram = Deepgram.createClient(this.params.key)
|
|
35
53
|
this.dg = deepgram.listen.live({
|
|
36
54
|
model: this.params.model,
|
|
@@ -40,7 +58,7 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
40
58
|
sample_rate: this.config.audioSampleRate,
|
|
41
59
|
encoding: "linear16",
|
|
42
60
|
multichannel: false,
|
|
43
|
-
|
|
61
|
+
endpointing: 10,
|
|
44
62
|
interim_results: false,
|
|
45
63
|
smart_format: true,
|
|
46
64
|
punctuate: true,
|
|
@@ -49,33 +67,39 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
49
67
|
numerals: true,
|
|
50
68
|
paragraphs: true,
|
|
51
69
|
profanity_filter: true,
|
|
52
|
-
utterances: false
|
|
53
|
-
})
|
|
54
|
-
await new Promise((resolve) => {
|
|
55
|
-
this.dg!.on(Deepgram.LiveTranscriptionEvents.Open, () => {
|
|
56
|
-
this.log("info", "Deepgram: connection open")
|
|
57
|
-
resolve(true)
|
|
58
|
-
})
|
|
70
|
+
utterances: false
|
|
59
71
|
})
|
|
60
72
|
|
|
61
|
-
/*
|
|
62
|
-
this.dg.on(Deepgram.LiveTranscriptionEvents.Close, () => {
|
|
63
|
-
this.log("info", "Deepgram: connection close")
|
|
64
|
-
})
|
|
73
|
+
/* hook onto Deepgram API events */
|
|
65
74
|
this.dg.on(Deepgram.LiveTranscriptionEvents.Transcript, async (data) => {
|
|
66
75
|
const text = data.channel?.alternatives[0].transcript ?? ""
|
|
67
76
|
if (text === "")
|
|
68
77
|
return
|
|
69
78
|
queue.emit("text", text)
|
|
70
79
|
})
|
|
80
|
+
this.dg.on(Deepgram.LiveTranscriptionEvents.Metadata, (data) => {
|
|
81
|
+
this.log("info", "Deepgram: metadata received")
|
|
82
|
+
})
|
|
83
|
+
this.dg.on(Deepgram.LiveTranscriptionEvents.Close, () => {
|
|
84
|
+
this.log("info", "Deepgram: connection close")
|
|
85
|
+
})
|
|
71
86
|
this.dg.on(Deepgram.LiveTranscriptionEvents.Error, (error: Error) => {
|
|
72
|
-
this.log("error", `Deepgram: ${error}`)
|
|
87
|
+
this.log("error", `Deepgram: ${error.message}`)
|
|
88
|
+
this.emit("error")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/* wait for Deepgram API to be available */
|
|
92
|
+
await new Promise((resolve) => {
|
|
93
|
+
this.dg!.once(Deepgram.LiveTranscriptionEvents.Open, () => {
|
|
94
|
+
this.log("info", "Deepgram: connection open")
|
|
95
|
+
resolve(true)
|
|
96
|
+
})
|
|
73
97
|
})
|
|
74
98
|
|
|
75
99
|
/* provide Duplex stream and internally attach to Deepgram API */
|
|
76
100
|
const dg = this.dg
|
|
77
101
|
this.stream = new Stream.Duplex({
|
|
78
|
-
write (chunk: Buffer, encoding
|
|
102
|
+
write (chunk: Buffer, encoding, callback) {
|
|
79
103
|
const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
|
|
80
104
|
if (data.byteLength === 0)
|
|
81
105
|
queue.emit("text", "")
|
|
@@ -83,19 +107,26 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
83
107
|
dg.send(data)
|
|
84
108
|
callback()
|
|
85
109
|
},
|
|
86
|
-
read (size
|
|
110
|
+
read (size) {
|
|
87
111
|
queue.once("text", (text: string) => {
|
|
88
|
-
|
|
89
|
-
this.push(text)
|
|
112
|
+
this.push(text)
|
|
90
113
|
})
|
|
114
|
+
},
|
|
115
|
+
final (callback) {
|
|
116
|
+
dg.requestClose()
|
|
91
117
|
}
|
|
92
118
|
})
|
|
93
119
|
}
|
|
120
|
+
|
|
121
|
+
/* close node */
|
|
94
122
|
async close () {
|
|
123
|
+
/* close stream */
|
|
95
124
|
if (this.stream !== null) {
|
|
96
125
|
this.stream.destroy()
|
|
97
126
|
this.stream = null
|
|
98
127
|
}
|
|
128
|
+
|
|
129
|
+
/* shutdown Deepgram API */
|
|
99
130
|
if (this.dg !== null)
|
|
100
131
|
this.dg.requestClose()
|
|
101
132
|
}
|
|
@@ -4,44 +4,62 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/* standard dependencies */
|
|
7
8
|
import Stream from "node:stream"
|
|
8
9
|
import { EventEmitter } from "node:events"
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
/* external dependencies */
|
|
10
12
|
import * as DeepL from "deepl-node"
|
|
11
13
|
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import SpeechFlowNode from "./speechflow-node"
|
|
16
|
+
|
|
17
|
+
/* SpeechFlow node for DeepL text-to-text translations */
|
|
12
18
|
export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
13
|
-
|
|
19
|
+
/* declare official node name */
|
|
20
|
+
public static name = "deepl"
|
|
21
|
+
|
|
22
|
+
/* internal state */
|
|
23
|
+
private deepl: DeepL.Translator | null = null
|
|
14
24
|
|
|
25
|
+
/* construct node */
|
|
15
26
|
constructor (id: string, opts: { [ id: string ]: any }, args: any[]) {
|
|
16
27
|
super(id, opts, args)
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
this.output = "text"
|
|
20
|
-
this.stream = null
|
|
21
|
-
|
|
29
|
+
/* declare node configuration parameters */
|
|
22
30
|
this.configure({
|
|
23
|
-
key:
|
|
24
|
-
src:
|
|
25
|
-
dst:
|
|
31
|
+
key: { type: "string", val: process.env.SPEECHFLOW_KEY_DEEPL },
|
|
32
|
+
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en-US)$/ },
|
|
33
|
+
dst: { type: "string", pos: 1, val: "en-US", match: /^(?:de|en-US)$/ },
|
|
34
|
+
optimize: { type: "string", pos: 2, val: "latency", match: /^(?:latency|quality)$/ }
|
|
26
35
|
})
|
|
36
|
+
|
|
37
|
+
/* declare node input/output format */
|
|
38
|
+
this.input = "text"
|
|
39
|
+
this.output = "text"
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/* open node */
|
|
29
43
|
async open () {
|
|
30
44
|
/* instantiate DeepL API SDK */
|
|
31
|
-
this.
|
|
45
|
+
this.deepl = new DeepL.Translator(this.params.key)
|
|
32
46
|
|
|
33
47
|
/* provide text-to-text translation */
|
|
34
48
|
const translate = async (text: string) => {
|
|
35
|
-
const result = await this.
|
|
36
|
-
splitSentences: "off"
|
|
49
|
+
const result = await this.deepl!.translateText(text, this.params.src, this.params.dst, {
|
|
50
|
+
splitSentences: "off",
|
|
51
|
+
modelType: this.params.optimize === "latency" ?
|
|
52
|
+
"latency_optimized" : "prefer_quality_optimized",
|
|
53
|
+
preserveFormatting: true,
|
|
54
|
+
formality: "prefer_more"
|
|
37
55
|
})
|
|
38
56
|
return (result?.text ?? text)
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
/* establish a duplex stream and connect it to
|
|
59
|
+
/* establish a duplex stream and connect it to DeepL translation */
|
|
42
60
|
const queue = new EventEmitter()
|
|
43
61
|
this.stream = new Stream.Duplex({
|
|
44
|
-
write (chunk: Buffer, encoding
|
|
62
|
+
write (chunk: Buffer, encoding, callback) {
|
|
45
63
|
const data = chunk.toString()
|
|
46
64
|
if (data === "") {
|
|
47
65
|
queue.emit("result", "")
|
|
@@ -64,13 +82,17 @@ export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
|
64
82
|
})
|
|
65
83
|
}
|
|
66
84
|
|
|
85
|
+
/* open node */
|
|
67
86
|
async close () {
|
|
87
|
+
/* close stream */
|
|
68
88
|
if (this.stream !== null) {
|
|
69
89
|
this.stream.destroy()
|
|
70
90
|
this.stream = null
|
|
71
91
|
}
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
|
|
93
|
+
/* shutdown DeepL API */
|
|
94
|
+
if (this.deepl !== null)
|
|
95
|
+
this.deepl = null
|
|
74
96
|
}
|
|
75
97
|
}
|
|
76
98
|
|