speechflow 0.9.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +57 -9
  3. package/dst/speechflow-node-a2a-ffmpeg.js +1 -0
  4. package/dst/speechflow-node-a2a-ffmpeg.js.map +1 -0
  5. package/dst/{speechflow-node-gemma.d.ts → speechflow-node-a2a-meter.d.ts} +2 -3
  6. package/dst/speechflow-node-a2a-meter.js +151 -0
  7. package/dst/speechflow-node-a2a-meter.js.map +1 -0
  8. package/dst/speechflow-node-a2a-mute.d.ts +16 -0
  9. package/dst/speechflow-node-a2a-mute.js +90 -0
  10. package/dst/speechflow-node-a2a-mute.js.map +1 -0
  11. package/dst/speechflow-node-a2a-vad.js +130 -289
  12. package/dst/speechflow-node-a2a-vad.js.map +1 -0
  13. package/dst/speechflow-node-a2a-wav.js +1 -0
  14. package/dst/speechflow-node-a2a-wav.js.map +1 -0
  15. package/dst/speechflow-node-a2t-deepgram.d.ts +3 -0
  16. package/dst/speechflow-node-a2t-deepgram.js +18 -2
  17. package/dst/speechflow-node-a2t-deepgram.js.map +1 -0
  18. package/dst/speechflow-node-t2a-elevenlabs.d.ts +3 -0
  19. package/dst/speechflow-node-t2a-elevenlabs.js +9 -1
  20. package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -0
  21. package/dst/speechflow-node-t2a-kokoro.js +1 -0
  22. package/dst/speechflow-node-t2a-kokoro.js.map +1 -0
  23. package/dst/speechflow-node-t2t-deepl.d.ts +3 -0
  24. package/dst/speechflow-node-t2t-deepl.js +9 -1
  25. package/dst/speechflow-node-t2t-deepl.js.map +1 -0
  26. package/dst/speechflow-node-t2t-format.js +1 -0
  27. package/dst/speechflow-node-t2t-format.js.map +1 -0
  28. package/dst/speechflow-node-t2t-ollama.js +1 -0
  29. package/dst/speechflow-node-t2t-ollama.js.map +1 -0
  30. package/dst/speechflow-node-t2t-openai.js +2 -1
  31. package/dst/speechflow-node-t2t-openai.js.map +1 -0
  32. package/dst/speechflow-node-t2t-subtitle.js +1 -0
  33. package/dst/speechflow-node-t2t-subtitle.js.map +1 -0
  34. package/dst/speechflow-node-t2t-transformers.js +10 -6
  35. package/dst/speechflow-node-t2t-transformers.js.map +1 -0
  36. package/dst/speechflow-node-x2x-trace.js +2 -1
  37. package/dst/speechflow-node-x2x-trace.js.map +1 -0
  38. package/dst/speechflow-node-xio-device.js +4 -1
  39. package/dst/speechflow-node-xio-device.js.map +1 -0
  40. package/dst/speechflow-node-xio-file.js +4 -1
  41. package/dst/speechflow-node-xio-file.js.map +1 -0
  42. package/dst/speechflow-node-xio-mqtt.js +8 -5
  43. package/dst/speechflow-node-xio-mqtt.js.map +1 -0
  44. package/dst/speechflow-node-xio-websocket.js +6 -5
  45. package/dst/speechflow-node-xio-websocket.js.map +1 -0
  46. package/dst/speechflow-node.d.ts +8 -1
  47. package/dst/speechflow-node.js +18 -2
  48. package/dst/speechflow-node.js.map +1 -0
  49. package/dst/speechflow-utils.d.ts +33 -0
  50. package/dst/speechflow-utils.js +183 -1
  51. package/dst/speechflow-utils.js.map +1 -0
  52. package/dst/speechflow.js +259 -16
  53. package/dst/speechflow.js.map +1 -0
  54. package/etc/speechflow.yaml +9 -7
  55. package/etc/stx.conf +1 -1
  56. package/etc/tsconfig.json +2 -2
  57. package/package.json +19 -12
  58. package/src/speechflow-node-a2a-meter.ts +129 -0
  59. package/src/speechflow-node-a2a-mute.ts +101 -0
  60. package/src/speechflow-node-a2a-vad.ts +266 -0
  61. package/src/speechflow-node-a2t-deepgram.ts +18 -2
  62. package/src/speechflow-node-t2a-elevenlabs.ts +9 -1
  63. package/src/speechflow-node-t2t-deepl.ts +9 -1
  64. package/src/speechflow-node-t2t-openai.ts +1 -1
  65. package/src/speechflow-node-t2t-transformers.ts +12 -7
  66. package/src/speechflow-node-x2x-trace.ts +1 -1
  67. package/src/speechflow-node-xio-device.ts +4 -1
  68. package/src/speechflow-node-xio-file.ts +3 -1
  69. package/src/speechflow-node-xio-mqtt.ts +8 -6
  70. package/src/speechflow-node-xio-websocket.ts +11 -11
  71. package/src/speechflow-node.ts +21 -2
  72. package/src/speechflow-utils.ts +195 -0
  73. package/src/speechflow.ts +245 -16
  74. package/dst/speechflow-node-deepgram.d.ts +0 -12
  75. package/dst/speechflow-node-deepgram.js +0 -220
  76. package/dst/speechflow-node-deepl.d.ts +0 -12
  77. package/dst/speechflow-node-deepl.js +0 -128
  78. package/dst/speechflow-node-device.d.ts +0 -13
  79. package/dst/speechflow-node-device.js +0 -205
  80. package/dst/speechflow-node-elevenlabs.d.ts +0 -13
  81. package/dst/speechflow-node-elevenlabs.js +0 -182
  82. package/dst/speechflow-node-ffmpeg.d.ts +0 -13
  83. package/dst/speechflow-node-ffmpeg.js +0 -152
  84. package/dst/speechflow-node-file.d.ts +0 -11
  85. package/dst/speechflow-node-file.js +0 -176
  86. package/dst/speechflow-node-format.d.ts +0 -11
  87. package/dst/speechflow-node-format.js +0 -80
  88. package/dst/speechflow-node-gemma.js +0 -213
  89. package/dst/speechflow-node-mqtt.d.ts +0 -13
  90. package/dst/speechflow-node-mqtt.js +0 -181
  91. package/dst/speechflow-node-opus.d.ts +0 -12
  92. package/dst/speechflow-node-opus.js +0 -135
  93. package/dst/speechflow-node-subtitle.d.ts +0 -12
  94. package/dst/speechflow-node-subtitle.js +0 -96
  95. package/dst/speechflow-node-t2t-gemma.d.ts +0 -13
  96. package/dst/speechflow-node-t2t-gemma.js +0 -233
  97. package/dst/speechflow-node-t2t-opus.d.ts +0 -12
  98. package/dst/speechflow-node-t2t-opus.js +0 -135
  99. package/dst/speechflow-node-trace.d.ts +0 -11
  100. package/dst/speechflow-node-trace.js +0 -88
  101. package/dst/speechflow-node-wav.d.ts +0 -11
  102. package/dst/speechflow-node-wav.js +0 -170
  103. package/dst/speechflow-node-websocket.d.ts +0 -13
  104. package/dst/speechflow-node-websocket.js +0 -275
  105. package/dst/speechflow-node-whisper-common.d.ts +0 -34
  106. package/dst/speechflow-node-whisper-common.js +0 -7
  107. package/dst/speechflow-node-whisper-ggml.d.ts +0 -1
  108. package/dst/speechflow-node-whisper-ggml.js +0 -97
  109. package/dst/speechflow-node-whisper-onnx.d.ts +0 -1
  110. package/dst/speechflow-node-whisper-onnx.js +0 -131
  111. package/dst/speechflow-node-whisper-worker-ggml.d.ts +0 -1
  112. package/dst/speechflow-node-whisper-worker-ggml.js +0 -97
  113. package/dst/speechflow-node-whisper-worker-onnx.d.ts +0 -1
  114. package/dst/speechflow-node-whisper-worker-onnx.js +0 -131
  115. package/dst/speechflow-node-whisper-worker.d.ts +0 -1
  116. package/dst/speechflow-node-whisper-worker.js +0 -116
  117. package/dst/speechflow-node-whisper-worker2.d.ts +0 -1
  118. package/dst/speechflow-node-whisper-worker2.js +0 -82
  119. package/dst/speechflow-node-whisper.d.ts +0 -19
  120. package/dst/speechflow-node-whisper.js +0 -604
