speechflow 1.4.5 → 1.5.1

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 (176) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +242 -7
  3. package/etc/claude.md +70 -0
  4. package/etc/speechflow.yaml +13 -11
  5. package/etc/stx.conf +7 -0
  6. package/package.json +7 -6
  7. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.d.ts +1 -0
  8. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +155 -0
  9. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -0
  10. package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +15 -0
  11. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +287 -0
  12. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -0
  13. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.d.ts +1 -0
  14. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +208 -0
  15. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +1 -0
  16. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +15 -0
  17. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +312 -0
  18. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +1 -0
  19. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.d.ts +1 -0
  20. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +161 -0
  21. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -0
  22. package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +13 -0
  23. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +208 -0
  24. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -0
  25. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +3 -3
  26. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  27. package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +14 -0
  28. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +233 -0
  29. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -0
  30. package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +12 -0
  31. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +125 -0
  32. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -0
  33. package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +0 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +28 -12
  35. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -0
  37. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +12 -8
  38. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  39. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +2 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.d.ts +1 -0
  42. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +55 -0
  43. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -0
  44. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +14 -0
  45. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +184 -0
  46. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -0
  47. package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +14 -0
  48. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +156 -0
  49. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -0
  50. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
  51. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +22 -17
  53. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  54. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +18 -0
  55. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +312 -0
  56. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
  57. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -14
  58. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  59. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +19 -0
  60. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +351 -0
  61. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -0
  62. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +16 -0
  63. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +204 -0
  64. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -0
  65. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +19 -14
  66. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  67. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +47 -8
  68. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  69. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +13 -0
  70. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +175 -0
  71. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
  72. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +14 -15
  73. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  74. package/speechflow-cli/dst/speechflow-node-t2t-format.js +10 -15
  75. package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +13 -0
  77. package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
  78. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
  79. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +80 -33
  80. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +78 -45
  82. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  83. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
  84. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  85. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +13 -14
  86. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  87. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +23 -27
  88. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
  90. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
  91. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  92. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
  93. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  94. package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
  95. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
  97. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
  98. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  99. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
  100. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
  102. package/speechflow-cli/dst/speechflow-node.js +13 -2
  103. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  104. package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
  105. package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
  106. package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
  107. package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
  108. package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
  109. package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
  110. package/speechflow-cli/dst/speechflow-utils.d.ts +34 -0
  111. package/speechflow-cli/dst/speechflow-utils.js +256 -35
  112. package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
  113. package/speechflow-cli/dst/speechflow.js +75 -26
  114. package/speechflow-cli/dst/speechflow.js.map +1 -1
  115. package/speechflow-cli/etc/biome.jsonc +2 -1
  116. package/speechflow-cli/etc/oxlint.jsonc +113 -11
  117. package/speechflow-cli/etc/stx.conf +2 -2
  118. package/speechflow-cli/etc/tsconfig.json +1 -1
  119. package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
  120. package/speechflow-cli/package.json +103 -94
  121. package/speechflow-cli/src/lib.d.ts +24 -0
  122. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
  123. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
  124. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
  125. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
  126. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -3
  127. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
  128. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
  129. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
  130. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
  131. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
  132. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
  133. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
  134. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
  135. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
  136. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
  137. package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +306 -0
  138. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -15
  139. package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
  140. package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
  141. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
  142. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +15 -9
  143. package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +153 -0
  144. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +14 -15
  145. package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
  146. package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
  147. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +58 -44
  148. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +59 -58
  149. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
  150. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +18 -18
  151. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +28 -32
  152. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
  153. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
  154. package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
  155. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
  156. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
  157. package/speechflow-cli/src/speechflow-node.ts +21 -8
  158. package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
  159. package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
  160. package/speechflow-cli/src/speechflow-utils.ts +314 -32
  161. package/speechflow-cli/src/speechflow.ts +84 -33
  162. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  163. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  164. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  165. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  166. package/speechflow-ui-db/dst/index.css +2 -2
  167. package/speechflow-ui-db/dst/index.js +37 -38
  168. package/speechflow-ui-db/etc/eslint.mjs +0 -1
  169. package/speechflow-ui-db/etc/tsc-client.json +3 -3
  170. package/speechflow-ui-db/package.json +12 -11
  171. package/speechflow-ui-db/src/app.vue +20 -6
  172. package/speechflow-ui-st/dst/index.js +26 -26
  173. package/speechflow-ui-st/etc/eslint.mjs +0 -1
  174. package/speechflow-ui-st/etc/tsc-client.json +3 -3
  175. package/speechflow-ui-st/package.json +12 -11
  176. package/speechflow-ui-st/src/app.vue +5 -12
