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.
Files changed (176) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +242 -7
  3. package/etc/claude.md +70 -0
  4. package/etc/speechflow.yaml +13 -11
  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 +3 -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 +312 -0
  56. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
  57. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -14
  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 +204 -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 +47 -8
  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 +175 -0
  71. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
  72. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +14 -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-google.d.ts +13 -0
  77. package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
  78. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
  79. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +80 -33
  80. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +78 -45
  82. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  83. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
  84. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  85. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +13 -14
  86. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  87. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +23 -27
  88. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
  90. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
  91. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  92. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
  93. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  94. package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
  95. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
  97. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
  98. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  99. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
  100. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
  102. package/speechflow-cli/dst/speechflow-node.js +13 -2
  103. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  104. package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
  105. package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
  106. package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
  107. package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
  108. package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
  109. package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
  110. package/speechflow-cli/dst/speechflow-utils.d.ts +34 -0
  111. package/speechflow-cli/dst/speechflow-utils.js +256 -35
  112. package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
  113. package/speechflow-cli/dst/speechflow.js +75 -26
  114. package/speechflow-cli/dst/speechflow.js.map +1 -1
  115. package/speechflow-cli/etc/biome.jsonc +2 -1
  116. package/speechflow-cli/etc/oxlint.jsonc +113 -11
  117. package/speechflow-cli/etc/stx.conf +2 -2
  118. package/speechflow-cli/etc/tsconfig.json +1 -1
  119. package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
  120. package/speechflow-cli/package.json +103 -94
  121. package/speechflow-cli/src/lib.d.ts +24 -0
  122. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
  123. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
  124. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
  125. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
  126. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -3
  127. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
  128. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
  129. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
  130. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
  131. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
  132. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
  133. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
  134. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
  135. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
  136. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
  137. package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +306 -0
  138. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -15
  139. package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
  140. package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
  141. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
  142. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +15 -9
  143. package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +153 -0
  144. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +14 -15
  145. package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
  146. package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
  147. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +58 -44
  148. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +59 -58
  149. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
  150. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +18 -18
  151. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +28 -32
  152. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
  153. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
  154. package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
  155. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
  156. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
  157. package/speechflow-cli/src/speechflow-node.ts +21 -8
  158. package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
  159. package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
  160. package/speechflow-cli/src/speechflow-utils.ts +314 -32
  161. package/speechflow-cli/src/speechflow.ts +84 -33
  162. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  163. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  164. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  165. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  166. package/speechflow-ui-db/dst/index.css +2 -2
  167. package/speechflow-ui-db/dst/index.js +37 -38
  168. package/speechflow-ui-db/etc/eslint.mjs +0 -1
  169. package/speechflow-ui-db/etc/tsc-client.json +3 -3
  170. package/speechflow-ui-db/package.json +12 -11
  171. package/speechflow-ui-db/src/app.vue +20 -6
  172. package/speechflow-ui-st/dst/index.js +26 -26
  173. package/speechflow-ui-st/etc/eslint.mjs +0 -1
  174. package/speechflow-ui-st/etc/tsc-client.json +3 -3
  175. package/speechflow-ui-st/package.json +12 -11
  176. 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
- if (chunk.payload === "") {
88
- this.push(chunk)
92
+ translate(chunk.payload).then((payload) => {
93
+ const chunkNew = chunk.clone()
94
+ chunkNew.payload = payload
95
+ this.push(chunkNew)
89
96
  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
- }
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 = 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) {
@@ -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", 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." }
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", 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." }
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", 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." }
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. \n" +
128
- "Do not show any prolog. \n" +
129
- "Do not show any epilog. \n" +
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", 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." }
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
- const model = this.params.model
175
- const models = await this.ollama.list()
176
- const exists = models.models.some((m) => m.name === model)
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
- this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
190
+ if (percent !== lastLoggedPercent) {
191
+ this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
192
+ lastLoggedPercent = percent
193
+ }
184
194
  }, 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
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((err) => {
241
- callback(err)
254
+ }).catch((error: unknown) => {
255
+ callback(utils.ensureError(error))
242
256
  })
243
257
  }
244
258
  }