speechflow 1.3.1 → 1.3.2

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 (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dst/speechflow-node-a2a-gender.d.ts +2 -0
  3. package/dst/speechflow-node-a2a-gender.js +137 -59
  4. package/dst/speechflow-node-a2a-gender.js.map +1 -1
  5. package/dst/speechflow-node-a2a-meter.d.ts +3 -1
  6. package/dst/speechflow-node-a2a-meter.js +80 -39
  7. package/dst/speechflow-node-a2a-meter.js.map +1 -1
  8. package/dst/speechflow-node-a2a-mute.d.ts +1 -0
  9. package/dst/speechflow-node-a2a-mute.js +37 -11
  10. package/dst/speechflow-node-a2a-mute.js.map +1 -1
  11. package/dst/speechflow-node-a2a-vad.d.ts +3 -0
  12. package/dst/speechflow-node-a2a-vad.js +194 -96
  13. package/dst/speechflow-node-a2a-vad.js.map +1 -1
  14. package/dst/speechflow-node-a2a-wav.js +27 -11
  15. package/dst/speechflow-node-a2a-wav.js.map +1 -1
  16. package/dst/speechflow-node-a2t-deepgram.d.ts +4 -0
  17. package/dst/speechflow-node-a2t-deepgram.js +136 -46
  18. package/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  19. package/dst/speechflow-node-t2a-elevenlabs.d.ts +2 -0
  20. package/dst/speechflow-node-t2a-elevenlabs.js +61 -12
  21. package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  22. package/dst/speechflow-node-t2a-kokoro.d.ts +1 -0
  23. package/dst/speechflow-node-t2a-kokoro.js +10 -4
  24. package/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  25. package/dst/speechflow-node-t2t-deepl.js +8 -4
  26. package/dst/speechflow-node-t2t-deepl.js.map +1 -1
  27. package/dst/speechflow-node-t2t-format.js +2 -2
  28. package/dst/speechflow-node-t2t-format.js.map +1 -1
  29. package/dst/speechflow-node-t2t-ollama.js +1 -1
  30. package/dst/speechflow-node-t2t-ollama.js.map +1 -1
  31. package/dst/speechflow-node-t2t-openai.js +1 -1
  32. package/dst/speechflow-node-t2t-openai.js.map +1 -1
  33. package/dst/speechflow-node-t2t-sentence.d.ts +1 -1
  34. package/dst/speechflow-node-t2t-sentence.js +34 -18
  35. package/dst/speechflow-node-t2t-sentence.js.map +1 -1
  36. package/dst/speechflow-node-t2t-subtitle.d.ts +0 -1
  37. package/dst/speechflow-node-t2t-subtitle.js +78 -190
  38. package/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  39. package/dst/speechflow-node-t2t-transformers.js +2 -2
  40. package/dst/speechflow-node-t2t-transformers.js.map +1 -1
  41. package/dst/speechflow-node-x2x-filter.js +4 -4
  42. package/dst/speechflow-node-x2x-trace.js +6 -13
  43. package/dst/speechflow-node-x2x-trace.js.map +1 -1
  44. package/dst/speechflow-node-xio-device.js +12 -8
  45. package/dst/speechflow-node-xio-device.js.map +1 -1
  46. package/dst/speechflow-node-xio-file.js +9 -3
  47. package/dst/speechflow-node-xio-file.js.map +1 -1
  48. package/dst/speechflow-node-xio-mqtt.js +5 -2
  49. package/dst/speechflow-node-xio-mqtt.js.map +1 -1
  50. package/dst/speechflow-node-xio-websocket.js +11 -11
  51. package/dst/speechflow-node-xio-websocket.js.map +1 -1
  52. package/dst/speechflow-node.d.ts +0 -2
  53. package/dst/speechflow-node.js +0 -3
  54. package/dst/speechflow-node.js.map +1 -1
  55. package/dst/speechflow-utils.d.ts +5 -0
  56. package/dst/speechflow-utils.js +77 -44
  57. package/dst/speechflow-utils.js.map +1 -1
  58. package/dst/speechflow.js +101 -82
  59. package/dst/speechflow.js.map +1 -1
  60. package/etc/eslint.mjs +1 -2
  61. package/etc/stx.conf +3 -3
  62. package/package.json +6 -6
  63. package/src/speechflow-node-a2a-gender.ts +148 -64
  64. package/src/speechflow-node-a2a-meter.ts +87 -40
  65. package/src/speechflow-node-a2a-mute.ts +39 -11
  66. package/src/speechflow-node-a2a-vad.ts +206 -100
  67. package/src/speechflow-node-a2a-wav.ts +27 -11
  68. package/src/speechflow-node-a2t-deepgram.ts +139 -43
  69. package/src/speechflow-node-t2a-elevenlabs.ts +65 -12
  70. package/src/speechflow-node-t2a-kokoro.ts +11 -4
  71. package/src/speechflow-node-t2t-deepl.ts +9 -4
  72. package/src/speechflow-node-t2t-format.ts +2 -2
  73. package/src/speechflow-node-t2t-ollama.ts +1 -1
  74. package/src/speechflow-node-t2t-openai.ts +1 -1
  75. package/src/speechflow-node-t2t-sentence.ts +37 -20
  76. package/src/speechflow-node-t2t-transformers.ts +4 -3
  77. package/src/speechflow-node-x2x-filter.ts +4 -4
  78. package/src/speechflow-node-x2x-trace.ts +1 -1
  79. package/src/speechflow-node-xio-device.ts +12 -8
  80. package/src/speechflow-node-xio-file.ts +9 -3
  81. package/src/speechflow-node-xio-mqtt.ts +5 -2
  82. package/src/speechflow-node-xio-websocket.ts +12 -12
  83. package/src/speechflow-utils.ts +78 -44
  84. package/src/speechflow.ts +114 -35
package/src/speechflow.ts CHANGED
@@ -176,6 +176,21 @@ type wsPeerInfo = {
176
176
  logPrefix: pkg.name
177
177
  })
