speechflow 1.5.0 → 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 (71) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +22 -0
  3. package/etc/speechflow.yaml +8 -8
  4. package/package.json +3 -3
  5. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +1 -1
  6. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  7. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +1 -1
  8. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  9. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +2 -12
  10. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  11. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +1 -1
  12. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  13. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +4 -9
  14. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -1
  15. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +1 -1
  16. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  17. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +1 -1
  18. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -1
  19. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +35 -2
  20. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -1
  21. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +36 -2
  22. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  23. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +36 -2
  24. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -1
  25. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +3 -2
  26. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  27. package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +13 -0
  28. package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
  29. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
  30. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +36 -2
  31. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  32. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +36 -2
  33. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  34. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +3 -2
  35. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +3 -2
  37. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  38. package/speechflow-cli/dst/speechflow-utils.d.ts +16 -0
  39. package/speechflow-cli/dst/speechflow-utils.js +133 -0
  40. package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow.js +7 -13
  42. package/speechflow-cli/dst/speechflow.js.map +1 -1
  43. package/speechflow-cli/etc/biome.jsonc +2 -1
  44. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  45. package/speechflow-cli/package.json +6 -5
  46. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +2 -2
  47. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +2 -2
  48. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +2 -12
  49. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +2 -2
  50. package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +8 -10
  51. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +2 -2
  52. package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +2 -2
  53. package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +3 -3
  54. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +3 -2
  55. package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +3 -2
  56. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +3 -2
  57. package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
  58. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +3 -2
  59. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +3 -2
  60. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +3 -2
  61. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +3 -2
  62. package/speechflow-cli/src/speechflow-utils.ts +189 -0
  63. package/speechflow-cli/src/speechflow.ts +11 -17
  64. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  65. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  66. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  67. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  68. package/speechflow-ui-db/dst/index.css +2 -2
  69. package/speechflow-ui-db/dst/index.js +37 -38
  70. package/speechflow-ui-db/package.json +7 -7
  71. package/speechflow-ui-st/package.json +7 -7
@@ -32,6 +32,7 @@
32
32
  "@aws-sdk/client-transcribe-streaming": "3.879.0",
33
33
  "@aws-sdk/client-translate": "3.879.0",
34
34
  "@aws-sdk/client-polly": "3.879.0",
35
+ "@google-cloud/translate": "9.2.0",
35
36
  "node-web-audio-api": "1.0.4",
36
37
  "object-path": "0.11.8",
37
38
  "ws": "8.18.3",
@@ -44,7 +45,7 @@
44
45
  "hapi-plugin-websocket": "2.4.11",
45
46
  "@opensumi/reconnecting-websocket": "4.4.0",
46
47
  "ollama": "0.5.17",
47
- "openai": "5.16.0",
48
+ "openai": "5.18.0",
48
49
  "@rse/ffmpeg": "1.4.2",
49
50
  "ffmpeg-stream": "1.0.1",
50
51
  "installed-packages": "1.0.13",
@@ -64,7 +65,7 @@
64
65
  "node-interval-tree": "2.1.2",
65
66
  "wrap-text": "1.0.10",
66
67
  "cli-table3": "0.6.5",
67
- "@rse/stx": "1.0.9"
68
+ "@rse/stx": "1.1.0"
68
69
  },
69
70
  "devDependencies": {
70
71
  "eslint": "9.34.0",
@@ -73,9 +74,9 @@
73
74
  "eslint-plugin-promise": "7.2.1",
74
75
  "eslint-plugin-import": "2.32.0",
75
76
  "eslint-plugin-node": "11.1.0",
76
- "typescript-eslint": "8.41.0",
77
- "@typescript-eslint/eslint-plugin": "8.41.0",
78
- "@typescript-eslint/parser": "8.41.0",
77
+ "typescript-eslint": "8.42.0",
78
+ "@typescript-eslint/eslint-plugin": "8.42.0",
79
+ "@typescript-eslint/parser": "8.42.0",
79
80
  "oxlint": "1.14.0",
80
81
  "eslint-plugin-oxlint": "1.14.0",
81
82
  "@biomejs/biome": "2.0.6",
@@ -255,8 +255,8 @@ export default class SpeechFlowNodeCompressor extends SpeechFlowNode {
255
255
  }
256
256
  this.push(chunk)
257
257
  callback()
258
- }).catch((error) => {
259
- callback(new Error(`compression failed: ${error}`))
258
+ }).catch((error: unknown) => {
259
+ callback(utils.ensureError(error, "compression failed"))
260
260
  })
