speechflow 2.4.1 → 2.4.3

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 (65) hide show
  1. package/.ase/service.log +450 -0
  2. package/.ase/service.yaml +1 -1
  3. package/.ase/task/TASK-flush.md +44 -0
  4. package/CHANGELOG.md +22 -0
  5. package/README.md +10 -9
  6. package/etc/speechflow.yaml +1 -1
  7. package/etc/stx.conf +21 -21
  8. package/package.json +12 -8
  9. package/speechflow-cli/dst/speechflow-main-cli.js +2 -2
  10. package/speechflow-cli/dst/speechflow-main-cli.js.map +1 -1
  11. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +2 -0
  12. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  13. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +2 -1
  14. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  15. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +28 -12
  16. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  17. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +9 -15
  18. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
  19. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +3 -0
  20. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +149 -101
  21. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  22. package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -1
  23. package/speechflow-cli/dst/speechflow-node-xio-exec.js +6 -4
  24. package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
  25. package/speechflow-cli/dst/speechflow-util-llm.js +8 -1
  26. package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -1
  27. package/speechflow-cli/etc/oxlint.jsonc +4 -0
  28. package/speechflow-cli/etc/stx.conf +4 -4
  29. package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +1 -14
  30. package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.65.0.patch +16 -0
  31. package/speechflow-cli/package.json +54 -48
  32. package/speechflow-cli/src/speechflow-main-cli.ts +1 -1
  33. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -1
  34. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +3 -2
  35. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +30 -13
  36. package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +12 -20
  37. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +162 -108
  38. package/speechflow-cli/src/speechflow-node-t2t-opus.ts +1 -1
  39. package/speechflow-cli/src/speechflow-node-xio-exec.ts +7 -5
  40. package/speechflow-cli/src/speechflow-util-llm.ts +10 -2
  41. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  42. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  43. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  44. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  45. package/speechflow-ui-db/dst/index.css +1 -1
  46. package/speechflow-ui-db/dst/index.js +579 -57
  47. package/speechflow-ui-db/etc/stx.conf +3 -2
  48. package/speechflow-ui-db/etc/vite-client.mts +1 -1
  49. package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.65.0.patch +16 -0
  50. package/speechflow-ui-db/package.json +26 -21
  51. package/speechflow-ui-st/dst/app-font-fa-brands-400.woff2 +0 -0
  52. package/speechflow-ui-st/dst/app-font-fa-regular-400.woff2 +0 -0
  53. package/speechflow-ui-st/dst/app-font-fa-solid-900.woff2 +0 -0
  54. package/speechflow-ui-st/dst/app-font-fa-v4compatibility.woff2 +0 -0
  55. package/speechflow-ui-st/dst/index.css +1 -1
  56. package/speechflow-ui-st/dst/index.js +593 -71
  57. package/speechflow-ui-st/etc/stx.conf +3 -2
  58. package/speechflow-ui-st/etc/vite-client.mts +6 -0
  59. package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.65.0.patch +16 -0
  60. package/speechflow-ui-st/package.json +26 -21
  61. package/speechflow-ui-st/src/app.vue +3 -3
  62. package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.60.0.patch +0 -12
  63. package/speechflow-cli/package.d/sherpa-onnx+1.12.25.patch +0 -12
  64. package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.60.0.patch +0 -12
  65. package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.60.0.patch +0 -12
@@ -9,25 +9,33 @@ import Stream from "node:stream"
9
9
 
10
10
  /* external dependencies */
11
11
  import * as Deepgram from "@deepgram/sdk"
12
+ import { V1Socket } from "@deepgram/sdk/listen/v1"
12
13
  import { DateTime, Duration } from "luxon"
13
14
 
14
15
  /* internal dependencies */
15
16
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
16
17
  import * as util from "./speechflow-util"
17
18
 
19
+ /* NOTICE: the connection arguments type is not publicly re-exported by the
20
+ Deepgram SDK, so derive it structurally from the connect method instead */
21
+ type DeepgramConnectArgs = Parameters<Deepgram.DeepgramClient["listen"]["v1"]["connect"]>[0]
22
+
18
23
  /* SpeechFlow node for Deepgram speech-to-text conversion */
19
24
  export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
20
25
  /* declare official node name */
21
26
  public static name = "a2t-deepgram"
