speechflow 0.9.8 → 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 (114) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE.txt +674 -0
  3. package/README.md +114 -17
  4. package/dst/speechflow-node-a2a-ffmpeg.js +1 -0
  5. package/dst/speechflow-node-a2a-ffmpeg.js.map +1 -0
  6. package/dst/{speechflow-node-deepl.d.ts → speechflow-node-a2a-meter.d.ts} +2 -2
  7. package/dst/speechflow-node-a2a-meter.js +147 -0
  8. package/dst/speechflow-node-a2a-meter.js.map +1 -0
  9. package/dst/speechflow-node-a2a-mute.d.ts +16 -0
  10. package/dst/speechflow-node-a2a-mute.js +90 -0
  11. package/dst/speechflow-node-a2a-mute.js.map +1 -0
  12. package/dst/{speechflow-node-whisper.d.ts → speechflow-node-a2a-vad.d.ts} +2 -5
  13. package/dst/speechflow-node-a2a-vad.js +272 -0
  14. package/dst/speechflow-node-a2a-vad.js.map +1 -0
  15. package/dst/speechflow-node-a2a-wav.js +1 -0
  16. package/dst/speechflow-node-a2a-wav.js.map +1 -0
  17. package/dst/speechflow-node-a2t-deepgram.js +2 -1
  18. package/dst/speechflow-node-a2t-deepgram.js.map +1 -0
  19. package/dst/speechflow-node-t2a-elevenlabs.js +1 -0
  20. package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -0
  21. package/dst/{speechflow-node-elevenlabs.d.ts → speechflow-node-t2a-kokoro.d.ts} +2 -2
  22. package/dst/speechflow-node-t2a-kokoro.js +148 -0
  23. package/dst/speechflow-node-t2a-kokoro.js.map +1 -0
  24. package/dst/speechflow-node-t2t-deepl.js +1 -0
  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-gemma.d.ts → speechflow-node-t2t-ollama.d.ts} +1 -1
  29. package/dst/{speechflow-node-gemma.js → speechflow-node-t2t-ollama.js} +41 -8
  30. package/dst/speechflow-node-t2t-ollama.js.map +1 -0
  31. package/dst/{speechflow-node-t2t-gemma.d.ts → speechflow-node-t2t-openai.d.ts} +2 -2
  32. package/dst/{speechflow-node-t2t-gemma.js → speechflow-node-t2t-openai.js} +43 -30
  33. package/dst/speechflow-node-t2t-openai.js.map +1 -0
  34. package/dst/speechflow-node-t2t-subtitle.js +1 -0
  35. package/dst/speechflow-node-t2t-subtitle.js.map +1 -0
  36. package/dst/{speechflow-node-opus.d.ts → speechflow-node-t2t-transformers.d.ts} +3 -1
  37. package/dst/speechflow-node-t2t-transformers.js +264 -0
  38. package/dst/speechflow-node-t2t-transformers.js.map +1 -0
  39. package/dst/speechflow-node-x2x-trace.js +3 -2
  40. package/dst/speechflow-node-x2x-trace.js.map +1 -0
  41. package/dst/speechflow-node-xio-device.js +1 -0
  42. package/dst/speechflow-node-xio-device.js.map +1 -0
  43. package/dst/speechflow-node-xio-file.js +1 -0
  44. package/dst/speechflow-node-xio-file.js.map +1 -0
  45. package/dst/speechflow-node-xio-mqtt.js +1 -0
  46. package/dst/speechflow-node-xio-mqtt.js.map +1 -0
  47. package/dst/speechflow-node-xio-websocket.js +1 -0
  48. package/dst/speechflow-node-xio-websocket.js.map +1 -0
  49. package/dst/speechflow-node.d.ts +3 -0
  50. package/dst/speechflow-node.js +10 -0
  51. package/dst/speechflow-node.js.map +1 -0
  52. package/dst/speechflow-utils.d.ts +33 -0
  53. package/dst/speechflow-utils.js +183 -1
  54. package/dst/speechflow-utils.js.map +1 -0
  55. package/dst/speechflow.js +295 -46
  56. package/dst/speechflow.js.map +1 -0
  57. package/etc/speechflow.yaml +14 -5
  58. package/etc/stx.conf +1 -1
  59. package/etc/tsconfig.json +2 -2
  60. package/package.json +17 -10
  61. package/src/speechflow-node-a2a-meter.ts +125 -0
  62. package/src/speechflow-node-a2a-mute.ts +101 -0
  63. package/src/speechflow-node-a2a-vad.ts +266 -0
  64. package/src/speechflow-node-a2t-deepgram.ts +1 -1
  65. package/src/speechflow-node-t2a-kokoro.ts +160 -0
  66. package/src/{speechflow-node-t2t-gemma.ts → speechflow-node-t2t-ollama.ts} +44 -10
  67. package/src/speechflow-node-t2t-openai.ts +246 -0
  68. package/src/speechflow-node-t2t-transformers.ts +249 -0
  69. package/src/speechflow-node-x2x-trace.ts +2 -2
  70. package/src/speechflow-node-xio-websocket.ts +5 -5
  71. package/src/speechflow-node.ts +12 -0
  72. package/src/speechflow-utils.ts +195 -0
  73. package/src/speechflow.ts +279 -46
  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.js +0 -128
  77. package/dst/speechflow-node-device.d.ts +0 -13
  78. package/dst/speechflow-node-device.js +0 -205
  79. package/dst/speechflow-node-elevenlabs.js +0 -182
  80. package/dst/speechflow-node-ffmpeg.d.ts +0 -13
  81. package/dst/speechflow-node-ffmpeg.js +0 -152
  82. package/dst/speechflow-node-file.d.ts +0 -11
  83. package/dst/speechflow-node-file.js +0 -176
  84. package/dst/speechflow-node-format.d.ts +0 -11
  85. package/dst/speechflow-node-format.js +0 -80
  86. package/dst/speechflow-node-mqtt.d.ts +0 -13
  87. package/dst/speechflow-node-mqtt.js +0 -181
  88. package/dst/speechflow-node-opus.js +0 -135
  89. package/dst/speechflow-node-subtitle.d.ts +0 -12
  90. package/dst/speechflow-node-subtitle.js +0 -96
  91. package/dst/speechflow-node-t2t-opus.d.ts +0 -12
  92. package/dst/speechflow-node-t2t-opus.js +0 -135
  93. package/dst/speechflow-node-trace.d.ts +0 -11
  94. package/dst/speechflow-node-trace.js +0 -88
  95. package/dst/speechflow-node-wav.d.ts +0 -11
  96. package/dst/speechflow-node-wav.js +0 -170
  97. package/dst/speechflow-node-websocket.d.ts +0 -13
  98. package/dst/speechflow-node-websocket.js +0 -275
  99. package/dst/speechflow-node-whisper-common.d.ts +0 -34
  100. package/dst/speechflow-node-whisper-common.js +0 -7
  101. package/dst/speechflow-node-whisper-ggml.d.ts +0 -1
  102. package/dst/speechflow-node-whisper-ggml.js +0 -97
  103. package/dst/speechflow-node-whisper-onnx.d.ts +0 -1
  104. package/dst/speechflow-node-whisper-onnx.js +0 -131
  105. package/dst/speechflow-node-whisper-worker-ggml.d.ts +0 -1
  106. package/dst/speechflow-node-whisper-worker-ggml.js +0 -97
  107. package/dst/speechflow-node-whisper-worker-onnx.d.ts +0 -1
  108. package/dst/speechflow-node-whisper-worker-onnx.js +0 -131
  109. package/dst/speechflow-node-whisper-worker.d.ts +0 -1
  110. package/dst/speechflow-node-whisper-worker.js +0 -116
  111. package/dst/speechflow-node-whisper-worker2.d.ts +0 -1
  112. package/dst/speechflow-node-whisper-worker2.js +0 -82
  113. package/dst/speechflow-node-whisper.js +0 -604
  114. package/src/speechflow-node-t2t-opus.ts +0 -111