261
261
  }
262
262
  },
@@ -176,8 +176,8 @@ export default class SpeechFlowNodeExpander extends SpeechFlowNode {
176
176
  chunk.payload = payload
177
177
  this.push(chunk)
178
178
  callback()
179
- }).catch((error) => {
180
- callback(new Error(`expansion failed: ${error}`))
179
+ }).catch((error: unknown) => {
180
+ callback(utils.ensureError(error, "expansion failed"))
181
181
  })
182
182
  }
183
183
  },
@@ -90,12 +90,7 @@ export default class SpeechFlowNodeFFmpeg extends SpeechFlowNode {
90
90
  "f": "opus"
91
91
  } : {})
92
92
  })
93
- try {
94
- this.ffmpeg.run()
95
- }
96
- catch (err) {
97
- throw new Error(`failed to start FFmpeg process: ${err}`)
98
- }
93
+ utils.run("starting FFmpeg process", () => this.ffmpeg!.run())
99
94
 
100
95
  /* establish a duplex stream and connect it to FFmpeg */
101
96
  this.stream = Stream.Duplex.from({
@@ -125,12 +120,7 @@ export default class SpeechFlowNodeFFmpeg extends SpeechFlowNode {
125
120
 
126
121
  /* shutdown FFmpeg */
127
122
  if (this.ffmpeg !== null) {
128
- try {
129
- this.ffmpeg.kill()
130
- }
131
- catch {
132
- /* ignore kill errors during cleanup */
133
- }
123
+ utils.run(() => this.ffmpeg!.kill(), () => {})
134
124
  this.ffmpeg = null
135
125
  }
136
126
  }
@@ -180,9 +180,9 @@ export default class SpeechFlowNodeFiller extends SpeechFlowNode {
180
180
  self.log("debug", `received data (${chunk.payload.length} bytes)`)
181
181
  this.push(chunk)
182
182
  }
183
- }).catch((error) => {
183
+ }).catch((error: unknown) => {
184
184
  if (!self.destroyed)
185
- self.log("error", `queue read error: ${error.message}`)
185
+ self.log("error", `queue read error: ${utils.ensureError(error).message}`)
186
186
  })
187
187
  },
188
188
  final (callback) {
@@ -224,8 +224,8 @@ export default class SpeechFlowNodeAWSTranscribe extends SpeechFlowNode {
224
224
  if (chunk.meta.size > 0)
225
225
  metastore.store(chunk.timestampStart, chunk.timestampEnd, chunk.meta)
226
226
  audioQueue.push(new Uint8Array(chunk.payload)) /* intentionally discard all time information */
227
- ensureAudioStreamActive().catch((err) => {
228
- self.log("error", `failed to start audio stream: ${err}`)
227
+ ensureAudioStreamActive().catch((error: unknown) => {
228
+ self.log("error", `failed to start audio stream: ${utils.ensureError(error).message}`)
229
229
  })
230
230
  }
231
231
  callback()
@@ -249,9 +249,9 @@ export default class SpeechFlowNodeAWSTranscribe extends SpeechFlowNode {
249
249
  self.log("debug", `received data (${chunk.payload.length} bytes): "${chunk.payload}"`)
250
250
  this.push(chunk)
251
251
  }
252
- }).catch((error) => {
252
+ }).catch((error: unknown) => {
253
253
  if (!self.destroyed)
254
- self.log("error", `queue read error: ${error.message}`)
254
+ self.log("error", `queue read error: ${utils.ensureError(error).message}`)
255
255
  })
256
256
  },
