speechflow 2.2.0 → 2.3.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.
- package/{etc/claude.md → AGENTS.md} +8 -3
- package/CHANGELOG.md +78 -1
- package/README.md +28 -4
- package/etc/speechflow.yaml +3 -1
- package/etc/stx.conf +1 -1
- package/package.json +6 -6
- package/speechflow-cli/dst/speechflow-main-api.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-main-api.js +57 -16
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-cli.js +2 -2
- package/speechflow-cli/dst/speechflow-main-config.js +1 -1
- package/speechflow-cli/dst/speechflow-main-graph.js +55 -21
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main-nodes.js +1 -1
- package/speechflow-cli/dst/speechflow-main-status.js +6 -3
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
- package/speechflow-cli/dst/speechflow-main.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +7 -10
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +8 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +9 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +2 -4
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +20 -12
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn-wt.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js +33 -11
- package/speechflow-cli/dst/speechflow-node-a2a-gtcrn.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-mute.js +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +4 -3
- package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +19 -11
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +8 -8
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +33 -29
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js +6 -5
- package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +2 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +34 -20
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +13 -5
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-google.js +3 -2
- package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +33 -27
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +16 -5
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-google.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -1
- package/speechflow-cli/dst/{speechflow-node-t2t-simulator.d.ts → speechflow-node-t2a-kitten.d.ts} +5 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js +194 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kitten.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +21 -9
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js +17 -5
- package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +21 -7
- package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-format.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +4 -2
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-opus.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +34 -14
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js +3 -3
- package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-translate.js +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +3 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-trace.js +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +18 -7
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-exec.js +23 -11
- package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +13 -7
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +25 -12
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-vban.js +32 -20
- package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +78 -62
- package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.d.ts +1 -0
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +63 -18
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node.js +5 -7
- package/speechflow-cli/dst/speechflow-node.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio-wt.js +31 -5
- package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-audio.js +25 -14
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-error.js +2 -2
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-llm.js +1 -1
- package/speechflow-cli/dst/speechflow-util-misc.d.ts +3 -2
- package/speechflow-cli/dst/speechflow-util-misc.js +63 -6
- package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-queue.d.ts +5 -17
- package/speechflow-cli/dst/speechflow-util-queue.js +57 -78
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +1 -1
- package/speechflow-cli/dst/speechflow-util-stream.js +35 -8
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
- package/speechflow-cli/dst/speechflow-util.js +1 -1
- package/speechflow-cli/dst/speechflow.d.ts +1 -1
- package/speechflow-cli/dst/speechflow.js +1 -1
- package/speechflow-cli/etc/eslint.mjs +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +2 -1
- package/speechflow-cli/etc/stx.conf +8 -2
- package/speechflow-cli/package.d/@ericedouard+vad-node-realtime+0.2.0.patch +2 -1
- package/speechflow-cli/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-cli/package.d/kitten-tts-js+0.1.2.patch +24 -0
- package/speechflow-cli/package.d/speex-resampler+3.0.1.patch +56 -0
- package/speechflow-cli/package.json +40 -30
- package/speechflow-cli/src/lib.d.ts +19 -1
- package/speechflow-cli/src/speechflow-main-api.ts +64 -19
- package/speechflow-cli/src/speechflow-main-cli.ts +2 -2
- package/speechflow-cli/src/speechflow-main-config.ts +1 -1
- package/speechflow-cli/src/speechflow-main-graph.ts +56 -22
- package/speechflow-cli/src/speechflow-main-nodes.ts +1 -1
- package/speechflow-cli/src/speechflow-main-status.ts +6 -3
- package/speechflow-cli/src/speechflow-main.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +7 -11
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +8 -6
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +10 -5
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -2
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +2 -4
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +20 -13
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn-wt.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-gtcrn.ts +43 -16
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-mute.ts +1 -1
- package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +4 -3
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +24 -12
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +10 -9
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +38 -31
- package/speechflow-cli/src/speechflow-node-a2a-wav.ts +6 -5
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +35 -22
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -6
- package/speechflow-cli/src/speechflow-node-a2t-google.ts +5 -4
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +39 -31
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +16 -5
- package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-google.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-kitten.ts +178 -0
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +21 -9
- package/speechflow-cli/src/speechflow-node-t2a-openai.ts +17 -5
- package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +21 -7
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-format.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +4 -2
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-opus.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-spellcheck.ts +1 -1
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +39 -15
- package/speechflow-cli/src/speechflow-node-t2t-summary.ts +3 -3
- package/speechflow-cli/src/speechflow-node-t2t-translate.ts +1 -1
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +4 -3
- package/speechflow-cli/src/speechflow-node-x2x-trace.ts +1 -1
- package/speechflow-cli/src/speechflow-node-xio-device.ts +21 -7
- package/speechflow-cli/src/speechflow-node-xio-exec.ts +25 -11
- package/speechflow-cli/src/speechflow-node-xio-file.ts +15 -7
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +28 -15
- package/speechflow-cli/src/speechflow-node-xio-vban.ts +35 -22
- package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +85 -69
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +67 -20
- package/speechflow-cli/src/speechflow-node.ts +7 -8
- package/speechflow-cli/src/speechflow-util-audio-wt.ts +46 -7
- package/speechflow-cli/src/speechflow-util-audio.ts +27 -15
- package/speechflow-cli/src/speechflow-util-error.ts +3 -3
- package/speechflow-cli/src/speechflow-util-llm.ts +1 -1
- package/speechflow-cli/src/speechflow-util-misc.ts +63 -6
- package/speechflow-cli/src/speechflow-util-queue.ts +60 -81
- package/speechflow-cli/src/speechflow-util-stream.ts +40 -8
- package/speechflow-cli/src/speechflow-util.ts +1 -1
- package/speechflow-cli/src/speechflow.ts +1 -1
- package/speechflow-ui-db/dst/index.css +1 -1
- package/speechflow-ui-db/dst/index.html +1 -1
- package/speechflow-ui-db/dst/index.js +15 -15
- package/speechflow-ui-db/etc/eslint.mjs +1 -1
- package/speechflow-ui-db/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-db/etc/stx.conf +1 -1
- package/speechflow-ui-db/etc/stylelint.js +1 -1
- package/speechflow-ui-db/etc/stylelint.yaml +1 -1
- package/speechflow-ui-db/etc/vite-client.mts +1 -1
- package/speechflow-ui-db/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-db/package.json +22 -16
- package/speechflow-ui-db/src/app.styl +1 -1
- package/speechflow-ui-db/src/app.vue +4 -4
- package/speechflow-ui-db/src/index.html +1 -1
- package/speechflow-ui-db/src/index.ts +1 -1
- package/speechflow-ui-st/dst/index.html +1 -1
- package/speechflow-ui-st/dst/index.js +31 -31
- package/speechflow-ui-st/etc/eslint.mjs +1 -1
- package/speechflow-ui-st/etc/oxlint.jsonc +1 -1
- package/speechflow-ui-st/etc/stx.conf +1 -1
- package/speechflow-ui-st/etc/stylelint.js +1 -1
- package/speechflow-ui-st/etc/stylelint.yaml +1 -1
- package/speechflow-ui-st/etc/vite-client.mts +1 -1
- package/speechflow-ui-st/package.d/@typescript-eslint+typescript-estree+8.57.2.patch +12 -0
- package/speechflow-ui-st/package.json +23 -17
- package/speechflow-ui-st/src/app.styl +1 -1
- package/speechflow-ui-st/src/app.vue +9 -8
- package/speechflow-ui-st/src/index.html +1 -1
- package/speechflow-ui-st/src/index.ts +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-simulator.js +0 -128
- package/speechflow-cli/dst/speechflow-node-t2t-simulator.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -21,8 +21,9 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
21
21
|
public static name = "xio-websocket"
|
|
22
22
|
|
|
23
23
|
/* internal state */
|
|
24
|
-
private server:
|
|
25
|
-
private client:
|
|
24
|
+
private server: ws.WebSocketServer | null = null
|
|
25
|
+
private client: ReconnWebSocket | null = null
|
|
26
|
+
private chunkQueue: util.AsyncQueue<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,7 +64,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
63
64
|
/* listen locally on a Websocket port */
|
|
64
65
|
const url = new URL(this.params.listen)
|
|
65
66
|
const websockets = new Set<ws.WebSocket>()
|
|
66
|
-
|
|
67
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
67
68
|
this.server = new ws.WebSocketServer({
|
|
68
69
|
host: url.hostname,
|
|
69
70
|
port: Number.parseInt(url.port, 10),
|
|
@@ -102,7 +103,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
102
103
|
else
|
|
103
104
|
buffer = Buffer.concat(data)
|
|
104
105
|
const chunk = util.streamChunkDecode(buffer)
|
|
105
|
-
chunkQueue
|
|
106
|
+
this.chunkQueue?.write(chunk)
|
|
106
107
|
})
|
|
107
108
|
})
|
|
108
109
|
this.server.on("error", (error) => {
|
|
@@ -124,9 +125,11 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
124
125
|
callback(new Error("still no WebSocket connections available"))
|
|
125
126
|
else {
|
|
126
127
|
const data = util.streamChunkEncode(chunk)
|
|
127
|
-
const
|
|
128
|
+
const sends: Promise<void>[] = []
|
|
129
|
+
const clients: ws.WebSocket[] = []
|
|
128
130
|
for (const websocket of websockets.values()) {
|
|
129
|
-
|
|
131
|
+
clients.push(websocket)
|
|
132
|
+
sends.push(new Promise<void>((resolve, reject) => {
|
|
130
133
|
websocket.send(data, (error) => {
|
|
131
134
|
if (error)
|
|
132
135
|
reject(error)
|
|
@@ -135,10 +138,27 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
135
138
|
})
|
|
136
139
|
}))
|
|
137
140
|
}
|
|
138
|
-
Promise.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
Promise.allSettled(sends).then((results) => {
|
|
142
|
+
let lastError: Error | null = null
|
|
143
|
+
for (let i = 0; i < results.length; i++) {
|
|
144
|
+
if (results[i].status === "rejected") {
|
|
145
|
+
const error = util.ensureError((results[i] as PromiseRejectedResult).reason)
|
|
146
|
+
self.log("warning", `failed to send to WebSocket client: ${error.message}`)
|
|
147
|
+
websockets.delete(clients[i])
|
|
148
|
+
clients[i].terminate()
|
|
149
|
+
lastError = error
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const failures = results.filter((r) => r.status === "rejected").length
|
|
153
|
+
if (failures > 0 && failures < results.length)
|
|
154
|
+
self.log("warning", "partial broadcast failure: " +
|
|
155
|
+
`${failures} of ${results.length} WebSocket clients failed`)
|
|
156
|
+
if (lastError !== null && failures === results.length)
|
|
157
|
+
callback(lastError)
|
|
158
|
+
else
|
|
159
|
+
callback()
|
|
160
|
+
}).catch((err: Error) => {
|
|
161
|
+
callback(err)
|
|
142
162
|
})
|
|
143
163
|
}
|
|
144
164
|
},
|
|
@@ -147,12 +167,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
147
167
|
callback()
|
|
148
168
|
},
|
|
149
169
|
read (size: number) {
|
|
150
|
-
if (self.params.mode === "w")
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
if (self.params.mode === "w") {
|
|
171
|
+
self.log("error", "read operation on write-only node")
|
|
172
|
+
this.push(null)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (self.chunkQueue === null)
|
|
176
|
+
return
|
|
177
|
+
const queue = self.chunkQueue
|
|
178
|
+
reads.add(queue.read().then((chunk) => {
|
|
153
179
|
this.push(chunk, "binary")
|
|
154
180
|
}).catch((err: Error) => {
|
|
155
181
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
182
|
+
if (queue.destroyed)
|
|
183
|
+
this.push(null)
|
|
156
184
|
}))
|
|
157
185
|
}
|
|
158
186
|
})
|
|
@@ -178,7 +206,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
178
206
|
const error = util.ensureError(ev.error)
|
|
179
207
|
this.log("error", `error of connection on URL ${this.params.connect}: ${error.message}`)
|
|
180
208
|
})
|
|
181
|
-
|
|
209
|
+
this.chunkQueue = new util.AsyncQueue<SpeechFlowChunk>()
|
|
182
210
|
this.client.addEventListener("message", (ev: MessageEvent) => {
|
|
183
211
|
if (this.params.mode === "w") {
|
|
184
212
|
this.log("warning", `connection to URL ${this.params.connect}: ` +
|
|
@@ -192,7 +220,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
192
220
|
}
|
|
193
221
|
const buffer = Buffer.from(ev.data)
|
|
194
222
|
const chunk = util.streamChunkDecode(buffer)
|
|
195
|
-
chunkQueue
|
|
223
|
+
this.chunkQueue?.write(chunk)
|
|
196
224
|
})
|
|
197
225
|
this.client.binaryType = "arraybuffer"
|
|
198
226
|
const self = this
|
|
@@ -207,7 +235,7 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
207
235
|
callback(new Error("write operation on read-only node"))
|
|
208
236
|
else if (chunk.type !== self.params.type)
|
|
209
237
|
callback(new Error(`written chunk is not of ${self.params.type} type`))
|
|
210
|
-
else if (
|
|
238
|
+
else if (self.client!.readyState !== ReconnWebSocket.OPEN)
|
|
211
239
|
callback(new Error("still no WebSocket connection available"))
|
|
212
240
|
else {
|
|
213
241
|
const data = util.streamChunkEncode(chunk)
|
|
@@ -220,12 +248,20 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
220
248
|
callback()
|
|
221
249
|
},
|
|
222
250
|
read (size: number) {
|
|
223
|
-
if (self.params.mode === "w")
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
if (self.params.mode === "w") {
|
|
252
|
+
self.log("error", "read operation on write-only node")
|
|
253
|
+
this.push(null)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
if (self.chunkQueue === null)
|
|
257
|
+
return
|
|
258
|
+
const queue = self.chunkQueue
|
|
259
|
+
reads.add(queue.read().then((chunk) => {
|
|
226
260
|
this.push(chunk, "binary")
|
|
227
261
|
}).catch((err: Error) => {
|
|
228
262
|
self.log("warning", `read on chunk queue operation failed: ${err}`)
|
|
263
|
+
if (queue.destroyed)
|
|
264
|
+
this.push(null)
|
|
229
265
|
}))
|
|
230
266
|
}
|
|
231
267
|
})
|
|
@@ -234,8 +270,19 @@ export default class SpeechFlowNodeXIOWebSocket extends SpeechFlowNode {
|
|
|
234
270
|
|
|
235
271
|
/* close node */
|
|
236
272
|
async close () {
|
|
273
|
+
/* drain and clear chunk queue reference */
|
|
274
|
+
if (this.chunkQueue !== null) {
|
|
275
|
+
this.chunkQueue.destroy()
|
|
276
|
+
this.chunkQueue = null
|
|
277
|
+
}
|
|
278
|
+
|
|
237
279
|
/* close WebSocket server */
|
|
238
280
|
if (this.server !== null) {
|
|
281
|
+
/* forcibly terminate all active client connections */
|
|
282
|
+
for (const client of this.server.clients)
|
|
283
|
+
client.terminate()
|
|
284
|
+
|
|
285
|
+
/* close connection */
|
|
239
286
|
await new Promise<void>((resolve, reject) => {
|
|
240
287
|
this.server!.close((error) => {
|
|
241
288
|
if (error) reject(error)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -9,7 +9,10 @@ import Events, { EventEmitter } from "node:events"
|
|
|
9
9
|
import Stream from "node:stream"
|
|
10
10
|
|
|
11
11
|
/* external dependencies */
|
|
12
|
-
import { DateTime, Duration }
|
|
12
|
+
import { DateTime, Duration } from "luxon"
|
|
13
|
+
|
|
14
|
+
/* internal dependencies */
|
|
15
|
+
import { deepClone } from "./speechflow-util-misc"
|
|
13
16
|
|
|
14
17
|
/* the definition of a single payload chunk passed through the SpeechFlow nodes */
|
|
15
18
|
export class SpeechFlowChunk {
|
|
@@ -33,7 +36,7 @@ export class SpeechFlowChunk {
|
|
|
33
36
|
this.kind,
|
|
34
37
|
this.type,
|
|
35
38
|
payload,
|
|
36
|
-
|
|
39
|
+
deepClone(this.meta)
|
|
37
40
|
)
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -140,16 +143,12 @@ export default class SpeechFlowNode extends Events.EventEmitter {
|
|
|
140
143
|
if (typeof this.args[spec[name].pos] !== spec[name].type)
|
|
141
144
|
throw new Error(`invalid type of positional parameter "${name}" ` +
|
|
142
145
|
`(has to be ${spec[name].type})`)
|
|
143
|
-
if ("match" in spec[name]
|
|
144
|
-
&& this.args[spec[name].pos].match(spec[name].match) === null)
|
|
145
|
-
throw new Error(`invalid value of positional parameter "${name}" ` +
|
|
146
|
-
`(has to match ${spec[name].match})`)
|
|
147
146
|
if ("match" in spec[name]
|
|
148
147
|
&& ( ( spec[name].match instanceof RegExp
|
|
149
148
|
&& this.args[spec[name].pos].match(spec[name].match) === null)
|
|
150
149
|
|| ( typeof spec[name].match === "function"
|
|
151
150
|
&& !spec[name].match(this.args[spec[name].pos]) ) ))
|
|
152
|
-
throw new Error(`invalid value "${this.
|
|
151
|
+
throw new Error(`invalid value "${this.args[spec[name].pos!]}" of positional parameter "${name}"`)
|
|
153
152
|
this.params[name] = this.args[spec[name].pos]
|
|
154
153
|
}
|
|
155
154
|
else if ("val" in spec[name] && spec[name].val !== undefined)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -15,7 +15,14 @@ interface StartCaptureMessage {
|
|
|
15
15
|
chunkId: string
|
|
16
16
|
expectedSamples: number
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
interface CancelCaptureMessage {
|
|
19
|
+
type: "cancel-capture"
|
|
20
|
+
chunkId: string
|
|
21
|
+
}
|
|
22
|
+
interface CancelAllCapturesMessage {
|
|
23
|
+
type: "cancel-all-captures"
|
|
24
|
+
}
|
|
25
|
+
type WorkletMessage = InputChunkMessage | StartCaptureMessage | CancelCaptureMessage | CancelAllCapturesMessage
|
|
19
26
|
interface ChunkData {
|
|
20
27
|
data: Float32Array
|
|
21
28
|
chunkId: string
|
|
@@ -24,6 +31,10 @@ interface ChunkStartedMessage {
|
|
|
24
31
|
type: "chunk-started"
|
|
25
32
|
chunkId: string
|
|
26
33
|
}
|
|
34
|
+
interface CaptureReadyMessage {
|
|
35
|
+
type: "capture-ready"
|
|
36
|
+
chunkId: string
|
|
37
|
+
}
|
|
27
38
|
interface CaptureCompleteMessage {
|
|
28
39
|
type: "capture-complete"
|
|
29
40
|
chunkId: string
|
|
@@ -43,9 +54,12 @@ class AudioSourceProcessor extends AudioWorkletProcessor {
|
|
|
43
54
|
|
|
44
55
|
/* receive input chunks */
|
|
45
56
|
this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
|
|
46
|
-
const { type
|
|
57
|
+
const { type } = event.data
|
|
47
58
|
if (type === "input-chunk")
|
|
48
|
-
this.pendingData.push({
|
|
59
|
+
this.pendingData.push({
|
|
60
|
+
data: event.data.data.pcmData,
|
|
61
|
+
chunkId: event.data.chunkId
|
|
62
|
+
})
|
|
49
63
|
})
|
|
50
64
|
}
|
|
51
65
|
|
|
@@ -114,7 +128,8 @@ class AudioSourceProcessor extends AudioWorkletProcessor {
|
|
|
114
128
|
/* audio capture node */
|
|
115
129
|
class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
116
130
|
/* internal state */
|
|
117
|
-
private
|
|
131
|
+
private static readonly CAPTURE_TTL = 30 * 1000
|
|
132
|
+
private activeCaptures = new Map<string, { data: number[], expectedSamples: number, createdAt: number }>()
|
|
118
133
|
|
|
119
134
|
/* node construction */
|
|
120
135
|
constructor () {
|
|
@@ -122,13 +137,28 @@ class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
|
122
137
|
|
|
123
138
|
/* receive start of capturing command */
|
|
124
139
|
this.port.addEventListener("message", (event: MessageEvent<WorkletMessage>) => {
|
|
125
|
-
const { type
|
|
140
|
+
const { type } = event.data
|
|
126
141
|
if (type === "start-capture") {
|
|
142
|
+
const chunkId = event.data.chunkId
|
|
127
143
|
this.activeCaptures.set(chunkId, {
|
|
128
144
|
data: [],
|
|
129
|
-
expectedSamples: event.data.expectedSamples
|
|
145
|
+
expectedSamples: event.data.expectedSamples,
|
|
146
|
+
createdAt: Date.now()
|
|
130
147
|
})
|
|
148
|
+
|
|
149
|
+
/* acknowledge capture registration */
|
|
150
|
+
const ready: CaptureReadyMessage = {
|
|
151
|
+
type: "capture-ready",
|
|
152
|
+
chunkId
|
|
153
|
+
}
|
|
154
|
+
this.port.postMessage(ready)
|
|
131
155
|
}
|
|
156
|
+
else if (type === "cancel-capture") {
|
|
157
|
+
const chunkId = event.data.chunkId
|
|
158
|
+
this.activeCaptures.delete(chunkId)
|
|
159
|
+
}
|
|
160
|
+
else if (type === "cancel-all-captures")
|
|
161
|
+
this.activeCaptures.clear()
|
|
132
162
|
})
|
|
133
163
|
}
|
|
134
164
|
|
|
@@ -145,6 +175,15 @@ class AudioCaptureProcessor extends AudioWorkletProcessor {
|
|
|
145
175
|
const frameCount = input[0].length
|
|
146
176
|
const channelCount = input.length
|
|
147
177
|
|
|
178
|
+
/* evict stale captures (TTL safety net) */
|
|
179
|
+
const currentTime = Date.now()
|
|
180
|
+
for (const [ chunkId, capture ] of this.activeCaptures) {
|
|
181
|
+
if ((currentTime - capture.createdAt) > AudioCaptureProcessor.CAPTURE_TTL)
|
|
182
|
+
this.activeCaptures.delete(chunkId)
|
|
183
|
+
}
|
|
184
|
+
if (this.activeCaptures.size === 0)
|
|
185
|
+
return true
|
|
186
|
+
|
|
148
187
|
/* iterate over all active captures */
|
|
149
188
|
for (const [ chunkId, capture ] of this.activeCaptures) {
|
|
150
189
|
/* convert planar to interleaved */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -18,14 +18,11 @@ export function audioBufferDuration (
|
|
|
18
18
|
buffer: Buffer,
|
|
19
19
|
sampleRate = 48000,
|
|
20
20
|
bitDepth = 16,
|
|
21
|
-
channels = 1
|
|
22
|
-
littleEndian = true
|
|
21
|
+
channels = 1
|
|
23
22
|
) {
|
|
24
23
|
/* sanity check parameters */
|
|
25
24
|
if (!Buffer.isBuffer(buffer))
|
|
26
25
|
throw new Error("invalid input (Buffer expected)")
|
|
27
|
-
if (littleEndian !== true)
|
|
28
|
-
throw new Error("only Little Endian supported")
|
|
29
26
|
if (sampleRate <= 0)
|
|
30
27
|
throw new Error("sample rate must be positive")
|
|
31
28
|
if (bitDepth <= 0 || bitDepth % 8 !== 0)
|
|
@@ -234,7 +231,7 @@ export class WebAudio {
|
|
|
234
231
|
this.pendingPromises.delete(chunkId)
|
|
235
232
|
const int16Data = new Int16Array(data.length)
|
|
236
233
|
for (let i = 0; i < data.length; i++)
|
|
237
|
-
int16Data[i] = Math.max(-32768, Math.min(32767, Math.round(data[i] *
|
|
234
|
+
int16Data[i] = Math.max(-32768, Math.min(32767, Math.round(data[i] * 32768)))
|
|
238
235
|
promise.resolve(int16Data)
|
|
239
236
|
}
|
|
240
237
|
}
|
|
@@ -248,10 +245,14 @@ export class WebAudio {
|
|
|
248
245
|
|
|
249
246
|
/* process single audio chunk */
|
|
250
247
|
public async process (int16Array: Int16Array): Promise<Int16Array> {
|
|
248
|
+
if (this.sourceNode === null || this.captureNode === null)
|
|
249
|
+
throw new Error("WebAudio not ready (not yet setup or already destroyed)")
|
|
251
250
|
const chunkId = `chunk_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
|
252
251
|
return new Promise<Int16Array>((resolve, reject) => {
|
|
253
252
|
const timeout = setTimeout(() => {
|
|
254
253
|
this.pendingPromises.delete(chunkId)
|
|
254
|
+
if (this.captureNode !== null)
|
|
255
|
+
this.captureNode.port.postMessage({ type: "cancel-capture", chunkId })
|
|
255
256
|
reject(new Error("processing timeout"))
|
|
256
257
|
}, (int16Array.length / this.channels / this.audioContext.sampleRate) * 1000 + 250)
|
|
257
258
|
if (this.captureNode !== null)
|
|
@@ -270,15 +271,22 @@ export class WebAudio {
|
|
|
270
271
|
})
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
/*
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
274
|
+
/* wait for capture-ready acknowledgment before sending data */
|
|
275
|
+
const readyHandler = (event: MessageEvent) => {
|
|
276
|
+
const { type: msgType, chunkId: msgChunkId } = event.data ?? {}
|
|
277
|
+
if (msgType === "capture-ready" && msgChunkId === chunkId) {
|
|
278
|
+
this.captureNode?.port.removeEventListener("message", readyHandler)
|
|
279
|
+
|
|
280
|
+
/* send input to source node */
|
|
281
|
+
this.sourceNode?.port.postMessage({
|
|
282
|
+
type: "input-chunk",
|
|
283
|
+
chunkId,
|
|
284
|
+
data: { pcmData: float32Data, channels: this.channels }
|
|
285
|
+
}, [ float32Data.buffer ])
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (this.captureNode !== null)
|
|
289
|
+
this.captureNode.port.addEventListener("message", readyHandler)
|
|
282
290
|
}
|
|
283
291
|
catch (error) {
|
|
284
292
|
clearTimeout(timeout)
|
|
@@ -291,6 +299,10 @@ export class WebAudio {
|
|
|
291
299
|
|
|
292
300
|
/* destroy object */
|
|
293
301
|
public async destroy (): Promise<void> {
|
|
302
|
+
/* cancel all worklet captures */
|
|
303
|
+
if (this.captureNode !== null)
|
|
304
|
+
this.captureNode.port.postMessage({ type: "cancel-all-captures" })
|
|
305
|
+
|
|
294
306
|
/* reject all pending promises */
|
|
295
307
|
shield(() => {
|
|
296
308
|
this.pendingPromises.forEach(({ reject, timeout }) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -193,7 +193,7 @@ export function runner<T> (
|
|
|
193
193
|
export function shield<T extends (void | Promise<void>)> (op: () => T) {
|
|
194
194
|
return run(
|
|
195
195
|
"shielded operation",
|
|
196
|
-
()
|
|
197
|
-
(_err) =>
|
|
196
|
+
() => op(),
|
|
197
|
+
(_err) => undefined as T
|
|
198
198
|
)
|
|
199
199
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -1,23 +1,80 @@
|
|
|
1
1
|
/*
|
|
2
2
|
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
-
** Copyright (c) 2024-
|
|
3
|
+
** Copyright (c) 2024-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/* external dependencies */
|
|
8
|
+
import { DateTime, Duration } from "luxon"
|
|
9
|
+
|
|
10
|
+
/* deep-clone a value while being aware of special class instances */
|
|
11
|
+
export const deepClone = (value: any): any => {
|
|
12
|
+
if (value === null || value === undefined || Number.isNaN(value))
|
|
13
|
+
return value
|
|
14
|
+
else if (typeof value !== "object")
|
|
15
|
+
return value
|
|
16
|
+
else if (Buffer.isBuffer(value))
|
|
17
|
+
return Buffer.from(value)
|
|
18
|
+
else if (value instanceof Uint8Array)
|
|
19
|
+
return new Uint8Array(value)
|
|
20
|
+
else if (value instanceof Duration)
|
|
21
|
+
return Duration.fromMillis(value.toMillis())
|
|
22
|
+
else if (value instanceof DateTime)
|
|
23
|
+
return DateTime.fromMillis(value.toMillis())
|
|
24
|
+
else if (Array.isArray(value))
|
|
25
|
+
return value.map((item) => deepClone(item))
|
|
26
|
+
else if (value instanceof Map) {
|
|
27
|
+
const result = new Map()
|
|
28
|
+
for (const [ k, v ] of value)
|
|
29
|
+
result.set(deepClone(k), deepClone(v))
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
else if (value instanceof Set) {
|
|
33
|
+
const result = new Set()
|
|
34
|
+
for (const v of value)
|
|
35
|
+
result.add(deepClone(v))
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
else if (Object.getPrototypeOf(value) === Object.prototype) {
|
|
39
|
+
const result: any = {}
|
|
40
|
+
for (const key of Object.keys(value))
|
|
41
|
+
result[key] = deepClone(value[key])
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
return structuredClone(value)
|
|
46
|
+
}
|
|
47
|
+
|
|
7
48
|
/* sleep: wait a duration of time and then resolve */
|
|
8
|
-
export function sleep (durationMs: number) {
|
|
49
|
+
export function sleep (durationMs: number, signal?: AbortSignal) {
|
|
9
50
|
return new Promise<void>((resolve) => {
|
|
10
|
-
|
|
51
|
+
const ac = new AbortController()
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
ac.abort()
|
|
11
54
|
resolve()
|
|
12
55
|
}, durationMs)
|
|
56
|
+
timer.unref()
|
|
57
|
+
if (signal !== undefined)
|
|
58
|
+
signal.addEventListener("abort", () => {
|
|
59
|
+
clearTimeout(timer)
|
|
60
|
+
resolve()
|
|
61
|
+
}, { once: true, signal: ac.signal })
|
|
13
62
|
})
|
|
14
63
|
}
|
|
15
64
|
|
|
16
65
|
/* timeout: wait a duration of time and then reject */
|
|
17
|
-
export function timeout (durationMs: number, info = "timeout") {
|
|
18
|
-
return new Promise<never>((
|
|
19
|
-
|
|
66
|
+
export function timeout (durationMs: number, info = "timeout", signal?: AbortSignal) {
|
|
67
|
+
return new Promise<never>((resolve, reject) => {
|
|
68
|
+
const ac = new AbortController()
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
ac.abort()
|
|
20
71
|
reject(new Error(info))
|
|
21
72
|
}, durationMs)
|
|
73
|
+
timer.unref()
|
|
74
|
+
if (signal !== undefined)
|
|
75
|
+
signal.addEventListener("abort", () => {
|
|
76
|
+
clearTimeout(timer)
|
|
77
|
+
resolve(undefined as never)
|
|
78
|
+
}, { once: true, signal: ac.signal })
|
|
22
79
|
})
|
|
23
80
|
}
|