speechflow 1.4.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +220 -7
- package/etc/claude.md +70 -0
- package/etc/speechflow.yaml +5 -3
- 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 +13 -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 +317 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +15 -13
- 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 +171 -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 +11 -6
- 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 +141 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +13 -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-ollama.js +44 -31
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +44 -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 +10 -12
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +22 -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 +18 -0
- package/speechflow-cli/dst/speechflow-utils.js +123 -35
- package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
- package/speechflow-cli/dst/speechflow.js +69 -14
- package/speechflow-cli/dst/speechflow.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +112 -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 +102 -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 +13 -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 +308 -0
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +15 -13
- 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 +12 -7
- package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +152 -0
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +13 -15
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +55 -42
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +58 -58
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +15 -16
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +27 -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 +125 -32
- package/speechflow-cli/src/speechflow.ts +74 -17
- package/speechflow-ui-db/dst/index.js +31 -31
- 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 +11 -10
- 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 +11 -10
- package/speechflow-ui-st/src/app.vue +5 -12
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
|
|
16
|
+
/* SpeechFlow node for AWS Translate text-to-text translations */
|
|
17
|
+
export default class SpeechFlowNodeAWSTranslate extends SpeechFlowNode {
|
|
18
|
+
/* declare official node name */
|
|
19
|
+
public static name = "awstranslate"
|
|
20
|
+
|
|
21
|
+
/* internal state */
|
|
22
|
+
private client: TranslateClient | null = null
|
|
23
|
+
|
|
24
|
+
/* construct node */
|
|
25
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
26
|
+
super(id, cfg, opts, args)
|
|
27
|
+
|
|
28
|
+
/* declare node configuration parameters */
|
|
29
|
+
this.configure({
|
|
30
|
+
key: { type: "string", val: process.env.SPEECHFLOW_AMAZON_KEY },
|
|
31
|
+
secKey: { type: "string", val: process.env.SPEECHFLOW_AMAZON_KEY_SEC },
|
|
32
|
+
region: { type: "string", val: "eu-central-1" },
|
|
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
|
+
/* sanity check parameters */
|
|
38
|
+
if (!this.params.key)
|
|
39
|
+
throw new Error("AWS Access Key not configured")
|
|
40
|
+
if (!this.params.secKey)
|
|
41
|
+
throw new Error("AWS Secret Access Key not configured")
|
|
42
|
+
|
|
43
|
+
/* sanity check situation */
|
|
44
|
+
if (this.params.src === this.params.dst)
|
|
45
|
+
throw new Error("source and destination languages cannot be the same")
|
|
46
|
+
|
|
47
|
+
/* declare node input/output format */
|
|
48
|
+
this.input = "text"
|
|
49
|
+
this.output = "text"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* one-time status of node */
|
|
53
|
+
async status () {
|
|
54
|
+
return {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* open node */
|
|
58
|
+
async open () {
|
|
59
|
+
/* connect to Amazon Translate API */
|
|
60
|
+
this.client = new TranslateClient({
|
|
61
|
+
region: this.params.region,
|
|
62
|
+
credentials: {
|
|
63
|
+
accessKeyId: this.params.key,
|
|
64
|
+
secretAccessKey: this.params.secKey
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
if (this.client === null)
|
|
68
|
+
throw new Error("failed to establish Amazon Translate client")
|
|
69
|
+
|
|
70
|
+
/* provide text-to-text translation */
|
|
71
|
+
const maxRetries = 10
|
|
72
|
+
const translate = async (text: string): Promise<string> => {
|
|
73
|
+
let attempt = 0
|
|
74
|
+
let lastError: unknown
|
|
75
|
+
while (attempt < maxRetries) {
|
|
76
|
+
try {
|
|
77
|
+
const cmd = new TranslateTextCommand({
|
|
78
|
+
SourceLanguageCode: this.params.src,
|
|
79
|
+
TargetLanguageCode: this.params.dst,
|
|
80
|
+
Text: text,
|
|
81
|
+
Settings: {
|
|
82
|
+
Formality: "INFORMAL",
|
|
83
|
+
Brevity: "ON"
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
const out = await this.client!.send(cmd)
|
|
87
|
+
return (out.TranslatedText ?? "").trim()
|
|
88
|
+
} catch (e: any) {
|
|
89
|
+
lastError = e
|
|
90
|
+
attempt += 1
|
|
91
|
+
|
|
92
|
+
/* simple backoff for transient errors */
|
|
93
|
+
const retriable =
|
|
94
|
+
e?.name === "ThrottlingException" ||
|
|
95
|
+
e?.name === "ServiceUnavailableException" ||
|
|
96
|
+
e?.$retryable === true
|
|
97
|
+
if (!retriable || attempt >= maxRetries)
|
|
98
|
+
break
|
|
99
|
+
const delayMs = Math.min(1000 * Math.pow(2, attempt - 1), 5000)
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* establish a duplex stream and connect it to AWS Translate */
|
|
107
|
+
this.stream = new Stream.Transform({
|
|
108
|
+
readableObjectMode: true,
|
|
109
|
+
writableObjectMode: true,
|
|
110
|
+
decodeStrings: false,
|
|
111
|
+
highWaterMark: 1,
|
|
112
|
+
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
113
|
+
if (Buffer.isBuffer(chunk.payload))
|
|
114
|
+
callback(new Error("invalid chunk payload type"))
|
|
115
|
+
else if (chunk.payload === "") {
|
|
116
|
+
this.push(chunk)
|
|
117
|
+
callback()
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
translate(chunk.payload).then((payload) => {
|
|
121
|
+
const chunkNew = chunk.clone()
|
|
122
|
+
chunkNew.payload = payload
|
|
123
|
+
this.push(chunkNew)
|
|
124
|
+
callback()
|
|
125
|
+
}).catch((err) => {
|
|
126
|
+
callback(err)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
final (callback) {
|
|
131
|
+
this.push(null)
|
|
132
|
+
callback()
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* close node */
|
|
138
|
+
async close () {
|
|
139
|
+
/* close Amazon Translate connection */
|
|
140
|
+
if (this.client !== null) {
|
|
141
|
+
this.client.destroy()
|
|
142
|
+
this.client = null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* close stream */
|
|
146
|
+
if (this.stream !== null) {
|
|
147
|
+
this.stream.destroy()
|
|
148
|
+
this.stream = null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
@@ -28,8 +28,8 @@ export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
|
28
28
|
/* declare node configuration parameters */
|
|
29
29
|
this.configure({
|
|
30
30
|
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)$/ },
|
|
31
|
+
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en|fr|it)$/ },
|
|
32
|
+
dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en|fr|it)$/ },
|
|
33
33
|
optimize: { type: "string", pos: 2, val: "latency", match: /^(?:latency|quality)$/ }
|
|
34
34
|
})
|
|
35
35
|
|
|
@@ -83,21 +83,19 @@ export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
|
|
|
83
83
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
84
84
|
if (Buffer.isBuffer(chunk.payload))
|
|
85
85
|
callback(new Error("invalid chunk payload type"))
|
|
86
|
+
else if (chunk.payload === "") {
|
|
87
|
+
this.push(chunk)
|
|
88
|
+
callback()
|
|
89
|
+
}
|
|
86
90
|
else {
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
translate(chunk.payload).then((payload) => {
|
|
92
|
+
const chunkNew = chunk.clone()
|
|
93
|
+
chunkNew.payload = payload
|
|
94
|
+
this.push(chunkNew)
|
|
89
95
|
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
|
-
}
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
callback(err)
|
|
98
|
+
})
|
|
101
99
|
}
|
|
102
100
|
},
|
|
103
101
|
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) {
|
|
@@ -8,7 +8,7 @@
|
|
|
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"
|
|
@@ -49,12 +49,12 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
49
49
|
"Focus ONLY on the word spelling.\n" +
|
|
50
50
|
"The text you have to correct is:\n",
|
|
51
51
|
chat: [
|
|
52
|
-
{ role: "user",
|
|
53
|
-
{ role: "
|
|
54
|
-
{ role: "user",
|
|
55
|
-
{ role: "
|
|
56
|
-
{ role: "user",
|
|
57
|
-
{ role: "
|
|
52
|
+
{ role: "user", content: "I luve my wyfe" },
|
|
53
|
+
{ role: "assistant", content: "I love my wife." },
|
|
54
|
+
{ role: "user", content: "The weether is wunderfull!" },
|
|
55
|
+
{ role: "assistant", content: "The weather is wonderful!" },
|
|
56
|
+
{ role: "user", content: "The life awesome but I'm hungry." },
|
|
57
|
+
{ role: "assistant", content: "The life is awesome, but I'm hungry." }
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
60
|
|
|
@@ -81,12 +81,12 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
81
81
|
"Fokussiere dich NUR auf die Rechtschreibung der Wörter.\n" +
|
|
82
82
|
"Der von dir zu korrigierende Text ist:\n",
|
|
83
83
|
chat: [
|
|
84
|
-
{ role: "user",
|
|
85
|
-
{ role: "
|
|
86
|
-
{ role: "user",
|
|
87
|
-
{ role: "
|
|
88
|
-
{ role: "user",
|
|
89
|
-
{ role: "
|
|
84
|
+
{ role: "user", content: "Ich ljebe meine Frao" },
|
|
85
|
+
{ role: "assistant", content: "Ich liebe meine Frau." },
|
|
86
|
+
{ role: "user", content: "Die Wedter ist wunderschoen." },
|
|
87
|
+
{ role: "assistant", content: "Das Wetter ist wunderschön." },
|
|
88
|
+
{ role: "user", content: "Das Leben einfach großartig aber ich bin hungrig." },
|
|
89
|
+
{ role: "assistant", content: "Das Leben ist einfach großartig, aber ich bin hungrig." }
|
|
90
90
|
]
|
|
91
91
|
},
|
|
92
92
|
|
|
@@ -106,12 +106,12 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
106
106
|
"Preserve the original meaning, tone, and nuance.\n" +
|
|
107
107
|
"Directly translate text from English (EN) to fluent and natural German (DE) language.\n",
|
|
108
108
|
chat: [
|
|
109
|
-
{ role: "user",
|
|
110
|
-
{ role: "
|
|
111
|
-
{ role: "user",
|
|
112
|
-
{ role: "
|
|
113
|
-
{ role: "user",
|
|
114
|
-
{ role: "
|
|
109
|
+
{ role: "user", content: "I love my wife." },
|
|
110
|
+
{ role: "assistant", content: "Ich liebe meine Frau." },
|
|
111
|
+
{ role: "user", content: "The weather is wonderful." },
|
|
112
|
+
{ role: "assistant", content: "Das Wetter ist wunderschön." },
|
|
113
|
+
{ role: "user", content: "The life is awesome." },
|
|
114
|
+
{ role: "assistant", content: "Das Leben ist einfach großartig." }
|
|
115
115
|
]
|
|
116
116
|
},
|
|
117
117
|
|
|
@@ -124,19 +124,19 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
124
124
|
"Do not chat.\n" +
|
|
125
125
|
"Do not show any explanations.\n" +
|
|
126
126
|
"Do not show any introduction.\n" +
|
|
127
|
-
"Do not show any preamble
|
|
128
|
-
"Do not show any prolog
|
|
129
|
-
"Do not show any epilog
|
|
127
|
+
"Do not show any preamble.\n" +
|
|
128
|
+
"Do not show any prolog.\n" +
|
|
129
|
+
"Do not show any epilog.\n" +
|
|
130
130
|
"Get to the point.\n" +
|
|
131
131
|
"Preserve the original meaning, tone, and nuance.\n" +
|
|
132
132
|
"Directly translate text from German (DE) to fluent and natural English (EN) language.\n",
|
|
133
133
|
chat: [
|
|
134
|
-
{ role: "user",
|
|
135
|
-
{ role: "
|
|
136
|
-
{ role: "user",
|
|
137
|
-
{ role: "
|
|
138
|
-
{ role: "user",
|
|
139
|
-
{ role: "
|
|
134
|
+
{ role: "user", content: "Ich liebe meine Frau." },
|
|
135
|
+
{ role: "assistant", content: "I love my wife." },
|
|
136
|
+
{ role: "user", content: "Das Wetter ist wunderschön." },
|
|
137
|
+
{ role: "assistant", content: "The weather is wonderful." },
|
|
138
|
+
{ role: "user", content: "Das Leben ist einfach großartig." },
|
|
139
|
+
{ role: "assistant", content: "The life is awesome." }
|
|
140
140
|
]
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -171,35 +171,48 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
|
|
|
171
171
|
this.ollama = new Ollama({ host: this.params.api })
|
|
172
172
|
|
|
173
173
|
/* ensure the model is available */
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
let models: ListResponse
|
|
175
|
+
try {
|
|
176
|
+
models = await this.ollama.list()
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
throw new Error(`failed to connect to Ollama API at ${this.params.api}: ${err}`)
|
|
180
|
+
}
|
|
181
|
+
const exists = models.models.some((m) => m.name === this.params.model)
|
|
177
182
|
if (!exists) {
|
|
178
|
-
this.log("info", `Ollama: model "${model}" still not present in Ollama -- ` +
|
|
183
|
+
this.log("info", `Ollama: model "${this.params.model}" still not present in Ollama -- ` +
|
|
179
184
|
"automatically downloading model")
|
|
180
185
|
let artifact = ""
|
|
181
186
|
let percent = 0
|
|
187
|
+
let lastLoggedPercent = -1
|
|
182
188
|
const interval = setInterval(() => {
|
|
183
|
-
|
|
189
|
+
if (percent !== lastLoggedPercent) {
|
|
190
|
+
this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
|
|
191
|
+
lastLoggedPercent = percent
|
|
192
|
+
}
|
|
184
193
|
}, 1000)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
try {
|
|
195
|
+
const progress = await this.ollama.pull({ model: this.params.model, stream: true })
|
|
196
|
+
for await (const event of progress) {
|
|
197
|
+
if (event.digest)
|
|
198
|
+
artifact = event.digest
|
|
199
|
+
if (event.completed && event.total)
|
|
200
|
+
percent = (event.completed / event.total) * 100
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
clearInterval(interval)
|
|
191
205
|
}
|
|
192
|
-
clearInterval(interval)
|
|
193
206
|
}
|
|
194
207
|
else
|
|
195
|
-
this.log("info", `Ollama: model "${model}" already present in Ollama`)
|
|
208
|
+
this.log("info", `Ollama: model "${this.params.model}" already present in Ollama`)
|
|
196
209
|
|
|
197
210
|
/* provide text-to-text translation */
|
|
198
211
|
const translate = async (text: string) => {
|
|
199
212
|
const key = `${this.params.src}-${this.params.dst}`
|
|
200
213
|
const cfg = this.setup[key]
|
|
201
214
|
const response = await this.ollama!.chat({
|
|
202
|
-
model,
|
|
215
|
+
model: this.params.model,
|
|
203
216
|
messages: [
|
|
204
217
|
{ role: "system", content: cfg.systemPrompt },
|
|
205
218
|
...cfg.chat,
|
|
@@ -49,12 +49,12 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
49
49
|
"Focus ONLY on the word spelling.\n" +
|
|
50
50
|
"The text you have to correct is:\n",
|
|
51
51
|
chat: [
|
|
52
|
-
{ role: "user",
|
|
53
|
-
{ role: "
|
|
54
|
-
{ role: "user",
|
|
55
|
-
{ role: "
|
|
56
|
-
{ role: "user",
|
|
57
|
-
{ role: "
|
|
52
|
+
{ role: "user", content: "I luve my wyfe" },
|
|
53
|
+
{ role: "assistant", content: "I love my wife." },
|
|
54
|
+
{ role: "user", content: "The weether is wunderfull!" },
|
|
55
|
+
{ role: "assistant", content: "The weather is wonderful!" },
|
|
56
|
+
{ role: "user", content: "The life awesome but I'm hungry." },
|
|
57
|
+
{ role: "assistant", content: "The life is awesome, but I'm hungry." }
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
60
|
|
|
@@ -81,12 +81,12 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
81
81
|
"Fokussiere dich NUR auf die Rechtschreibung der Wörter.\n" +
|
|
82
82
|
"Der von dir zu korrigierende Text ist:\n",
|
|
83
83
|
chat: [
|
|
84
|
-
{ role: "user",
|
|
85
|
-
{ role: "
|
|
86
|
-
{ role: "user",
|
|
87
|
-
{ role: "
|
|
88
|
-
{ role: "user",
|
|
89
|
-
{ role: "
|
|
84
|
+
{ role: "user", content: "Ich ljebe meine Frao" },
|
|
85
|
+
{ role: "assistant", content: "Ich liebe meine Frau." },
|
|
86
|
+
{ role: "user", content: "Die Wedter ist wunderschoen." },
|
|
87
|
+
{ role: "assistant", content: "Das Wetter ist wunderschön." },
|
|
88
|
+
{ role: "user", content: "Das Leben einfach großartig aber ich bin hungrig." },
|
|
89
|
+
{ role: "assistant", content: "Das Leben ist einfach großartig, aber ich bin hungrig." }
|
|
90
90
|
]
|
|
91
91
|
},
|
|
92
92
|
|
|
@@ -106,12 +106,12 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
106
106
|
"Preserve the original meaning, tone, and nuance.\n" +
|
|
107
107
|
"Directly translate text from English (EN) to fluent and natural German (DE) language.\n",
|
|
108
108
|
chat: [
|
|
109
|
-
{ role: "user",
|
|
110
|
-
{ role: "
|
|
111
|
-
{ role: "user",
|
|
112
|
-
{ role: "
|
|
113
|
-
{ role: "user",
|
|
114
|
-
{ role: "
|
|
109
|
+
{ role: "user", content: "I love my wife." },
|
|
110
|
+
{ role: "assistant", content: "Ich liebe meine Frau." },
|
|
111
|
+
{ role: "user", content: "The weather is wonderful." },
|
|
112
|
+
{ role: "assistant", content: "Das Wetter ist wunderschön." },
|
|
113
|
+
{ role: "user", content: "The life is awesome." },
|
|
114
|
+
{ role: "assistant", content: "Das Leben ist einfach großartig." }
|
|
115
115
|
]
|
|
116
116
|
},
|
|
117
117
|
|
|
@@ -122,21 +122,21 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
122
122
|
"Output only the requested text.\n" +
|
|
123
123
|
"Do not use markdown.\n" +
|
|
124
124
|
"Do not chat.\n" +
|
|
125
|
-
"Do not show any explanations
|
|
125
|
+
"Do not show any explanations.\n" +
|
|
126
126
|
"Do not show any introduction.\n" +
|
|
127
|
-
"Do not show any preamble
|
|
128
|
-
"Do not show any prolog
|
|
129
|
-
"Do not show any epilog
|
|
127
|
+
"Do not show any preamble.\n" +
|
|
128
|
+
"Do not show any prolog.\n" +
|
|
129
|
+
"Do not show any epilog.\n" +
|
|
130
130
|
"Get to the point.\n" +
|
|
131
131
|
"Preserve the original meaning, tone, and nuance.\n" +
|
|
132
132
|
"Directly translate text from German (DE) to fluent and natural English (EN) language.\n",
|
|
133
133
|
chat: [
|
|
134
|
-
{ role: "user",
|
|
135
|
-
{ role: "
|
|
136
|
-
{ role: "user",
|
|
137
|
-
{ role: "
|
|
138
|
-
{ role: "user",
|
|
139
|
-
{ role: "
|
|
134
|
+
{ role: "user", content: "Ich liebe meine Frau." },
|
|
135
|
+
{ role: "assistant", content: "I love my wife." },
|
|
136
|
+
{ role: "user", content: "Das Wetter ist wunderschön." },
|
|
137
|
+
{ role: "assistant", content: "The weather is wonderful." },
|
|
138
|
+
{ role: "user", content: "Das Leben ist einfach großartig." },
|
|
139
|
+
{ role: "assistant", content: "The life is awesome." }
|
|
140
140
|
]
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -147,11 +147,11 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
147
147
|
|
|
148
148
|
/* declare node configuration parameters */
|
|
149
149
|
this.configure({
|
|
150
|
-
src: { type: "string", pos: 0, val: "de",
|
|
151
|
-
dst: { type: "string", pos: 1, val: "en",
|
|
152
|
-
key: { type: "string",
|
|
153
|
-
api: { type: "string",
|
|
154
|
-
model: { type: "string",
|
|
150
|
+
src: { type: "string", pos: 0, val: "de", match: /^(?:de|en)$/ },
|
|
151
|
+
dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en)$/ },
|
|
152
|
+
key: { type: "string", val: process.env.SPEECHFLOW_OPENAI_KEY, match: /^.+$/ },
|
|
153
|
+
api: { type: "string", val: "https://api.openai.com/v1", match: /^https?:\/\/.+/ },
|
|
154
|
+
model: { type: "string", val: "gpt-5-mini", match: /^.+$/ }
|
|
155
155
|
})
|
|
156
156
|
|
|
157
157
|
/* tell effective mode */
|
|
@@ -168,34 +168,36 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
168
168
|
|
|
169
169
|
/* open node */
|
|
170
170
|
async open () {
|
|
171
|
+
/* validate API key */
|
|
172
|
+
if (!this.params.key)
|
|
173
|
+
throw new Error("OpenAI API key is required")
|
|
174
|
+
|
|
171
175
|
/* instantiate OpenAI API */
|
|
172
176
|
this.openai = new OpenAI({
|
|
173
177
|
baseURL: this.params.api,
|
|
174
178
|
apiKey: this.params.key,
|
|
175
|
-
|
|
179
|
+
timeout: 30000
|
|
176
180
|
})
|
|
177
181
|
|
|
178
182
|
/* provide text-to-text translation */
|
|
179
183
|
const translate = async (text: string) => {
|
|
180
184
|
const key = `${this.params.src}-${this.params.dst}`
|
|
181
185
|
const cfg = this.setup[key]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
temperature:
|
|
187
|
-
n: 1,
|
|
186
|
+
if (!this.openai)
|
|
187
|
+
throw new Error("OpenAI client not available")
|
|
188
|
+
const completion = await this.openai.chat.completions.create({
|
|
189
|
+
model: this.params.model,
|
|
190
|
+
temperature: this.params.model.endsWith("-mini") ? 1.0 : 0.7,
|
|
188
191
|
messages: [
|
|
189
192
|
{ role: "system", content: cfg.systemPrompt },
|
|
190
193
|
...cfg.chat,
|
|
191
194
|
{ role: "user", content: text }
|
|
192
195
|
]
|
|
193
196
|
})
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return translation
|
|
197
|
+
const content = completion?.choices?.[0]?.message?.content
|
|
198
|
+
if (!content)
|
|
199
|
+
throw new Error("OpenAI API returned empty content")
|
|
200
|
+
return content
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
/* establish a duplex stream and connect it to OpenAI */
|
|
@@ -207,21 +209,19 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
|
|
|
207
209
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
208
210
|
if (Buffer.isBuffer(chunk.payload))
|
|
209
211
|
callback(new Error("invalid chunk payload type"))
|
|
212
|
+
else if (chunk.payload === "") {
|
|
213
|
+
this.push(chunk)
|
|
214
|
+
callback()
|
|
215
|
+
}
|
|
210
216
|
else {
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
translate(chunk.payload).then((payload) => {
|
|
218
|
+
const chunkNew = chunk.clone()
|
|
219
|
+
chunkNew.payload = payload
|
|
220
|
+
this.push(chunkNew)
|
|
213
221
|
callback()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const chunkNew = chunk.clone()
|
|
218
|
-
chunkNew.payload = payload
|
|
219
|
-
this.push(chunkNew)
|
|
220
|
-
callback()
|
|
221
|
-
}).catch((err) => {
|
|
222
|
-
callback(err)
|
|
223
|
-
})
|
|
224
|
-
}
|
|
222
|
+
}).catch((err) => {
|
|
223
|
+
callback(err)
|
|
224
|
+
})
|
|
225
225
|
}
|
|
226
226
|
},
|
|
227
227
|
final (callback) {
|
|
@@ -53,7 +53,7 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
53
53
|
/* clear destruction flag */
|
|
54
54
|
this.destroyed = false
|
|
55
55
|
|
|
56
|
-
/* work off queued
|
|
56
|
+
/* work off queued text frames */
|
|
57
57
|
let workingOff = false
|
|
58
58
|
const workOffQueue = async () => {
|
|
59
59
|
if (this.destroyed)
|
|
@@ -122,8 +122,8 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
122
122
|
}
|
|
123
123
|
element2.chunk.timestampStart = element.chunk.timestampStart
|
|
124
124
|
element2.chunk.payload =
|
|
125
|
-
element.chunk.payload as string + " " +
|
|
126
|
-
element2.chunk.payload as string
|
|
125
|
+
(element.chunk.payload as string) + " " +
|
|
126
|
+
(element2.chunk.payload as string)
|
|
127
127
|
this.queueSplit.delete()
|
|
128
128
|
this.queueSplit.touch()
|
|
129
129
|
}
|
|
@@ -193,19 +193,19 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
193
193
|
&& element.type === "text-frame"
|
|
194
194
|
&& element.complete === true) {
|
|
195
195
|
while (true) {
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
196
|
+
const nextElement = self.queueSend.peek()
|
|
197
|
+
if (nextElement === undefined)
|
|
198
198
|
break
|
|
199
|
-
else if (
|
|
199
|
+
else if (nextElement.type === "text-eof") {
|
|
200
200
|
this.push(null)
|
|
201
201
|
self.queueSend.walk(+1)
|
|
202
202
|
break
|
|
203
203
|
}
|
|
204
|
-
else if (
|
|
205
|
-
&&
|
|
204
|
+
else if (nextElement.type === "text-frame"
|
|
205
|
+
&& nextElement.complete !== true)
|
|
206
206
|
break
|
|
207
|
-
self.log("info", `send text: ${JSON.stringify(
|
|
208
|
-
this.push(
|
|
207
|
+
self.log("info", `send text: ${JSON.stringify(nextElement.chunk.payload)}`)
|
|
208
|
+
this.push(nextElement.chunk)
|
|
209
209
|
self.queueSend.walk(+1)
|
|
210
210
|
self.queue.trim()
|
|
211
211
|
}
|