speechflow 1.3.1 → 1.4.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 (156) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/etc/stx.conf +54 -58
  3. package/package.json +25 -106
  4. package/{etc → speechflow-cli/etc}/eslint.mjs +1 -2
  5. package/speechflow-cli/etc/stx.conf +77 -0
  6. package/speechflow-cli/package.json +116 -0
  7. package/{src → speechflow-cli/src}/speechflow-node-a2a-gender.ts +148 -64
  8. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +217 -0
  9. package/{src → speechflow-cli/src}/speechflow-node-a2a-mute.ts +39 -11
  10. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +384 -0
  11. package/{src → speechflow-cli/src}/speechflow-node-a2a-wav.ts +27 -11
  12. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +313 -0
  13. package/{src → speechflow-cli/src}/speechflow-node-t2a-elevenlabs.ts +59 -12
  14. package/{src → speechflow-cli/src}/speechflow-node-t2a-kokoro.ts +11 -4
  15. package/{src → speechflow-cli/src}/speechflow-node-t2t-deepl.ts +9 -4
  16. package/{src → speechflow-cli/src}/speechflow-node-t2t-format.ts +2 -2
  17. package/{src → speechflow-cli/src}/speechflow-node-t2t-ollama.ts +1 -1
  18. package/{src → speechflow-cli/src}/speechflow-node-t2t-openai.ts +1 -1
  19. package/{src → speechflow-cli/src}/speechflow-node-t2t-sentence.ts +37 -20
  20. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +276 -0
  21. package/{src → speechflow-cli/src}/speechflow-node-t2t-transformers.ts +4 -3
  22. package/{src → speechflow-cli/src}/speechflow-node-x2x-filter.ts +9 -5
  23. package/{src → speechflow-cli/src}/speechflow-node-x2x-trace.ts +16 -8
  24. package/{src → speechflow-cli/src}/speechflow-node-xio-device.ts +12 -8
  25. package/{src → speechflow-cli/src}/speechflow-node-xio-file.ts +9 -3
  26. package/{src → speechflow-cli/src}/speechflow-node-xio-mqtt.ts +5 -2
  27. package/{src → speechflow-cli/src}/speechflow-node-xio-websocket.ts +12 -12
  28. package/{src → speechflow-cli/src}/speechflow-node.ts +7 -0
  29. package/{src → speechflow-cli/src}/speechflow-utils.ts +78 -44
  30. package/{src → speechflow-cli/src}/speechflow.ts +188 -53
  31. package/speechflow-ui-db/etc/eslint.mjs +106 -0
  32. package/speechflow-ui-db/etc/htmllint.json +55 -0
  33. package/speechflow-ui-db/etc/stx.conf +79 -0
  34. package/speechflow-ui-db/etc/stylelint.js +46 -0
  35. package/speechflow-ui-db/etc/stylelint.yaml +33 -0
  36. package/speechflow-ui-db/etc/tsc-client.json +30 -0
  37. package/speechflow-ui-db/etc/tsc.node.json +9 -0
  38. package/speechflow-ui-db/etc/vite-client.mts +63 -0
  39. package/speechflow-ui-db/package.d/htmllint-cli+0.0.7.patch +20 -0
  40. package/speechflow-ui-db/package.json +75 -0
  41. package/speechflow-ui-db/src/app-icon.ai +1989 -4
  42. package/speechflow-ui-db/src/app-icon.svg +26 -0
  43. package/speechflow-ui-db/src/app.styl +64 -0
  44. package/speechflow-ui-db/src/app.vue +221 -0
  45. package/speechflow-ui-db/src/index.html +23 -0
  46. package/speechflow-ui-db/src/index.ts +26 -0
  47. package/{dst/speechflow.d.ts → speechflow-ui-db/src/lib.d.ts} +5 -3
  48. package/speechflow-ui-db/src/tsconfig.json +3 -0
  49. package/speechflow-ui-st/etc/eslint.mjs +106 -0
  50. package/speechflow-ui-st/etc/htmllint.json +55 -0
  51. package/speechflow-ui-st/etc/stx.conf +79 -0
  52. package/speechflow-ui-st/etc/stylelint.js +46 -0
  53. package/speechflow-ui-st/etc/stylelint.yaml +33 -0
  54. package/speechflow-ui-st/etc/tsc-client.json +30 -0
  55. package/speechflow-ui-st/etc/tsc.node.json +9 -0
  56. package/speechflow-ui-st/etc/vite-client.mts +63 -0
  57. package/speechflow-ui-st/package.d/htmllint-cli+0.0.7.patch +20 -0
  58. package/speechflow-ui-st/package.json +79 -0
  59. package/speechflow-ui-st/src/app-icon.ai +1989 -4
  60. package/speechflow-ui-st/src/app-icon.svg +26 -0
  61. package/speechflow-ui-st/src/app.styl +64 -0
  62. package/speechflow-ui-st/src/app.vue +142 -0
  63. package/speechflow-ui-st/src/index.html +23 -0
  64. package/speechflow-ui-st/src/index.ts +26 -0
  65. package/speechflow-ui-st/src/lib.d.ts +9 -0
  66. package/speechflow-ui-st/src/tsconfig.json +3 -0
  67. package/dst/speechflow-node-a2a-ffmpeg.d.ts +0 -13
  68. package/dst/speechflow-node-a2a-ffmpeg.js +0 -153
  69. package/dst/speechflow-node-a2a-ffmpeg.js.map +0 -1
  70. package/dst/speechflow-node-a2a-gender.d.ts +0 -18
  71. package/dst/speechflow-node-a2a-gender.js +0 -271
  72. package/dst/speechflow-node-a2a-gender.js.map +0 -1
  73. package/dst/speechflow-node-a2a-meter.d.ts +0 -12
  74. package/dst/speechflow-node-a2a-meter.js +0 -155
  75. package/dst/speechflow-node-a2a-meter.js.map +0 -1
  76. package/dst/speechflow-node-a2a-mute.d.ts +0 -16
  77. package/dst/speechflow-node-a2a-mute.js +0 -91
  78. package/dst/speechflow-node-a2a-mute.js.map +0 -1
  79. package/dst/speechflow-node-a2a-vad.d.ts +0 -16
  80. package/dst/speechflow-node-a2a-vad.js +0 -285
  81. package/dst/speechflow-node-a2a-vad.js.map +0 -1
  82. package/dst/speechflow-node-a2a-wav.d.ts +0 -11
  83. package/dst/speechflow-node-a2a-wav.js +0 -195
  84. package/dst/speechflow-node-a2a-wav.js.map +0 -1
  85. package/dst/speechflow-node-a2t-deepgram.d.ts +0 -15
  86. package/dst/speechflow-node-a2t-deepgram.js +0 -255
  87. package/dst/speechflow-node-a2t-deepgram.js.map +0 -1
  88. package/dst/speechflow-node-t2a-elevenlabs.d.ts +0 -16
  89. package/dst/speechflow-node-t2a-elevenlabs.js +0 -195
  90. package/dst/speechflow-node-t2a-elevenlabs.js.map +0 -1
  91. package/dst/speechflow-node-t2a-kokoro.d.ts +0 -13
  92. package/dst/speechflow-node-t2a-kokoro.js +0 -149
  93. package/dst/speechflow-node-t2a-kokoro.js.map +0 -1
  94. package/dst/speechflow-node-t2t-deepl.d.ts +0 -15
  95. package/dst/speechflow-node-t2t-deepl.js +0 -142
  96. package/dst/speechflow-node-t2t-deepl.js.map +0 -1
  97. package/dst/speechflow-node-t2t-format.d.ts +0 -11
  98. package/dst/speechflow-node-t2t-format.js +0 -82
  99. package/dst/speechflow-node-t2t-format.js.map +0 -1
  100. package/dst/speechflow-node-t2t-ollama.d.ts +0 -13
  101. package/dst/speechflow-node-t2t-ollama.js +0 -247
  102. package/dst/speechflow-node-t2t-ollama.js.map +0 -1
  103. package/dst/speechflow-node-t2t-openai.d.ts +0 -13
  104. package/dst/speechflow-node-t2t-openai.js +0 -227
  105. package/dst/speechflow-node-t2t-openai.js.map +0 -1
  106. package/dst/speechflow-node-t2t-sentence.d.ts +0 -17
  107. package/dst/speechflow-node-t2t-sentence.js +0 -234
  108. package/dst/speechflow-node-t2t-sentence.js.map +0 -1
  109. package/dst/speechflow-node-t2t-subtitle.d.ts +0 -13
  110. package/dst/speechflow-node-t2t-subtitle.js +0 -278
  111. package/dst/speechflow-node-t2t-subtitle.js.map +0 -1
  112. package/dst/speechflow-node-t2t-transformers.d.ts +0 -14
  113. package/dst/speechflow-node-t2t-transformers.js +0 -265
  114. package/dst/speechflow-node-t2t-transformers.js.map +0 -1
  115. package/dst/speechflow-node-x2x-filter.d.ts +0 -11
  116. package/dst/speechflow-node-x2x-filter.js +0 -117
  117. package/dst/speechflow-node-x2x-filter.js.map +0 -1
  118. package/dst/speechflow-node-x2x-trace.d.ts +0 -11
  119. package/dst/speechflow-node-x2x-trace.js +0 -111
  120. package/dst/speechflow-node-x2x-trace.js.map +0 -1
  121. package/dst/speechflow-node-xio-device.d.ts +0 -13
  122. package/dst/speechflow-node-xio-device.js +0 -226
  123. package/dst/speechflow-node-xio-device.js.map +0 -1
  124. package/dst/speechflow-node-xio-file.d.ts +0 -11
  125. package/dst/speechflow-node-xio-file.js +0 -210
  126. package/dst/speechflow-node-xio-file.js.map +0 -1
  127. package/dst/speechflow-node-xio-mqtt.d.ts +0 -13
  128. package/dst/speechflow-node-xio-mqtt.js +0 -185
  129. package/dst/speechflow-node-xio-mqtt.js.map +0 -1
  130. package/dst/speechflow-node-xio-websocket.d.ts +0 -13
  131. package/dst/speechflow-node-xio-websocket.js +0 -278
  132. package/dst/speechflow-node-xio-websocket.js.map +0 -1
  133. package/dst/speechflow-node.d.ts +0 -65
  134. package/dst/speechflow-node.js +0 -180
  135. package/dst/speechflow-node.js.map +0 -1
  136. package/dst/speechflow-utils.d.ts +0 -69
  137. package/dst/speechflow-utils.js +0 -486
  138. package/dst/speechflow-utils.js.map +0 -1
  139. package/dst/speechflow.js +0 -768
  140. package/dst/speechflow.js.map +0 -1
  141. package/src/speechflow-node-a2a-meter.ts +0 -130
  142. package/src/speechflow-node-a2a-vad.ts +0 -285
  143. package/src/speechflow-node-a2t-deepgram.ts +0 -234
  144. package/src/speechflow-node-t2t-subtitle.ts +0 -149
  145. /package/{etc → speechflow-cli/etc}/biome.jsonc +0 -0
  146. /package/{etc → speechflow-cli/etc}/oxlint.jsonc +0 -0
  147. /package/{etc → speechflow-cli/etc}/speechflow.bat +0 -0
  148. /package/{etc → speechflow-cli/etc}/speechflow.sh +0 -0
  149. /package/{etc → speechflow-cli/etc}/speechflow.yaml +0 -0
  150. /package/{etc → speechflow-cli/etc}/tsconfig.json +0 -0
  151. /package/{package.d → speechflow-cli/package.d}/@ericedouard+vad-node-realtime+0.2.0.patch +0 -0
  152. /package/{src → speechflow-cli/src}/lib.d.ts +0 -0
  153. /package/{src → speechflow-cli/src}/speechflow-logo.ai +0 -0
  154. /package/{src → speechflow-cli/src}/speechflow-logo.svg +0 -0
  155. /package/{src → speechflow-cli/src}/speechflow-node-a2a-ffmpeg.ts +0 -0
  156. /package/{tsconfig.json → speechflow-cli/tsconfig.json} +0 -0
