speechflow 0.9.9 → 1.0.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 (110) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +48 -1
  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 +147 -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.js +2 -1
  16. package/dst/speechflow-node-a2t-deepgram.js.map +1 -0
  17. package/dst/speechflow-node-t2a-elevenlabs.js +1 -0
  18. package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -0
  19. package/dst/speechflow-node-t2a-kokoro.js +1 -0
  20. package/dst/speechflow-node-t2a-kokoro.js.map +1 -0
  21. package/dst/speechflow-node-t2t-deepl.js +1 -0
  22. package/dst/speechflow-node-t2t-deepl.js.map +1 -0
  23. package/dst/speechflow-node-t2t-format.js +1 -0
  24. package/dst/speechflow-node-t2t-format.js.map +1 -0
  25. package/dst/speechflow-node-t2t-ollama.js +1 -0
  26. package/dst/speechflow-node-t2t-ollama.js.map +1 -0
  27. package/dst/speechflow-node-t2t-openai.js +1 -0
  28. package/dst/speechflow-node-t2t-openai.js.map +1 -0
  29. package/dst/speechflow-node-t2t-subtitle.js +1 -0
  30. package/dst/speechflow-node-t2t-subtitle.js.map +1 -0
  31. package/dst/speechflow-node-t2t-transformers.js +10 -6
  32. package/dst/speechflow-node-t2t-transformers.js.map +1 -0
  33. package/dst/speechflow-node-x2x-trace.js +1 -0
  34. package/dst/speechflow-node-x2x-trace.js.map +1 -0
  35. package/dst/speechflow-node-xio-device.js +1 -0
  36. package/dst/speechflow-node-xio-device.js.map +1 -0
  37. package/dst/speechflow-node-xio-file.js +1 -0
  38. package/dst/speechflow-node-xio-file.js.map +1 -0
  39. package/dst/speechflow-node-xio-mqtt.js +1 -0
  40. package/dst/speechflow-node-xio-mqtt.js.map +1 -0
  41. package/dst/speechflow-node-xio-websocket.js +1 -0
  42. package/dst/speechflow-node-xio-websocket.js.map +1 -0
  43. package/dst/speechflow-node.d.ts +3 -0
  44. package/dst/speechflow-node.js +10 -0
  45. package/dst/speechflow-node.js.map +1 -0
  46. package/dst/speechflow-utils.d.ts +33 -0
  47. package/dst/speechflow-utils.js +183 -1
  48. package/dst/speechflow-utils.js.map +1 -0
  49. package/dst/speechflow.js +209 -6
  50. package/dst/speechflow.js.map +1 -0
  51. package/etc/speechflow.yaml +5 -3
  52. package/etc/stx.conf +1 -1
  53. package/etc/tsconfig.json +2 -2
  54. package/package.json +14 -8
  55. package/src/speechflow-node-a2a-meter.ts +125 -0
  56. package/src/speechflow-node-a2a-mute.ts +101 -0
  57. package/src/speechflow-node-a2a-vad.ts +266 -0
  58. package/src/speechflow-node-a2t-deepgram.ts +1 -1
  59. package/src/speechflow-node-t2t-transformers.ts +12 -7
  60. package/src/speechflow-node-xio-websocket.ts +5 -5
  61. package/src/speechflow-node.ts +12 -0
  62. package/src/speechflow-utils.ts +195 -0
  63. package/src/speechflow.ts +193 -6
  64. package/dst/speechflow-node-deepgram.d.ts +0 -12
  65. package/dst/speechflow-node-deepgram.js +0 -220
  66. package/dst/speechflow-node-deepl.d.ts +0 -12
  67. package/dst/speechflow-node-deepl.js +0 -128
  68. package/dst/speechflow-node-device.d.ts +0 -13
  69. package/dst/speechflow-node-device.js +0 -205
  70. package/dst/speechflow-node-elevenlabs.d.ts +0 -13
  71. package/dst/speechflow-node-elevenlabs.js +0 -182
  72. package/dst/speechflow-node-ffmpeg.d.ts +0 -13
  73. package/dst/speechflow-node-ffmpeg.js +0 -152
  74. package/dst/speechflow-node-file.d.ts +0 -11
  75. package/dst/speechflow-node-file.js +0 -176
  76. package/dst/speechflow-node-format.d.ts +0 -11
  77. package/dst/speechflow-node-format.js +0 -80
  78. package/dst/speechflow-node-gemma.js +0 -213
  79. package/dst/speechflow-node-mqtt.d.ts +0 -13
  80. package/dst/speechflow-node-mqtt.js +0 -181
  81. package/dst/speechflow-node-opus.d.ts +0 -12
  82. package/dst/speechflow-node-opus.js +0 -135
  83. package/dst/speechflow-node-subtitle.d.ts +0 -12
  84. package/dst/speechflow-node-subtitle.js +0 -96
  85. package/dst/speechflow-node-t2t-gemma.d.ts +0 -13
  86. package/dst/speechflow-node-t2t-gemma.js +0 -233
  87. package/dst/speechflow-node-t2t-opus.d.ts +0 -12
  88. package/dst/speechflow-node-t2t-opus.js +0 -135
  89. package/dst/speechflow-node-trace.d.ts +0 -11
  90. package/dst/speechflow-node-trace.js +0 -88
  91. package/dst/speechflow-node-wav.d.ts +0 -11
  92. package/dst/speechflow-node-wav.js +0 -170
  93. package/dst/speechflow-node-websocket.d.ts +0 -13
  94. package/dst/speechflow-node-websocket.js +0 -275
  95. package/dst/speechflow-node-whisper-common.d.ts +0 -34
  96. package/dst/speechflow-node-whisper-common.js +0 -7
  97. package/dst/speechflow-node-whisper-ggml.d.ts +0 -1
  98. package/dst/speechflow-node-whisper-ggml.js +0 -97
  99. package/dst/speechflow-node-whisper-onnx.d.ts +0 -1
  100. package/dst/speechflow-node-whisper-onnx.js +0 -131
  101. package/dst/speechflow-node-whisper-worker-ggml.d.ts +0 -1
  102. package/dst/speechflow-node-whisper-worker-ggml.js +0 -97
  103. package/dst/speechflow-node-whisper-worker-onnx.d.ts +0 -1
  104. package/dst/speechflow-node-whisper-worker-onnx.js +0 -131
  105. package/dst/speechflow-node-whisper-worker.d.ts +0 -1
  106. package/dst/speechflow-node-whisper-worker.js +0 -116
  107. package/dst/speechflow-node-whisper-worker2.d.ts +0 -1
  108. package/dst/speechflow-node-whisper-worker2.js +0 -82
  109. package/dst/speechflow-node-whisper.d.ts +0 -19
  110. 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,7 @@ 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"
