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
@@ -12,6 +12,7 @@ import OpenAI from "openai"
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: OpenAI.ChatCompletionMessageParam[] }
@@ -49,12 +50,12 @@ export default class SpeechFlowNodeOpenAI 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 SpeechFlowNodeOpenAI 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 SpeechFlowNodeOpenAI 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
 
@@ -122,21 +123,21 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
122
123
  "Output only the requested text.\n" +
123
124
  "Do not use markdown.\n" +
124
125
  "Do not chat.\n" +
125
- "Do not show any explanations. \n" +
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
  }
@@ -147,11 +148,11 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
147
148
 
148
149
  /* declare node configuration parameters */
149
150
  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" }
151
+ src: { type: "string", pos: 0, val: "de", match: /^(?:de|en)$/ },
152
+ dst: { type: "string", pos: 1, val: "en", match: /^(?:de|en)$/ },
153
+ key: { type: "string", val: process.env.SPEECHFLOW_OPENAI_KEY, match: /^.+$/ },
154
+ api: { type: "string", val: "https://api.openai.com/v1", match: /^https?:\/\/.+/ },
155
+ model: { type: "string", val: "gpt-5-mini", match: /^.+$/ }
155
156
  })
156
157
 
157
158
  /* tell effective mode */
@@ -168,34 +169,36 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
168
169
 
169
170
  /* open node */
170
171
  async open () {
172
+ /* validate API key */
173
+ if (!this.params.key)
174
+ throw new Error("OpenAI API key is required")
175
+
171
176
  /* instantiate OpenAI API */
172
177
  this.openai = new OpenAI({
173
178
  baseURL: this.params.api,
174
179
  apiKey: this.params.key,
175
- dangerouslyAllowBrowser: true
180
+ timeout: 30000
176
181
  })
177
182
 
178
183
  /* provide text-to-text translation */
179
184
  const translate = async (text: string) => {
180
185
  const key = `${this.params.src}-${this.params.dst}`
181
186
  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,
187
+ if (!this.openai)
188
+ throw new Error("OpenAI client not available")
189
+ const completion = await this.openai.chat.completions.create({
190
+ model: this.params.model,
191
+ temperature: this.params.model.endsWith("-mini") ? 1.0 : 0.7,
188
192
  messages: [
189
193
  { role: "system", content: cfg.systemPrompt },
190
194
  ...cfg.chat,
191
195
  { role: "user", content: text }
192
196
  ]
193
197
  })
194
- const completion = await stream.finalChatCompletion()
195
- const translation = completion.choices[0].message.content!
196
- if (!stream.ended)
197
- stream.abort()
198
- return translation
198
+ const content = completion?.choices?.[0]?.message?.content
199
+ if (!content)
200
+ throw new Error("OpenAI API returned empty content")
201
+ return content
199
202
  }
200
203
 
201
204
  /* establish a duplex stream and connect it to OpenAI */
@@ -207,21 +210,19 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
207
210
  transform (chunk: SpeechFlowChunk, encoding, callback) {
208
211
  if (Buffer.isBuffer(chunk.payload))
209
212
  callback(new Error("invalid chunk payload type"))
213
+ else if (chunk.payload === "") {
214
+ this.push(chunk)
215
+ callback()
216
+ }
210
217
  else {
211
- if (chunk.payload === "") {
212
- this.push(chunk)
218
+ translate(chunk.payload).then((payload) => {
219
+ const chunkNew = chunk.clone()
220
+ chunkNew.payload = payload
221
+ this.push(chunkNew)
213
222
  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
- }
223
+ }).catch((error: unknown) => {
224
+ callback(utils.ensureError(error))
225
+ })
225
226
  }
226
227
  },
227
228
  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
  }
@@ -18,6 +18,7 @@ import HAPIWebSocket from "hapi-plugin-websocket"
18
18
 
19
19
  /* internal dependencies */
20
20
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
21
+ import * as utils from "./speechflow-utils"
21
22
 