package/src/speechflow.ts CHANGED
@@ -8,17 +8,24 @@
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"
14
19
  import CLIio from "cli-io"
15
20
  import yargs from "yargs"
21
+ import { hideBin } from "yargs/helpers"
16
22
  import jsYAML from "js-yaml"
17
23
  import FlowLink from "flowlink"
18
24
  import objectPath from "object-path"
19
25
  import installedPackages from "installed-packages"
20
26
  import dotenvx from "@dotenvx/dotenvx"
21
27
  import syspath from "syspath"
28
+ import * as arktype from "arktype"
22
29
 
23
30
  /* internal dependencies */
24
31
  import SpeechFlowNode from "./speechflow-node"
@@ -27,6 +34,15 @@ import pkg from "../package.json"
27
34
  /* central CLI context */
28
35
  let cli: CLIio | null = null
29
36
 
37
+ type wsPeerCtx = {
38
+ peer: string
39
+ }
40
+ type wsPeerInfo = {
41
+ ctx: wsPeerCtx
42
+ ws: WebSocket
43
+ req: http.IncomingMessage
44
+ }
45
+
30
46
  /* establish asynchronous environment */
31
47
  ;(async () => {
32
48
  /* determine system paths */
@@ -36,6 +52,7 @@ let cli: CLIio | null = null
36
52
  })