@@ -31,6 +31,33 @@ export function audioBufferDuration (
31
31
  return totalSamples / sampleRate
32
32
  }
33
33
 
34
+ /* calculate duration of an audio array */
35
+ export function audioArrayDuration (
36
+ arr: Float32Array,
37
+ sampleRate = 48000,
38
+ channels = 1
39
+ ) {
40
+ const totalSamples = arr.length / channels
41
+ return totalSamples / sampleRate
42
+ }
43
+
44
+ /* helper function: convert Buffer in PCM/I16 to Float32Array in PCM/F32 format */
45
+ export function convertBufToF32 (buf: Buffer, littleEndian = true) {
46
+ const dataView = new DataView(buf.buffer)
47
+ const arr = new Float32Array(buf.length / 2)
48
+ for (let i = 0; i < arr.length; i++)
49
+ arr[i] = dataView.getInt16(i * 2, littleEndian) / 32768
50
+ return arr
51
+ }
52
+
53
+ /* helper function: convert Float32Array in PCM/F32 to Buffer in PCM/I16 format */
54
+ export function convertF32ToBuf (arr: Float32Array) {
55
+ const int16Array = new Int16Array(arr.length)
56
+ for (let i = 0; i < arr.length; i++)
57
+ int16Array[i] = Math.max(-32768, Math.min(32767, Math.round(arr[i] * 32768)))
58
+ return Buffer.from(int16Array.buffer)
59
+ }
60
+
34
61
  /* create a Duplex/Transform stream which has
35
62
  object-mode on Writable side and buffer/string-mode on Readable side */