22
27
 
23
28
  /* internal state */
24
- private dg: Deepgram.LiveClient | null = null
29
+ private dg: V1Socket | null = null
25
30
  private closing = false
26
31
  private reconfiguring = false
27
32
  private connectionTimeout: ReturnType<typeof setTimeout> | null = null
28
33
  private queue: util.AsyncQueue<SpeechFlowChunk | null> | null = null
29
34
  private metastore: util.TimeStore<Map<string, any>> | null = null
30
35
  private suspended = false
36
+ private opening = false
37
+ private openReject: ((error: Error) => void) | null = null
38
+ private lastChunk: SpeechFlowChunk | null = null
31
39
 
32
40
  /* construct node */
33
41
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -85,9 +93,9 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
85
93
  if (suspended === this.suspended)
86
94
  return
87
95
  this.log("info", `switching to ${suspended ? "SUSPENDED" : "UNSUSPENDED"} operation`)
88
- this.suspended = suspended
89
96
  if (suspended) {
90
97
  /* going suspended -- tear down Deepgram API connection */
98
+ this.suspended = true
91
99
  this.reconfiguring = true
92
100
  try {
93
101
  await this.closeConnection()
@@ -95,10 +103,24 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
95
103
  finally {
96
104
  this.reconfiguring = false
97
105
  }
106
+
107
+ /* re-emit last intermediate chunk as final one, as its regular
108
+ finalization will never arrive anymore -- with an appended
109
+ ellipsis as truncation marker (and sentence boundary for
110
+ downstream sentence splitting) */
111
+ if (this.lastChunk !== null && this.lastChunk.kind === "intermediate" && this.queue !== null) {
112
+ this.log("info", "sending last non-final chunk as final chunk")
113
+ const chunk = this.lastChunk.clone()
114
+ chunk.kind = "final"
115
+ chunk.payload = `${chunk.payload}…`
116
+ this.queue.write(chunk)
117
+ this.lastChunk = chunk
118
+ }
98
119
  }
99
120
  else {
100
121
  /* going unsuspended -- re-establish Deepgram API connection */
101
122
  await this.openConnection()
123
+ this.suspended = false
102
124
  }
103
125
  }
104
126
 
@@ -107,23 +129,32 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
107
129
  /* configure Deepgram connection options */
108
130
  const interim = this.params.interim as boolean
109
131
  const endpointing = this.params.endpointing as number
110
- const options: Deepgram.LiveSchema = {
132
+ /* NOTICE: the Deepgram API v5 SDK expects the boolean flags as their
133
+ string representations, as they are passed as URL query parameters.
134
+ The "filler_words" flag is a valid live API parameter, but is not
135
+ covered by the generated SDK types, so pass it via "queryParams". */
136
+ const options: DeepgramConnectArgs = {
137
+ /* NOTICE: the Deepgram API v5 SDK applies the client-level "apiKey" only
138
+ to its REST endpoints -- the WebSocket connect path never consults the
139
+ authentication provider and instead requires the "Authorization" header
140
+ to be passed explicitly, or else the server closes the handshake */
141
+ Authorization: `Token ${this.params.key}`,
111
142
  mip_opt_out: true,
112
143
  model: this.params.model,
113
144
  version: this.params.version,
114
145
  channels: this.config.audioChannels,
115
146
  sample_rate: this.config.audioSampleRate,
116
147
  encoding: "linear16",
117
- multichannel: false,
118
- endpointing: endpointing > 0 ? endpointing : false,
119
- interim_results: interim,
120
- smart_format: false,
121
- punctuate: true,
122
- filler_words: true,
123
- numerals: false,
124
- diarize: false,
125
- profanity_filter: false,
126
- redact: false
148
+ multichannel: "false",
149
+ endpointing: endpointing > 0 ? endpointing : "false",
150
+ interim_results: interim ? "true" : "false",
151
+ smart_format: "false",
152
+ punctuate: "true",
153
+ numerals: "false",
154
+ diarize: "false",
155
+ profanity_filter: "false",
156
+ redact: "false",
157
+ queryParams: { filler_words: "true" }
127
158
  }
128
159
  const model = this.params.model as string
129
160
  const language = this.params.language as string
@@ -149,52 +180,52 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
149
180
  }
150
181
 