22
23
  type wsPeerCtx = {
23
24
  peer: string
@@ -65,18 +66,18 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
65
66
  if (typeof chunk.payload !== "string")
66
67
  throw new Error("chunk payload type must be string")
67
68
  const convertSingle = (
68
- start: Duration,
69
- end: Duration,
70
- text: string,
71
- word?: string,
72
- occurence?: number
69
+ start: Duration,
70
+ end: Duration,
71
+ text: string,
72
+ word?: string,
73
+ occurrence?: number
73
74
  ) => {
74
75
  if (word) {
75
- occurence ??= 1
76
+ occurrence ??= 1
76
77
  let match = 1
77
78
  word = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
78
79
  text = text.replaceAll(new RegExp(`\\b${word}\\b`, "g"), (m) => {
79
- if (match++ === occurence)
80
+ if (match++ === occurrence)
80
81
  return `<b>${m}</b>`
81
82
  else
82
83
  return m
@@ -102,12 +103,12 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
102
103
  output += convertSingle(chunk.timestampStart, chunk.timestampEnd, chunk.payload)
103
104
  const words = (chunk.meta.get("words") ?? []) as
104
105
  { word: string, start: Duration, end: Duration }[]
105
- const occurences = new Map<string, number>()
106
+ const occurrences = new Map<string, number>()
106
107
  for (const word of words) {
107
- let occurence = occurences.get(word.word) ?? 0
108
- occurence++
109
- occurences.set(word.word, occurence)
110
- output += convertSingle(word.start, word.end, chunk.payload, word.word, occurence)
108
+ let occurrence = occurrences.get(word.word) ?? 0
109
+ occurrence++
110
+ occurrences.set(word.word, occurrence)
111
+ output += convertSingle(word.start, word.end, chunk.payload, word.word, occurrence)
111
112
  }
112
113
  }
113
114
  else
@@ -145,8 +146,8 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
145
146
  chunkNew.payload = payload
146
147
  this.push(chunkNew)
147
148
  callback()
148
- }).catch((err) => {
149
- callback(err)
149
+ }).catch((error: unknown) => {
150
+ callback(utils.ensureError(error))
150
151
  })
151
152
  }
152
153
  }
@@ -222,9 +223,8 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
222
223
  }
223
224
  }
224
225
  },
225
- handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
226
- return h.response({}).code(204)
227
- }
226
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) =>
227
+ h.response({}).code(204)
228
228
  })
229
229
 
230
230
  await this.hapi.start()
@@ -259,7 +259,7 @@ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
259
259
  }
260
260
  }
261
261
 
262
- /* open node */
262
+ /* close node */
263
263
  async close () {
264
264
  /* close stream */
265
265
  if (this.stream !== null) {
@@ -13,6 +13,7 @@ import * as Transformers from "@huggingface/transformers"
13
13
 
14
14
  /* internal dependencies */
15
15
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
16
+ import * as utils from "./speechflow-utils"
16
17
 
17
18
  /* internal utility types */
18
19
  type ConfigEntry = { systemPrompt: string, chat: Array<{ role: string, content: string }> }
@@ -46,11 +47,11 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
46
47
  "Preserve the original meaning, tone, and nuance.\n" +
47
48
  "Directly translate text from English (EN) to fluent and natural German (DE) language.\n",
48
49
  chat: [
49
- { role: "user", content: "I love my wife." },
50
+ { role: "user", content: "I love my wife." },
50
51
  { role: "assistant", content: "Ich liebe meine Frau." },
51
- { role: "user", content: "The weather is wonderful." },
52
+ { role: "user", content: "The weather is wonderful." },
52
53
  { role: "assistant", content: "Das Wetter ist wunderschön." },
53
- { role: "user", content: "The live is awesome." },
54
+ { role: "user", content: "The life is awesome." },
54
55
  { role: "assistant", content: "Das Leben ist einfach großartig." }
55
56
  ]
56
57
  },
@@ -65,19 +66,19 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
65
66
  "Do not chat.\n" +
66
67
  "Do not show any explanations.\n" +
67
68
  "Do not show any introduction.\n" +
68
- "Do not show any preamble. \n" +
69
- "Do not show any prolog. \n" +
70
- "Do not show any epilog. \n" +
69
+ "Do not show any preamble.\n" +
70
+ "Do not show any prolog.\n" +
71
+ "Do not show any epilog.\n" +
71
72
  "Get to the point.\n" +
72
73
  "Preserve the original meaning, tone, and nuance.\n" +
73
74
  "Directly translate text from German (DE) to fluent and natural English (EN) language.\n",
74
75
  chat: [
75
- { role: "user", content: "Ich liebe meine Frau." },
76
+ { role: "user", content: "Ich liebe meine Frau." },
76
77
  { role: "assistant", content: "I love my wife." },
77
- { role: "user", content: "Das Wetter ist wunderschön." },
78
+ { role: "user", content: "Das Wetter ist wunderschön." },
78
79
  { role: "assistant", content: "The weather is wonderful." },
79
- { role: "user", content: "Das Leben ist einfach großartig." },
80
- { role: "assistant", content: "The live is awesome." }
80
+ { role: "user", content: "Das Leben ist einfach großartig." },
81
+ { role: "assistant", content: "The life is awesome." }
81
82
  ]
82
83
  }