178
178
 
179
+ /* catch uncaught exceptions */
180
+ process.on("uncaughtException", (err) => {
181
+ cli!.log("error", `uncaught exception: ${err.message}`)
182
+ process.exit(1)
183
+ })
184
+
185
+ /* catch unhandled promise rejections */
186
+ process.on("unhandledRejection", (reason) => {
187
+ if (reason instanceof Error)
188
+ cli!.log("error", `unhandled rejection: ${reason.message}`)
189
+ else
190
+ cli!.log("error", `unhandled rejection: ${reason}`)
191
+ process.exit(1)
192
+ })
193
+
179
194
  /* provide startup information */
180
195
  cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
181
196
 
@@ -185,21 +200,6 @@ type wsPeerInfo = {
185
200
  for (const key of Object.keys(result.parsed))
186
201
  cli.log("info", `loaded environment variable "${key}" from ".env" files`)
187
202
 
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
203
  /* sanity check usage */
204
204
  let n = 0
205
205
  if (typeof args.e === "string" && args.e !== "") n++
@@ -220,7 +220,16 @@ type wsPeerInfo = {
220
220
  throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
221
221
  const [ , id, file ] = m
222
222
  const yaml = await cli.input(file, { encoding: "utf8" })
223
- const obj: any = jsYAML.load(yaml)
223
+ let obj: any
224
+ try {
225
+ obj = jsYAML.load(yaml)
226
+ }
227
+ catch (err) {
228
+ if (err instanceof Error)
229
+ throw new Error(`failed to parse YAML configuration: ${err.message}`)
230
+ else
231
+ throw new Error(`failed to parse YAML configuration: ${err}`)
232
+ }
224
233
  if (obj[id] === undefined)
225
234
  throw new Error(`no such id "${id}" found in configuration file`)
226
235
  config = obj[id] as string
@@ -308,7 +317,14 @@ type wsPeerInfo = {
308
317
  for (const name of Object.keys(nodes)) {
309
318
  cli!.log("info", `gathering status of node <${name}>`)
310
319
  const node = new nodes[name](name, cfg, {}, [])
311
- const status = await node.status()
320
+ const status = await Promise.race<{ [ key: string ]: string | number }>([
321
+ node.status(),
322
+ new Promise((resolve, reject) => setTimeout(() =>
323
+ reject(new Error("timeout")), 10 * 1000))
324
+ ]).catch((err: Error) => {
325
+ cli!.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
326
+ return {} as { [ key: string ]: string | number }
327
+ })
312
328
  if (Object.keys(status).length > 0) {
313
329
  let first = true
314
330
  for (const key of Object.keys(status)) {
@@ -337,9 +353,9 @@ type wsPeerInfo = {
337
353
  }
338
354
  catch (err) {
339
355
  if (err instanceof Error && err.name === "FlowLinkError")
340
- cli!.log("error", `failed to parse SpeechFlow configuration: ${err.toString()}"`)
356
+ cli!.log("error", `failed to parse SpeechFlow configuration: ${err.toString()}`)
341
357
  else if (err instanceof Error)
342
- cli!.log("error", `failed to parse SpeechFlow configuration: ${err.message}"`)
358
+ cli!.log("error", `failed to parse SpeechFlow configuration: ${err.message}`)
343
359
  else
344
360
  cli!.log("error", "failed to parse SpeechFlow configuration: internal error")
345
361
  process.exit(1)
@@ -438,9 +454,13 @@ type wsPeerInfo = {
438
454
  /* open node */
439
455
  cli!.log("info", `open node <${node.id}>`)
440
456
  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}>`)
457
+ await Promise.race<void>([
458
+ node.open(),
459
+ new Promise((resolve, reject) => setTimeout(() =>
460
+ reject(new Error("timeout")), 10 * 1000))
461
+ ]).catch((err: Error) => {
462
+ cli!.log("error", `[${node.id}]: failed to open node <${node.id}>: ${err.message}`)
463
+ throw new Error(`failed to open node <${node.id}>: ${err.message}`)
444
464
  })
445
465
  }
446
466
 
@@ -462,9 +482,10 @@ type wsPeerInfo = {
462
482
  }
463
483
  }
464
484
 
465
- /* graph processing: PASS 6: track stream finishing */
485
+ /* graph processing: PASS 5: track stream finishing */
466
486
  const activeNodes = new Set<SpeechFlowNode>()
467
487
  const finishEvents = new EventEmitter()
488
+ finishEvents.setMaxListeners(graphNodes.size + 10)
468
489
  for (const node of graphNodes) {
469
490
  if (node.stream === null)
470
491
  throw new Error(`stream of node <${node.id}> still not initialized`)
@@ -512,9 +533,12 @@ type wsPeerInfo = {
512
533
  throw new Error(`external request failed: no such node <${name}>`)
513
534
  }
514
535
  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}`)
536
+ await Promise.race<void>([
537
+ foundNode.receiveRequest(args),
538
+ new Promise((resolve, reject) => setTimeout(() =>
539
+ reject(new Error("timeout")), 10 * 1000))
540
+ ]).catch((err: Error) => {
541
+ cli!.log("warning", `external request to node <${name}> failed: ${err.message}`)
518
542
  })
519
543
  }
520
544
  }
@@ -623,7 +647,7 @@ type wsPeerInfo = {
623
647
  }
624
648
  })
625
649
  await hapi.start()
626
- cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.address}:${args.port}`)
650
+ cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.a}:${args.p}`)
627
651
 
