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.
Files changed (166) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +220 -7
  3. package/etc/claude.md +70 -0
  4. package/etc/speechflow.yaml +5 -3
  5. package/etc/stx.conf +7 -0
  6. package/package.json +7 -6
  7. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.d.ts +1 -0
  8. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +155 -0
  9. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -0
  10. package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +15 -0
  11. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +287 -0
  12. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -0
  13. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.d.ts +1 -0
  14. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +208 -0
  15. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +1 -0
  16. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +15 -0
  17. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +312 -0
  18. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +1 -0
  19. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.d.ts +1 -0
  20. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +161 -0
  21. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -0
  22. package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +13 -0
  23. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +208 -0
  24. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -0
  25. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +13 -3
  26. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  27. package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +14 -0
  28. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +233 -0
  29. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -0
  30. package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +12 -0
  31. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +125 -0
  32. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -0
  33. package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +0 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +28 -12
  35. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -0
  37. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +12 -8
  38. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  39. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +2 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.d.ts +1 -0
  42. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +55 -0
  43. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -0
  44. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +14 -0
  45. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +184 -0
  46. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -0
  47. package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +14 -0
  48. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +156 -0
  49. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -0
  50. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
  51. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +22 -17
  53. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  54. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +18 -0
  55. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +317 -0
  56. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
  57. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +15 -13
  58. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  59. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +19 -0
  60. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +351 -0
  61. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -0
  62. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +16 -0
  63. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +171 -0
  64. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -0
  65. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +19 -14
  66. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  67. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +11 -6
  68. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  69. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +13 -0
  70. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +141 -0
  71. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
  72. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +13 -15
  73. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  74. package/speechflow-cli/dst/speechflow-node-t2t-format.js +10 -15
  75. package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +44 -31
  77. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  78. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +44 -45
  79. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  80. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
  81. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  82. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +10 -12
  83. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  84. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +22 -27
  85. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  86. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
  87. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
  88. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
  90. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  91. package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
  92. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  93. package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
  94. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
  95. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
  97. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  98. package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
  99. package/speechflow-cli/dst/speechflow-node.js +13 -2
  100. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
  102. package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
  103. package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
  104. package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
  105. package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
  106. package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
  107. package/speechflow-cli/dst/speechflow-utils.d.ts +18 -0
  108. package/speechflow-cli/dst/speechflow-utils.js +123 -35
  109. package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
  110. package/speechflow-cli/dst/speechflow.js +69 -14
  111. package/speechflow-cli/dst/speechflow.js.map +1 -1
  112. package/speechflow-cli/etc/oxlint.jsonc +112 -11
  113. package/speechflow-cli/etc/stx.conf +2 -2
  114. package/speechflow-cli/etc/tsconfig.json +1 -1
  115. package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
  116. package/speechflow-cli/package.json +102 -94
  117. package/speechflow-cli/src/lib.d.ts +24 -0
  118. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
  119. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
  120. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
  121. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
  122. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +13 -3
  123. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
  124. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
  125. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
  126. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
  127. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
  128. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
  129. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
  130. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
  131. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
  132. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
  133. package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +308 -0
  134. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +15 -13
  135. package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
  136. package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
  137. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
  138. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +12 -7
  139. package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +152 -0
  140. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +13 -15
  141. package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
  142. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +55 -42
  143. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +58 -58
  144. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
  145. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +15 -16
  146. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +27 -32
  147. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
  148. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
  149. package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
  150. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
  151. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
  152. package/speechflow-cli/src/speechflow-node.ts +21 -8
  153. package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
  154. package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
  155. package/speechflow-cli/src/speechflow-utils.ts +125 -32
  156. package/speechflow-cli/src/speechflow.ts +74 -17
  157. package/speechflow-ui-db/dst/index.js +31 -31
  158. package/speechflow-ui-db/etc/eslint.mjs +0 -1
  159. package/speechflow-ui-db/etc/tsc-client.json +3 -3
  160. package/speechflow-ui-db/package.json +11 -10
  161. package/speechflow-ui-db/src/app.vue +20 -6
  162. package/speechflow-ui-st/dst/index.js +26 -26
  163. package/speechflow-ui-st/etc/eslint.mjs +0 -1
  164. package/speechflow-ui-st/etc/tsc-client.json +3 -3
  165. package/speechflow-ui-st/package.json +11 -10
  166. 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