37
53
 
38
54
  /* parse command-line arguments */
55
+ const coerce = (arg: string) => Array.isArray(arg) ? arg[arg.length - 1] : arg
39
56
  const args = await yargs()
40
57
  /* eslint @stylistic/indent: off */
41
58
  .usage(
@@ -43,34 +60,95 @@ let cli: CLIio | null = null
43
60
  "[-h|--help] " +
44
61
  "[-V|--version] " +
45
62
  "[-v|--verbose <level>] " +
63
+ "[-a|--address <ip-address>] " +
64
+ "[-p|--port <tcp-port>] " +
46
65
  "[-C|--cache <directory>] " +
47
66
  "[-e|--expression <expression>] " +
48
67
  "[-f|--file <file>] " +
49
68
  "[-c|--config <id>@<yaml-config-file>] " +
50
69
  "[<argument> [...]]"
51
70
  )
52
- .help("h").alias("h", "help").default("h", false)
53
- .describe("h", "show usage help")
54
- .boolean("V").alias("V", "version").default("V", false)
55
- .describe("V", "show program version information")
56
- .string("v").nargs("v", 1).alias("v", "log-level").default("v", "warning")
57
- .describe("v", "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')")
58
- .string("C").nargs("C", 1).alias("C", "cache").default("C", path.join(dataDir, "cache"))
59
- .describe("C", "directory for cached files (primarily AI model files)")
60
- .string("e").nargs("e", 1).alias("e", "expression").default("e", "")
61
- .describe("e", "FlowLink expression string")
62
- .string("f").nargs("f", 1).alias("f", "file").default("f", "")
63
- .describe("f", "FlowLink expression file")
64
- .string("c").nargs("c", 1).alias("c", "config-file").default("c", "")
65
- .describe("c", "FlowLink expression reference into YAML file (in format <id>@<file>)")
71
+ .option("V", {
72
+ alias: "version",
73
+ type: "boolean",
74
+ array: false,
75
+ coerce,
76
+ default: false,
77
+ describe: "show program version information"
78
+ })
79
+ .option("v", {
80
+ alias: "log-level",
81
+ type: "string",
82
+ array: false,
83
+ coerce,
84
+ nargs: 1,
85
+ default: "warning",
86
+ describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
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
+ })
106
+ .option("C", {
107
+ alias: "cache",
108
+ type: "string",
109
+ array: false,
110
+ coerce,
111
+ nargs: 1,
112
+ default: path.join(dataDir, "cache"),
113
+ describe: "directory for cached files (primarily AI model files)"
114
+ })
115
+ .option("e", {
116
+ alias: "expression",
117
+ type: "string",
118
+ array: false,
119
+ coerce,
120
+ nargs: 1,
121
+ default: "",
122
+ describe: "FlowLink expression string"
123
+ })
124
+ .option("f", {
125
+ alias: "file",
126
+ type: "string",
127
+ array: false,
128
+ coerce,
129
+ nargs: 1,
130
+ default: "",
131
+ describe: "FlowLink expression file"
132
+ })
133
+ .option("c", {
134
+ alias: "config",
135
+ type: "string",
136
+ array: false,
137
+ coerce,
138
+ nargs: 1,
139
+ default: "",
140
+ describe: "FlowLink expression reference into YAML file (in format <id>@<file>)"
141
+ })
142
+ .help("h", "show usage help")
143
+ .alias("h", "help")
144
+ .showHelpOnFail(true)
66
145
  .version(false)