@@ -11,6 +11,7 @@ import Stream from "node:stream"
11
11
  import { EventEmitter } from "node:events"
12
12
  import http from "node:http"
13
13
  import * as HAPI from "@hapi/hapi"
14
+ import Inert from "@hapi/inert"
14
15
  import WebSocket from "ws"
15
16
  import HAPIWebSocket from "hapi-plugin-websocket"
16
17
  import HAPIHeader from "hapi-plugin-header"
@@ -32,7 +33,7 @@ import chalk from "chalk"
32
33
 
33
34
  /* internal dependencies */
34
35
  import SpeechFlowNode from "./speechflow-node"
35
- import pkg from "../package.json"
36
+ import pkg from "../../package.json"
36
37
 
37
38
  /* central CLI context */
38
39
  let cli: CLIio | null = null
@@ -67,6 +68,7 @@ type wsPeerInfo = {
67
68
  "[-a|--address <ip-address>] " +
68
69
  "[-p|--port <tcp-port>] " +
69
70
  "[-C|--cache <directory>] " +
71
+ "[-d|--dashboard <type>:<id>:<name>[,...]] " +
70
72
  "[-e|--expression <expression>] " +
71
73
  "[-f|--file <file>] " +
72
74
  "[-c|--config <id>@<yaml-config-file>] " +
@@ -125,6 +127,15 @@ type wsPeerInfo = {
125
127
  default: path.join(dataDir, "cache"),
126
128
  describe: "directory for cached files (primarily AI model files)"
127
129
  })