- if (chunk.payload === "") {
88
- this.push(chunk)
91
+ translate(chunk.payload).then((payload) => {
92
+ const chunkNew = chunk.clone()
93
+ chunkNew.payload = payload
94
+ this.push(chunkNew)
89
95
  callback()
90
- }
91
- else {
92
- translate(chunk.payload).then((payload) => {
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 = async (text: string) => {
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
- if (chunk.payload === "") {
55
- this.push(chunk)
56
- callback()
57
- }
58
- else {
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", content: "I luve my wyfe" },
53
- { role: "system", content: "I love my wife." },
54
- { role: "user", content: "The weether is wunderfull!" },
55
- { role: "system", content: "The weather is wonderful!" },
56
- { role: "user", content: "The live awesome but I'm hungry." },
57
- { role: "system", content: "The live is awesome, but I'm hungry." }
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", content: "Ich ljebe meine Frao" },
85
- { role: "system", content: "Ich liebe meine Frau." },
86
- { role: "user", content: "Die Wedter ist wunderschoen." },
87
- { role: "system", content: "Das Wetter ist wunderschön." },
88
- { role: "user", content: "Das Leben einfach großartig aber ich bin hungrig." },
89
- { role: "system", content: "Das Leben ist einfach großartig, aber ich bin hungrig." }
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", content: "I love my wife." },
110
- { role: "system", content: "Ich liebe meine Frau." },
111
- { role: "user", content: "The weather is wonderful." },
112
- { role: "system", content: "Das Wetter ist wunderschön." },
113
- { role: "user", content: "The live is awesome." },
114
- { role: "system", content: "Das Leben ist einfach großartig." }
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. \n" +
128
- "Do not show any prolog. \n" +
129
- "Do not show any epilog. \n" +
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", content: "Ich liebe meine Frau." },
135
- { role: "system", content: "I love my wife." },
136
- { role: "user", content: "Das Wetter ist wunderschön." },
137
- { role: "system", content: "The weather is wonderful." },
138
- { role: "user", content: "Das Leben ist einfach großartig." },
139
- { role: "system", content: "The live is awesome." }
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
- const model = this.params.model
175
- const models = await this.ollama.list()
176
- const exists = models.models.some((m) => m.name === model)
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
- this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
189
+ if (percent !== lastLoggedPercent) {
190
+ this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
191
+ lastLoggedPercent = percent
192
+ }
184
193
  }, 1000)
185
- const progress = await this.ollama.pull({ model, stream: true })
186
- for await (const event of progress) {
187
- if (event.digest)
188
- artifact = event.digest
189
- if (event.completed && event.total)
190
- percent = (event.completed / event.total) * 100
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", content: "I luve my wyfe" },
53
- { role: "system", content: "I love my wife." },
54
- { role: "user", content: "The weether is wunderfull!" },
55
- { role: "system", content: "The weather is wonderful!" },
56
- { role: "user", content: "The live awesome but I'm hungry." },
57
- { role: "system", content: "The live is awesome, but I'm hungry." }
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", content: "Ich ljebe meine Frao" },
85
- { role: "system", content: "Ich liebe meine Frau." },
86
- { role: "user", content: "Die Wedter ist wunderschoen." },
87
- { role: "system", content: "Das Wetter ist wunderschön." },
88
- { role: "user", content: "Das Leben einfach großartig aber ich bin hungrig." },
89
- { role: "system", content: "Das Leben ist einfach großartig, aber ich bin hungrig." }
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", content: "I love my wife." },
110
- { role: "system", content: "Ich liebe meine Frau." },
111
- { role: "user", content: "The weather is wonderful." },
112
- { role: "system", content: "Das Wetter ist wunderschön." },
113
- { role: "user", content: "The live is awesome." },
114
- { role: "system", content: "Das Leben ist einfach großartig." }
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. \n" +
125
+ "Do not show any explanations.\n" +
126
126
  "Do not show any introduction.\n" +
127
- "Do not show any preamble. \n" +
128
- "Do not show any prolog. \n" +
129
- "Do not show any epilog. \n" +
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", content: "Ich liebe meine Frau." },
135
- { role: "system", content: "I love my wife." },
136
- { role: "user", content: "Das Wetter ist wunderschön." },
137
- { role: "system", content: "The weather is wonderful." },
138
- { role: "user", content: "Das Leben ist einfach großartig." },
139
- { role: "system", content: "The live is awesome." }
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", match: /^(?:de|en)$/ },
151
- dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en)$/ },
152
- key: { type: "string", val: process.env.SPEECHFLOW_OPENAI_KEY },
153
- api: { type: "string", val: "https://api.openai.com/v1", match: /^https?:\/\/.+?:\d+$/ },
154
- model: { type: "string", val: "gpt-4o-mini" }
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
- dangerouslyAllowBrowser: true
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
- const stream = this.openai!.chat.completions.stream({
183
- stream: true,
184
- model: this.params.model,
185
- seed: null,
186
- temperature: 0.7,
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 completion = await stream.finalChatCompletion()
195
- const translation = completion.choices[0].message.content!
196
- if (!stream.ended)
197
- stream.abort()
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
- if (chunk.payload === "") {
212
- this.push(chunk)
217
+ translate(chunk.payload).then((payload) => {
218
+ const chunkNew = chunk.clone()
219
+ chunkNew.payload = payload
220
+ this.push(chunkNew)
213
221
  callback()
214
- }
215
- else {
216
- translate(chunk.payload).then((payload) => {
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 audio frames */
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 element = self.queueSend.peek()
197
- if (element === undefined)
196
+ const nextElement = self.queueSend.peek()
197
+ if (nextElement === undefined)
198
198
  break
199
- else if (element.type === "text-eof") {
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 (element.type === "text-frame"
205
- && element.complete !== true)
204
+ else if (nextElement.type === "text-frame"
205
+ && nextElement.complete !== true)
206
206
  break
207
- self.log("info", `send text: ${JSON.stringify(element.chunk.payload)}`)
208
- this.push(element.chunk)
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
  }