257
257
  final (callback) {
@@ -259,12 +259,10 @@ export default class SpeechFlowNodeAWSTranscribe extends SpeechFlowNode {
259
259
  callback()
260
260
  return
261
261
  }
262
- try {
263
- self.client.destroy()
264
- }
265
- catch (error) {
266
- self.log("warning", `error closing Amazon Transcribe connection: ${error}`)
267
- }
262
+ utils.run(
263
+ () => self.client!.destroy(),
264
+ (error: Error) => self.log("warning", `error closing Amazon Transcribe connection: ${error}`)
265
+ )
268
266
  audioQueue.push(null) /* do not push null to stream, let Amazon Transcribe do it */
269
267
  audioQueue.destroy()
270
268
  callback()
@@ -232,9 +232,9 @@ export default class SpeechFlowNodeDeepgram extends SpeechFlowNode {
232
232
  self.log("debug", `received data (${chunk.payload.length} bytes)`)
233
233
  this.push(chunk)
234
234
  }
235
- }).catch((error) => {
235
+ }).catch((error: unknown) => {
236
236
  if (!self.destroyed)
237
- self.log("error", `queue read error: ${error.message}`)
237
+ self.log("error", `queue read error: ${utils.ensureError(error).message}`)
238
238
  })
239
239
  },
240
240
  final (callback) {
@@ -279,9 +279,9 @@ export default class SpeechFlowNodeOpenAITranscribe extends SpeechFlowNode {
279
279
  self.log("debug", `received data (${chunk.payload.length} bytes)`)
280
280
  this.push(chunk)
281
281
  }
282
- }).catch((error) => {
282
+ }).catch((error: unknown) => {
283
283
  if (!self.destroyed)
284
- self.log("error", `queue read error: ${error.message}`)
284
+ self.log("error", `queue read error: ${utils.ensureError(error).message}`)
285
285
  })
286
286
  },
287
287
  final (callback) {
@@ -17,6 +17,7 @@ import {
17
17
 
18
18
  /* internal dependencies */
19
19
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
20
+ import * as utils from "./speechflow-utils"
20
21
 
21
22
  /* SpeechFlow node for AWS Polly text-to-speech conversion */
22
23
  export default class SpeechFlowNodeAWSPolly extends SpeechFlowNode {
@@ -144,9 +145,8 @@ export default class SpeechFlowNodeAWSPolly extends SpeechFlowNode {
144
145
  chunkNew.payload = buffer
145
146
  this.push(chunkNew)
146
147
  callback()
147
- }).catch((error) => {
148
- callback(error instanceof Error ?
149
- error : new Error(`failed to send to AWS Polly: ${String(error)}`))
148
+ }).catch((error: unknown) => {
149
+ callback(utils.ensureError(error, "failed to send to AWS Polly"))
150
150
  })
151
151
  }
152
152
  else
@@ -13,6 +13,7 @@ import SpeexResampler from "speex-resampler"
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
  /* SpeechFlow node for Kokoro text-to-speech conversion */
18
19
  export default class SpeechFlowNodeKokoro extends SpeechFlowNode {
@@ -141,8 +142,8 @@ export default class SpeechFlowNodeKokoro extends SpeechFlowNode {
141
142
  chunk.payload = buffer
142
143
  this.push(chunk)
143
144
  callback()
144
- }).catch((err) => {
145
- callback(err)
145
+ }).catch((error: unknown) => {
146
+ callback(utils.ensureError(error))
146
147
  })
147
148
  }
148
149
  },
@@ -12,6 +12,7 @@ import { TranslateClient, TranslateTextCommand } from "@aws-sdk/client-translate
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 AWS Translate text-to-text translations */
17
18
  export default class SpeechFlowNodeAWSTranslate extends SpeechFlowNode {
@@ -122,8 +123,8 @@ export default class SpeechFlowNodeAWSTranslate extends SpeechFlowNode {
122
123
  chunkNew.payload = payload
123
124
  this.push(chunkNew)
124
125
  callback()
125
- }).catch((err) => {
126
- callback(err)
126
+ }).catch((error: unknown) => {
127
+ callback(utils.ensureError(error))
127
128
  })
128
129
  }
129
130
  },
@@ -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 {
@@ -93,8 +94,8 @@ export default class SpeechFlowNodeDeepL extends SpeechFlowNode {
93
94
  chunkNew.payload = payload
94
95
  this.push(chunkNew)
95
96
  callback()
96
- }).catch((err) => {
97
- callback(err)
97
+ }).catch((error: unknown) => {
98
+ callback(utils.ensureError(error))
98
99
  })
99
100
  }
100
101
  },