83
84
  }
@@ -114,7 +115,7 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
114
115
  artifact += `:${progress.file}`
115
116
  let percent = 0
116
117
  if (typeof progress.loaded === "number" && typeof progress.total === "number")
117
- percent = (progress.loaded as number / progress.total as number) * 100
118
+ percent = (progress.loaded / progress.total) * 100
118
119
  else if (typeof progress.progress === "number")
119
120
  percent = progress.progress
120
121
  if (percent > 0)
@@ -123,7 +124,7 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
123
124
  const interval = setInterval(() => {
124
125
  for (const [ artifact, percent ] of progressState) {
125
126
  this.log("info", `downloaded ${percent.toFixed(2)}% of artifact "${artifact}"`)
126
- if (percent >= 1.0)
127
+ if (percent >= 100.0)
127
128
  progressState.delete(artifact)
128
129
  }
129
130
  }, 1000)
@@ -163,9 +164,8 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
163
164
  const translate = async (text: string) => {
164
165
  if (this.params.model === "OPUS") {
165
166
  const result = await this.translator!(text)
166
- return Array.isArray(result) ?
167
- (result[0] as Transformers.TranslationSingle).translation_text :
168
- (result as Transformers.TranslationSingle).translation_text
167
+ const single = Array.isArray(result) ? result[0] : result
168
+ return (single as Transformers.TranslationSingle).translation_text
169
169
  }
170
170
  else if (this.params.model === "SmolLM3") {
171
171
  const key = `SmolLM3:${this.params.src}-${this.params.dst}`
@@ -184,13 +184,11 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
184
184
  skip_special_tokens: true
185
185
  })
186
186
  })
187
- const generatedText = Array.isArray(result) ?
188
- (result[0] as Transformers.TextGenerationSingle).generated_text :
189
- (result as Transformers.TextGenerationSingle).generated_text
190
- const response = typeof generatedText === "string" ?
187
+ const single = Array.isArray(result) ? result[0] : result
188
+ const generatedText = (single as Transformers.TextGenerationSingle).generated_text
189
+ return typeof generatedText === "string" ?
191
190
  generatedText :
192
191
  generatedText.at(-1)!.content
193
- return response
194
192
  }
195
193
  else
196
194
  throw new Error("invalid model")
@@ -205,21 +203,19 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
205
203
  transform (chunk: SpeechFlowChunk, encoding, callback) {
206
204
  if (Buffer.isBuffer(chunk.payload))
207
205
  callback(new Error("invalid chunk payload type"))
206
+ else if (chunk.payload === "") {
207
+ this.push(chunk)
208
+ callback()
209
+ }
208
210
  else {
209
- if (chunk.payload === "") {
211
+ translate(chunk.payload).then((payload) => {
212
+ chunk = chunk.clone()
213
+ chunk.payload = payload
210
214
  this.push(chunk)
211
215
  callback()
212
- }
213
- else {
214
- translate(chunk.payload).then((payload) => {
215
- chunk = chunk.clone()
216
- chunk.payload = payload
217
- this.push(chunk)
218
- callback()
219
- }).catch((err) => {
220
- callback(err)
221
- })
222
- }
216
+ }).catch((error: unknown) => {
217
+ callback(utils.ensureError(error))
218
+ })
223
219
  }
224
220
  },