67
146
  .strict()
68
- .showHelpOnFail(true)
69
147
  .demand(0)
70
- .parse(process.argv.slice(2))
148
+ .parse(hideBin(process.argv))
71
149
 
72
150
  /* short-circuit version request */
73
- if (args.version) {
151
+ if (args.V) {
74
152
  process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
75
153
  process.stderr.write(`${pkg.description}\n`)
76
154
  process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
@@ -81,7 +159,7 @@ let cli: CLIio | null = null
81
159
  /* establish CLI environment */
82
160
  cli = new CLIio({
83
161
  encoding: "utf8",
84
- logLevel: args.logLevel,
162
+ logLevel: args.v,
85
163
  logTime: true,
86
164
  logPrefix: pkg.name
87
165
  })
@@ -112,28 +190,28 @@ let cli: CLIio | null = null
112
190
 
113
191
  /* sanity check usage */
114
192
  let n = 0
115
- if (typeof args.expression === "string" && args.expression !== "") n++
116
- if (typeof args.expressionFile === "string" && args.expressionFile !== "") n++
117
- if (typeof args.configFile === "string" && args.configFile !== "") n++
193
+ if (typeof args.e === "string" && args.e !== "") n++
194
+ if (typeof args.f === "string" && args.f !== "") n++
195
+ if (typeof args.c === "string" && args.c !== "") n++
118
196
  if (n !== 1)
119
197
  throw new Error("cannot use more than one FlowLink specification source (either option -e, -f or -c)")
120
198
 
121
199
  /* read configuration */
122
200
  let config = ""
123
- if (typeof args.expression === "string" && args.expression !== "")
124
- config = args.expression
125
- else if (typeof args.expressionFile === "string" && args.expressionFile !== "")
126
- config = await cli.input(args.expressionFile, { encoding: "utf8" })
127
- else if (typeof args.configFile === "string" && args.configFile !== "") {
128
- const m = args.configFile.match(/^(.+?)@(.+)$/)
201
+ if (typeof args.e === "string" && args.e !== "")
202
+ config = args.e
203
+ else if (typeof args.f === "string" && args.f !== "")
204
+ config = await cli.input(args.f, { encoding: "utf8" })
205
+ else if (typeof args.c === "string" && args.c !== "") {
206
+ const m = args.c.match(/^(.+?)@(.+)$/)
129
207
  if (m === null)
130
- throw new Error("invalid configuration file specification (expected \"<key>@<yaml-config-file>\")")
131
- const [ , key, file ] = m
208
+ throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
209
+ const [ , id, file ] = m
132
210
  const yaml = await cli.input(file, { encoding: "utf8" })
133
211
  const obj: any = jsYAML.load(yaml)
134
- if (obj[key] === undefined)
135
- throw new Error(`no such key "${key}" found in configuration file`)
136
- config = obj[key] as string
212
+ if (obj[id] === undefined)
213
+ throw new Error(`no such id "${id}" found in configuration file`)
214
+ config = obj[id] as string
137
215
  }
138
216
 
139
217
  /* track the available SpeechFlow nodes */
@@ -143,13 +221,18 @@ let cli: CLIio | null = null
143
221
  const pkgsI = [
144
222
  "./speechflow-node-a2a-ffmpeg.js",
145
223
  "./speechflow-node-a2a-wav.js",
224
+ "./speechflow-node-a2a-mute.js",
225
+ "./speechflow-node-a2a-meter.js",
226
+ "./speechflow-node-a2a-vad.js",
146
227
  "./speechflow-node-a2t-deepgram.js",
147
228
  "./speechflow-node-t2a-elevenlabs.js",
229
+ "./speechflow-node-t2a-kokoro.js",
148
230
  "./speechflow-node-t2t-deepl.js",
149
- "./speechflow-node-t2t-format.js",
150
- "./speechflow-node-t2t-gemma.js",
151
- "./speechflow-node-t2t-opus.js",
231
+ "./speechflow-node-t2t-openai.js",
232
+ "./speechflow-node-t2t-ollama.js",
233
+ "./speechflow-node-t2t-transformers.js",
152
234
  "./speechflow-node-t2t-subtitle.js",
235
+ "./speechflow-node-t2t-format.js",
153
236
  "./speechflow-node-x2x-trace.js",
154
237
  "./speechflow-node-xio-device.js",
155
238
  "./speechflow-node-xio-file.js",
@@ -191,16 +274,16 @@ let cli: CLIio | null = null
191
274
  cli!.log("debug", msg)
192
275
  }
193
276
  })
194
- let nodenum = 1
195
277
  const variables = { argv: args._, env: process.env }
196
278
  const graphNodes = new Set<SpeechFlowNode>()
279
+ const nodeNums = new Map<typeof SpeechFlowNode, number>()
197
280
  const cfg = {
198
281
  audioChannels: 1,
199
282
  audioBitDepth: 16,
200
283
  audioLittleEndian: true,
201
284
  audioSampleRate: 48000,
202
285
  textEncoding: "utf8",
203
- cacheDir: args.cache
286
+ cacheDir: args.C
204
287
  }
205
288
  let ast: unknown
206
289
  try {
@@ -229,17 +312,19 @@ let cli: CLIio | null = null
229
312
  throw new Error(`unknown node "${id}"`)
230
313
  let node: SpeechFlowNode
231
314
  try {
232
- 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)
233
319
  }
234
320
  catch (err) {
235
321
  /* fatal error */
236
322
  if (err instanceof Error)
237
- cli!.log("error", `creation of "${id}[${nodenum}]" node failed: ${err.message}`)
323
+ cli!.log("error", `creation of <${id}> node failed: ${err.message}`)
238
324
  else
239
- cli!.log("error", `creation of "${id}"[${nodenum}] node failed: ${err}`)
325
+ cli!.log("error", `creation of <${id}> node failed: ${err}`)
240
326
  process.exit(1)
241
327
  }
242
- nodenum++
243
328
  const params = Object.keys(node.params)
244
329
  .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
245
330
  cli!.log("info", `create node "${node.id}" (${params})`)
@@ -254,9 +339,9 @@ let cli: CLIio | null = null
254
339
  }