23
29
 
24
30
  /* internal dependencies */
25
31
  import SpeechFlowNode from "./speechflow-node"
@@ -28,6 +34,15 @@ import pkg from "../package.json"
28
34
  /* central CLI context */
29
35
  let cli: CLIio | null = null
30
36
 
37
+ type wsPeerCtx = {
38
+ peer: string
39
+ }
40
+ type wsPeerInfo = {
41
+ ctx: wsPeerCtx
42
+ ws: WebSocket
43
+ req: http.IncomingMessage
44
+ }
45
+
31
46
  /* establish asynchronous environment */
32
47
  ;(async () => {
33
48
  /* determine system paths */
@@ -45,6 +60,8 @@ let cli: CLIio | null = null
45
60
  "[-h|--help] " +
46
61
  "[-V|--version] " +
47
62
  "[-v|--verbose <level>] " +
63
+ "[-a|--address <ip-address>] " +
64
+ "[-p|--port <tcp-port>] " +
48
65
  "[-C|--cache <directory>] " +
49
66
  "[-e|--expression <expression>] " +
50
67
  "[-f|--file <file>] " +
@@ -68,6 +85,24 @@ let cli: CLIio | null = null
68
85
  default: "warning",
69
86
  describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
70
87
  })
88
+ .option("a", {
89
+ alias: "address",
90
+ type: "string",
91
+ array: false,
92
+ coerce,
93
+ nargs: 1,
94
+ default: "0.0.0.0",
95
+ describe: "IP address for REST/WebSocket API"
96
+ })
97
+ .option("p", {
98
+ alias: "port",
99
+ type: "number",
100
+ array: false,
101
+ coerce,
102
+ nargs: 1,
103
+ default: 8484,
104
+ describe: "TCP port for REST/WebSocket API"
105
+ })
71
106
  .option("C", {
72
107
  alias: "cache",
73
108
  type: "string",
@@ -186,6 +221,9 @@ let cli: CLIio | null = null
186
221
  const pkgsI = [
187
222
  "./speechflow-node-a2a-ffmpeg.js",
188
223
  "./speechflow-node-a2a-wav.js",
224
+ "./speechflow-node-a2a-mute.js",
225
+ "./speechflow-node-a2a-meter.js",
226
+ "./speechflow-node-a2a-vad.js",
189
227
  "./speechflow-node-a2t-deepgram.js",
190
228
  "./speechflow-node-t2a-elevenlabs.js",
191
229
  "./speechflow-node-t2a-kokoro.js",
@@ -193,7 +231,6 @@ let cli: CLIio | null = null
193
231
  "./speechflow-node-t2t-openai.js",
194
232
  "./speechflow-node-t2t-ollama.js",
195
233
  "./speechflow-node-t2t-transformers.js",
196
- "./speechflow-node-t2t-opus.js",
197
234
  "./speechflow-node-t2t-subtitle.js",
198
235
  "./speechflow-node-t2t-format.js",
199
236
  "./speechflow-node-x2x-trace.js",
@@ -237,9 +274,9 @@ let cli: CLIio | null = null
237
274
  cli!.log("debug", msg)
238
275
  }
239
276
  })