225
221
  final (callback) {
@@ -9,12 +9,16 @@ import Stream from "node:stream"
9
9
 
10
10
  /* internal dependencies */
11
11
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
12
+ import * as utils from "./speechflow-utils"
12
13
 
13
14
  /* SpeechFlow node for data flow filtering (based on meta information) */
14
15
  export default class SpeechFlowNodeFilter extends SpeechFlowNode {
15
16
  /* declare official node name */
16
17
  public static name = "filter"
17
18
 
19
+ /* cached regular expression instance */
20
+ private cachedRegExp = new utils.CachedRegExp()
21
+
18
22
  /* construct node */
19
23
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
20
24
  super(id, cfg, opts, args)
@@ -50,33 +54,33 @@ export default class SpeechFlowNodeFilter extends SpeechFlowNode {
50
54
  val2 instanceof RegExp ?
51
55
  val2 :
52
56
  typeof val2 === "string" ?
53
- new RegExp(val2) :
54
- new RegExp(val2.toString()))
57
+ this.cachedRegExp.compile(val2) :
58
+ this.cachedRegExp.compile(val2.toString()))
59
+ if (regexp === null) {
60
+ /* fallback to literal string comparison on invalid regex */
61
+ this.log("warning", `invalid regular expression: "${val2}"`)
62
+ return (op === "~~" ? (str === val2) : (str !== val2))
63
+ }
55
64
  return (op === "~~" ? regexp.test(str) : !regexp.test(str))
56
65
  }
57
66
  else {
58
67
  /* non-equal comparison */
59
- const coerceNum = (val: any) => {
60
- return typeof val === "number" ? val : (
61
- typeof val === "string" && val.match(/^[\d+-]+$/) ? parseInt(val) : (
68
+ const coerceNum = (val: any) =>
69
+ typeof val === "number" ? val : (
70
+ typeof val === "string" && val.match(/^[\d+-]+$/) ? Number.parseInt(val, 10) : (
62
71
  typeof val === "string" && val.match(/^[\d.+-]+$/) ?
63
- parseFloat(val) :
72
+ Number.parseFloat(val) :
64
73
  Number(val)
65
74
  )
66
75
  )
67
- }
68
76
  const num1 = coerceNum(val1)
69
77
  const num2 = coerceNum(val2)
70
78
  return (
71
- op === "<" ?
72
- (num1 < num2) :
73
- op === "<=" ?
74
- (num1 <= num2) :
75
- op === ">=" ?
76
- (num1 >= num2) :
77
- op === ">" ?
78
- (num1 > num2) :
79
- false
79
+ op === "<" ? (num1 < num2) :
80
+ op === "<=" ? (num1 <= num2) :
81
+ op === ">=" ? (num1 >= num2) :
82
+ op === ">" ? (num1 > num2) :
83
+ false
80
84
  )
81
85
  }
82
86
  }
@@ -48,6 +48,23 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
48
48
  this.log(level, msg)
49
49
  }
50
50
 
51
+ /* helper functions for formatting */
52
+ const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
53
+ const fmtMeta = (meta: Map<string, any>) => {
54
+ if (meta.size === 0)
55
+ return "none"
56
+ else
57
+ return `{ ${Array.from(meta.entries())
58
+ .map(([ k, v ]) => `${k}: ${JSON.stringify(v)}`)
59
+ .join(", ")
60
+ } }`
61
+ }
62
+ const fmtChunkBase = (chunk: SpeechFlowChunk) =>
63
+ `chunk: type=${chunk.type} ` +
64
+ `kind=${chunk.kind} ` +
65
+ `start=${fmtTime(chunk.timestampStart)} ` +
66
+ `end=${fmtTime(chunk.timestampEnd)} `
67
+
51
68
  /* provide Transform stream */
52
69
  const self = this
