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.
Files changed (35) hide show
  1. package/README.md +30 -0
  2. package/dst/speechflow-node-deepgram.d.ts +10 -0
  3. package/dst/speechflow-node-deepgram.js +44 -23
  4. package/dst/speechflow-node-deepl.d.ts +10 -0
  5. package/dst/speechflow-node-deepl.js +30 -12
  6. package/dst/speechflow-node-device.d.ts +11 -0
  7. package/dst/speechflow-node-device.js +73 -14
  8. package/dst/speechflow-node-elevenlabs.d.ts +10 -0
  9. package/dst/speechflow-node-elevenlabs.js +14 -2
  10. package/dst/speechflow-node-ffmpeg.d.ts +11 -0
  11. package/dst/speechflow-node-ffmpeg.js +114 -0
  12. package/dst/speechflow-node-file.d.ts +9 -0
  13. package/dst/speechflow-node-file.js +71 -13
  14. package/dst/speechflow-node-gemma.d.ts +11 -0
  15. package/dst/speechflow-node-gemma.js +152 -0
  16. package/dst/speechflow-node-websocket.d.ts +11 -0
  17. package/dst/speechflow-node-websocket.js +34 -6
  18. package/dst/speechflow-node.d.ts +38 -0
  19. package/dst/speechflow-node.js +28 -10
  20. package/dst/speechflow.d.ts +1 -0
  21. package/dst/speechflow.js +128 -43
  22. package/etc/tsconfig.json +2 -0
  23. package/package.json +25 -11
  24. package/src/speechflow-node-deepgram.ts +55 -24
  25. package/src/speechflow-node-deepl.ts +38 -16
  26. package/src/speechflow-node-device.ts +88 -14
  27. package/src/speechflow-node-elevenlabs.ts +19 -2
  28. package/src/speechflow-node-ffmpeg.ts +122 -0
  29. package/src/speechflow-node-file.ts +76 -14
  30. package/src/speechflow-node-gemma.ts +169 -0
  31. package/src/speechflow-node-websocket.ts +52 -13
  32. package/src/speechflow-node.ts +43 -21
  33. package/src/speechflow.ts +144 -47
  34. package/dst/speechflow-util.js +0 -37
  35. 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 speechflow_node_file_1 = __importDefault(require("./speechflow-node-file"));
17
- const speechflow_node_device_1 = __importDefault(require("./speechflow-node-device"));
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(`${package_json_1.default.name} ${package_json_1.default.version} <${package_json_1.default.homepage}>\n`);
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
- /* configuration of nodes */
109
- const nodes = {
110
- "file": speechflow_node_file_1.default,
111
- "device": speechflow_node_device_1.default,
112
- "websocket": speechflow_node_websocket_1.default,
113
- "deepgram": speechflow_node_deepgram_1.default,
114
- "deepl": speechflow_node_deepl_1.default,
115
- "elevenlabs": speechflow_node_elevenlabs_1.default
116
- };
117
- /* parse configuration into node graph */
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 SpeechFlow node "${id}"`);
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", `created SpeechFlow node "${node.id}" (${params})`);
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 SpeechFlow node "${node1.id}" to node "${node2.id}"`);
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 1: activate and sanity check nodes */
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 2: activate streams */
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 outgoing node "${node.id}" still not initialized`);
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
- if (node.output !== other.input)
188
- throw new Error(`${node.output} output node "${node.id}" cannot be " +
189
- "connected to ${other.input} input node "${other.id}" (payload is incompatible)`);
190
- cli.log("info", `connecting stream of node "${node.id}" to stream of node "${other.id}"`);
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", `closing node "${node.id}"`);
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
@@ -14,6 +14,8 @@
14
14
  "lib": [ "es2022" ],
15
15
  "skipLibCheck": true,
16
16
  "noEmit": false,
17
+ "declaration": true,
18
+ "emitDeclarationOnly": false,
17
19
  "types": [ "node" ],
18
20
  "rootDir": "../src"
19
21
  },
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "speechflow",
3
- "version": "0.9.0",
4
- "x-stdver": "0.9.0-EA",
5
- "x-release": "2025-04-26",
3
+ "version": "0.9.2",
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",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "git+https://github.com/rse/speechflow"
16
+ "url": "git+https://github.com/rse/speechflow.git"
17
17
  },
18
18
  "dependencies": {
19
19
  "cli-io": "0.9.13",
@@ -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": "6.0.1",
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
- "get-stream": "9.0.1"
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.7",
48
- "eslint-plugin-oxlint": "0.16.7",
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": [ "!get-stream" ],
73
+ "upd": [],
69
74
  "engines": {
70
- "node": ">=20.6.0"
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
- export default class SpeechFlowNodeDevice extends SpeechFlowNode {
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-2", pos: 0 }, /* FIXME: nova-3 multiligual */
32
+ model: { type: "string", val: "nova-3", pos: 0 },
19
33
  version: { type: "string", val: "latest", pos: 1 },
20
- language: { type: "string", val: "de", pos: 2 }
34
+ language: { type: "string", val: "multi", pos: 2 }
21
35
  })
22
- }
23
- async open () {
36
+
37
+ /* declare node input/output format */
24
38
  this.input = "audio"
25
39
  this.output = "text"
26
- this.stream = null
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
- /* connect to Deepgram API */
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
- // endpointing: false, /* FIXME: ? */
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
- /* hooks onto Deepgram API events */
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: BufferEncoding, callback: (error?: Error | null | undefined) => void) {
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: number) {
110
+ read (size) {
87
111
  queue.once("text", (text: string) => {
88
- if (text !== "")
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
- import SpeechFlowNode from "./speechflow-node"
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
- private translator: DeepL.Translator | null = null
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
- this.input = "text"
19
- this.output = "text"
20
- this.stream = null
21
-
29
+ /* declare node configuration parameters */
22
30
  this.configure({
23
- key: { type: "string", val: process.env.SPEECHFLOW_KEY_DEEPL },
24
- src: { type: "string", pos: 0, val: "de", match: /^(?:de|en-US)$/ },
25
- dst: { type: "string", pos: 1, val: "en-US", match: /^(?:de|en-US)$/ }
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.translator = new DeepL.Translator(this.params.key)
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.translator!.translateText(text, this.params.src, this.params.dst, {
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 the translation */
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: BufferEncoding, callback: (error?: Error | null | undefined) => void) {
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
- if (this.translator !== null)
73
- this.translator = null
92
+
93
+ /* shutdown DeepL API */
94
+ if (this.deepl !== null)
95
+ this.deepl = null
74
96
  }
75
97
  }
76
98