240
- let nodenum = 1
241
277
  const variables = { argv: args._, env: process.env }
242
278
  const graphNodes = new Set<SpeechFlowNode>()
279
+ const nodeNums = new Map<typeof SpeechFlowNode, number>()
243
280
  const cfg = {
244
281
  audioChannels: 1,
245
282
  audioBitDepth: 16,
@@ -275,17 +312,19 @@ let cli: CLIio | null = null
275
312
  throw new Error(`unknown node "${id}"`)
276
313
  let node: SpeechFlowNode
277
314
  try {
278
- node = new nodes[id](`${id}[${nodenum}]`, cfg, opts, args)
315
+ let num = nodeNums.get(nodes[id]) ?? 0
316
+ nodeNums.set(nodes[id], ++num)
317
+ const name = num === 1 ? id : `${id}:${num}`
318
+ node = new nodes[id](name, cfg, opts, args)
279
319
  }
280
320
  catch (err) {
281
321
  /* fatal error */
282
322
  if (err instanceof Error)
283
- cli!.log("error", `creation of "${id}[${nodenum}]" node failed: ${err.message}`)
323
+ cli!.log("error", `creation of <${id}> node failed: ${err.message}`)
284
324
  else
285
- cli!.log("error", `creation of "${id}"[${nodenum}] node failed: ${err}`)
325
+ cli!.log("error", `creation of <${id}> node failed: ${err}`)
286
326
  process.exit(1)
287
327
  }
288
- nodenum++
289
328
  const params = Object.keys(node.params)
290
329
  .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
291
330
  cli!.log("info", `create node "${node.id}" (${params})`)
@@ -403,6 +442,150 @@ let cli: CLIio | null = null
403
442
  })
404
443
  }
405
444
 