@@ -23,6 +23,7 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
23
23
  /* internal state */
24
24
  private broker: MQTT.MqttClient | null = null
25
25
  private clientId: string = (new UUID(1)).format()
26
+ private chunkQueue: utils.SingleQueue<SpeechFlowChunk> | null = null
26
27
 
27
28
  /* construct node */
28
29
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -63,6 +64,10 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
63
64
  throw new Error("writing to MQTT requires a topicWrite parameter")
64
65
  if ((this.params.mode === "r" || this.params.mode === "rw") && this.params.topicRead === "")
65
66
  throw new Error("reading from MQTT requires a topicRead parameter")
67
+ if (this.params.username !== "" && this.params.password === "")
68
+ throw new Error("username provided but password is missing")
69
+ if (this.params.username === "" && this.params.password !== "")
70
+ throw new Error("password provided but username is missing")
66
71
 
67
72
  /* connect remotely to a MQTT broker */
68
73
  this.broker = MQTT.connect(this.params.url, {
@@ -85,7 +90,7 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
85
90
  if (this.params.mode !== "w" && !packet.sessionPresent)
86
91
  this.broker!.subscribe([ this.params.topicRead ], (err) => {
87
92
  if (err)
88
- this.log("error", `failed to subscribe to MQTT topic "${this.params.topicRead}": ${err.message}`)
93
+ this.log("warning", `failed to subscribe to MQTT topic "${this.params.topicRead}": ${err.message}`)
89
94
  })
90
95
  })
