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
@@ -29,12 +29,12 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
29
29
  public static name = "sentence"
30
30
 
31
31
  /* internal state */
32
- private static speexInitialized = false
33
32
  private queue = new utils.Queue<TextQueueElement>()
34
33
  private queueRecv = this.queue.pointerUse("recv")
35
34
  private queueSplit = this.queue.pointerUse("split")
36
35
  private queueSend = this.queue.pointerUse("send")
37
36
  private destroyed = false
37
+ private workingOffTimer: ReturnType<typeof setTimeout> | null = null
38
38
 
39
39
  /* construct node */
40
40
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -53,11 +53,7 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
53
53
  /* clear destruction flag */
54
54
  this.destroyed = false
55
55
 
56
- /* pass-through logging */
57
- const log = (level: string, msg: string) => { this.log(level, msg) }
58
-
59
56
  /* work off queued audio frames */
60
- let workingOffTimer: ReturnType<typeof setTimeout> | null = null
61
57
  let workingOff = false
62
58
  const workOffQueue = async () => {
63
59
  if (this.destroyed)
@@ -67,14 +63,14 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
67
63
  if (workingOff)
68
64
  return
69
65
  workingOff = true
70
- if (workingOffTimer !== null) {
71
- clearTimeout(workingOffTimer)
72
- workingOffTimer = null
66
+ if (this.workingOffTimer !== null) {
67
+ clearTimeout(this.workingOffTimer)
68
+ this.workingOffTimer = null
73
69
  }
74
70
  this.queue.off("write", workOffQueue)
75
71
 
76
72
  /* try to work off one or more chunks */
77
- while (true) {
73
+ while (!this.destroyed) {
78
74
  const element = this.queueSplit.peek()
79
75
  if (element === undefined)
80
76
  break
@@ -136,10 +132,12 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
136
132
  }
137
133
  }
138
134
 
139
- /* re-initiate working off round */
135
+ /* re-initiate working off round (if still not destroyed) */
140
136
  workingOff = false
141
- workingOffTimer = setTimeout(workOffQueue, 100)
142
- this.queue.once("write", workOffQueue)
137
+ if (!this.destroyed) {
138
+ this.workingOffTimer = setTimeout(workOffQueue, 100)
139
+ this.queue.once("write", workOffQueue)
140
+ }
143
141
  }
144
142
  this.queue.once("write", workOffQueue)
145
143
 
@@ -153,12 +151,14 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
153
151
 
154
152
  /* receive text chunk (writable side of stream) */
155
153
  write (chunk: SpeechFlowChunk, encoding, callback) {
156
- if (Buffer.isBuffer(chunk.payload))
154
+ if (self.destroyed)
155
+ callback(new Error("stream already destroyed"))
156
+ else if (Buffer.isBuffer(chunk.payload))
157
157
  callback(new Error("expected text input as string chunks"))
158
158
  else if (chunk.payload.length === 0)
159
159
  callback()
160
160
  else {
161
- log("info", `received text: ${JSON.stringify(chunk.payload)}`)
161
+ self.log("info", `received text: ${JSON.stringify(chunk.payload)}`)
162
162
  self.queueRecv.append({ type: "text-frame", chunk })
163
163
  callback()
164
164
  }
@@ -166,6 +166,10 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
166
166
 
167
167
  /* receive no more text chunks (writable side of stream) */
168
168
  final (callback) {
169
+ if (self.destroyed) {
170
+ callback()
171
+ return
172
+ }
169
173
  /* signal end of file */
170
174
  self.queueRecv.append({ type: "text-eof" })
171
175
  callback()
@@ -173,8 +177,12 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
173
177
 
174
178
  /* send text chunk(s) (readable side of stream) */
175
179
  read (_size) {
176
- /* flush pending audio chunks */
180
+ /* flush pending text chunks */
177
181
  const flushPendingChunks = () => {
182
+ if (self.destroyed) {
183
+ this.push(null)
184
+ return
185
+ }
178
186
  const element = self.queueSend.peek()
179
187
  if (element !== undefined
180
188
  && element.type === "text-eof") {
@@ -196,13 +204,13 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
196
204
  else if (element.type === "text-frame"
197
205
  && element.complete !== true)
198
206
  break
199
- log("info", `send text: ${JSON.stringify(element.chunk.payload)}`)
207
+ self.log("info", `send text: ${JSON.stringify(element.chunk.payload)}`)
200
208
  this.push(element.chunk)
201
209
  self.queueSend.walk(+1)
202
210
  self.queue.trim()
203
211
  }
204
212
  }
205
- else
213
+ else if (!self.destroyed)
206
214
  self.queue.once("write", flushPendingChunks)
207
215
  }
208
216
  flushPendingChunks()
@@ -212,13 +220,22 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
212
220
 
213
221
  /* close node */
214
222
  async close () {
223
+ /* indicate destruction */
224
+ this.destroyed = true
225
+
226
+ /* clean up timer */
227
+ if (this.workingOffTimer !== null) {
228
+ clearTimeout(this.workingOffTimer)
229
+ this.workingOffTimer = null
230
+ }
231
+
232
+ /* remove any pending event listeners */
233
+ this.queue.removeAllListeners("write")
234
+
215
235
  /* close stream */
216
236
  if (this.stream !== null) {
217
237
  this.stream.destroy()
218
238
  this.stream = null
219
239
  }
220
-
221
- /* indicate destruction */
222
- this.destroyed = true
223
240
  }
224
241
  }
@@ -0,0 +1,276 @@
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
+ /* standard dependencies */
8
+ import path from "node:path"
9
+ import http from "node:http"
10
+ import Stream from "node:stream"
11
+
12
+ /* external dependencies */
13
+ import { Duration } from "luxon"
14
+ import * as HAPI from "@hapi/hapi"
15
+ import Inert from "@hapi/inert"
16
+ import WebSocket from "ws"
17
+ import HAPIWebSocket from "hapi-plugin-websocket"
18
+
19
+ /* internal dependencies */
20
+ import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
21
+
22
+ type wsPeerCtx = {
23
+ peer: string
24
+ }
25
+ type wsPeerInfo = {
26
+ ctx: wsPeerCtx
27
+ ws: WebSocket
28
+ req: http.IncomingMessage
29
+ }
30
+
31
+ /* SpeechFlow node for subtitle (text-to-text) "translations" */
32
+ export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
33
+ /* declare official node name */
34
+ public static name = "subtitle"
35
+
36
+ /* internal state */
37
+ private sequenceNo = 1
38
+ private hapi: HAPI.Server | null = null
39
+
40
+ /* construct node */
41
+ constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
42
+ super(id, cfg, opts, args)
43
+
44
+ /* declare node configuration parameters */
45
+ this.configure({
46
+ format: { type: "string", pos: 0, val: "srt", match: /^(?:srt|vtt)$/ },
47
+ words: { type: "boolean", val: false },
48
+ mode: { type: "string", val: "export", match: /^(?:export|render)$/ },
49
+ addr: { type: "string", val: "127.0.0.1" },
50
+ port: { type: "number", val: 8585 }
51
+ })
52
+
53
+ /* declare node input/output format */
54
+ this.input = "text"
55
+ this.output = this.params.mode === "export" ? "text" : "none"
56
+ }
57
+
58
+ /* open node */
59
+ async open () {
60
+ if (this.params.mode === "export") {
61
+ this.sequenceNo = 1
62
+
63
+ /* provide text-to-subtitle conversion */
64
+ const convert = async (chunk: SpeechFlowChunk) => {
65
+ if (typeof chunk.payload !== "string")
66
+ throw new Error("chunk payload type must be string")
67
+ const convertSingle = (
68
+ start: Duration,
69
+ end: Duration,
70
+ text: string,
71
+ word?: string,
72
+ occurence?: number
73
+ ) => {
74
+ if (word) {
75
+ occurence ??= 1
76
+ let match = 1
77
+ word = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
78
+ text = text.replaceAll(new RegExp(`\\b${word}\\b`, "g"), (m) => {
79
+ if (match++ === occurence)
80
+ return `<b>${m}</b>`
81
+ else
82
+ return m
83
+ })
84
+ }
85
+ if (this.params.format === "srt") {
86
+ const startFmt = start.toFormat("hh:mm:ss,SSS")
87
+ const endFmt = end.toFormat("hh:mm:ss,SSS")
88
+ text = `${this.sequenceNo++}\n` +
89
+ `${startFmt} --> ${endFmt}\n` +
90
+ `${text}\n\n`
91
+ }
92
+ else if (this.params.format === "vtt") {
93
+ const startFmt = start.toFormat("hh:mm:ss.SSS")
94
+ const endFmt = end.toFormat("hh:mm:ss.SSS")
95
+ text = `${startFmt} --> ${endFmt}\n` +
96
+ `${text}\n\n`
97
+ }
98
+ return text
99
+ }
100
+ let output = ""
101
+ if (this.params.words) {
102
+ output += convertSingle(chunk.timestampStart, chunk.timestampEnd, chunk.payload)
103
+ const words = (chunk.meta.get("words") ?? []) as
104
+ { word: string, start: Duration, end: Duration }[]
105
+ const occurences = new Map<string, number>()
106
+ for (const word of words) {
107
+ let occurence = occurences.get(word.word) ?? 0
108
+ occurence++
109
+ occurences.set(word.word, occurence)
110
+ output += convertSingle(word.start, word.end, chunk.payload, word.word, occurence)
111
+ }
112
+ }
113
+ else
114
+ output += convertSingle(chunk.timestampStart, chunk.timestampEnd, chunk.payload)
115
+ return output
116
+ }
117
+
118
+ /* establish a duplex stream */
119
+ const self = this
120
+ let firstChunk = true
121
+ this.stream = new Stream.Transform({
122
+ readableObjectMode: true,
123
+ writableObjectMode: true,
124
+ decodeStrings: false,
125
+ highWaterMark: 1,
126
+ transform (chunk: SpeechFlowChunk, encoding, callback) {
127
+ if (firstChunk && self.params.format === "vtt") {
128
+ this.push(new SpeechFlowChunk(
129
+ Duration.fromMillis(0), Duration.fromMillis(0),
130
+ "final", "text",
131
+ "WEBVTT\n\n"
132
+ ))
133
+ firstChunk = false
134
+ }
135
+ if (Buffer.isBuffer(chunk.payload))
136
+ callback(new Error("invalid chunk payload type"))
137
+ else {
138
+ if (chunk.payload === "") {
139
+ this.push(chunk)
140
+ callback()
141
+ }
142
+ else {
143
+ convert(chunk).then((payload) => {
144
+ const chunkNew = chunk.clone()
145
+ chunkNew.payload = payload
146
+ this.push(chunkNew)
147
+ callback()
148
+ }).catch((err) => {
149
+ callback(err)
150
+ })
151
+ }
152
+ }
153
+ },
154
+ final (callback) {
155
+ this.push(null)
156
+ callback()
157
+ }
158
+ })
159
+ }
160
+ else if (this.params.mode === "render") {
161
+ /* establish REST/WebSocket API */
162
+ const wsPeers = new Map<string, wsPeerInfo>()
163
+ this.hapi = new HAPI.Server({
164
+ address: this.params.addr,
165
+ port: this.params.port
166
+ })
167
+ await this.hapi.register({ plugin: Inert })
168
+ await this.hapi.register({ plugin: HAPIWebSocket })
169
+ this.hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
170
+ if (event.error instanceof Error)
171
+ this.log("error", `HAPI: request-error: ${event.error.message}`)
172
+ else
173
+ this.log("error", `HAPI: request-error: ${event.error}`)
174
+ })
175
+ this.hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
176
+ if (tags.error) {
177
+ const err = event.error
178
+ if (err instanceof Error)
179
+ this.log("error", `HAPI: log: ${err.message}`)
180
+ else
181
+ this.log("error", `HAPI: log: ${err}`)
182
+ }
183
+ })
184
+ this.hapi.route({
185
+ method: "GET",
186
+ path: "/{param*}",
187
+ handler: {
188
+ directory: {
189
+ path: path.join(__dirname, "../../speechflow-ui-st/dst"),
190
+ redirectToSlash: true,
191
+ index: true
192
+ }
193
+ }
194
+ })
195
+ this.hapi.route({
196
+ method: "POST",
197
+ path: "/api",
198
+ options: {
199
+ payload: {
200
+ output: "data",
201
+ parse: true,
202
+ allow: "application/json"
203
+ },
204
+ plugins: {
205
+ websocket: {
206
+ autoping: 30 * 1000,
207
+ connect: (args: any) => {
208
+ const ctx: wsPeerCtx = args.ctx
209
+ const ws: WebSocket = args.ws
210
+ const req: http.IncomingMessage = args.req
211
+ const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
212
+ ctx.peer = peer
213
+ wsPeers.set(peer, { ctx, ws, req })
214
+ this.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
215
+ },
216
+ disconnect: (args: any) => {
217
+ const ctx: wsPeerCtx = args.ctx
218
+ const peer = ctx.peer
219
+ wsPeers.delete(peer)
220
+ this.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
221
+ }
222
+ }
223
+ }
224
+ },
225
+ handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
226
+ return h.response({}).code(204)
227
+ }
228
+ })
229
+
230
+ await this.hapi.start()
231
+ this.log("info", `HAPI: started REST/WebSocket network service: http://${this.params.addr}:${this.params.port}`)
232
+
233
+ const emit = (chunk: SpeechFlowChunk) => {
234
+ const data = JSON.stringify(chunk)
235
+ for (const info of wsPeers.values())
236
+ info.ws.send(data)
237
+ }
238
+
239
+ this.stream = new Stream.Writable({
240
+ objectMode: true,
241
+ decodeStrings: false,
242
+ highWaterMark: 1,
243
+ write (chunk: SpeechFlowChunk, encoding, callback) {
244
+ if (Buffer.isBuffer(chunk.payload))
245
+ callback(new Error("invalid chunk payload type"))
246
+ else {
247
+ if (chunk.payload === "")
248
+ callback()
249
+ else {
250
+ emit(chunk)
251
+ callback()
252
+ }
253
+ }
254
+ },
255
+ final (callback) {
256
+ callback()
257
+ }
258
+ })
259
+ }
260
+ }
261
+
262
+ /* open node */
263
+ async close () {
264
+ /* close stream */
265
+ if (this.stream !== null) {
266
+ this.stream.destroy()
267
+ this.stream = null
268
+ }
269
+
270
+ /* shutdown HAPI */
271
+ if (this.hapi !== null) {
272
+ await this.hapi.stop()
273
+ this.hapi = null
274
+ }
275
+ }
276
+ }
@@ -104,7 +104,7 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
104
104
 