36
63
  export function createTransformStreamForWritableSide () {
@@ -210,3 +237,171 @@ export class DoubleQueue<T0, T1> extends EventEmitter {
210
237
  })
211
238
  }
212
239
  }
240
+
241
+ /* queue element */
242
+ export type QueueElement = { type: string }
243
+
244
+ /* queue pointer */
245
+ export class QueuePointer<T extends QueueElement> extends EventEmitter {
246
+ /* internal state */
247
+ private index = 0
248
+
249
+ /* construction */
250
+ constructor (
251
+ private name: string,
252
+ private queue: Queue<T>
253
+ ) {
254
+ super()
255
+ }
256
+
257
+ /* positioning operations */
258
+ maxPosition () {
259
+ return this.queue.elements.length
260
+ }
261
+ position (index?: number): number {
262
+ if (index !== undefined) {
263
+ this.index = index
264
+ if (this.index < 0)
265
+ this.index = 0
266
+ else if (this.index >= this.queue.elements.length)
267
+ this.index = this.queue.elements.length
268
+ this.emit("position", this.index)
269
+ }
270
+ return this.index
271
+ }
272
+ walk (num: number) {
273
+ if (num > 0) {
274
+ for (let i = 0; i < num && this.index < this.queue.elements.length; i++)
275
+ this.index++
276
+ this.emit("position", { start: this.index })
277
+ }
278
+ else if (num < 0) {
279
+ for (let i = 0; i < Math.abs(num) && this.index > 0; i++)
280
+ this.index--
281
+ this.emit("position", { start: this.index })
282
+ }
283
+ }
284
+ walkForwardUntil (type: T["type"]) {
285
+ while (this.index < this.queue.elements.length
286
+ && this.queue.elements[this.index].type !== type)
287
+ this.index++
288
+ this.emit("position", { start: this.index })
289
+ }
290
+ walkBackwardUntil (type: T["type"]) {
291
+ while (this.index > 0
292
+ && this.queue.elements[this.index].type !== type)
293
+ this.index--
294
+ this.emit("position", { start: this.index })
295
+ }
296
+
297
+ /* search operations */
298
+ searchForward (type: T["type"]) {
299
+ let position = this.index
300
+ while (position < this.queue.elements.length
301
+ && this.queue.elements[position].type !== type)
302
+ position++
303
+ this.emit("search", { start: this.index, end: position })
304
+ return position
305
+ }
306
+ searchBackward (type: T["type"]) {
307
+ let position = this.index
308
+ while (position > 0
309
+ && this.queue.elements[position].type !== type)
310
+ position--
311
+ this.emit("search", { start: position, end: this.index })
312
+ return position
313
+ }
314
+
315
+ /* reading operations */
316
+ peek (position?: number) {
317
+ if (position === undefined)
318
+ position = this.index
319
+ else {
320
+ if (position < 0)
321
+ position = 0
322
+ else if (position > this.queue.elements.length)
323
+ position = this.queue.elements.length
324
+ }
325
+ const element = this.queue.elements[position]
326
+ this.queue.emit("read", { start: position, end: position })
327
+ return element
328
+ }
329
+ read () {
330
+ const element = this.queue.elements[this.index]
331
+ if (this.index < this.queue.elements.length)
332
+ this.index++
333
+ this.queue.emit("read", { start: this.index - 1, end: this.index - 1 })
334
+ return element
335
+ }
336
+ slice (size?: number) {
337
+ let slice: T[]
338
+ const start = this.index
339
+ if (size !== undefined) {
340
+ if (size < 0)
341
+ size = 0
342
+ else if (size > this.queue.elements.length - this.index)
343
+ size = this.queue.elements.length - this.index
344
+ slice = this.queue.elements.slice(this.index, size)
345
+ this.index += size
346
+ }
347
+ else {
348
+ slice = this.queue.elements.slice(this.index)
349
+ this.index = this.queue.elements.length
350
+ }
351
+ this.queue.emit("read", { start, end: this.index })
352
+ return slice
353
+ }
354
+
355
+ /* writing operations */
356
+ touch () {
357
+ if (this.index >= this.queue.elements.length)
358
+ throw new Error("cannot touch after last element")
359
+ this.queue.emit("write", { start: this.index, end: this.index + 1 })
360
+ }
361
+ append (element: T) {
362
+ this.queue.elements.push(element)
363
+ this.index = this.queue.elements.length
364
+ this.queue.emit("write", { start: this.index - 1, end: this.index - 1 })
365
+ }
366
+ insert (element: T) {
367
+ this.queue.elements.splice(this.index++, 0, element)
368
+ this.queue.emit("write", { start: this.index - 1, end: this.index })
369
+ }
370
+ delete () {
371
+ if (this.index >= this.queue.elements.length)
372
+ throw new Error("cannot delete after last element")
373
+ this.queue.elements.splice(this.index, 1)
374
+ this.queue.emit("write", { start: this.index, end: this.index })
375
+ }
376
+ }
377
+
378
+ /* queue */
379
+ export class Queue<T extends QueueElement> extends EventEmitter {
380
+ public elements: T[] = []
381
+ private pointers = new Map<string, QueuePointer<T>>()
382
+ pointerUse (name: string): QueuePointer<T> {
383
+ if (!this.pointers.has(name))
384
+ this.pointers.set(name, new QueuePointer<T>(name, this))
385
+ return this.pointers.get(name)!
386
+ }
387
+ pointerDelete (name: string): void {
388
+ if (!this.pointers.has(name))
389
+ throw new Error("pointer not exists")
390
+ this.pointers.delete(name)
391
+ }
392
+ trim (): void {
393
+ /* determine minimum pointer position */
394
+ let min = this.elements.length
395
+ for (const pointer of this.pointers.values())
396
+ if (min > pointer.position())
397
+ min = pointer.position()
398
+
399
+ /* trim the maximum amount of first elements */
400
+ this.elements.splice(0, min)
401
+
402
+ /* shift all pointers */
403
+ for (const pointer of this.pointers.values())
404
+ pointer.position(pointer.position() - min)
405
+ }
406
+ }
407
+
package/src/speechflow.ts CHANGED
@@ -8,6 +8,11 @@
8
8
  import path from "node:path"