91
96
  this.broker.on("reconnect", () => {
@@ -94,49 +99,48 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
94
99
  this.broker.on("disconnect", (packet: MQTT.IDisconnectPacket) => {
95
100
  this.log("info", `connection closed to MQTT ${this.params.url}`)
96
101
  })
97
- const chunkQueue = new utils.SingleQueue<SpeechFlowChunk>()
102
+ this.chunkQueue = new utils.SingleQueue<SpeechFlowChunk>()
98
103
  this.broker.on("message", (topic: string, payload: Buffer, packet: MQTT.IPublishPacket) => {
99
- if (topic !== this.params.topicRead)
104
+ if (topic !== this.params.topicRead || this.params.mode === "w")
100
105
  return
101
106
  try {
102
107
  const chunk = utils.streamChunkDecode(payload)
103
- chunkQueue.write(chunk)
108
+ this.chunkQueue!.write(chunk)
104
109
  }
105
110
  catch (_err: any) {
106
111
  this.log("warning", `received invalid CBOR chunk from MQTT ${this.params.url}`)
107
112
  }
108
113
  })
109
- const broker = this.broker
110
- const topicWrite = this.params.topicWrite
111
- const type = this.params.type
112
- const mode = this.params.mode
114
+ const self = this
113
115
  this.stream = new Stream.Duplex({
114
116
  writableObjectMode: true,
115
117
  readableObjectMode: true,
116
118
  decodeStrings: false,
117
119
  highWaterMark: 1,
118
120
  write (chunk: SpeechFlowChunk, encoding, callback) {
119
- if (mode === "r")
121
+ if (self.params.mode === "r")
120
122
  callback(new Error("write operation on read-only node"))
121
- else if (chunk.type !== type)
122
- callback(new Error(`written chunk is not of ${type} type`))
123
- else if (!broker.connected)
123
+ else if (chunk.type !== self.params.type)
124
+ callback(new Error(`written chunk is not of ${self.params.type} type`))
125
+ else if (!self.broker!.connected)
124
126
  callback(new Error("still no MQTT connection available"))
125
127
  else {
126
128
  const data = Buffer.from(utils.streamChunkEncode(chunk))
127
- broker.publish(topicWrite, data, { qos: 2, retain: false }, (err) => {
129
+ self.broker!.publish(self.params.topicWrite, data, { qos: 2, retain: false }, (err) => {
128
130
  if (err)
129
- callback(new Error(`failed to publish to MQTT topic "${topicWrite}": ${err}`))
131
+ callback(new Error(`failed to publish to MQTT topic "${self.params.topicWrite}": ${err}`))
130
132
  else
131
133
  callback()
132
134
  })
133
135
  }
134
136
  },
135
137
  read (size: number) {
136
- if (mode === "w")
138
+ if (self.params.mode === "w")
137
139
  throw new Error("read operation on write-only node")
138
- chunkQueue.read().then((chunk) => {
140
+ self.chunkQueue!.read().then((chunk) => {
139
141
  this.push(chunk, "binary")
142
+ }).catch((err: Error) => {
143
+ self.log("warning", `read on chunk queue operation failed: ${err}`)
140
144
  })
141
145
  }
142
146
  })
@@ -144,6 +148,9 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
144
148
 
145
149
  /* close node */
146
150
  async close () {
151
+ /* clear chunk queue reference */
152
+ this.chunkQueue = null
153
+
147
154
  /* close MQTT broker */
148
155
  if (this.broker !== null) {
149
156
  if (this.broker.connected)
@@ -66,7 +66,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
66
66
  const chunkQueue = new utils.SingleQueue<SpeechFlowChunk>()
67
67
  this.server = new ws.WebSocketServer({
68
68
  host: url.hostname,
69
- port: Number.parseInt(url.port),
69
+ port: Number.parseInt(url.port, 10),
70
70
  path: url.pathname
71
71
  })
72
72
  this.server.on("listening", () => {
@@ -108,18 +108,17 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
108
108
  this.server.on("error", (error) => {
109
109
  this.log("error", `error of some connection on URL ${this.params.listen}: ${error.message}`)
110
110
  })
111
- const type = this.params.type
112
- const mode = this.params.mode
111
+ const self = this
113
112
  this.stream = new Stream.Duplex({
114
113
  writableObjectMode: true,
115
114
  readableObjectMode: true,
116
115
  decodeStrings: false,
117
116
  highWaterMark: 1,
118
117
  write (chunk: SpeechFlowChunk, encoding, callback) {
119
- if (mode === "r")
118
+ if (self.params.mode === "r")
120
119
  callback(new Error("write operation on read-only node"))
121
- else if (chunk.type !== type)
122
- callback(new Error(`written chunk is not of ${type} type`))
120
+ else if (chunk.type !== self.params.type)
121
+ callback(new Error(`written chunk is not of ${self.params.type} type`))
123
122
  else if (websockets.size === 0)
124
123
  callback(new Error("still no Websocket connections available"))
125
124
  else {
@@ -137,17 +136,18 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
137
136
  }
138
137
  Promise.all(results).then(() => {
139
138
  callback()
140
- }).catch((errors: Error[]) => {
141
- const error = new Error(errors.map((e) => e.message).join("; "))
139
+ }).catch((error: Error) => {
142
140
  callback(error)
143
141
  })
144
142
  }
145
143
  },
146
144
  read (size: number) {
147
- if (mode === "w")
145
+ if (self.params.mode === "w")
148
146
  throw new Error("read operation on write-only node")
149
147
  chunkQueue.read().then((chunk) => {
150
148
  this.push(chunk, "binary")
149
+ }).catch((err: Error) => {
150
+ self.log("warning", `read on chunk queue operation failed: ${err}`)
151
151
  })
152
152
  }
153
153
  })
@@ -188,33 +188,33 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
188
188
  const chunk = utils.streamChunkDecode(buffer)
189
189
  chunkQueue.write(chunk)
190
190
  })
191
- const client = this.client
192
- client.binaryType = "arraybuffer"
193
- const type = this.params.type
194
- const mode = this.params.mode
191
+ this.client.binaryType = "arraybuffer"
192
+ const self = this
195
193
  this.stream = new Stream.Duplex({
196
194
  writableObjectMode: true,
197
195
  readableObjectMode: true,
198
196
  decodeStrings: false,
199
197
  highWaterMark: 1,
200
198
  write (chunk: SpeechFlowChunk, encoding, callback) {
201
- if (mode === "r")
199
+ if (self.params.mode === "r")
202
200
  callback(new Error("write operation on read-only node"))
203
- else if (chunk.type !== type)
204
- callback(new Error(`written chunk is not of ${type} type`))
205
- else if (!client.OPEN)
201
+ else if (chunk.type !== self.params.type)
202
+ callback(new Error(`written chunk is not of ${self.params.type} type`))
203
+ else if (!self.client!.OPEN)
206
204
  callback(new Error("still no Websocket connection available"))
207
205
  else {
208
206
  const data = utils.streamChunkEncode(chunk)
209
- client.send(data)
207
+ self.client!.send(data)
210
208
  callback()
211
209
  }
212
210
  },
213
211
  read (size: number) {
214
- if (mode === "w")
212
+ if (self.params.mode === "w")
215
213
  throw new Error("read operation on write-only node")
216
214
  chunkQueue.read().then((chunk) => {
217
215
  this.push(chunk, "binary")
216
+ }).catch((err: Error) => {
217
+ self.log("warning", `read on chunk queue operation failed: ${err}`)
218
218
  })
219
219
  }
220
220
  })
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /* standard dependencies */
8
- import Events from "node:events"
8
+ import Events, { EventEmitter } from "node:events"
9
9
  import Stream from "node:stream"
10
10
 
11
11
  /* external dependencies */
@@ -62,6 +62,7 @@ export default class SpeechFlowNode extends Events.EventEmitter {
62
62
  timeOpen: DateTime<boolean> | undefined
63
63
  timeZero: DateTime<boolean> = DateTime.fromMillis(0)
64
64
  timeZeroOffset: Duration<boolean> = Duration.fromMillis(0)
65
+ _accessBus: ((name: string) => EventEmitter) | null = null
65
66
 
66
67
  /* the default constructor */
67
68
  constructor (
@@ -87,20 +88,32 @@ export default class SpeechFlowNode extends Events.EventEmitter {
87
88
  }
88
89
 
89
90
  /* receive external request */
90
- async receiveRequest (args: any[]) {
91
+ async receiveRequest (args: any[]): Promise<void> {
91
92
  /* no-op */
92
93
  }
93
94
 
94
95
  /* send external response */
95
- sendResponse (args: any[]) {
96
+ sendResponse (args: any[]): void {
96
97
  this.emit("send-response", args)
97
98
  }
98
99
 
99
- /* emit dashboard information */
100
- dashboardInfo (type: "audio", id: string, kind: "final" | "intermediate", value: number): void
101
- dashboardInfo (type: "text", id: string, kind: "final" | "intermediate", value: string): void
102
- dashboardInfo (type: "audio" | "text", id: string, kind: "final" | "intermediate", value: number | string): void {
103
- this.emit("dashboard-info", { type, id, kind, value })
100
+ /* receive dashboard information */
101
+ async receiveDashboard (type: "audio" | "text", id: string, kind: "final" | "intermediate", value: number | string): Promise<void> {
102
+ /* no-op */
103
+ }
104
+
105
+ /* send dashboard information */
106
+ sendDashboard (type: "audio", id: string, kind: "final" | "intermediate", value: number): void
107
+ sendDashboard (type: "text", id: string, kind: "final" | "intermediate", value: string): void
108
+ sendDashboard (type: "audio" | "text", id: string, kind: "final" | "intermediate", value: number | string): void {
109
+ this.emit("send-dashboard", { type, id, kind, value })
110
+ }
111
+
112
+ /* access communication bus */
113
+ accessBus (name: string): EventEmitter {
114
+ if (this._accessBus === null)
115
+ throw new Error("access to communication bus still not possible")
116
+ return this._accessBus(name)
104
117
  }
105
118
 
106
119
  /* INTERNAL: utility function: create "params" attribute from constructor of sub-classes */
@@ -0,0 +1,172 @@
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 types */
8
+ interface InputChunkMessage {
9
+ type: "input-chunk"
10
+ chunkId: string
11
+ data: { pcmData: Float32Array, channels: number }
12
+ }
13
+ interface StartCaptureMessage {
14
+ type: "start-capture"
15
+ chunkId: string
16
+ expectedSamples: number
17
+ }
18
+ type WorkletMessage = InputChunkMessage | StartCaptureMessage
19
+ interface ChunkData {
20
+ data: Float32Array
21
+ chunkId: string
22
+ }
23
+ interface ChunkStartedMessage {
24
+ type: "chunk-started"
25
+ chunkId: string
26
+ }
27
+ interface CaptureCompleteMessage {
28
+ type: "capture-complete"
29
+ chunkId: string
30
+ data: number[]
31
+ }
32
+
33
+ /* audio source node */
34
+ class AudioSourceProcessor extends AudioWorkletProcessor {
35
+ /* internal state */
36
+ private pendingData: ChunkData[] = []
37
+ private currentChunk: ChunkData | null = null
38
+ private currentOffset = 0
39
+
40
+ /* node construction */
41
+ constructor() {
42
+ super()
43
+
44
+ /* receive input chunks */
45
+ this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
46
+ const { type, chunkId } = event.data
47
+ if (type === "input-chunk")
48
+ this.pendingData.push({ data: event.data.data.pcmData, chunkId })
49
+ })
50
+ }
51
+
52
+ /* process audio frame */
53
+ process(
54
+ inputs: Float32Array[][], /* unused */
55
+ outputs: Float32Array[][],
56
+ parameters: Record<string, Float32Array> /* unused */
57
+ ): boolean {
58
+ /* determine output */
59
+ const output = outputs[0]
60
+ if (!output || output.length === 0)
61
+ return true
62
+ const frameCount = output[0].length
63
+ const channelCount = output.length
64
+
65
+ /* get current chunk if we don't have one */
66
+ if (this.currentChunk === null && this.pendingData.length > 0) {
67
+ this.currentChunk = this.pendingData.shift()!
68
+ this.currentOffset = 0
69
+
70
+ /* signal chunk start */
71
+ const message: ChunkStartedMessage = {
72
+ type: "chunk-started",
73
+ chunkId: this.currentChunk.chunkId
74
+ }
75
+ this.port.postMessage(message)
76
+ }
77
+
78
+ /* process input */
79
+ if (this.currentChunk) {
80
+ /* output current chunk */
81
+ const samplesPerChannel = this.currentChunk.data.length / channelCount
82
+ const remainingFrames = samplesPerChannel - this.currentOffset
83
+ const framesToProcess = Math.min(frameCount, remainingFrames)
84
+
85
+ /* copy data from current chunk (interleaved to planar) */
86
+ for (let frame = 0; frame < framesToProcess; frame++) {
87
+ for (let ch = 0; ch < channelCount; ch++) {
88
+ const interleavedIndex = (this.currentOffset + frame) * channelCount + ch
89
+ output[ch][frame] = this.currentChunk.data[interleavedIndex] ?? 0
90
+ }
91
+ }
92
+
93
+ /* zero-pad remaining output if needed */
94
+ for (let frame = framesToProcess; frame < frameCount; frame++)
95
+ for (let ch = 0; ch < channelCount; ch++)
96
+ output[ch][frame] = 0
97
+
98
+ /* check if current chunk is finished */
99
+ this.currentOffset += framesToProcess
100
+ if (this.currentOffset >= samplesPerChannel) {
101
+ this.currentChunk = null
102
+ this.currentOffset = 0
103
+ }
104
+ }
105
+ else {
106
+ /* output silence when no input */
107
+ for (let ch = 0; ch < channelCount; ch++)
108
+ output[ch].fill(0)
109
+ }
110
+ return true
111
+ }
112
+ }
113
+
114
+ /* audio capture node */
115
+ class AudioCaptureProcessor extends AudioWorkletProcessor {
116
+ /* internal state */
117
+ private activeCaptures = new Map<string, { data: number[], expectedSamples: number }>()
118
+
119
+ /* node construction */
120
+ constructor() {
121
+ super()
122
+
123
+ /* receive start of capturing command */
124
+ this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
125
+ const { type, chunkId } = event.data
126
+ if (type === "start-capture") {
127
+ this.activeCaptures.set(chunkId, {
128
+ data: [],
129
+ expectedSamples: event.data.expectedSamples
130
+ })
131
+ }
132
+ })
133
+ }
134
+
135
+ /* process audio frame */
136
+ process(
137
+ inputs: Float32Array[][],
138
+ outputs: Float32Array[][], /* unused */
139
+ parameters: Record<string, Float32Array> /* unused */
140
+ ): boolean {
141
+ /* determine input */
142
+ const input = inputs[0]
143
+ if (!input || input.length === 0 || this.activeCaptures.size === 0)
144
+ return true
145
+ const frameCount = input[0].length
146
+ const channelCount = input.length
147
+
148
+ /* iterate over all active captures */
149
+ for (const [ chunkId, capture ] of this.activeCaptures) {
150
+ /* convert planar to interleaved */
151
+ for (let frame = 0; frame < frameCount; frame++)
152
+ for (let ch = 0; ch < channelCount; ch++)
153
+ capture.data.push(input[ch][frame])
154
+
155
+ /* send back captured data */
156
+ if (capture.data.length >= capture.expectedSamples) {
157
+ const message: CaptureCompleteMessage = {
158
+ type: "capture-complete",
159
+ chunkId,
160
+ data: capture.data.slice(0, capture.expectedSamples)
161
+ }
162
+ this.port.postMessage(message)
163
+ this.activeCaptures.delete(chunkId)
164
+ }
165
+ }
166
+ return true
167
+ }
168
+ }
169
+
170
+ /* register the new audio nodes */
171
+ registerProcessor("source", AudioSourceProcessor)
172
+ registerProcessor("capture", AudioCaptureProcessor)
@@ -0,0 +1,147 @@
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
+
10
+ /* external dependencies */
11
+ import { AudioContext, AudioWorkletNode } from "node-web-audio-api"
12
+
13
+ export class WebAudio {
14
+ /* internal state */
15
+ public audioContext: AudioContext
16
+ public sourceNode: AudioWorkletNode | null = null
17
+ public captureNode: AudioWorkletNode | null = null
18
+ private pendingPromises = new Map<string, {
19
+ resolve: (value: Int16Array) => void
20
+ reject: (error: Error) => void
21
+ timeout: ReturnType<typeof setTimeout>
22
+ }>()
23
+
24
+ /* construct object */
25
+ constructor(
26
+ public sampleRate: number,
27
+ public channels: number
28
+ ) {
29
+ /* create new audio context */
30
+ this.audioContext = new AudioContext({
31
+ sampleRate,
32
+ latencyHint: "interactive"
33
+ })
34
+ }
35
+
36
+ /* setup object */
37
+ public async setup (): Promise<void> {
38
+ /* ensure audio context is not suspended */
39
+ if (this.audioContext.state === "suspended")
40
+ await this.audioContext.resume()
41
+
42
+ /* add audio worklet module */
43
+ const url = path.resolve(__dirname, "speechflow-utils-audio-wt.js")
44
+ await this.audioContext.audioWorklet.addModule(url)
45
+
46
+ /* create source node */
47
+ this.sourceNode = new AudioWorkletNode(this.audioContext, "source", {
48
+ numberOfInputs: 0,
49
+ numberOfOutputs: 1,
50
+ outputChannelCount: [ this.channels ]
51
+ })
52
+
53
+ /* create capture node */
54
+ this.captureNode = new AudioWorkletNode(this.audioContext, "capture", {
55
+ numberOfInputs: 1,
56
+ numberOfOutputs: 0
57
+ })
58
+ this.captureNode!.port.addEventListener("message", (event) => {
59
+ const { type, chunkId, data } = event.data ?? {}
60
+ if (type === "capture-complete") {
61
+ const promise = this.pendingPromises.get(chunkId)
62
+ if (promise) {
63
+ clearTimeout(promise.timeout)
64
+ this.pendingPromises.delete(chunkId)
65
+ const int16Data = new Int16Array(data.length)
66
+ for (let i = 0; i < data.length; i++)
67
+ int16Data[i] = Math.max(-32768, Math.min(32767, Math.round(data[i] * 32767)))
68
+ promise.resolve(int16Data)
69
+ }
70
+ }
71
+ })
72
+
73
+ /* start ports */
74
+ this.sourceNode.port.start()
75
+ this.captureNode!.port.start()
76
+ }
77
+
78
+ /* process single audio chunk */
79
+ public async process (int16Array: Int16Array): Promise<Int16Array> {
80
+ const chunkId = `chunk_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
81
+ return new Promise<Int16Array>((resolve, reject) => {
82
+ const timeout = setTimeout(() => {
83
+ this.pendingPromises.delete(chunkId)
84
+ reject(new Error("processing timeout"))
85
+ }, (int16Array.length / this.audioContext.sampleRate) * 1000 + 250)
86
+ if (this.captureNode !== null)
87
+ this.pendingPromises.set(chunkId, { resolve, reject, timeout })
88
+ try {
89
+ const float32Data = new Float32Array(int16Array.length)
90
+ for (let i = 0; i < int16Array.length; i++)
91
+ float32Data[i] = int16Array[i] / 32768.0
92
+
93
+ /* start capture first */
94
+ if (this.captureNode !== null) {
95
+ this.captureNode?.port.postMessage({
96
+ type: "start-capture",
97
+ chunkId,
98
+ expectedSamples: int16Array.length
99
+ })
100
+ }
101
+
102
+ /* small delay to ensure capture is ready before sending data */
103
+ setTimeout(() => {
104
+ /* send input to source node */
105
+ this.sourceNode?.port.postMessage({
106
+ type: "input-chunk",
107
+ chunkId,
108
+ data: { pcmData: float32Data, channels: this.channels }
109
+ }, [ float32Data.buffer ])
110
+ }, 5)
111
+ }
112
+ catch (error) {
113
+ clearTimeout(timeout)
114
+ if (this.captureNode !== null)
115
+ this.pendingPromises.delete(chunkId)
116
+ reject(new Error(`failed to process chunk: ${error}`))
117
+ }
118
+ })
119
+ }
120
+
121
+ public async destroy (): Promise<void> {
122
+ /* reject all pending promises */
123
+ try {
124
+ this.pendingPromises.forEach(({ reject, timeout }) => {
125
+ clearTimeout(timeout)
126
+ reject(new Error("WebAudio destroyed"))
127
+ })
128
+ this.pendingPromises.clear()
129
+ }
130
+ catch (_err) {
131
+ /* ignored - cleanup during shutdown */
132
+ }
133
+
134
+ /* disconnect nodes */
135
+ if (this.sourceNode !== null) {
136
+ this.sourceNode.disconnect()
137
+ this.sourceNode = null
138
+ }
139
+ if (this.captureNode !== null) {
140
+ this.captureNode.disconnect()
141
+ this.captureNode = null
142
+ }
143
+
144
+ /* stop context */
145
+ await this.audioContext.close()
146
+ }
147
+ }