@@ -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
+ }
@@ -12,6 +12,7 @@ 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 }> }
@@ -250,8 +251,8 @@ export default class SpeechFlowNodeOllama extends SpeechFlowNode {
250
251
  chunkNew.payload = payload
251
252
  this.push(chunkNew)
252
253
  callback()
253
- }).catch((err) => {
254
- callback(err)
254
+ }).catch((error: unknown) => {
255
+ callback(utils.ensureError(error))
255
256
  })
256
257
  }
257
258
  }
@@ -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[] }
@@ -219,8 +220,8 @@ export default class SpeechFlowNodeOpenAI extends SpeechFlowNode {
219
220
  chunkNew.payload = payload
220
221
  this.push(chunkNew)
221
222
  callback()
222
- }).catch((err) => {
223
- callback(err)
223
+ }).catch((error: unknown) => {
224
+ callback(utils.ensureError(error))
224
225
  })
225
226
  }
226
227
  },
@@ -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
@@ -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
  }
@@ -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 }> }
@@ -212,8 +213,8 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
212
213
  chunk.payload = payload
213
214
  this.push(chunk)
214
215
  callback()
215
- }).catch((err) => {
216
- callback(err)
216
+ }).catch((error: unknown) => {
217
+ callback(utils.ensureError(error))
217
218
  })
218
219
  }
219
220
  },
@@ -7,6 +7,7 @@
7
7
  /* standard dependencies */
8
8
  import Stream from "node:stream"
9
9
  import { EventEmitter } from "node:events"
10
+ import { type, type Type } from "arktype"
10
11
 
11
12
  /* external dependencies */
12
13
  import { DateTime, Duration } from "luxon"
@@ -16,6 +17,194 @@ import * as IntervalTree from "node-interval-tree"
16
17
  /* internal dependencies */
17
18
  import { SpeechFlowChunk } from "./speechflow-node"
18
19
 