151
182
  /* connect to Deepgram API */
152
- const deepgram = Deepgram.createClient(this.params.key)
153
- this.dg = deepgram.listen.live(options)
183
+ const deepgram = new Deepgram.DeepgramClient({ apiKey: this.params.key })
184
+ this.dg = await deepgram.listen.v1.connect(options)
154
185
 
155
- /* hook onto Deepgram API events */
156
- this.dg.on(Deepgram.LiveTranscriptionEvents.Transcript, async (data) => {
186
+ /* hook onto Deepgram API messages */
187
+ this.dg.on("message", (message: V1Socket.Response) => {
157
188
  if (this.closing || this.queue === null || this.metastore === null)
158
189
  return
159
- const text = (data.channel?.alternatives[0]?.transcript ?? "") as string
160
- const words = (data.channel?.alternatives[0]?.words ?? []) as
161
- { word: string, punctuated_word?: string, start: number, end: number }[]
162
- const isFinal = (data.is_final as boolean) ?? false
163
- const speechFinal = (data.speech_final as boolean) ?? false
164
- const kind = (isFinal || (endpointing > 0 && speechFinal)) ? "final" : "intermediate"
165
- if (text === "")
166
- this.log("info", `empty/dummy text received (start: ${data.start}s, duration: ${data.duration.toFixed(2)}s)`)
167
- else {
168
- this.log("info", `text received (start: ${data.start}s, ` +
169
- `duration: ${data.duration.toFixed(2)}s, kind: ${kind}): ` +
170
- `"${text}"`)
171
- const start = Duration.fromMillis(data.start * 1000).plus(this.timeZeroOffset)
172
- const end = start.plus({ seconds: data.duration })
173
- const metas = this.metastore.fetch(start, end)
174
- const meta = metas.toReversed().reduce((prev: Map<string, any>, curr: Map<string, any>) => {
175
- curr.forEach((val, key) => { prev.set(key, val) })
176
- return prev
177
- }, new Map<string, any>())
178
- this.metastore.prune(start)
179
- meta.set("words", words.map((word) => {
180
- const start = Duration.fromMillis(word.start * 1000).plus(this.timeZeroOffset)
181
- const end = Duration.fromMillis(word.end * 1000).plus(this.timeZeroOffset)
182
- return { word: word.punctuated_word ?? word.word, start, end }
183
- }))
184
- const chunk = new SpeechFlowChunk(start, end, kind, "text", text, meta)
185
- this.queue.write(chunk)
190
+ if (message.type === "SpeechStarted")
191
+ this.log("info", "speech started", message)
192
+ else if (message.type === "UtteranceEnd")
193
+ this.log("info", "utterance end received", message)
194
+ else if (message.type === "Metadata")
195
+ this.log("info", "metadata received")
196
+ else if (message.type === "Results") {
197
+ const data = message
198
+ const text = data.channel?.alternatives[0]?.transcript ?? ""
199
+ const words = data.channel?.alternatives[0]?.words ?? []
200
+ const isFinal = data.is_final ?? false
201
+ const speechFinal = data.speech_final ?? false
202
+ const kind = (isFinal || ((this.params.endpointing as number) > 0 && speechFinal)) ? "final" : "intermediate"
203
+ if (text === "")
204
+ this.log("info", `empty/dummy text received (start: ${data.start}s, duration: ${data.duration.toFixed(2)}s)`)
205
+ else {
206
+ this.log("info", `text received (start: ${data.start}s, ` +
207
+ `duration: ${data.duration.toFixed(2)}s, kind: ${kind}): ` +
208
+ `"${text}"`)
209
+ const start = Duration.fromMillis(data.start * 1000).plus(this.timeZeroOffset)
210
+ const end = start.plus({ seconds: data.duration })
211
+ const metas = this.metastore.fetch(start, end)
212
+ const meta = metas.toReversed().reduce((prev: Map<string, any>, curr: Map<string, any>) => {
213
+ curr.forEach((val, key) => { prev.set(key, val) })
214
+ return prev
215
+ }, new Map<string, any>())
216
+ this.metastore.prune(start)
217
+ meta.set("words", words.map((word) => {
218
+ const wordStart = Duration.fromMillis(word.start * 1000).plus(this.timeZeroOffset)
219
+ const wordEnd = Duration.fromMillis(word.end * 1000).plus(this.timeZeroOffset)
220
+ return { word: word.punctuated_word ?? word.word, start: wordStart, end: wordEnd }
221
+ }))
222
+ const chunk = new SpeechFlowChunk(start, end, kind, "text", text, meta)
223
+ this.queue.write(chunk)
224
+ this.lastChunk = chunk
225
+ }
186
226
  }
187
227
  })
188
- this.dg.on(Deepgram.LiveTranscriptionEvents.SpeechStarted, (data) => {
189
- this.log("info", "speech started", data)
190
- })
191
- this.dg.on(Deepgram.LiveTranscriptionEvents.UtteranceEnd, (data) => {
192
- this.log("info", "utterance end received", data)
193
- })
194
- this.dg.on(Deepgram.LiveTranscriptionEvents.Metadata, (data) => {
195
- this.log("info", "metadata received")
196
- })
197
- this.dg.on(Deepgram.LiveTranscriptionEvents.Close, () => {
228
+ this.dg.on("close", () => {
198
229
  this.log("info", "connection close")
199
230
  /* NOTICE: suppress EOF signalling while reconfiguring (mute toggle),
200
231
  since the connection is being torn down deliberately and the
@@ -203,36 +234,18 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
203
234
  this.queue.write(null)
204
235
  })
205
236
 
206
- /* wait for Deepgram API to be available */
207
- await new Promise((resolve, reject) => {
208
- this.connectionTimeout = setTimeout(() => {
209
- if (this.connectionTimeout !== null) {
210
- this.connectionTimeout = null
211
- reject(new Error("Deepgram: timeout waiting for connection open"))
212
- }
213
- }, 8000)
214
- this.dg!.once(Deepgram.LiveTranscriptionEvents.Open, () => {
215
- this.log("info", "connection open")
216
- if (this.connectionTimeout !== null) {
217
- clearTimeout(this.connectionTimeout)
218
- this.connectionTimeout = null
219
- }
220
- resolve(true)
221
- })
222
- this.dg!.once(Deepgram.LiveTranscriptionEvents.Error, (err: Error) => {
223
- if (this.connectionTimeout !== null) {
224
- clearTimeout(this.connectionTimeout)
225
- this.connectionTimeout = null
226
- }
227
- reject(err)
228
- })
229
- })
230
-
231
- /* NOTICE: register permanent Error handler only AFTER the open
232
- handshake -- during open, the transient .once above is the sole
233
- Error listener so the caller's promise rejects without a parallel
234
- stream emission tearing down the graph prematurely. */
235
- this.dg.on(Deepgram.LiveTranscriptionEvents.Error, (error: Error) => {
237
+ /* NOTICE: the Deepgram API socket supports only a single handler per
238
+ event, so the open handshake and the permanent error handling must
239
+ share one error handler which dispatches on the "opening" flag:
240
+ during open, the error is routed solely into the caller's promise
241
+ rejection, without a parallel stream emission tearing down the
242
+ graph prematurely. */
243
+ this.dg.on("error", (error: Error) => {
244
+ if (this.opening) {
245
+ if (this.openReject !== null)
246
+ this.openReject(error)
247
+ return
248
+ }
236
249
  this.log("warning", `error: ${error.message}`)
237
250
  /* NOTICE: do not write null to the queue here -- a transient error
238
251
  must not be misinterpreted as end-of-stream by downstream nodes;
@@ -244,6 +257,53 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
244
257
  if (!this.closing && this.stream !== null)
245
258
  this.stream.emit("error", error)
246
259
  })
260
+
261
+ /* NOTICE: the Deepgram API v5 SDK returns a socket which has not yet
262
+ dialed, as it is internally created with the "startClosed" option,
263
+ so the connection has to be established explicitly -- and only once
264
+ all event handlers above are registered, as the connect operation
265
+ re-registers the underlying socket listeners */
266
+ this.dg.connect()
267
+
268
+ /* clear pending connection timer (idempotent) */
269
+ const clearConnectionTimeout = () => {
270
+ if (this.connectionTimeout !== null) {
271
+ clearTimeout(this.connectionTimeout)
272
+ this.connectionTimeout = null
273
+ }
274
+ }
275
+
276
+ /* wait for Deepgram API to be available */
277
+ this.opening = true
278
+ let opened = false
279
+ try {
280
+ await new Promise((resolve, reject) => {
281
+ this.openReject = reject
282
+ this.connectionTimeout = setTimeout(() => {
283
+ if (this.connectionTimeout !== null) {
284
+ this.connectionTimeout = null
285
+ reject(new Error("Deepgram: timeout waiting for connection open"))
286
+ }
287
+ }, 8000)
288
+ this.dg!.waitForOpen().then(() => {
289
+ this.log("info", "connection open")
290
+ clearConnectionTimeout()
291
+ resolve(true)
292
+ }).catch((error: unknown) => {
293
+ clearConnectionTimeout()
294
+ reject(util.ensureError(error, "failed to open Deepgram connection"))
295
+ })
296
+ })
297
+ opened = true
298
+ }
299
+ finally {
300
+ this.opening = false
301
+ this.openReject = null
302
+
303
+ /* discard half-established connection on failure */
304
+ if (!opened)
305
+ await this.closeConnection()
306
+ }
247
307
  }
248
308
 
249
309
  /* close Deepgram API connection */
@@ -257,8 +317,7 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
257
317
  /* close Deepgram connection and remove listeners */
258
318
  if (this.dg !== null) {
259
319
  try {
260
- this.dg.removeAllListeners()
261
- this.dg.requestClose()
320
+ this.dg.close()
262
321
  this.log("info", "connection closed")
263
322
  }
264
323
  catch (error) {
@@ -271,18 +330,19 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
271
330
  /* one-time status of node */
272
331
  async status () {
273
332
  let balance = 0
333
+ if (!this.params.keyAdm) {
334
+ this.log("info", "no Deepgram admin API key configured -- skipping balance fetch")
335
+ return { balance: balance.toFixed(2) }
336
+ }
274
337
  try {
275
- const deepgram = Deepgram.createClient(this.params.keyAdm)
276
- const response = await deepgram.manage.getProjects()
277
- if (response !== null && response.error === null && response.result?.projects) {
278
- for (const project of response.result.projects) {
279
- const balanceResponse = await deepgram.manage.getProjectBalances(project.project_id)
280
- if (balanceResponse !== null && balanceResponse.error === null && balanceResponse.result?.balances)
281
- balance += balanceResponse.result.balances[0]?.amount ?? 0
282
- }
338
+ const deepgram = new Deepgram.DeepgramClient({ apiKey: this.params.keyAdm })
339
+ const response = await deepgram.manage.v1.projects.list()
340
+ for (const project of response.projects ?? []) {
341
+ if (project.project_id === undefined)
342
+ continue
343
+ const balanceResponse = await deepgram.manage.v1.projects.billing.balances.list(project.project_id)
344
+ balance += balanceResponse.balances?.[0]?.amount ?? 0
283
345
  }
284
- else if (response !== null && response.error !== null)
285
- this.log("warning", `API error fetching projects: ${response.error}`)
286
346
  }
287
347
  catch (error) {
288
348
  this.log("warning", `failed to fetch balance: ${error}`)
@@ -305,6 +365,9 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
305
365
  /* create a store for the meta information */
306
366
  this.metastore = new util.TimeStore<Map<string, any>>()
307
367
 
368
+ /* reset last emitted text chunk */
369
+ this.lastChunk = null
370
+
308
371
  /* determine initial suspended state from configuration */
309
372
  this.suspended = this.params.suspended as boolean
310
373
 
@@ -342,10 +405,7 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
342
405
  self.metastore.store(chunk.timestampStart, chunk.timestampEnd, chunk.meta)
343
406
  try {
344
407
  /* send buffer (and intentionally discard all time information) */
345
- self.dg.send(chunk.payload.buffer.slice(
346
- chunk.payload.byteOffset,
347
- chunk.payload.byteOffset + chunk.payload.byteLength
348
- ))
408
+ self.dg.sendMedia(chunk.payload)
349
409
  }
350
410
  catch (error) {
351
411
  callback(util.ensureError(error, "failed to send to Deepgram"))
@@ -363,14 +423,7 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
363
423
  }
364
424
 
365
425
  /* close Deepgram API */
366
- if (self.dg !== null) {
367
- try {
368
- self.dg.requestClose()
369
- }
370
- catch (error) {
371
- self.log("warning", `error closing Deepgram connection: ${error}`)
372
- }
373
- }
426
+ await self.closeConnection()
374
427
 
375
428
  /* await all read operations */
376
429
  await reads.awaitAll()
@@ -421,6 +474,7 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
421
474
  /* signal EOF to any pending read operations */
422
475
  if (this.queue !== null) {
423
476
  this.queue.write(null)
477
+ this.queue.destroy()
424
478
  this.queue = null
425
479
  }
426
480
 
@@ -86,7 +86,7 @@ export default class SpeechFlowNodeT2TOPUS extends SpeechFlowNode {
86
86
  const translate = async (text: string) => {
87
87
  const result = await this.translator!(text)
88
88
  const single = Array.isArray(result) ? result[0] : result
89
- return (single as Transformers.TranslationSingle).translation_text
89
+ return (single as Transformers.TranslationOutput[number]).translation_text
90
90
  }
91
91
 
92
92
  /* establish a transform stream and connect it to Transformers */
@@ -93,7 +93,7 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
93
93
  })
94
94
 
95
95
  /* handle subprocess errors */
96
- this.subprocess.on("error", (err) => {
96
+ this.subprocess.nodeChildProcess.on("error", (err: Error) => {
97
97
  this.log("error", `subprocess error: ${err.message}`)
98
98
  /* NOTICE: do not emit("error") on the node itself, since nothing
99
99
  listens for it and it would become an uncaughtException tearing
@@ -104,7 +104,7 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
104
104
  })
105
105
 
106
106
  /* handle subprocess exit */
107
- this.subprocess.on("exit", (code, signal) => {
107
+ this.subprocess.nodeChildProcess.on("exit", (code: number | null, signal: string | null) => {
108
108
  if (code !== 0 && code !== null)
109
109
  this.log("warning", `subprocess exited with code ${code}`)
110
110
  else if (signal)
@@ -126,10 +126,12 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
126
126
  /* dispatch according to mode */
127
127
  if (this.params.mode === "rw") {
128
128
  /* bidirectional mode: both stdin and stdout */
129
+ /* NOTICE: the object form of Stream.Duplex.from() accepts Node streams at
130
+ run-time, but @types/node types it for Web streams only, so we have to cast */
129
131
  this.stream = Stream.Duplex.from({
130
132
  readable: this.subprocess.stdout,
131
133
  writable: this.subprocess.stdin
132
- })
134
+ } as unknown as Parameters<typeof Stream.Duplex.from>[0])
133
135
  const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, highWaterMark)
134
136
  const wrapper2 = util.createTransformStreamForReadableSide(
135
137
  this.params.type, () => this.timeZero, highWaterMark,
@@ -176,8 +178,8 @@ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
176
178
  }
177
179
 
178
180
  /* remove event listeners to prevent errors during kill sequence */
179
- this.subprocess.removeAllListeners("error")
180
- this.subprocess.removeAllListeners("exit")
181
+ this.subprocess.nodeChildProcess.removeAllListeners("error")
182
+ this.subprocess.nodeChildProcess.removeAllListeners("exit")
181
183
 
182
184
  /* wait for subprocess to exit gracefully */
183
185
  const ac2 = new AbortController()
@@ -333,10 +333,18 @@ export class LLM extends EventEmitter {
333
333
  throw new Error(`failed to perform HuggingFace Transformers text generation: ${err}`, { cause: err })
334
334
  })
335
335
  const single = Array.isArray(result) ? result[0] : result
336
- const generatedText = (single as Transformers.TextGenerationSingle).generated_text
337
- const content = typeof generatedText === "string" ?
336
+ const generatedText = (single as Transformers.TextGenerationOutput[number]).generated_text
337
+ const messageContent = typeof generatedText === "string" ?
338
338
  generatedText :
339
339
  generatedText.at(-1)?.content
340
+
341
+ /* flatten the potentially multi-modal message content into plain text */
342
+ const content = typeof messageContent === "string" ?
343
+ messageContent :
344
+ messageContent
345
+ ?.filter((part) => part.type === "text")
346
+ .map((part) => part.text as string)
347
+ .join("")
340
348
  if (!content)
341
349
  throw new Error("HuggingFace Transformers API returned empty content")
342
350
  return content