9
9
  import Stream from "node:stream"
10
10
  import { EventEmitter } from "node:events"
11
+ import http from "node:http"
12
+ import * as HAPI from "@hapi/hapi"
13
+ import WebSocket from "ws"
14
+ import HAPIWebSocket from "hapi-plugin-websocket"
15
+ import HAPIHeader from "hapi-plugin-header"
11
16
 
12
17
  /* external dependencies */
13
18
  import { DateTime } from "luxon"
@@ -20,6 +25,9 @@ import objectPath from "object-path"
20
25
  import installedPackages from "installed-packages"
21
26
  import dotenvx from "@dotenvx/dotenvx"
22
27
  import syspath from "syspath"
28
+ import * as arktype from "arktype"
29
+ import Table from "cli-table3"
30
+ import chalk from "chalk"
23
31
 
24
32
  /* internal dependencies */
25
33
  import SpeechFlowNode from "./speechflow-node"
@@ -28,6 +36,15 @@ import pkg from "../package.json"
28
36
  /* central CLI context */
29
37
  let cli: CLIio | null = null
30
38
 
39
+ type wsPeerCtx = {
40
+ peer: string
41
+ }
42
+ type wsPeerInfo = {
43
+ ctx: wsPeerCtx
44
+ ws: WebSocket
45
+ req: http.IncomingMessage
46
+ }
47
+
31
48
  /* establish asynchronous environment */