255
340
  catch (err) {
256
341
  if (err instanceof Error && err.name === "FlowLinkError")
257
- cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.toString()}"`)
342
+ cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.toString()}`)
258
343
  else if (err instanceof Error)
259
- cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.message}"`)
344
+ cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.message}`)
260
345
  else
261
346
  cli!.log("error", "failed to materialize SpeechFlow configuration: internal error")
262
347
  process.exit(1)
@@ -357,8 +442,152 @@ let cli: CLIio | null = null
357
442
  })
358
443
  }
359
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
+
360
589
  /* start of internal stream processing */
361
- cli!.log("info", "everything established -- stream processing in SpeechFlow graph starts")
590
+ cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
362
591
 
363
592
  /* gracefully shutdown process */
364
593
  let shuttingDown = false
@@ -367,9 +596,13 @@ let cli: CLIio | null = null
367
596
  return
368
597
  shuttingDown = true
369
598
  if (signal === "finished")
370
- cli!.log("info", "streams of all nodes finished -- shutting down service")
599
+ cli!.log("info", "**** streams of all nodes finished -- shutting down service ****")
371
600
  else
372
- cli!.log("warning", `received signal ${signal} -- shutting down service`)
601
+ cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
602
+
603
+ /* shutdown HAPI service */
604
+ cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
605
+ await hapi.stop()
373
606
 
374
607
  /* graph processing: PASS 1: disconnect node streams */
375
608
  for (const node of graphNodes) {
@@ -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
- }