445
+ /* define external request/response structure */
446
+ const requestValidator = arktype.type({
447
+ request: "string",
448
+ node: "string",
449
+ args: "unknown[]"
450
+ })
451
+
452
+ /* forward external request to target node in graph */
453
+ const consumeExternalRequest = async (_req: any) => {
454
+ const req = requestValidator(_req)
455
+ if (req instanceof arktype.type.errors)
456
+ throw new Error(`invalid request: ${req.summary}`)
457
+ if (req.request !== "COMMAND")
458
+ throw new Error("invalid external request (command expected)")
459
+ const name = req.node as string
460
+ const args = req.args as any[]
461
+ const foundNode = Array.from(graphNodes).find((node) => node.id === name)
462
+ if (foundNode === undefined)
463
+ cli!.log("warning", `external request failed: no such node <${name}>`)
464
+ else {
465
+ await foundNode.receiveRequest(args).catch((err: Error) => {
466
+ cli!.log("warning", `external request to node <${name}> failed: ${err}`)
467
+ throw new Error(`external request to node <${name}> failed: ${err}`)
468
+ })
469
+ }
470
+ }
471
+
472
+ /* establish REST/WebSocket API */
473
+ const wsPeers = new Map<string, wsPeerInfo>()
474
+ const hapi = new HAPI.Server({
475
+ address: args.a,
476
+ port: args.p
477
+ })
478
+ await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
479
+ await hapi.register({ plugin: HAPIWebSocket })
480
+ hapi.events.on("response", (request: HAPI.Request) => {
481
+ let protocol = `HTTP/${request.raw.req.httpVersion}`
482
+ const ws = request.websocket()
483
+ if (ws.mode === "websocket") {
484
+ const wsVersion = (ws.ws as any).protocolVersion ??
485
+ request.headers["sec-websocket-version"] ?? "13?"
486
+ protocol = `WebSocket/${wsVersion}+${protocol}`
487
+ }
488
+ const msg =
489
+ "remote=" + request.info.remoteAddress + ", " +
490
+ "method=" + request.method.toUpperCase() + ", " +
491
+ "url=" + request.url.pathname + ", " +
492
+ "protocol=" + protocol + ", " +
493
+ "response=" + ("statusCode" in request.response ? request.response.statusCode : "<unknown>")
494
+ cli!.log("info", `HAPI: request: ${msg}`)
495
+ })
496
+ hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
497
+ if (event.error instanceof Error)
498
+ cli!.log("error", `HAPI: request-error: ${event.error.message}`)
499
+ else
500
+ cli!.log("error", `HAPI: request-error: ${event.error}`)
501
+ })
502
+ hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
503
+ if (tags.error) {
504
+ const err = event.error
505
+ if (err instanceof Error)
506
+ cli!.log("error", `HAPI: log: ${err.message}`)
507
+ else
508
+ cli!.log("error", `HAPI: log: ${err}`)
509
+ }
510
+ })
511
+ hapi.route({
512
+ method: "GET",
513
+ path: "/api/{req}/{node}/{params*}",
514
+ options: {
515
+ },
516
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
517
+ const peer = request.info.remoteAddress
518
+ const req = {
519
+ request: request.params.req,
520
+ node: request.params.node,
521
+ args: (request.params.params as string ?? "").split("/").filter((seg) => seg !== "")
522
+ }
523
+ cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
524
+ return consumeExternalRequest(req).then(() => {
525
+ return h.response({ response: "OK" }).code(200)
526
+ }).catch((err) => {
527
+ return h.response({ response: "ERROR", data: err.message }).code(417)
528
+ })
529
+ }
530
+ })
531
+ hapi.route({
532
+ method: "POST",
533
+ path: "/api",
534
+ options: {
535
+ payload: {
536
+ output: "data",
537
+ parse: true,
538
+ allow: "application/json"
539
+ },
540
+ plugins: {
541
+ websocket: {
542
+ autoping: 30 * 1000,
543
+ connect: (args: any) => {
544
+ const ctx: wsPeerCtx = args.ctx
545
+ const ws: WebSocket = args.ws
546
+ const req: http.IncomingMessage = args.req
547
+ const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
548
+ ctx.peer = peer
549
+ wsPeers.set(peer, { ctx, ws, req })
550
+ cli!.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
551
+ },
552
+ disconnect: (args: any) => {
553
+ const ctx: wsPeerCtx = args.ctx
554
+ const peer = ctx.peer
555
+ wsPeers.delete(peer)
556
+ cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
557
+ }
558
+ }
559
+ }
560
+ },
561
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
562
+ /* on WebSocket message transfer */
563
+ const peer = request.info.remoteAddress
564
+ const req = requestValidator(request.payload)
565
+ if (req instanceof arktype.type.errors)
566
+ return h.response({ response: "ERROR", data: `invalid request: ${req.summary}` }).code(417)
567
+ cli!.log("info", `HAPI: peer ${peer}: POST: ${JSON.stringify(req)}`)
568
+ return consumeExternalRequest(req).then(() => {
569
+ return h.response({ response: "OK" }).code(200)
570
+ }).catch((err: Error) => {
571
+ return h.response({ response: "ERROR", data: err.message }).code(417)
572
+ })
573
+ }
574
+ })
575
+ await hapi.start()
576
+ cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.address}:${args.port}`)
577
+
578
+ /* hook for sendResponse method of nodes */
579
+ for (const node of graphNodes) {
580
+ node.on("send-response", (args: any[]) => {
581
+ const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
582
+ for (const [ peer, info ] of wsPeers.entries()) {
583
+ cli!.log("info", `HAPI: peer ${peer}: ${data}`)
584
+ info.ws.send(data)
585
+ }
586
+ })
587
+ }
588
+
406
589
  /* start of internal stream processing */
407
590
  cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
408
591
 
@@ -417,6 +600,10 @@ let cli: CLIio | null = null
417
600
  else
418
601
  cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
419
602
 
603
+ /* shutdown HAPI service */
604
+ cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
605
+ await hapi.stop()
606
+
420
607
  /* graph processing: PASS 1: disconnect node streams */
421
608
  for (const node of graphNodes) {
422
609
  if (node.stream === null) {
@@ -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
- }