32
49
  ;(async () => {
33
50
  /* determine system paths */
@@ -45,12 +62,16 @@ let cli: CLIio | null = null
45
62
  "[-h|--help] " +
46
63
  "[-V|--version] " +
47
64
  "[-v|--verbose <level>] " +
65
+ "[-a|--address <ip-address>] " +
66
+ "[-p|--port <tcp-port>] " +
48
67
  "[-C|--cache <directory>] " +
68
+ "[-S|--status] " +
49
69
  "[-e|--expression <expression>] " +
50
70
  "[-f|--file <file>] " +
51
71
  "[-c|--config <id>@<yaml-config-file>] " +
52
72
  "[<argument> [...]]"
53
73
  )
74
+ .version(false)
54
75
  .option("V", {
55
76
  alias: "version",
56
77
  type: "boolean",
@@ -68,6 +89,24 @@ let cli: CLIio | null = null
68
89
  default: "warning",
69
90
  describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
70
91
  })
92
+ .option("a", {
93
+ alias: "address",
94
+ type: "string",
95
+ array: false,
96
+ coerce,
97
+ nargs: 1,
98
+ default: "0.0.0.0",
99
+ describe: "IP address for REST/WebSocket API"
100
+ })
101
+ .option("p", {
102
+ alias: "port",
103
+ type: "number",
104
+ array: false,
105
+ coerce,
106
+ nargs: 1,
107
+ default: 8484,
108
+ describe: "TCP port for REST/WebSocket API"
109
+ })
71
110
  .option("C", {
72
111
  alias: "cache",
73
112
  type: "string",
@@ -77,6 +116,14 @@ let cli: CLIio | null = null
77
116
  default: path.join(dataDir, "cache"),
78
117
  describe: "directory for cached files (primarily AI model files)"
79
118
  })
119
+ .option("S", {
120
+ alias: "status",
121
+ type: "boolean",
122
+ array: false,
123
+ coerce,
124
+ default: false,
125
+ describe: "show one-time status of nodes"
126
+ })
80
127
  .option("e", {
81
128
  alias: "expression",
82
129
  type: "string",
@@ -107,7 +154,6 @@ let cli: CLIio | null = null
107
154
  .help("h", "show usage help")
108
155
  .alias("h", "help")
109
156
  .showHelpOnFail(true)
110
- .version(false)
111
157
  .strict()
112
158
  .demand(0)
113
159
  .parse(hideBin(process.argv))
@@ -186,6 +232,9 @@ let cli: CLIio | null = null
186
232
  const pkgsI = [
187
233
  "./speechflow-node-a2a-ffmpeg.js",
188
234
  "./speechflow-node-a2a-wav.js",
235
+ "./speechflow-node-a2a-mute.js",
236
+ "./speechflow-node-a2a-meter.js",
237
+ "./speechflow-node-a2a-vad.js",
189
238
  "./speechflow-node-a2t-deepgram.js",
190
239
  "./speechflow-node-t2a-elevenlabs.js",
191
240
  "./speechflow-node-t2a-kokoro.js",
@@ -193,7 +242,6 @@ let cli: CLIio | null = null
193
242
  "./speechflow-node-t2t-openai.js",
194
243
  "./speechflow-node-t2t-ollama.js",
195
244
  "./speechflow-node-t2t-transformers.js",
196
- "./speechflow-node-t2t-opus.js",
197
245
  "./speechflow-node-t2t-subtitle.js",
198
246
  "./speechflow-node-t2t-format.js",
199
247
  "./speechflow-node-x2x-trace.js",
@@ -231,15 +279,7 @@ let cli: CLIio | null = null
231
279
  }
232
280
  }
233
281
 
234
- /* graph processing: PASS 1: parse DSL and create and connect nodes */
235
- const flowlink = new FlowLink<SpeechFlowNode>({
236
- trace: (msg: string) => {
237
- cli!.log("debug", msg)
238
- }
239
- })
240
- let nodenum = 1
241
- const variables = { argv: args._, env: process.env }
242
- const graphNodes = new Set<SpeechFlowNode>()
282
+ /* static configuration */
243
283
  const cfg = {
244
284
  audioChannels: 1,
245
285
  audioBitDepth: 16,
@@ -248,6 +288,45 @@ let cli: CLIio | null = null
248
288
  textEncoding: "utf8",
249
289
  cacheDir: args.C
250
290
  }
291
+
292
+ /* handle one-time status query of nodes */
293
+ if (args.S) {
294
+ const table = new Table({
295
+ head: [
296
+ chalk.reset.bold("NODE"),
297
+ chalk.reset.bold("PROPERTY"),
298
+ chalk.reset.bold("VALUE")
299
+ ],
300
+ colWidths: [ 15, 15, 50 - (2 * 2 + 2 * 3) ],
301
+ style: { "padding-left": 1, "padding-right": 1, border: [ "grey" ], compact: true },
302
+ chars: { "left-mid": "", mid: "", "mid-mid": "", "right-mid": "" }
303
+ })
304
+ for (const name of Object.keys(nodes)) {
305
+ cli!.log("info", `gathering status of node <${name}>`)
306
+ const node = new nodes[name](name, cfg, {}, [])
307
+ const status = await node.status()
308
+ if (Object.keys(status).length > 0) {
309
+ let first = true
310
+ for (const key of Object.keys(status)) {
311
+ table.push([ first ? chalk.bold(name) : "", key, chalk.blue(status[key]) ])
312
+ first = false
313
+ }
314
+ }
315
+ }
316
+ const output = table.toString()
317
+ process.stdout.write(output + "\n")
318
+ process.exit(0)
319
+ }
320
+
321
+ /* graph processing: PASS 1: parse DSL and create and connect nodes */
322
+ const flowlink = new FlowLink<SpeechFlowNode>({
323
+ trace: (msg: string) => {
324
+ cli!.log("debug", msg)
325
+ }
326
+ })
327
+ const variables = { argv: args._, env: process.env }
328
+ const graphNodes = new Set<SpeechFlowNode>()
329
+ const nodeNums = new Map<typeof SpeechFlowNode, number>()
251
330
  let ast: unknown
252
331
  try {
253
332
  ast = flowlink.compile(config)
@@ -275,17 +354,19 @@ let cli: CLIio | null = null
275
354
  throw new Error(`unknown node "${id}"`)
276
355
  let node: SpeechFlowNode
277
356
  try {
278
- node = new nodes[id](`${id}[${nodenum}]`, cfg, opts, args)
357
+ let num = nodeNums.get(nodes[id]) ?? 0
358
+ nodeNums.set(nodes[id], ++num)
359
+ const name = num === 1 ? id : `${id}:${num}`
360
+ node = new nodes[id](name, cfg, opts, args)
279
361
  }
280
362
  catch (err) {
281
363
  /* fatal error */
282
364
  if (err instanceof Error)
283
- cli!.log("error", `creation of "${id}[${nodenum}]" node failed: ${err.message}`)
365
+ cli!.log("error", `creation of <${id}> node failed: ${err.message}`)
284
366
  else
285
- cli!.log("error", `creation of "${id}"[${nodenum}] node failed: ${err}`)
367
+ cli!.log("error", `creation of <${id}> node failed: ${err}`)
286
368
  process.exit(1)
287
369
  }
288
- nodenum++
289
370
  const params = Object.keys(node.params)
290
371
  .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
291
372
  cli!.log("info", `create node "${node.id}" (${params})`)
@@ -403,6 +484,150 @@ let cli: CLIio | null = null
403
484
  })
404
485
  }