53
70
  this.stream = new Stream.Transform({
@@ -57,22 +74,9 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
57
74
  highWaterMark: 1,
58
75
  transform (chunk: SpeechFlowChunk, encoding, callback) {
59
76
  let error: Error | undefined
60
- const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
61
- const fmtMeta = (meta: Map<string, any>) => {
62
- if (meta.size === 0)
63
- return "none"
64
- else
65
- return `{ ${Array.from(meta.entries())
66
- .map(([ k, v ]) => `${k}: ${JSON.stringify(v)}`)
67
- .join(", ")
68
- } }`
69
- }
70
77
  if (Buffer.isBuffer(chunk.payload)) {
71
78
  if (self.params.type === "audio")
72
- log("debug", `chunk: type=${chunk.type} ` +
73
- `kind=${chunk.kind} ` +
74
- `start=${fmtTime(chunk.timestampStart)} ` +
75
- `end=${fmtTime(chunk.timestampEnd)} ` +
79
+ log("debug", fmtChunkBase(chunk) +
76
80
  `payload-type=Buffer payload-length=${chunk.payload.byteLength} ` +
77
81
  `meta=${fmtMeta(chunk.meta)}`)
78
82
  else
@@ -80,15 +84,12 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
80
84
  }
81
85
  else {
82
86
  if (self.params.type === "text") {
83
- log("debug", `chunk: type=${chunk.type} ` +
84
- `kind=${chunk.kind} ` +
85
- `start=${fmtTime(chunk.timestampStart)} ` +
86
- `end=${fmtTime(chunk.timestampEnd)} ` +
87
+ log("debug", fmtChunkBase(chunk) +
87
88
  `payload-type=String payload-length=${chunk.payload.length} ` +
88
89
  `payload-content="${chunk.payload.toString()}" ` +
89
90
  `meta=${fmtMeta(chunk.meta)}`)
90
91
  if (self.params.dashboard !== "")
91
- self.dashboardInfo("text", self.params.dashboard, chunk.kind, chunk.payload.toString())
92
+ self.sendDashboard("text", self.params.dashboard, chunk.kind, chunk.payload.toString())
92
93
  }
93
94
  else
94
95
  error = new Error(`${self.params.type} chunk: seen String instead of Buffer chunk type`)
@@ -69,17 +69,15 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
69
69
  const devices = PortAudio.getDevices()
70
70
  for (const device of devices)
71
71
  this.log("info", `found audio device "${device.name}" ` +
72
- `(inputs: ${device.maxInputChannels}, outputs: ${device.maxOutputChannels}`)
73
- const device = devices.find((device) => {
74
- return (
75
- ( ( mode === "r" && device.maxInputChannels > 0)
76
- || (mode === "w" && device.maxOutputChannels > 0)
77
- || (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
78
- || (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
79
- && device.name.match(name)
80
- && device.hostAPIName === api.name
81
- )
82
- })
72
+ `(inputs: ${device.maxInputChannels}, outputs: ${device.maxOutputChannels})`)
73
+ const device = devices.find((device) => (
74
+ ( ( mode === "r" && device.maxInputChannels > 0)
75
+ || (mode === "w" && device.maxOutputChannels > 0)
76
+ || (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
77
+ || (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
78
+ && device.name.match(name)
79
+ && device.hostAPIName === api.name
80
+ ))
83
81
  if (!device)
84
82
  throw new Error(`invalid audio device "${name}" (of audio API type "${type}")`)
85
83
  return device
@@ -197,20 +195,14 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
197
195
  async close () {
198
196
  /* shutdown PortAudio */
199
197
  if (this.io !== null) {
200
- await new Promise<void>((resolve, reject) => {
201
- this.io!.abort((err?: Error) => {
202
- if (err)
203
- reject(err)
204
- else
205
- resolve()
198
+ await new Promise<void>((resolve) => {
199
+ this.io!.abort(() => {
200
+ resolve()
206
201
  })
207
202
  })
208
- await new Promise<void>((resolve, reject) => {
209
- this.io!.quit((err?: Error) => {
210
- if (err)
211
- reject(err)
212
- else
213
- resolve()
203
+ await new Promise<void>((resolve) => {
204
+ this.io!.quit(() => {
205
+ resolve()
214
206
  })
215
207
  })
216
208
  this.io = null