130
+ .option("d", {
131
+ alias: "dashboard",
132
+ type: "string",
133
+ array: false,
134
+ coerce,
135
+ nargs: 1,
136
+ default: "",
137
+ describe: "list of dashboard block types and names"
138
+ })
128
139
  .option("e", {
129
140
  alias: "expression",
130
141
  type: "string",
@@ -176,6 +187,21 @@ type wsPeerInfo = {
176
187
  logPrefix: pkg.name
177
188
  })
178
189
 
190
+ /* catch uncaught exceptions */
191
+ process.on("uncaughtException", (err) => {
192
+ cli!.log("error", `uncaught exception: ${err.message}`)
193
+ process.exit(1)
194
+ })
195
+
196
+ /* catch unhandled promise rejections */
197
+ process.on("unhandledRejection", (reason) => {
198
+ if (reason instanceof Error)
199
+ cli!.log("error", `unhandled rejection: ${reason.message}`)
200
+ else
201
+ cli!.log("error", `unhandled rejection: ${reason}`)
202
+ process.exit(1)
203
+ })
204
+
179
205
  /* provide startup information */
180
206
  cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
181
207
 
@@ -185,21 +211,6 @@ type wsPeerInfo = {
185
211
  for (const key of Object.keys(result.parsed))
186
212
  cli.log("info", `loaded environment variable "${key}" from ".env" files`)
187
213
 
188
- /* handle uncaught exceptions */
189
- process.on("uncaughtException", async (err: Error) => {
190
- cli!.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`)
191
- process.exit(1)
192
- })
193
-
194
- /* handle unhandled promise rejections */
195
- process.on("unhandledRejection", async (reason, promise) => {
196
- if (reason instanceof Error)
197
- cli!.log("error", `promise rejection not handled: ${reason.message}: ${reason.stack}`)
198
- else
199
- cli!.log("error", `promise rejection not handled: ${reason}`)
200
- process.exit(1)
201
- })
202
-
203
214
  /* sanity check usage */
204
215
  let n = 0
205
216
  if (typeof args.e === "string" && args.e !== "") n++
@@ -220,7 +231,16 @@ type wsPeerInfo = {
220
231
  throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
221
232
  const [ , id, file ] = m
222
233
  const yaml = await cli.input(file, { encoding: "utf8" })
223
- const obj: any = jsYAML.load(yaml)
234
+ let obj: any
235
+ try {
236
+ obj = jsYAML.load(yaml)
237
+ }
238
+ catch (err) {
239
+ if (err instanceof Error)
240
+ throw new Error(`failed to parse YAML configuration: ${err.message}`)
241
+ else
242
+ throw new Error(`failed to parse YAML configuration: ${err}`)
243
+ }
224
244
  if (obj[id] === undefined)
225
245
  throw new Error(`no such id "${id}" found in configuration file`)
226
246
  config = obj[id] as string
@@ -308,7 +328,14 @@ type wsPeerInfo = {
308
328
  for (const name of Object.keys(nodes)) {
309
329
  cli!.log("info", `gathering status of node <${name}>`)
310
330
  const node = new nodes[name](name, cfg, {}, [])
311
- const status = await node.status()
331
+ const status = await Promise.race<{ [ key: string ]: string | number }>([
332
+ node.status(),
333
+ new Promise<never>((resolve, reject) => setTimeout(() =>
334
+ reject(new Error("timeout")), 10 * 1000))
335
+ ]).catch((err: Error) => {
336
+ cli!.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
337
+ return {} as { [ key: string ]: string | number }
338
+ })
312
339
  if (Object.keys(status).length > 0) {
313
340
  let first = true
314
341
  for (const key of Object.keys(status)) {
@@ -336,12 +363,9 @@ type wsPeerInfo = {
336
363
  ast = flowlink.compile(config)
337
364
  }
338
365
  catch (err) {
339
- if (err instanceof Error && err.name === "FlowLinkError")
340
- cli!.log("error", `failed to parse SpeechFlow configuration: ${err.toString()}"`)
341
- else if (err instanceof Error)
342
- cli!.log("error", `failed to parse SpeechFlow configuration: ${err.message}"`)
343
- else
344
- cli!.log("error", "failed to parse SpeechFlow configuration: internal error")
366
+ const errorMsg = err instanceof Error && err.name === "FlowLinkError"
367
+ ? err.toString() : (err instanceof Error ? err.message : "internal error")
368
+ cli!.log("error", `failed to parse SpeechFlow configuration: ${errorMsg}`)
345
369
  process.exit(1)
346
370
  }
347
371
  try {
@@ -358,10 +382,11 @@ type wsPeerInfo = {
358
382
  throw new Error(`unknown node <${id}>`)
359
383
  let node: SpeechFlowNode
360
384
  try {
361
- let num = nodeNums.get(nodes[id]) ?? 0
362
- nodeNums.set(nodes[id], ++num)
385
+ const NodeClass = nodes[id]
386
+ let num = nodeNums.get(NodeClass) ?? 0
387
+ nodeNums.set(NodeClass, ++num)
363
388
  const name = num === 1 ? id : `${id}:${num}`
364
- node = new nodes[id](name, cfg, opts, args)
389
+ node = new NodeClass(name, cfg, opts, args)
365
390
  }
366
391
  catch (err) {
367
392
  /* fatal error */
@@ -384,12 +409,9 @@ type wsPeerInfo = {
384
409
  })
385
410
  }
386
411
  catch (err) {
387
- if (err instanceof Error && err.name === "FlowLinkError")
388
- cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.toString()}`)
389
- else if (err instanceof Error)
390
- cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.message}`)
391
- else
392
- cli!.log("error", "failed to materialize SpeechFlow configuration: internal error")
412
+ const errorMsg = err instanceof Error && err.name === "FlowLinkError"
413
+ ? err.toString() : (err instanceof Error ? err.message : "internal error")
414
+ cli!.log("error", `failed to materialize SpeechFlow configuration: ${errorMsg}`)
393
415
  process.exit(1)
394
416
  }
395
417
 
@@ -438,9 +460,13 @@ type wsPeerInfo = {
438
460
  /* open node */
439
461
  cli!.log("info", `open node <${node.id}>`)
440
462
  node.setTimeZero(timeZero)
441
- await node.open().catch((err: Error) => {
442
- cli!.log("error", `[${node.id}]: ${err.message}`)
443
- throw new Error(`failed to open node <${node.id}>`)
463
+ await Promise.race<void>([
464
+ node.open(),
465
+ new Promise<never>((resolve, reject) => setTimeout(() =>
466
+ reject(new Error("timeout")), 10 * 1000))
467
+ ]).catch((err: Error) => {
468
+ cli!.log("error", `[${node.id}]: failed to open node <${node.id}>: ${err.message}`)
469
+ throw new Error(`failed to open node <${node.id}>: ${err.message}`)
444
470
  })
445
471
  }
446
472
 
@@ -462,9 +488,10 @@ type wsPeerInfo = {
462
488
  }
463
489
  }
464
490
 
465
- /* graph processing: PASS 6: track stream finishing */
491
+ /* graph processing: PASS 5: track stream finishing */
466
492
  const activeNodes = new Set<SpeechFlowNode>()
467
493
  const finishEvents = new EventEmitter()
494
+ finishEvents.setMaxListeners(graphNodes.size + 10)
468
495
  for (const node of graphNodes) {
469
496
  if (node.stream === null)
470
497
  throw new Error(`stream of node <${node.id}> still not initialized`)
@@ -512,9 +539,12 @@ type wsPeerInfo = {
512
539
  throw new Error(`external request failed: no such node <${name}>`)
513
540
  }
514
541
  else {
515
- await foundNode.receiveRequest(args).catch((err: Error) => {
516
- cli!.log("warning", `external request to node <${name}> failed: ${err}`)
517
- throw new Error(`external request to node <${name}> failed: ${err}`)
542
+ await Promise.race<void>([
543
+ foundNode.receiveRequest(args),
544
+ new Promise<never>((resolve, reject) => setTimeout(() =>
545
+ reject(new Error("timeout")), 10 * 1000))
546
+ ]).catch((err: Error) => {
547
+ cli!.log("warning", `external request to node <${name}> failed: ${err.message}`)
518
548
  })
519
549
  }
520
550
  }
@@ -525,6 +555,7 @@ type wsPeerInfo = {
525
555
  address: args.a,
526
556
  port: args.p
527
557
  })
558
+ await hapi.register({ plugin: Inert })
528
559
  await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
529
560
  await hapi.register({ plugin: HAPIWebSocket })
530
561
  hapi.events.on("response", (request: HAPI.Request) => {
@@ -558,6 +589,29 @@ type wsPeerInfo = {
558
589
  cli!.log("error", `HAPI: log: ${err}`)
559
590
  }
560
591
  })
592
+ hapi.route({
593
+ method: "GET",
594
+ path: "/{param*}",
595
+ handler: {
596
+ directory: {
597
+ path: path.join(__dirname, "../../speechflow-ui-db/dst"),
598
+ redirectToSlash: true,
599
+ index: true
600
+ }
601
+ }
602
+ })
603
+ hapi.route({
604
+ method: "GET",
605
+ path: "/api/dashboard",
606
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
607
+ const config = []
608
+ for (const block of args.d.split(",")) {
609
+ const [ type, id, name ] = block.split(":")
610
+ config.push({ type, id, name })
611
+ }
612
+ return h.response(config).code(200)
613
+ }
614
+ })
561
615
  hapi.route({
562
616
  method: "GET",
563
617
  path: "/api/{req}/{node}/{params*}",
@@ -565,10 +619,13 @@ type wsPeerInfo = {
565
619
  },
566
620
  handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
567
621
  const peer = request.info.remoteAddress
622
+ const params = request.params.params as string ?? ""
623
+ if (params.length > 1000)
624
+ return h.response({ response: "ERROR", data: "parameters too long" }).code(400)
568
625
  const req = {
569
626
  request: request.params.req,
570
627
  node: request.params.node,
571
- args: (request.params.params as string ?? "").split("/").filter((seg) => seg !== "")
628
+ args: params.split("/").filter((seg) => seg !== "")
572
629
  }
573
630
  cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
574
631
  return consumeExternalRequest(req).then(() => {
@@ -583,9 +640,10 @@ type wsPeerInfo = {
583
640
  path: "/api",
584
641
  options: {
585
642
  payload: {
586
- output: "data",
587
- parse: true,
588
- allow: "application/json"
643
+ output: "data",
644
+ parse: true,
645
+ allow: "application/json",
646
+ maxBytes: 1 * 1024 * 1024
589
647
  },
590
648
  plugins: {
591
649
  websocket: {
@@ -601,8 +659,10 @@ type wsPeerInfo = {
601
659
  },
602
660
  disconnect: (args: any) => {
603
661
  const ctx: wsPeerCtx = args.ctx
662
+ const ws: WebSocket = args.ws
604
663
  const peer = ctx.peer
605
664
  wsPeers.delete(peer)
665
+ ws.removeAllListeners()
606
666
  cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
607
667
  }
608
668
  }
@@ -623,14 +683,35 @@ type wsPeerInfo = {
623
683
  }
624
684
  })
625
685
  await hapi.start()
626
- cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.address}:${args.port}`)
686
+ cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.a}:${args.p}`)
627
687
 
628
688
  /* hook for sendResponse method of nodes */
629
689
  for (const node of graphNodes) {
630
690
  node.on("send-response", (args: any[]) => {
631
691
  const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
632
692
  for (const [ peer, info ] of wsPeers.entries()) {
633
- cli!.log("info", `HAPI: peer ${peer}: ${data}`)
693
+ cli!.log("debug", `HAPI: remote peer ${peer}: sending ${data}`)
694
+ if (info.ws.readyState === WebSocket.OPEN)
695
+ info.ws.send(data)
696
+ }
697
+ })
698
+ }
699
+
700
+ /* hook for dashboardInfo method of nodes */
701
+ for (const node of graphNodes) {
702
+ node.on("dashboard-info", (info: {
703
+ type: string,
704
+ id: string,
705
+ kind: "final" | "intermediate",
706
+ value: string | number
707
+ }) => {
708
+ const data = JSON.stringify({
709
+ response: "DASHBOARD",
710
+ node: "",
711
+ args: [ info.type, info.id, info.kind, info.value ]
712
+ })
713
+ for (const [ peer, info ] of wsPeers.entries()) {
714
+ cli!.log("debug", `HAPI: dashboard peer ${peer}: send ${data}`)
634
715
  info.ws.send(data)
635
716
  }
636
717
  })
@@ -653,9 +734,39 @@ type wsPeerInfo = {
653
734
  cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
654
735
 
655
736
  /* shutdown HAPI service */
656
- cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
737
+ cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.a}:${args.p}`)
657
738
  await hapi.stop({ timeout: 2000 })
658
739
 
740
+ /* clear WebSocket connections */
741
+ if (wsPeers.size > 0) {
742
+ cli!.log("info", "HAPI: closing WebSocket connections")
743
+ const closePromises: Promise<void>[] = []
744
+ for (const [ peer, info ] of wsPeers.entries()) {
745
+ closePromises.push(new Promise<void>((resolve, reject) => {
746
+ if (info.ws.readyState !== WebSocket.OPEN)
747
+ resolve()
748
+ else {
749
+ const timeout = setTimeout(() => {
750
+ reject(new Error(`timeout for peer ${peer}`))
751
+ }, 2 * 1000)
752
+ info.ws.once("close", () => {
753
+ clearTimeout(timeout)
754
+ resolve()
755
+ })
756
+ info.ws.close()
757
+ }
758
+ }))
759
+ }
760
+ await Promise.race([
761
+ Promise.all(closePromises),
762
+ new Promise((resolve, reject) =>
763
+ setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
764
+ ]).catch((err) => {
765
+ cli!.log("warning", `HAPI: WebSockets failed to close: ${err}`)
766
+ })
767
+ wsPeers.clear()
768
+ }
769
+
659
770
  /* graph processing: PASS 1: disconnect node streams */
660
771
  for (const node of graphNodes) {
661
772
  if (node.stream === null) {
@@ -685,8 +796,12 @@ type wsPeerInfo = {
685
796
  /* graph processing: PASS 2: close nodes */
686
797
  for (const node of graphNodes) {
687
798
  cli!.log("info", `close node <${node.id}>`)
688
- await node.close().catch((err) => {
689
- cli!.log("warning", `node <${node.id}> failed to close: ${err}`)
799
+ await Promise.race<void>([
800
+ node.close(),
801
+ new Promise<never>((resolve, reject) => setTimeout(() =>
802
+ reject(new Error("timeout")), 10 * 1000))
803
+ ]).catch((err: Error) => {
804
+ cli!.log("warning", `node <${node.id}> failed to close: ${err.message}`)
690
805
  })
691
806
  }
692
807
 
@@ -705,6 +820,12 @@ type wsPeerInfo = {
705
820
  graphNodes.delete(node)
706
821
  }
707
822
 
823
+ /* clear event emitters */
824
+ finishEvents.removeAllListeners()
825
+
826
+ /* clear active nodes */
827
+ activeNodes.clear()
828
+
708
829
  /* terminate process */
709
830
  if (signal === "finished") {
710
831
  cli!.log("info", "terminate process (exit code 0)")
@@ -715,24 +836,38 @@ type wsPeerInfo = {
715
836
  process.exit(1)
716
837
  }
717
838
  }
839
+
840
+ /* hook into regular finish */
718
841
  finishEvents.on("finished", () => { shutdown("finished") })
842
+
843
+ /* hook into process signals */
719
844
  process.on("SIGINT", () => { shutdown("SIGINT") })
720
845
  process.on("SIGUSR1", () => { shutdown("SIGUSR1") })
721
846
  process.on("SIGUSR2", () => { shutdown("SIGUSR2") })
722
847
  process.on("SIGTERM", () => { shutdown("SIGTERM") })
848
+
849
+ /* re-hook into uncaught exception handler */
850
+ process.removeAllListeners("uncaughtException")
723
851
  process.on("uncaughtException", (err) => {
724
- cli!.log("error", `uncaught exception: ${err}`)
852
+ cli!.log("error", `uncaught exception: ${err.message}`)
725
853
  shutdown("exception")
726
854
  })
855
+
856
+ /* re-hook into unhandled promise rejection handler */
857
+ process.removeAllListeners("unhandledRejection")
727
858
  process.on("unhandledRejection", (reason) => {
728
- cli!.log("error", `unhandled rejection: ${reason}`)
859
+ if (reason instanceof Error)
860
+ cli!.log("error", `unhandled rejection: ${reason.message}`)
861
+ else
862
+ cli!.log("error", `unhandled rejection: ${reason}`)
729
863
  shutdown("exception")
730
864
  })
731
865
  })().catch((err: Error) => {
866
+ /* top-level exception handling */
732
867
  if (cli !== null)
733
- cli.log("error", err.message)
868
+ cli.log("error", `${err.message}:\n${err.stack}`)
734
869
  else
735
- process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message} ${err.stack}\n`)
870
+ process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
736
871
  process.exit(1)
737
872
  })
738
873
 
@@ -0,0 +1,106 @@
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
+ import pluginJs from "@eslint/js"
8
+ import pluginStd from "neostandard"
9
+ import pluginN from "eslint-plugin-n"
10
+ import pluginImport from "eslint-plugin-import"
11
+ import pluginPromise from "eslint-plugin-promise"
12
+ import pluginVue from "eslint-plugin-vue"
13
+ import pluginTS from "typescript-eslint"
14
+ import globals from "globals"
15
+ import parserTS from "@typescript-eslint/parser"
16
+ import parserVue from "vue-eslint-parser"
17
+
18
+ export default [
19
+ pluginJs.configs.recommended,
20
+ ...pluginTS.configs.strict,
21
+ ...pluginTS.configs.stylistic,
22
+ ...pluginStd({
23
+ ignores: pluginStd.resolveIgnoresFromGitignore()
24
+ }),
25
+ ...pluginVue.configs["flat/recommended"],
26
+ {
27
+ plugins: {
28
+ "n": pluginN,
29
+ "import": pluginImport,
30
+ "promise": pluginPromise
31
+ },
32
+ files: [ "**/*.{vue,ts}" ],
33
+ ignores: [ "dst/" ],
34
+ languageOptions: {
35
+ ecmaVersion: 2022,
36
+ sourceType: "module",
37
+ parser: parserVue,
38
+ parserOptions: {
39
+ parser: parserTS,
40
+ extraFileExtensions: [ ".vue" ],
41
+ ecmaFeatures: {
42
+ jsx: false
43
+ }
44
+ },
45
+ globals: {
46
+ ...globals.browser,
47
+ ...globals.node,
48
+ ...globals.commonjs
49
+ }
50
+ },
51
+ rules: {
52
+ "curly": "off",
53
+ "require-atomic-updates": "off",
54
+ "dot-notation": "off",
55
+ "no-labels": "off",
56
+ "no-useless-constructor": "off",
57
+
58
+ "@stylistic/indent": [ "error", 4, { SwitchCase: 1 } ],
59
+ "@stylistic/linebreak-style": [ "error", "unix" ],
60
+ "@stylistic/semi": [ "error", "never" ],
61
+ "@stylistic/operator-linebreak": [ "error", "after", { overrides: { "&&": "before", "||": "before", ":": "after" } } ],
62
+ "@stylistic/brace-style": [ "error", "stroustrup", { allowSingleLine: true } ],
63
+ "@stylistic/quotes": [ "error", "double" ],
64
+
65
+ "@stylistic/no-multi-spaces": "off",
66
+ "@stylistic/no-multi-spaces": "off",
67
+ "@stylistic/no-multiple-empty-lines": "off",
68
+ "@stylistic/key-spacing": "off",
69
+ "@stylistic/object-property-newline": "off",
70
+ "@stylistic/space-in-parens": "off",
71
+ "@stylistic/array-bracket-spacing": "off",
72
+ "@stylistic/lines-between-class-members": "off",
73
+ "@stylistic/multiline-ternary": "off",
74
+ "@stylistic/quote-props": "off",
75
+
76
+ "vue/html-indent": "off",
77
+ "vue/v-bind-style": [ "error", "longform" ],
78
+ "vue/max-attributes-per-line": "off",
79
+ "vue/html-self-closing": "off",
80
+ "vue/no-multi-spaces": "off",
81
+ "vue/html-closing-bracket-newline": "off",
82
+ "vue/html-closing-bracket-spacing": "off",
83
+ "vue/singleline-html-element-content-newline": "off",
84
+ "vue/no-v-html": "off",
85
+ "vue/v-on-style": "off",
86
+ "vue/component-tags-order": "off",
87
+ "vue/first-attribute-linebreak": "off",
88
+ "vue/attributes-order": "off",
89
+ "vue/component-definition-name-casing": "off",
90
+ "vue/block-order": "off",
91
+
92
+ "@typescript-eslint/no-empty-function": "off",
93
+ "@typescript-eslint/no-explicit-any": "off",
94
+ "@typescript-eslint/no-unused-vars": "off",
95
+ "@typescript-eslint/ban-ts-comment": "off",
96
+ "@typescript-eslint/no-this-alias": "off",
97
+ "@typescript-eslint/no-non-null-assertion": "off",
98
+ "@typescript-eslint/consistent-type-definitions": "off",
99
+ "@typescript-eslint/array-type": "off",
100
+ "@typescript-eslint/no-extraneous-class": "off",
101
+ "@typescript-eslint/consistent-indexed-object-style": "off",
102
+ "@typescript-eslint/adjacent-overload-signatures": "off"
103
+ }
104
+ }
105
+ ]
106
+
@@ -0,0 +1,55 @@
1
+ {
2
+ "plugins": [],
3
+ "maxerr": false,
4
+ "raw-ignore-regex": false,
5
+ "attr-bans": [ "align", "background", "bgcolor", "border", "frameborder", "longdesc", "marginwidth", "marginheight", "scrolling", "style", "width" ],
6
+ "indent-delta": false,
7
+ "indent-style": "nonmixed",
8
+ "indent-width": 4,
9
+ "indent-width-cont": false,
10
+ "spec-char-escape": true,
11
+ "text-ignore-regex": false,
12
+ "tag-bans": [ "b", "i" ],
13
+ "tag-close": true,
14
+ "tag-name-lowercase": true,
15
+ "tag-name-match": true,
16
+ "tag-self-close": false,
17
+ "doctype-first": false,
18
+ "doctype-html5": false,
19
+ "attr-name-style": "dash",
20
+ "attr-name-ignore-regex": false,
21
+ "attr-no-dup": true,
22
+ "attr-no-unsafe-char": true,
23
+ "attr-order": false,
24
+ "attr-quote-style": "double",
25
+ "attr-req-value": true,
26
+ "attr-new-line": false,
27
+ "attr-validate": true,
28
+ "id-no-dup": true,
29
+ "id-class-no-ad": true,
30
+ "id-class-style": "underscore",
31
+ "class-no-dup": true,
32
+ "class-style": false,
33
+ "id-class-ignore-regex": false,
34
+ "img-req-alt": true,
35
+ "img-req-src": true,
36
+ "html-valid-content-model": true,
37
+ "head-valid-content-model": true,
38
+ "href-style": false,
39
+ "label-req-for": true,
40
+ "line-end-style": "lf",
41
+ "line-no-trailing-whitespace": false,
42
+ "line-max-len": false,
43
+ "line-max-len-ignore-regex": false,
44
+ "head-req-title": true,
45
+ "title-no-dup": true,
46
+ "title-max-len": 60,
47
+ "html-req-lang": false,
48
+ "lang-style": "case",
49
+ "fig-req-figcaption": false,
50
+ "focusable-tabindex-style": false,
51
+ "input-radio-req-name": true,
52
+ "input-req-label": false,
53
+ "table-req-caption": false,
54
+ "table-req-header": false
55
+ }
@@ -0,0 +1,79 @@
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
+ # (internal): create patch set for NPM dependencies
8
+ patch-make
9
+ npm shrinkwrap && \
10
+ patch-package --patch-dir package.d \
11
+ "@typescript-eslint/typescript-estree" \
12
+ htmllint-cli && \
13
+ shx rm -f npm-shrinkwrap.json
14
+
15
+ # (internal): apply patch set for NPM dependencies
16
+ patch-apply
17
+ patch-package --patch-dir package.d
18
+
19
+ # static code analysis (continuous file watching)
20
+ lint-watch
21
+ nodemon --exec "npm start lint" --watch src --ext html,styl,js,ts,vue
22
+
23
+ # static code analysis
24
+ lint : lint-vue-tsc lint-eslint lint-stylelint lint-htmllint
25
+
26
+ # static code analysis (Vue language)
27
+ lint-vue-tsc
28
+ vue-tsc --project etc/tsc-client.json --noEmit
29
+
30
+ # static code analysis (TypeScript language)
31
+ lint-eslint
32
+ eslint --config etc/eslint.mjs src/**/*.vue src/**/*.ts
33
+
34
+ # static code analysis (CSS/Stylus languages)
35
+ lint-stylelint
36
+ stylelint --config etc/stylelint.yaml src/**/*.styl src/**/*.vue
37
+
38
+ # static code analysis (HTML language)
39
+ lint-htmllint
40
+ htmllint --rc etc/htmllint.json src/**/*.html
41
+
42
+ # build components for production
43
+ build
44
+ npm start build-client
45
+
46
+ # build components for development
47
+ build-dev
48
+ npm start build-client-dev
49
+
50
+ # build client components for production
51
+ build-client
52
+ vite --config etc/vite-client.mts build --mode production
53
+
54
+ # build client components for development
55
+ build-client-dev
56
+ vite --config etc/vite-client.mts build --mode development
57
+
58
+ # build client components for development (continuous file watching)
59
+ build-client-dev-watch
60
+ cross-env NODE_OPTIONS="--max-old-space-size=4096" \
61
+ vite --config etc/vite-client.mts build --mode development --watch
62
+
63
+ # run server
64
+ server
65
+ serve -d --listen 12345 --single dst
66
+
67
+ # remove all development-only NPM dependencies
68
+ prune
69
+ npm prune --omit=dev
70
+
71
+ # remove all generated artifacts (reverse of "npm start build")
72
+ clean
73
+ shx rm -rf dst-stage1 dst-stage2
74
+
75
+ # remove all generated artifacts (reverse of "npm install" and "npm start build")
76
+ clean:dist : clean
77
+ shx rm -f package-lock.json && \
78
+ shx rm -rf node_modules
79
+