405
486
 
487
+ /* define external request/response structure */
488
+ const requestValidator = arktype.type({
489
+ request: "string",
490
+ node: "string",
491
+ args: "unknown[]"
492
+ })
493
+
494
+ /* forward external request to target node in graph */
495
+ const consumeExternalRequest = async (_req: any) => {
496
+ const req = requestValidator(_req)
497
+ if (req instanceof arktype.type.errors)
498
+ throw new Error(`invalid request: ${req.summary}`)
499
+ if (req.request !== "COMMAND")
500
+ throw new Error("invalid external request (command expected)")
501
+ const name = req.node as string
502
+ const args = req.args as any[]
503
+ const foundNode = Array.from(graphNodes).find((node) => node.id === name)
504
+ if (foundNode === undefined)
505
+ cli!.log("warning", `external request failed: no such node <${name}>`)
506
+ else {
507
+ await foundNode.receiveRequest(args).catch((err: Error) => {
508
+ cli!.log("warning", `external request to node <${name}> failed: ${err}`)
509
+ throw new Error(`external request to node <${name}> failed: ${err}`)
510
+ })
511
+ }
512
+ }
513
+
514
+ /* establish REST/WebSocket API */
515
+ const wsPeers = new Map<string, wsPeerInfo>()
516
+ const hapi = new HAPI.Server({
517
+ address: args.a,
518
+ port: args.p
519
+ })
520
+ await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
521
+ await hapi.register({ plugin: HAPIWebSocket })
522
+ hapi.events.on("response", (request: HAPI.Request) => {
523
+ let protocol = `HTTP/${request.raw.req.httpVersion}`
524
+ const ws = request.websocket()
525
+ if (ws.mode === "websocket") {
526
+ const wsVersion = (ws.ws as any).protocolVersion ??
527
+ request.headers["sec-websocket-version"] ?? "13?"
528
+ protocol = `WebSocket/${wsVersion}+${protocol}`
529
+ }
530
+ const msg =
531
+ "remote=" + request.info.remoteAddress + ", " +
532
+ "method=" + request.method.toUpperCase() + ", " +
533
+ "url=" + request.url.pathname + ", " +
534
+ "protocol=" + protocol + ", " +
535
+ "response=" + ("statusCode" in request.response ? request.response.statusCode : "<unknown>")
536
+ cli!.log("info", `HAPI: request: ${msg}`)
537
+ })
538
+ hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
539
+ if (event.error instanceof Error)
540
+ cli!.log("error", `HAPI: request-error: ${event.error.message}`)
541
+ else
542
+ cli!.log("error", `HAPI: request-error: ${event.error}`)
543
+ })
544
+ hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
545
+ if (tags.error) {
546
+ const err = event.error
547
+ if (err instanceof Error)
548
+ cli!.log("error", `HAPI: log: ${err.message}`)
549
+ else
550
+ cli!.log("error", `HAPI: log: ${err}`)
551
+ }
552
+ })
553
+ hapi.route({
554
+ method: "GET",
555
+ path: "/api/{req}/{node}/{params*}",
556
+ options: {
557
+ },
558
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
559
+ const peer = request.info.remoteAddress
560
+ const req = {
561
+ request: request.params.req,
562
+ node: request.params.node,
563
+ args: (request.params.params as string ?? "").split("/").filter((seg) => seg !== "")
564
+ }
565
+ cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
566
+ return consumeExternalRequest(req).then(() => {
567
+ return h.response({ response: "OK" }).code(200)
568
+ }).catch((err) => {
569
+ return h.response({ response: "ERROR", data: err.message }).code(417)
570
+ })
571
+ }
572
+ })
573
+ hapi.route({
574
+ method: "POST",
575
+ path: "/api",
576
+ options: {
577
+ payload: {
578
+ output: "data",
579
+ parse: true,
580
+ allow: "application/json"
581
+ },
582
+ plugins: {
583
+ websocket: {
584
+ autoping: 30 * 1000,
585
+ connect: (args: any) => {
586
+ const ctx: wsPeerCtx = args.ctx
587
+ const ws: WebSocket = args.ws
588
+ const req: http.IncomingMessage = args.req
589
+ const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
590
+ ctx.peer = peer
591
+ wsPeers.set(peer, { ctx, ws, req })
592
+ cli!.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
593
+ },
594
+ disconnect: (args: any) => {
595
+ const ctx: wsPeerCtx = args.ctx
596
+ const peer = ctx.peer
597
+ wsPeers.delete(peer)
598
+ cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
599
+ }
600
+ }
601
+ }
602
+ },
603
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
604
+ /* on WebSocket message transfer */
605
+ const peer = request.info.remoteAddress
606
+ const req = requestValidator(request.payload)
607
+ if (req instanceof arktype.type.errors)
608
+ return h.response({ response: "ERROR", data: `invalid request: ${req.summary}` }).code(417)
609
+ cli!.log("info", `HAPI: peer ${peer}: POST: ${JSON.stringify(req)}`)
610
+ return consumeExternalRequest(req).then(() => {
611
+ return h.response({ response: "OK" }).code(200)
612
+ }).catch((err: Error) => {
613
+ return h.response({ response: "ERROR", data: err.message }).code(417)
614
+ })
615
+ }
616
+ })
617
+ await hapi.start()
618
+ cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.address}:${args.port}`)
619
+
620
+ /* hook for sendResponse method of nodes */
621
+ for (const node of graphNodes) {
622
+ node.on("send-response", (args: any[]) => {
623
+ const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
624
+ for (const [ peer, info ] of wsPeers.entries()) {
625
+ cli!.log("info", `HAPI: peer ${peer}: ${data}`)
626
+ info.ws.send(data)
627
+ }
628
+ })
629
+ }
630
+
406
631
  /* start of internal stream processing */
407
632
  cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
408
633
 
@@ -417,6 +642,10 @@ let cli: CLIio | null = null
417
642
  else
418
643
  cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
419
644
 
645
+ /* shutdown HAPI service */
646
+ cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
647
+ await hapi.stop()
648
+
420
649
  /* graph processing: PASS 1: disconnect node streams */
421
650
  for (const node of graphNodes) {
422
651
  if (node.stream === null) {
@@ -491,7 +720,7 @@ let cli: CLIio | null = null
491
720
  if (cli !== null)
492
721
  cli.log("error", err.message)
493
722
  else
494
- process.stderr.write(`${pkg.name}: ERROR: ${err.message}\n`)
723
+ process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message} ${err.stack}\n`)
495
724
  process.exit(1)
496
725
  })
497
726
 
@@ -1,12 +0,0 @@
1
- import SpeechFlowNode from "./speechflow-node";
2
- export default class SpeechFlowNodeDeepgram extends SpeechFlowNode {
3
- static name: string;
4
- private dg;
5
- constructor(id: string, cfg: {
6
- [id: string]: any;
7
- }, opts: {
8
- [id: string]: any;
9
- }, args: any[]);
10
- open(): Promise<void>;
11
- close(): Promise<void>;
12
- }