105
105
  /* open node */
106
106
  async open () {
107
- let model: string = ""
107
+ let model = ""
108
108
 
109
109
  /* track download progress when instantiating Transformers engine and model */
110
110
  const progressState = new Map<string, number>()
@@ -138,7 +138,6 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
138
138
  progress_callback: progressCallback
139
139
  })
140
140
  this.translator = await pipeline
141
- clearInterval(interval)
142
141
  if (this.translator === null)
143
142
  throw new Error("failed to instantiate translator pipeline")
144
143
  }
@@ -151,13 +150,15 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
151
150
  progress_callback: progressCallback
152
151
  })
153
152
  this.generator = await pipeline
154
- clearInterval(interval)
155
153
  if (this.generator === null)
156
154
  throw new Error("failed to instantiate generator pipeline")
157
155
  }
158
156
  else
159
157
  throw new Error("invalid model")
160
158
 
159
+ /* clear progress interval again */
160
+ clearInterval(interval)
161
+
161
162
  /* provide text-to-text translation */
162
163
  const translate = async (text: string) => {
163
164
  if (this.params.model === "OPUS") {
@@ -23,7 +23,7 @@ export default class SpeechFlowNodeFilter extends SpeechFlowNode {
23
23
  this.configure({
24
24
  type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
25
25
  name: { type: "string", pos: 1, val: "filter", match: /^.+?$/ },
26
- var: { type: "string", pos: 2, val: "", match: /^(?:meta:.+|payload:(?:length|text)|time:(?:start|end))$/ },
26
+ var: { type: "string", pos: 2, val: "", match: /^(?:meta:.+|payload:(?:length|text)|time:(?:start|end)|kind|type)$/ },
27
27
  op: { type: "string", pos: 3, val: "==", match: /^(?:<|<=|==|!=|~~|!~|>=|>)$/ },
28
28
  val: { type: "string", pos: 4, val: "", match: /^.*$/ }
29
29
  })
@@ -94,13 +94,17 @@ export default class SpeechFlowNodeFilter extends SpeechFlowNode {
94
94
  const m = self.params.var.match(/^meta:(.+)$/)
95
95
  if (m !== null)
96
96
  val1 = chunk.meta.get(m[1]) ?? ""
97
- else if (self.params.key === "payload:length")
97
+ else if (self.params.var === "kind")
98
+ val1 = chunk.kind
99
+ else if (self.params.var === "type")
100
+ val1 = chunk.type
101
+ else if (self.params.var === "payload:length")
98
102
  val1 = chunk.payload.length
99
- else if (self.params.key === "payload:text")
103
+ else if (self.params.var === "payload:text")
100
104
  val1 = (self.params.type === "text" ? chunk.payload as string : "")
101
- else if (self.params.key === "time:start")
105
+ else if (self.params.var === "time:start")
102
106
  val1 = chunk.timestampStart.toMillis()
103
- else if (self.params.key === "time:end")
107
+ else if (self.params.var === "time:end")
104
108
  val1 = chunk.timestampEnd.toMillis()
105
109
  if (comparison(val1, self.params.op, val2)) {
106
110
  self.log("info", `[${self.params.name}]: passing through ${chunk.type} chunk`)
@@ -24,10 +24,15 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
24
24
 
25
25
  /* declare node configuration parameters */
26
26
  this.configure({
27
- type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
28
- name: { type: "string", pos: 1, val: "trace" }
27
+ type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
28
+ name: { type: "string", pos: 1, val: "trace" },
29
+ dashboard: { type: "string", val: "" }
29
30
  })
30
31
 
32
+ /* sanity check parameters */
33
+ if (this.params.dashboard !== "" && this.params.type === "audio")
34
+ throw new Error("only trace nodes of type \"text\" can export to dashboard")
35
+
31
36
  /* declare node input/output format */
32
37
  this.input = this.params.type
33
38
  this.output = this.params.type
@@ -44,7 +49,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
44
49
  }
45
50
 
46
51
  /* provide Transform stream */
47
- const type = this.params.type
52
+ const self = this
48
53
  this.stream = new Stream.Transform({
49
54
  writableObjectMode: true,
50
55
  readableObjectMode: true,
@@ -63,7 +68,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
63
68
  } }`
64
69
  }
65
70
  if (Buffer.isBuffer(chunk.payload)) {
66
- if (type === "audio")
71
+ if (self.params.type === "audio")
67
72
  log("debug", `chunk: type=${chunk.type} ` +
68
73
  `kind=${chunk.kind} ` +
69
74
  `start=${fmtTime(chunk.timestampStart)} ` +
@@ -71,19 +76,22 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
71
76
  `payload-type=Buffer payload-length=${chunk.payload.byteLength} ` +
72
77
  `meta=${fmtMeta(chunk.meta)}`)
73
78
  else
74
- error = new Error(`${type} chunk: seen Buffer instead of String chunk type`)
79
+ error = new Error(`${self.params.type} chunk: seen Buffer instead of String chunk type`)
75
80
  }
76
81
  else {
77
- if (type === "text")
78
- log("debug", `${type} chunk: type=${chunk.type} ` +
82
+ if (self.params.type === "text") {
83
+ log("debug", `chunk: type=${chunk.type} ` +
79
84
  `kind=${chunk.kind} ` +
80
85
  `start=${fmtTime(chunk.timestampStart)} ` +
81
86
  `end=${fmtTime(chunk.timestampEnd)} ` +
82
87
  `payload-type=String payload-length=${chunk.payload.length} ` +
83
88
  `payload-content="${chunk.payload.toString()}" ` +
84
89
  `meta=${fmtMeta(chunk.meta)}`)
90
+ if (self.params.dashboard !== "")
91
+ self.dashboardInfo("text", self.params.dashboard, chunk.kind, chunk.payload.toString())
92
+ }
85
93
  else
86
- error = new Error(`${type} chunk: seen String instead of Buffer chunk type`)
94
+ error = new Error(`${self.params.type} chunk: seen String instead of Buffer chunk type`)
87
95
  }
88
96
  if (error !== undefined)
89
97
  callback(error)
@@ -159,8 +159,7 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
159
159
 
160
160
  /* convert regular stream into object-mode stream */
161
161
  const wrapper = utils.createTransformStreamForReadableSide("audio", () => this.timeZero)
162
- this.stream.pipe(wrapper)
163
- this.stream = wrapper
162
+ this.stream = Stream.compose(this.stream, wrapper)
164
163
  }
165
164
  else if (this.params.mode === "w") {
166
165
  /* output device */
@@ -180,8 +179,7 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
180
179
 
181
180
  /* convert regular stream into object-mode stream */
182
181
  const wrapper = utils.createTransformStreamForWritableSide()
183
- wrapper.pipe(this.stream)
184
- this.stream = wrapper
182
+ this.stream = Stream.compose(wrapper, this.stream)
185
183
  }
186
184
  else
187
185
  throw new Error(`device "${device.id}" does not have any input or output channels`)
@@ -200,13 +198,19 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
200
198
  /* shutdown PortAudio */
201
199
  if (this.io !== null) {
202
200
  await new Promise<void>((resolve, reject) => {
203
- this.io!.abort(() => {
204
- resolve()
201
+ this.io!.abort((err?: Error) => {
202
+ if (err)
203
+ reject(err)
204
+ else
205
+ resolve()
205
206
  })
206
207
  })
207
208
  await new Promise<void>((resolve, reject) => {
208
- this.io!.quit(() => {
209
- resolve()
209
+ this.io!.quit((err?: Error) => {
210
+ if (err)
211
+ reject(err)
212
+ else
213
+ resolve()
210
214
  })
211
215
  })
212
216
  this.io = null
@@ -178,9 +178,15 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
178
178
  async close () {
179
179
  /* shutdown stream */
180
180
  if (this.stream !== null) {
181
- await new Promise<void>((resolve) => {
182
- if (this.stream instanceof Stream.Writable || this.stream instanceof Stream.Duplex)
183
- this.stream.end(() => { resolve() })
181
+ await new Promise<void>((resolve, reject) => {
182
+ if (this.stream instanceof Stream.Writable || this.stream instanceof Stream.Duplex) {
183
+ this.stream.end((err?: Error) => {
184
+ if (err)
185
+ reject(err)
186
+ else
187
+ resolve()
188
+ })
189
+ }
184
190
  else
185
191
  resolve()
186
192
  })
@@ -83,7 +83,10 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
83
83
  this.broker.on("connect", (packet: MQTT.IConnackPacket) => {
84
84
  this.log("info", `connection opened to MQTT ${this.params.url}`)
85
85
  if (this.params.mode !== "w" && !packet.sessionPresent)
86
- this.broker!.subscribe([ this.params.topicRead ], () => {})
86
+ this.broker!.subscribe([ this.params.topicRead ], (err) => {
87
+ if (err)
88
+ this.log("error", `failed to subscribe to MQTT topic "${this.params.topicRead}": ${err.message}`)
89
+ })
87
90
  })
88
91
  this.broker.on("reconnect", () => {
89
92
  this.log("info", `connection re-opened to MQTT ${this.params.url}`)
@@ -141,7 +144,7 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
141
144
 
142
145
  /* close node */
143
146
  async close () {
144
- /* close Websocket server */
147
+ /* close MQTT broker */
145
148
  if (this.broker !== null) {
146
149
  if (this.broker.connected)
147
150
  this.broker.end()