628
652
  /* hook for sendResponse method of nodes */
629
653
  for (const node of graphNodes) {
@@ -631,7 +655,8 @@ type wsPeerInfo = {
631
655
  const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
632
656
  for (const [ peer, info ] of wsPeers.entries()) {
633
657
  cli!.log("info", `HAPI: peer ${peer}: ${data}`)
634
- info.ws.send(data)
658
+ if (info.ws.readyState === WebSocket.OPEN)
659
+ info.ws.send(data)
635
660
  }
636
661
  })
637
662
  }
@@ -653,9 +678,39 @@ type wsPeerInfo = {
653
678
  cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
654
679
 
655
680
  /* shutdown HAPI service */
656
- cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
681
+ cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.a}:${args.p}`)
657
682
  await hapi.stop({ timeout: 2000 })
658
683
 
684
+ /* clear WebSocket connections */
685
+ if (wsPeers.size > 0) {
686
+ cli!.log("info", "HAPI: closing WebSocket connections")
687
+ const closePromises: Promise<void>[] = []
688
+ for (const [ peer, info ] of wsPeers.entries()) {
689
+ closePromises.push(new Promise<void>((resolve, reject) => {
690
+ if (info.ws.readyState !== WebSocket.OPEN)
691
+ resolve()
692
+ else {
693
+ const timeout = setTimeout(() => {
694
+ reject(new Error(`timeout for peer ${peer}`))
695
+ }, 2 * 1000)
696
+ info.ws.once("close", () => {
697
+ clearTimeout(timeout)
698
+ resolve()
699
+ })
700
+ info.ws.close()
701
+ }
702
+ }))
703
+ }
704
+ await Promise.race([
705
+ Promise.all(closePromises),
706
+ new Promise((resolve, reject) =>
707
+ setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
708
+ ]).catch((err) => {
709
+ cli!.log("warning", `HAPI: WebSockets failed to close: ${err}`)
710
+ })
711
+ wsPeers.clear()
712
+ }
713
+
659
714
  /* graph processing: PASS 1: disconnect node streams */
660
715
  for (const node of graphNodes) {
661
716
  if (node.stream === null) {
@@ -685,8 +740,12 @@ type wsPeerInfo = {
685
740
  /* graph processing: PASS 2: close nodes */
686
741
  for (const node of graphNodes) {
687
742
  cli!.log("info", `close node <${node.id}>`)
688
- await node.close().catch((err) => {
689
- cli!.log("warning", `node <${node.id}> failed to close: ${err}`)
743
+ await Promise.race<void>([
744
+ node.close(),
745
+ new Promise((resolve, reject) => setTimeout(() =>
746
+ reject(new Error("timeout")), 10 * 1000))
747
+ ]).catch((err: Error) => {
748
+ cli!.log("warning", `node <${node.id}> failed to close: ${err.message}`)
690
749
  })
691
750
  }
692
751
 
@@ -705,6 +764,12 @@ type wsPeerInfo = {
705
764
  graphNodes.delete(node)
706
765
  }
707
766
 
767
+ /* clear event emitters */
768
+ finishEvents.removeAllListeners()
769
+
770
+ /* clear active nodes */
771
+ activeNodes.clear()
772
+
708
773
  /* terminate process */
709
774
  if (signal === "finished") {
710
775
  cli!.log("info", "terminate process (exit code 0)")
@@ -715,24 +780,38 @@ type wsPeerInfo = {
715
780
  process.exit(1)
716
781
  }
717
782
  }
783
+
784
+ /* hook into regular finish */
718
785
  finishEvents.on("finished", () => { shutdown("finished") })
786
+
787
+ /* hook into process signals */
719
788
  process.on("SIGINT", () => { shutdown("SIGINT") })
720
789
  process.on("SIGUSR1", () => { shutdown("SIGUSR1") })
721
790
  process.on("SIGUSR2", () => { shutdown("SIGUSR2") })
722
791
  process.on("SIGTERM", () => { shutdown("SIGTERM") })
792
+
793
+ /* re-hook into uncaught exception handler */
794
+ process.removeAllListeners("uncaughtException")
723
795
  process.on("uncaughtException", (err) => {
724
- cli!.log("error", `uncaught exception: ${err}`)
796
+ cli!.log("error", `uncaught exception: ${err.message}`)
725
797
  shutdown("exception")
726
798
  })
799
+
800
+ /* re-hook into unhandled promise rejection handler */
801
+ process.removeAllListeners("unhandledRejection")
727
802
  process.on("unhandledRejection", (reason) => {
728
- cli!.log("error", `unhandled rejection: ${reason}`)
803
+ if (reason instanceof Error)
804
+ cli!.log("error", `unhandled rejection: ${reason.message}`)
805
+ else
806
+ cli!.log("error", `unhandled rejection: ${reason}`)
729
807
  shutdown("exception")
730
808
  })
731
809
  })().catch((err: Error) => {
810
+ /* top-level exception handling */
732
811
  if (cli !== null)
733
- cli.log("error", err.message)
812
+ cli.log("error", `${err.message}:\n${err.stack}`)
734
813
  else
735
- process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message} ${err.stack}\n`)
814
+ process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
736
815
  process.exit(1)
737
816
  })
738
817