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.
- package/CHANGELOG.md +35 -0
- package/README.md +242 -7
- package/etc/claude.md +70 -0
- package/etc/speechflow.yaml +13 -11
- package/etc/stx.conf +7 -0
- package/package.json +7 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +155 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +287 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +208 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +15 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +312 -0
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +161 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +208 -0
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +233 -0
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +125 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +0 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +28 -12
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +12 -8
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +2 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +55 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +184 -0
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +156 -0
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +22 -17
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +18 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +312 -0
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -14
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +19 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +351 -0
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +16 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +204 -0
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +19 -14
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +47 -8
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +175 -0
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +14 -15
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +10 -15
- package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +80 -33
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +78 -45
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +13 -14
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +23 -27
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
- package/speechflow-cli/dst/speechflow-node.js +13 -2
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
- package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
- package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-utils.d.ts +34 -0
- package/speechflow-cli/dst/speechflow-utils.js +256 -35
- package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
- package/speechflow-cli/dst/speechflow.js +75 -26
- package/speechflow-cli/dst/speechflow.js.map +1 -1
- package/speechflow-cli/etc/biome.jsonc +2 -1
- package/speechflow-cli/etc/oxlint.jsonc +113 -11
- package/speechflow-cli/etc/stx.conf +2 -2
- package/speechflow-cli/etc/tsconfig.json +1 -1
- package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
- package/speechflow-cli/package.json +103 -94
- package/speechflow-cli/src/lib.d.ts +24 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -3
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
- package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +306 -0
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -15
- package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
- package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +15 -9
- package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +153 -0
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +14 -15
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +58 -44
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +59 -58
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +18 -18
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +28 -32
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
- package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
- package/speechflow-cli/src/speechflow-node.ts +21 -8
- package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
- package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
- package/speechflow-cli/src/speechflow-utils.ts +314 -32
- package/speechflow-cli/src/speechflow.ts +84 -33
- package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
- package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
- package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
- package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
- package/speechflow-ui-db/dst/index.css +2 -2
- package/speechflow-ui-db/dst/index.js +37 -38
- package/speechflow-ui-db/etc/eslint.mjs +0 -1
- package/speechflow-ui-db/etc/tsc-client.json +3 -3
- package/speechflow-ui-db/package.json +12 -11
- package/speechflow-ui-db/src/app.vue +20 -6
- package/speechflow-ui-st/dst/index.js +26 -26
- package/speechflow-ui-st/etc/eslint.mjs +0 -1
- package/speechflow-ui-st/etc/tsc-client.json +3 -3
- package/speechflow-ui-st/package.json +12 -11
- 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("
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
192
|
-
|
|
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
|
|
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
|
|
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
|
-
/*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|