speechflow 1.4.5 → 1.5.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/CHANGELOG.md +35 -0
- package/README.md +242 -7
- package/etc/claude.md +70 -0
- package/etc/speechflow.yaml +13 -11
- package/etc/stx.conf +7 -0
- package/package.json +7 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +155 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +287 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +208 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +312 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +161 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +208 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +233 -0
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +125 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +28 -12
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +12 -8
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +2 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +55 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +184 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +156 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +22 -17
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +18 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +312 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -14
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +19 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +351 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +16 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +204 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +19 -14
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +47 -8
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +175 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +14 -15
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +10 -15
- package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +80 -33
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +78 -45
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +13 -14
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +23 -27
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
- package/speechflow-cli/dst/speechflow-node.js +13 -2
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
- package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-utils.d.ts +34 -0
- package/speechflow-cli/dst/speechflow-utils.js +256 -35
- package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
- package/speechflow-cli/dst/speechflow.js +75 -26
- package/speechflow-cli/dst/speechflow.js.map +1 -1
- package/speechflow-cli/etc/biome.jsonc +2 -1
- package/speechflow-cli/etc/oxlint.jsonc +113 -11
- package/speechflow-cli/etc/stx.conf +2 -2
- package/speechflow-cli/etc/tsconfig.json +1 -1
- package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
- package/speechflow-cli/package.json +103 -94
- package/speechflow-cli/src/lib.d.ts +24 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -3
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
- package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +306 -0
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -15
- package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
- package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +15 -9
- package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +153 -0
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +14 -15
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +58 -44
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +59 -58
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +18 -18
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +28 -32
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
- package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
- package/speechflow-cli/src/speechflow-node.ts +21 -8
- package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
- package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
- package/speechflow-cli/src/speechflow-utils.ts +314 -32
- package/speechflow-cli/src/speechflow.ts +84 -33
- package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
- package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
- package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
- package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
- package/speechflow-ui-db/dst/index.css +2 -2
- package/speechflow-ui-db/dst/index.js +37 -38
- package/speechflow-ui-db/etc/eslint.mjs +0 -1
- package/speechflow-ui-db/etc/tsc-client.json +3 -3
- package/speechflow-ui-db/package.json +12 -11
- package/speechflow-ui-db/src/app.vue +20 -6
- package/speechflow-ui-st/dst/index.js +26 -26
- package/speechflow-ui-st/etc/eslint.mjs +0 -1
- package/speechflow-ui-st/etc/tsc-client.json +3 -3
- package/speechflow-ui-st/package.json +12 -11
- package/speechflow-ui-st/src/app.vue +5 -12
|
@@ -0,0 +1,153 @@
|
|
|
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 { TranslateClient, TranslateTextCommand } from "@aws-sdk/client-translate"
|
|
12
|
+
|
|
13
|
+
/* internal dependencies */
|
|
14
|
+
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
15
|
+
import * as utils from "./speechflow-utils"
|
|
16
|
+
|
|
17
|
+
/* SpeechFlow node for AWS Translate text-to-text translations */
|
|
18
|
+
export default class SpeechFlowNodeAWSTranslate extends SpeechFlowNode {
|
|
19
|
+
/* declare official node name */
|
|
20
|
+
public static name = "awstranslate"
|
|
21
|
+
|
|
22
|
+
/* internal state */
|
|
23
|
+
private client: TranslateClient | null = null
|
|
24
|
+
|
|
25
|
+
/* construct node */
|
|
26
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
27
|
+
super(id, cfg, opts, args)
|
|
28
|
+
|
|
29
|
+
/* declare node configuration parameters */
|
|
30
|
+
this.configure({
|
|
31
|
+
key: { type: "string", val: process.env.SPEECHFLOW_AMAZON_KEY },
|
|
32
|
+
secKey: { type: "string", val: process.env.SPEECHFLOW_AMAZON_KEY_SEC },
|
|
33
|
+
region: { type: "string", val: "eu-central-1" },
|
|
34
|
+
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en|fr|it)$/ },
|
|
35
|
+
dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en|fr|it)$/ }
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/* sanity check parameters */
|
|
39
|
+
if (!this.params.key)
|
|
40
|
+
throw new Error("AWS Access Key not configured")
|
|
41
|
+
if (!this.params.secKey)
|
|
42
|
+
throw new Error("AWS Secret Access Key not configured")
|
|
43
|
+
|
|
44
|
+
/* sanity check situation */
|
|
45
|
+
if (this.params.src === this.params.dst)
|
|
46
|
+
throw new Error("source and destination languages cannot be the same")
|
|
47
|
+
|
|
48
|
+
/* declare node input/output format */
|
|
49
|
+
this.input = "text"
|
|
50
|
+
this.output = "text"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* one-time status of node */
|
|
54
|
+
async status () {
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* open node */
|
|
59
|
+
async open () {
|
|
60
|
+
/* connect to Amazon Translate API */
|
|
61
|
+
this.client = new TranslateClient({
|
|
62
|
+
region: this.params.region,
|
|
63
|
+
credentials: {
|
|
64
|
+
accessKeyId: this.params.key,
|
|
65
|
+
secretAccessKey: this.params.secKey
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
if (this.client === null)
|
|
69
|
+
throw new Error("failed to establish Amazon Translate client")
|
|
70
|
+
|
|
71
|
+
/* provide text-to-text translation */
|
|
72
|
+
const maxRetries = 10
|
|
73
|
+
const translate = async (text: string): Promise<string> => {
|
|
74
|
+
let attempt = 0
|
|
75
|
+
let lastError: unknown
|
|
76
|
+
while (attempt < maxRetries) {
|
|
77
|
+
try {
|
|
78
|
+
const cmd = new TranslateTextCommand({
|
|
79
|
+
SourceLanguageCode: this.params.src,
|
|
80
|
+
TargetLanguageCode: this.params.dst,
|
|
81
|
+
Text: text,
|
|
82
|
+
Settings: {
|
|
83
|
+
Formality: "INFORMAL",
|
|
84
|
+
Brevity: "ON"
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
const out = await this.client!.send(cmd)
|
|
88
|
+
return (out.TranslatedText ?? "").trim()
|
|
89
|
+
} catch (e: any) {
|
|
90
|
+
lastError = e
|
|
91
|
+
attempt += 1
|
|
92
|
+
|
|
93
|
+
/* simple backoff for transient errors */
|
|
94
|
+
const retriable =
|
|
95
|
+
e?.name === "ThrottlingException" ||
|
|
96
|
+
e?.name === "ServiceUnavailableException" ||
|
|
97
|
+
e?.$retryable === true
|
|
98
|
+
if (!retriable || attempt >= maxRetries)
|
|
99
|
+
break
|
|
100
|
+
const delayMs = Math.min(1000 * Math.pow(2, attempt - 1), 5000)
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* establish a duplex stream and connect it to AWS Translate */
|
|
108
|
+
this.stream = new Stream.Transform({
|
|
109
|
+
readableObjectMode: true,
|
|
110
|
+
writableObjectMode: true,
|
|
111
|
+
decodeStrings: false,
|
|
112
|
+
highWaterMark: 1,
|
|
113
|
+
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
114
|
+
if (Buffer.isBuffer(chunk.payload))
|
|
115
|
+
callback(new Error("invalid chunk payload type"))
|
|
116
|
+
else if (chunk.payload === "") {
|
|
117
|
+
this.push(chunk)
|
|
118
|
+
callback()
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
translate(chunk.payload).then((payload) => {
|
|
122
|
+
const chunkNew = chunk.clone()
|
|
123
|
+
chunkNew.payload = payload
|
|
124
|
+
this.push(chunkNew)
|
|
125
|
+
callback()
|
|
126
|
+
}).catch((error: unknown) => {
|
|
127
|
+
callback(utils.ensureError(error))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
final (callback) {
|
|
132
|
+
this.push(null)
|
|
133
|
+
callback()
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* close node */
|
|
139
|
+
async close () {
|
|
140
|
+
/* close Amazon Translate connection */
|
|
141
|
+
if (this.client !== null) {
|
|
142
|
+
this.client.destroy()
|
|
143
|
+
this.client = null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* close stream */
|
|
147
|
+
if (this.stream !== null) {
|
|
148
|
+
this.stream.destroy()
|
|
149
|
+
this.stream = null
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
@@ -12,6 +12,7 @@ import * as DeepL from "deepl-node"
|
|
|
12
12
|
|
|
13
13
|
/* internal dependencies */
|
|
14
14
|
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
15
|
+
import * as utils from "./speechflow-utils"
|
|
15
16
|
|
|
16
17
|
/* SpeechFlow node for DeepL text-to-text translations */
|
|
17
18
|
export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
@@ -28,8 +29,8 @@ export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
|
28
29
|
/* declare node configuration parameters */
|
|
29
30
|
this.configure({
|
|
30
31
|
key: { type: "string", val: process.env.SPEECHFLOW_DEEPL_KEY ?? "" },
|
|
31
|
-
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en)$/ },
|
|
32
|
-
dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en)$/ },
|
|
32
|
+
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en|fr|it)$/ },
|
|
33
|
+
dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en|fr|it)$/ },
|
|
33
34
|
optimize: { type: "string", pos: 2, val: "latency", match: /^(?:latency|quality)$/ }
|
|
34
35
|
})
|
|
35
36
|
|
|
@@ -83,21 +84,19 @@ export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
|
83
84
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
84
85
|
if (Buffer.isBuffer(chunk.payload))
|
|
85
86
|
callback(new Error("invalid chunk payload type"))
|
|
87
|
+
else if (chunk.payload === "") {
|
|
88
|
+
this.push(chunk)
|
|
89
|
+
callback()
|
|
90
|
+
}
|
|
86
91
|
else {
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
translate(chunk.payload).then((payload) => {
|
|
93
|
+
const chunkNew = chunk.clone()
|
|
94
|
+
chunkNew.payload = payload
|
|
95
|
+
this.push(chunkNew)
|
|
89
96
|
callback()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const chunkNew = chunk.clone()
|
|
94
|
-
chunkNew.payload = payload
|
|
95
|
-
this.push(chunkNew)
|
|
96
|
-
callback()
|
|
97
|
-
}).catch((err) => {
|
|
98
|
-
callback(err)
|
|
99
|
-
})
|
|
100
|
-
}
|
|
97
|
+
}).catch((error: unknown) => {
|
|
98
|
+
callback(utils.ensureError(error))
|
|
99
|
+
})
|
|
101
100
|
}
|
|
102
101
|
},
|
|
103
102
|
final (callback) {
|
|
@@ -35,7 +35,7 @@ export default class SpeechFlowNodeFormat extends SpeechFlowNode {
|
|
|
35
35
|
/* open node */
|
|
36
36
|
async open () {
|
|
37
37
|
/* provide text-to-text formatter */
|
|
38
|
-
const format =
|
|
38
|
+
const format = (text: string) => {
|
|
39
39
|
text = wrapText(text, this.params.width)
|
|
40
40
|
text = text.replace(/([^\n])$/, "$1\n")
|
|
41
41
|
return text
|
|
@@ -50,21 +50,16 @@ export default class SpeechFlowNodeFormat extends SpeechFlowNode {
|
|
|
50
50
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
51
51
|
if (Buffer.isBuffer(chunk.payload))
|
|
52
52
|
callback(new Error("invalid chunk payload type"))
|
|
53
|
+
else if (chunk.payload === "") {
|
|
54
|
+
this.push(chunk)
|
|
55
|
+
callback()
|
|
56
|
+
}
|
|
53
57
|
else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
format(chunk.payload).then((payload) => {
|
|
60
|
-
const chunkNew = chunk.clone()
|
|
61
|
-
chunkNew.payload = payload
|
|
62
|
-
this.push(chunkNew)
|
|
63
|
-
callback()
|
|
64
|
-
}).catch((err) => {
|
|
65
|
-
callback(err)
|
|
66
|
-
})
|
|
67
|
-
}
|
|
58
|
+
const payload = format(chunk.payload)
|
|
59
|
+
const chunkNew = chunk.clone()
|
|
60
|
+
chunkNew.payload = payload
|
|
61
|
+
this.push(chunkNew)
|
|
62
|
+
callback()
|
|
68
63
|
}
|
|
69
64
|
},
|
|
70
65
|
final (callback) {
|
|
@@ -0,0 +1,133 @@
|
|
|
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 { TranslationServiceClient } from "@google-cloud/translate"
|
|
12
|
+
import * as arktype from "arktype"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
16
|
+
import * as utils from "./speechflow-utils"
|
|
17
|
+
|
|
18
|
+
/* SpeechFlow node for Google Translate text-to-text translations */
|
|
19
|
+
export default class SpeechFlowNodeGoogle extends SpeechFlowNode {
|
|
20
|
+
/* declare official node name */
|
|
21
|
+
public static name = "google"
|
|
22
|
+
|
|
23
|
+
/* internal state */
|
|
24
|
+
private client: TranslationServiceClient | 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
|
+
key: { type: "string", val: process.env.SPEECHFLOW_GOOGLE_KEY ?? "" },
|
|
33
|
+
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en|fr|it)$/ },
|
|
34
|
+
dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en|fr|it)$/ }
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/* validate API key and project */
|
|
38
|
+
if (this.params.key === "")
|
|
39
|
+
throw new Error("Google Cloud API credentials JSON key is required")
|
|
40
|
+
|
|
41
|
+
/* sanity check situation */
|
|
42
|
+
if (this.params.src === this.params.dst)
|
|
43
|
+
throw new Error("source and destination languages cannot be the same")
|
|
44
|
+
|
|
45
|
+
/* declare node input/output format */
|
|
46
|
+
this.input = "text"
|
|
47
|
+
this.output = "text"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* one-time status of node */
|
|
51
|
+
async status () {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* open node */
|
|
56
|
+
async open () {
|
|
57
|
+
/* instantiate Google Translate client */
|
|
58
|
+
const data = utils.run("Google Cloud API credentials key", () =>
|
|
59
|
+
JSON.parse(this.params.key))
|
|
60
|
+
const credentials = utils.importObject("Google Cloud API credentials key",
|
|
61
|
+
data,
|
|
62
|
+
arktype.type({
|
|
63
|
+
project_id: "string",
|
|
64
|
+
private_key: "string",
|
|
65
|
+
client_email: "string",
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
this.client = new TranslationServiceClient({
|
|
69
|
+
credentials: {
|
|
70
|
+
private_key: credentials.private_key,
|
|
71
|
+
client_email: credentials.client_email
|
|
72
|
+
},
|
|
73
|
+
projectId: credentials.project_id
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/* provide text-to-text translation */
|
|
77
|
+
const translate = utils.runner("Google Translate API", async (text: string) => {
|
|
78
|
+
const [ response ] = await this.client!.translateText({
|
|
79
|
+
parent: `projects/${credentials.project_id}/locations/global`,
|
|
80
|
+
contents: [ text ],
|
|
81
|
+
mimeType: "text/plain",
|
|
82
|
+
sourceLanguageCode: this.params.src,
|
|
83
|
+
targetLanguageCode: this.params.dst
|
|
84
|
+
})
|
|
85
|
+
return response.translations?.[0]?.translatedText ?? text
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/* establish a duplex stream and connect it to Google Translate */
|
|
89
|
+
this.stream = new Stream.Transform({
|
|
90
|
+
readableObjectMode: true,
|
|
91
|
+
writableObjectMode: true,
|
|
92
|
+
decodeStrings: false,
|
|
93
|
+
highWaterMark: 1,
|
|
94
|
+
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
95
|
+
if (Buffer.isBuffer(chunk.payload))
|
|
96
|
+
callback(new Error("invalid chunk payload type"))
|
|
97
|
+
else if (chunk.payload === "") {
|
|
98
|
+
this.push(chunk)
|
|
99
|
+
callback()
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
translate(chunk.payload).then((payload) => {
|
|
103
|
+
const chunkNew = chunk.clone()
|
|
104
|
+
chunkNew.payload = payload
|
|
105
|
+
this.push(chunkNew)
|
|
106
|
+
callback()
|
|
107
|
+
}).catch((error: unknown) => {
|
|
108
|
+
callback(utils.ensureError(error))
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
final (callback) {
|
|
113
|
+
this.push(null)
|
|
114
|
+
callback()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* close node */
|
|
120
|
+
async close () {
|
|
121
|
+
/* close stream */
|
|
122
|
+
if (this.stream !== null) {
|
|
123
|
+
this.stream.destroy()
|
|
124
|
+
this.stream = null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* shutdown Google Translate client */
|
|
128
|
+
if (this.client !== null) {
|
|
129
|
+
this.client.close()
|
|
130
|
+
this.client = null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import Stream from "node:stream"
|
|
9
9
|
|
|
10
10
|
/* external dependencies */
|
|
11
|
-
import { Ollama } from "ollama"
|
|
11
|
+
import { Ollama, type ListResponse } from "ollama"
|
|
12
12
|
|
|
13
13
|
/* internal dependencies */
|
|
14
14
|
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
15
|
+
import * as utils from "./speechflow-utils"
|
|
15
16
|
|
|
16
17
|
/* internal utility types */
|
|
17
18
|
type ConfigEntry = { systemPrompt: string, chat: Array<{ role: string, content: string }> }
|
|
@@ -49,12 +50,12 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
49
50
|
"Focus ONLY on the word spelling.\n" +
|
|
50
51
|
"The text you have to correct is:\n",
|
|
51
52
|
chat: [
|
|
52
|
-
{ role: "user",
|
|
53
|
-
{ role: "
|
|
54
|
-
{ role: "user",
|
|
55
|
-
{ role: "
|
|
56
|
-
{ role: "user",
|
|
57
|
-
{ role: "
|
|
53
|
+
{ role: "user", content: "I luve my wyfe" },
|
|
54
|
+
{ role: "assistant", content: "I love my wife." },
|
|
55
|
+
{ role: "user", content: "The weether is wunderfull!" },
|
|
56
|
+
{ role: "assistant", content: "The weather is wonderful!" },
|
|
57
|
+
{ role: "user", content: "The life awesome but I'm hungry." },
|
|
58
|
+
{ role: "assistant", content: "The life is awesome, but I'm hungry." }
|
|
58
59
|
]
|
|
59
60
|
},
|
|
60
61
|
|
|
@@ -81,12 +82,12 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
81
82
|
"Fokussiere dich NUR auf die Rechtschreibung der Wörter.\n" +
|
|
82
83
|
"Der von dir zu korrigierende Text ist:\n",
|
|
83
84
|
chat: [
|
|
84
|
-
{ role: "user",
|
|
85
|
-
{ role: "
|
|
86
|
-
{ role: "user",
|
|
87
|
-
{ role: "
|
|
88
|
-
{ role: "user",
|
|
89
|
-
{ role: "
|
|
85
|
+
{ role: "user", content: "Ich ljebe meine Frao" },
|
|
86
|
+
{ role: "assistant", content: "Ich liebe meine Frau." },
|
|
87
|
+
{ role: "user", content: "Die Wedter ist wunderschoen." },
|
|
88
|
+
{ role: "assistant", content: "Das Wetter ist wunderschön." },
|
|
89
|
+
{ role: "user", content: "Das Leben einfach großartig aber ich bin hungrig." },
|
|
90
|
+
{ role: "assistant", content: "Das Leben ist einfach großartig, aber ich bin hungrig." }
|
|
90
91
|
]
|
|
91
92
|
},
|
|
92
93
|
|
|
@@ -106,12 +107,12 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
106
107
|
"Preserve the original meaning, tone, and nuance.\n" +
|
|
107
108
|
"Directly translate text from English (EN) to fluent and natural German (DE) language.\n",
|
|
108
109
|
chat: [
|
|
109
|
-
{ role: "user",
|
|
110
|
-
{ role: "
|
|
111
|
-
{ role: "user",
|
|
112
|
-
{ role: "
|
|
113
|
-
{ role: "user",
|
|
114
|
-
{ role: "
|
|
110
|
+
{ role: "user", content: "I love my wife." },
|
|
111
|
+
{ role: "assistant", content: "Ich liebe meine Frau." },
|
|
112
|
+
{ role: "user", content: "The weather is wonderful." },
|
|
113
|
+
{ role: "assistant", content: "Das Wetter ist wunderschön." },
|
|
114
|
+
{ role: "user", content: "The life is awesome." },
|
|
115
|
+
{ role: "assistant", content: "Das Leben ist einfach großartig." }
|
|
115
116
|
]
|
|
116
117
|
},
|
|
117
118
|
|
|
@@ -124,19 +125,19 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
124
125
|
"Do not chat.\n" +
|
|
125
126
|
"Do not show any explanations.\n" +
|
|
126
127
|
"Do not show any introduction.\n" +
|
|
127
|
-
"Do not show any preamble
|
|
128
|
-
"Do not show any prolog
|
|
129
|
-
"Do not show any epilog
|
|
128
|
+
"Do not show any preamble.\n" +
|
|
129
|
+
"Do not show any prolog.\n" +
|
|
130
|
+
"Do not show any epilog.\n" +
|
|
130
131
|
"Get to the point.\n" +
|
|
131
132
|
"Preserve the original meaning, tone, and nuance.\n" +
|
|
132
133
|
"Directly translate text from German (DE) to fluent and natural English (EN) language.\n",
|
|
133
134
|
chat: [
|
|
134
|
-
{ role: "user",
|
|
135
|
-
{ role: "
|
|
136
|
-
{ role: "user",
|
|
137
|
-
{ role: "
|
|
138
|
-
{ role: "user",
|
|
139
|
-
{ role: "
|
|
135
|
+
{ role: "user", content: "Ich liebe meine Frau." },
|
|
136
|
+
{ role: "assistant", content: "I love my wife." },
|
|
137
|
+
{ role: "user", content: "Das Wetter ist wunderschön." },
|
|
138
|
+
{ role: "assistant", content: "The weather is wonderful." },
|
|
139
|
+
{ role: "user", content: "Das Leben ist einfach großartig." },
|
|
140
|
+
{ role: "assistant", content: "The life is awesome." }
|
|
140
141
|
]
|
|
141
142
|
}
|
|
142
143
|
}
|
|
@@ -171,35 +172,48 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
171
172
|
this.ollama = new Ollama({ host: this.params.api })
|
|
172
173
|
|
|
173
174
|
/* ensure the model is available */
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
let models: ListResponse
|
|
176
|
+
try {
|
|
177
|
+
models = await this.ollama.list()
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
throw new Error(`failed to connect to Ollama API at ${this.params.api}: ${err}`)
|
|
181
|
+
}
|
|
182
|
+
const exists = models.models.some((m) => m.name === this.params.model)
|
|
177
183
|
if (!exists) {
|
|
178
|
-
this.log("info", `Ollama: model "${model}" still not present in Ollama -- ` +
|
|
184
|
+
this.log("info", `Ollama: model "${this.params.model}" still not present in Ollama -- ` +
|
|
179
185
|
"automatically downloading model")
|
|
180
186
|
let artifact = ""
|
|
181
187
|
let percent = 0
|
|
188
|
+
let lastLoggedPercent = -1
|
|
182
189
|
const interval = setInterval(() => {
|
|
183
|
-
|
|
190
|
+
if (percent !== lastLoggedPercent) {
|
|
191
|
+
this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
|
|
192
|
+
lastLoggedPercent = percent
|
|
193
|
+
}
|
|
184
194
|
}, 1000)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
try {
|
|
196
|
+
const progress = await this.ollama.pull({ model: this.params.model, stream: true })
|
|
197
|
+
for await (const event of progress) {
|
|
198
|
+
if (event.digest)
|
|
199
|
+
artifact = event.digest
|
|
200
|
+
if (event.completed && event.total)
|
|
201
|
+
percent = (event.completed / event.total) * 100
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
clearInterval(interval)
|
|
191
206
|
}
|
|
192
|
-
clearInterval(interval)
|
|
193
207
|
}
|
|
194
208
|
else
|
|
195
|
-
this.log("info", `Ollama: model "${model}" already present in Ollama`)
|
|
209
|
+
this.log("info", `Ollama: model "${this.params.model}" already present in Ollama`)
|
|
196
210
|
|
|
197
211
|
/* provide text-to-text translation */
|
|
198
212
|
const translate = async (text: string) => {
|
|
199
213
|
const key = `${this.params.src}-${this.params.dst}`
|
|
200
214
|
const cfg = this.setup[key]
|
|
201
215
|
const response = await this.ollama!.chat({
|
|
202
|
-
model,
|
|
216
|
+
model: this.params.model,
|
|
203
217
|
messages: [
|
|
204
218
|
{ role: "system", content: cfg.systemPrompt },
|
|
205
219
|
...cfg.chat,
|
|
@@ -237,8 +251,8 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
237
251
|
chunkNew.payload = payload
|
|
238
252
|
this.push(chunkNew)
|
|
239
253
|
callback()
|
|
240
|
-
}).catch((
|
|
241
|
-
callback(
|
|
254
|
+
}).catch((error: unknown) => {
|
|
255
|
+
callback(utils.ensureError(error))
|
|
242
256
|
})
|
|
243
257
|
}
|
|
244
258
|
}
|