20
+ /* helper function for retrieving an Error object */
21
+ export function ensureError (error: unknown, prefix?: string): Error {
22
+ if (error instanceof Error && prefix === undefined)
23
+ return error
24
+ let msg = error instanceof Error ?
25
+ error.message : String(error)
26
+ if (prefix)
27
+ msg = `${prefix}: ${msg}`
28
+ return new Error(msg, { cause: error })
29
+ }
30
+
31
+ /* helper function for retrieving a Promise object */
32
+ export function ensurePromise<T> (arg: T | Promise<T>): Promise<T> {
33
+ if (!(arg instanceof Promise))
34
+ arg = Promise.resolve(arg)
35
+ return arg
36
+ }
37
+
38
+ /* helper function for running the finally code of "run" */
39
+ function runFinally (onfinally?: () => void) {
40
+ if (!onfinally)
41
+ return
42
+ try { onfinally() }
43
+ catch (_arg: unknown) { /* ignored */ }
44
+ }
45
+
46
+ /* helper type for ensuring T contains no Promise */
47
+ type runNoPromise<T> =
48
+ [ T ] extends [ Promise<any> ] ? never : T
49
+
50
+ /* run a synchronous or asynchronous action */
51
+ export function run<T, X extends runNoPromise<T> | never> (
52
+ action: () => X,
53
+ oncatch?: (error: Error) => X | never,
54
+ onfinally?: () => void
55
+ ): X
56
+ export function run<T, X extends runNoPromise<T> | never> (
57
+ description: string,
58
+ action: () => X,
59
+ oncatch?: (error: Error) => X | never,
60
+ onfinally?: () => void
61
+ ): X
62
+ export function run<T, X extends (T | Promise<T>)> (
63
+ action: () => X,
64
+ oncatch?: (error: Error) => X,
65
+ onfinally?: () => void
66
+ ): Promise<T>
67
+ export function run<T, X extends (T | Promise<T>)> (
68
+ description: string,
69
+ action: () => X,
70
+ oncatch?: (error: Error) => X,
71
+ onfinally?: () => void
72
+ ): Promise<T>
73
+ export function run<T> (
74
+ ...args: any[]
75
+ ): T | Promise<T> | never {
76
+ /* support overloaded signatures */
77
+ let description: string | undefined
78
+ let action: () => T | Promise<T> | never
79
+ let oncatch: (error: Error) => T | Promise<T> | never
80
+ let onfinally: () => void
81
+ if (typeof args[0] === "string") {
82
+ description = args[0]
83
+ action = args[1]
84
+ oncatch = args[2]
85
+ onfinally = args[3]
86
+ }
87
+ else {
88
+ action = args[0]
89
+ oncatch = args[1]
90
+ onfinally = args[2]
91
+ }
92
+
93
+ /* perform the action */
94
+ let result: T | Promise<T>
95
+ try {
96
+ result = action()
97
+ }
98
+ catch (arg: unknown) {
99
+ /* synchronous case (error branch) */
100
+ let error = ensureError(arg, description)
101
+ if (oncatch) {
102
+ try {
103
+ result = oncatch(error)
104
+ }
105
+ catch (arg: unknown) {
106
+ error = ensureError(arg, description)
107
+ runFinally(onfinally)
108
+ throw error
109
+ }
110
+ runFinally(onfinally)
111
+ return result
112
+ }
113
+ runFinally(onfinally)
114
+ throw error
115
+ }
116
+ if (result instanceof Promise) {
117
+ /* asynchronous case (result or error branch) */
118
+ return result.catch((arg: unknown) => {
119
+ /* asynchronous case (error branch) */
120
+ let error = ensureError(arg, description)
121
+ if (oncatch) {
122
+ try {
123
+ return ensurePromise(oncatch(error))
124
+ }
125
+ catch (arg: unknown) {
126
+ error = ensureError(arg, description)
127
+ return Promise.reject(error)
128
+ }
129
+ }
130
+ return Promise.reject(error)
131
+ }).finally(() => {
132
+ /* asynchronous case (result and error branch) */
133
+ runFinally(onfinally)
134
+ })
135
+ }
136
+ else {
137
+ /* synchronous case (result branch) */
138
+ runFinally(onfinally)
139
+ return result
140
+ }
141
+ }
142
+
143
+ /* run a synchronous or asynchronous action */
144
+ /* eslint @typescript-eslint/unified-signatures: off */
145
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
146
+ action: F,
147
+ oncatch?: (error: Error) => X | never,
148
+ onfinally?: () => void
149
+ ): F
150
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
151
+ description: string,
152
+ action: F,
153
+ oncatch?: (error: Error) => X | never,
154
+ onfinally?: () => void
155
+ ): F
156
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
157
+ action: F,
158
+ oncatch?: (error: Error) => X,
159
+ onfinally?: () => void
160
+ ): F
161
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
162
+ description: string,
163
+ action: F,
164
+ oncatch?: (error: Error) => X,
165
+ onfinally?: () => void
166
+ ): F
167
+ export function runner<T> (
168
+ ...args: any[]
169
+ ): (...args: any[]) => T | Promise<T> | never {
170
+ /* support overloaded signatures */
171
+ let description: string | undefined
172
+ let action: (...args: any[]) => T | Promise<T> | never
173
+ let oncatch: (error: Error) => T | Promise<T> | never
174
+ let onfinally: () => void
175
+ if (typeof args[0] === "string") {
176
+ description = args[0]
177
+ action = args[1]
178
+ oncatch = args[2]
179
+ onfinally = args[3]
180
+ }
181
+ else {
182
+ action = args[0]
183
+ oncatch = args[1]
184
+ onfinally = args[2]
185
+ }
186
+
187
+ /* wrap the "run" operation on "action" into function
188
+ which exposes the signature of "action" */
189
+ return (...args: any[]) => {
190
+ if (description)
191
+ return run(description, () => action(...args), oncatch, onfinally)
192
+ else
193
+ return run(() => action(...args), oncatch, onfinally)
194
+ }
195
+ }
196
+
197
+ /* import an object with parsing and strict error handling */
198
+ export function importObject<T>(name: string, arg: object | string, validator: Type<T, {}>): T {
199
+ const obj: object = typeof arg === "string" ?
200
+ run(`${name}: parsing JSON`, () => JSON.parse(arg)) :
201
+ arg
202
+ const result = validator(obj)
203
+ if (result instanceof type.errors)
204
+ throw new Error(`${name}: validation: ${result.summary}`)
205
+ return result as T
206
+ }
207
+
19
208
  /* calculate duration of an audio buffer */
20
209
  export function audioBufferDuration (
21
210
  buffer: Buffer,