speechflow 1.6.0 → 1.6.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 +7 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/speechflow-cli/dst/speechflow-main-api.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-main-api.js +319 -0
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-cli.d.ts +28 -0
- package/speechflow-cli/dst/speechflow-main-cli.js +271 -0
- package/speechflow-cli/dst/speechflow-main-cli.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-config.d.ts +9 -0
- package/speechflow-cli/dst/speechflow-main-config.js +27 -0
- package/speechflow-cli/dst/speechflow-main-config.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-graph.d.ts +34 -0
- package/speechflow-cli/dst/speechflow-main-graph.js +367 -0
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-nodes.d.ts +10 -0
- package/speechflow-cli/dst/speechflow-main-nodes.js +60 -0
- package/speechflow-cli/dst/speechflow-main-nodes.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-status.d.ts +11 -0
- package/speechflow-cli/dst/speechflow-main-status.js +60 -0
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main.d.ts +7 -0
- package/speechflow-cli/dst/speechflow-main.js +127 -0
- package/speechflow-cli/dst/speechflow-main.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +5 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +5 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +5 -6
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +5 -5
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +6 -6
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +5 -5
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +5 -5
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +27 -27
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +4 -4
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +7 -7
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/{speechflow-utils-audio-wt.js → speechflow-util-audio-wt.js} +1 -1
- package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +22 -0
- package/speechflow-cli/dst/speechflow-util-audio.js +251 -0
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-error.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-util-error.js +131 -0
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-queue.d.ts +68 -0
- package/speechflow-cli/dst/speechflow-util-queue.js +338 -0
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +18 -0
- package/speechflow-cli/dst/speechflow-util-stream.js +219 -0
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-webaudio-wt.js +124 -0
- package/speechflow-cli/dst/speechflow-util-webaudio-wt.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-utils-audio.js → speechflow-util-webaudio.js} +2 -2
- package/speechflow-cli/dst/speechflow-util-webaudio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util.d.ts +4 -0
- package/speechflow-cli/dst/speechflow-util.js +26 -0
- package/speechflow-cli/dst/speechflow-util.js.map +1 -0
- package/speechflow-cli/dst/speechflow.js +3 -912
- package/speechflow-cli/dst/speechflow.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +4 -1
- package/speechflow-cli/package.json +1 -0
- package/speechflow-cli/src/speechflow-main-api.ts +315 -0
- package/speechflow-cli/src/speechflow-main-cli.ts +259 -0
- package/speechflow-cli/src/speechflow-main-config.ts +17 -0
- package/speechflow-cli/src/speechflow-main-graph.ts +372 -0
- package/speechflow-cli/src/speechflow-main-nodes.ts +61 -0
- package/speechflow-cli/src/speechflow-main-status.ts +70 -0
- package/speechflow-cli/src/speechflow-main.ts +106 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +5 -6
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +5 -5
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +5 -6
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +5 -5
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +7 -7
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +5 -5
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +5 -5
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +5 -5
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +2 -2
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-xio-device.ts +5 -5
- package/speechflow-cli/src/speechflow-node-xio-file.ts +9 -10
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +5 -5
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +7 -7
- package/speechflow-cli/src/{speechflow-utils-audio.ts → speechflow-util-audio.ts} +131 -1
- package/speechflow-cli/src/speechflow-util-error.ts +184 -0
- package/speechflow-cli/src/speechflow-util-queue.ts +320 -0
- package/speechflow-cli/src/speechflow-util-stream.ts +197 -0
- package/speechflow-cli/src/speechflow-util.ts +10 -0
- package/speechflow-cli/src/speechflow.ts +3 -953
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +0 -208
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +0 -15
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +0 -312
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +0 -18
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +0 -312
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +0 -19
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +0 -351
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +0 -16
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +0 -204
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +0 -13
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +0 -175
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +0 -1
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +0 -1
- package/speechflow-cli/dst/speechflow-utils-audio.js.map +0 -1
- package/speechflow-cli/dst/speechflow-utils.d.ts +0 -108
- package/speechflow-cli/dst/speechflow-utils.js +0 -746
- package/speechflow-cli/dst/speechflow-utils.js.map +0 -1
- package/speechflow-cli/src/speechflow-utils.ts +0 -810
- /package/speechflow-cli/dst/{speechflow-node-a2a-dynamics-wt.d.ts → speechflow-util-audio-wt.d.ts} +0 -0
- /package/speechflow-cli/dst/{speechflow-utils-audio-wt.d.ts → speechflow-util-webaudio-wt.d.ts} +0 -0
- /package/speechflow-cli/dst/{speechflow-utils-audio.d.ts → speechflow-util-webaudio.d.ts} +0 -0
- /package/speechflow-cli/src/{speechflow-utils-audio-wt.ts → speechflow-util-audio-wt.ts} +0 -0
|
@@ -10,6 +10,136 @@ import path from "node:path"
|
|
|
10
10
|
/* external dependencies */
|
|
11
11
|
import { AudioContext, AudioWorkletNode } from "node-web-audio-api"
|
|
12
12
|
|
|
13
|
+
/* calculate duration of an audio buffer */
|
|
14
|
+
export function audioBufferDuration (
|
|
15
|
+
buffer: Buffer,
|
|
16
|
+
sampleRate = 48000,
|
|
17
|
+
bitDepth = 16,
|
|
18
|
+
channels = 1,
|
|
19
|
+
littleEndian = true
|
|
20
|
+
) {
|
|
21
|
+
/* sanity check parameters */
|
|
22
|
+
if (!Buffer.isBuffer(buffer))
|
|
23
|
+
throw new Error("invalid input (Buffer expected)")
|
|
24
|
+
if (littleEndian !== true)
|
|
25
|
+
throw new Error("only Little Endian supported")
|
|
26
|
+
if (sampleRate <= 0)
|
|
27
|
+
throw new Error("sample rate must be positive")
|
|
28
|
+
if (bitDepth <= 0 || bitDepth % 8 !== 0)
|
|
29
|
+
throw new Error("bit depth must be positive and multiple of 8")
|
|
30
|
+
if (channels <= 0)
|
|
31
|
+
throw new Error("channels must be positive")
|
|
32
|
+
|
|
33
|
+
/* calculate duration */
|
|
34
|
+
const bytesPerSample = bitDepth / 8
|
|
35
|
+
const totalSamples = buffer.length / (bytesPerSample * channels)
|
|
36
|
+
return totalSamples / sampleRate
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* calculate duration of an audio array */
|
|
40
|
+
export function audioArrayDuration (
|
|
41
|
+
arr: Float32Array,
|
|
42
|
+
sampleRate = 48000,
|
|
43
|
+
channels = 1
|
|
44
|
+
) {
|
|
45
|
+
/* sanity check parameters */
|
|
46
|
+
if (arr.length === 0)
|
|
47
|
+
return 0
|
|
48
|
+
if (sampleRate <= 0)
|
|
49
|
+
throw new Error("sample rate must be positive")
|
|
50
|
+
if (channels <= 0)
|
|
51
|
+
throw new Error("channels must be positive")
|
|
52
|
+
|
|
53
|
+
/* calculate duration */
|
|
54
|
+
const totalSamples = arr.length / channels
|
|
55
|
+
return totalSamples / sampleRate
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* helper function: convert Buffer in PCM/I16 to Float32Array in PCM/F32 format */
|
|
59
|
+
export function convertBufToF32 (buf: Buffer, littleEndian = true) {
|
|
60
|
+
if (buf.length % 2 !== 0)
|
|
61
|
+
throw new Error("buffer length must be even for 16-bit samples")
|
|
62
|
+
const dataView = new DataView(buf.buffer)
|
|
63
|
+
const arr = new Float32Array(buf.length / 2)
|
|
64
|
+
for (let i = 0; i < arr.length; i++)
|
|
65
|
+
arr[i] = dataView.getInt16(i * 2, littleEndian) / 32768
|
|
66
|
+
return arr
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* helper function: convert Float32Array in PCM/F32 to Buffer in PCM/I16 format */
|
|
70
|
+
export function convertF32ToBuf (arr: Float32Array) {
|
|
71
|
+
if (arr.length === 0)
|
|
72
|
+
return Buffer.alloc(0)
|
|
73
|
+
const int16Array = new Int16Array(arr.length)
|
|
74
|
+
for (let i = 0; i < arr.length; i++) {
|
|
75
|
+
let sample = arr[i]
|
|
76
|
+
if (Number.isNaN(sample))
|
|
77
|
+
sample = 0
|
|
78
|
+
int16Array[i] = Math.max(-32768, Math.min(32767, Math.round(sample * 32768)))
|
|
79
|
+
}
|
|
80
|
+
return Buffer.from(int16Array.buffer)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* helper function: convert Buffer in PCM/I16 to Int16Array */
|
|
84
|
+
export function convertBufToI16 (buf: Buffer, littleEndian = true) {
|
|
85
|
+
if (buf.length % 2 !== 0)
|
|
86
|
+
throw new Error("buffer length must be even for 16-bit samples")
|
|
87
|
+
const dataView = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
88
|
+
const arr = new Int16Array(buf.length / 2)
|
|
89
|
+
for (let i = 0; i < buf.length / 2; i++)
|
|
90
|
+
arr[i] = dataView.getInt16(i * 2, littleEndian)
|
|
91
|
+
return arr
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* helper function: convert In16Array in PCM/I16 to Buffer */
|
|
95
|
+
export function convertI16ToBuf (arr: Int16Array, littleEndian = true) {
|
|
96
|
+
if (arr.length === 0)
|
|
97
|
+
return Buffer.alloc(0)
|
|
98
|
+
const buf = Buffer.allocUnsafe(arr.length * 2)
|
|
99
|
+
for (let i = 0; i < arr.length; i++) {
|
|
100
|
+
if (littleEndian)
|
|
101
|
+
buf.writeInt16LE(arr[i], i * 2)
|
|
102
|
+
else
|
|
103
|
+
buf.writeInt16BE(arr[i], i * 2)
|
|
104
|
+
}
|
|
105
|
+
return buf
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* process Int16Array in fixed-size segments */
|
|
109
|
+
export async function processInt16ArrayInSegments (
|
|
110
|
+
data: Int16Array<ArrayBuffer>,
|
|
111
|
+
segmentSize: number,
|
|
112
|
+
processor: (segment: Int16Array<ArrayBuffer>) => Promise<Int16Array<ArrayBuffer>>
|
|
113
|
+
): Promise<Int16Array<ArrayBuffer>> {
|
|
114
|
+
/* process full segments */
|
|
115
|
+
let i = 0
|
|
116
|
+
while ((i + segmentSize) <= data.length) {
|
|
117
|
+
const segment = data.slice(i, i + segmentSize)
|
|
118
|
+
const result = await processor(segment)
|
|
119
|
+
data.set(result, i)
|
|
120
|
+
i += segmentSize
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* process final partial segment if it exists */
|
|
124
|
+
if (i < data.length) {
|
|
125
|
+
const len = data.length - i
|
|
126
|
+
const segment = new Int16Array(segmentSize)
|
|
127
|
+
segment.set(data.slice(i), 0)
|
|
128
|
+
segment.fill(0, len, segmentSize)
|
|
129
|
+
const result = await processor(segment)
|
|
130
|
+
data.set(result.slice(0, len), i)
|
|
131
|
+
}
|
|
132
|
+
return data
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* helper functions for linear/decibel conversions */
|
|
136
|
+
export function lin2dB (x: number): number {
|
|
137
|
+
return 20 * Math.log10(Math.max(x, 1e-12))
|
|
138
|
+
}
|
|
139
|
+
export function dB2lin (db: number): number {
|
|
140
|
+
return Math.pow(10, db / 20)
|
|
141
|
+
}
|
|
142
|
+
|
|
13
143
|
export class WebAudio {
|
|
14
144
|
/* internal state */
|
|
15
145
|
public audioContext: AudioContext
|
|
@@ -40,7 +170,7 @@ export class WebAudio {
|
|
|
40
170
|
await this.audioContext.resume()
|
|
41
171
|
|
|
42
172
|
/* add audio worklet module */
|
|
43
|
-
const url = path.resolve(__dirname, "speechflow-
|
|
173
|
+
const url = path.resolve(__dirname, "speechflow-util-audio-wt.js")
|
|
44
174
|
await this.audioContext.audioWorklet.addModule(url)
|
|
45
175
|
|
|
46
176
|
/* create source node */
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
/* helper function for retrieving an Error object */
|
|
8
|
+
export function ensureError (error: unknown, prefix?: string, debug = false): Error {
|
|
9
|
+
if (error instanceof Error && prefix === undefined && debug === false)
|
|
10
|
+
return error
|
|
11
|
+
let msg = error instanceof Error ?
|
|
12
|
+
error.message : String(error)
|
|
13
|
+
if (prefix)
|
|
14
|
+
msg = `${prefix}: ${msg}`
|
|
15
|
+
if (debug && error instanceof Error)
|
|
16
|
+
msg = `${msg}\n${error.stack}`
|
|
17
|
+
return new Error(msg, { cause: error })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* helper function for retrieving a Promise object */
|
|
21
|
+
export function ensurePromise<T> (arg: T | Promise<T>): Promise<T> {
|
|
22
|
+
if (!(arg instanceof Promise))
|
|
23
|
+
arg = Promise.resolve(arg)
|
|
24
|
+
return arg
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* helper function for running the finally code of "run" */
|
|
28
|
+
function runFinally (onfinally?: () => void) {
|
|
29
|
+
if (!onfinally)
|
|
30
|
+
return
|
|
31
|
+
try { onfinally() }
|
|
32
|
+
catch (_error: unknown) { /* ignored */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* helper type for ensuring T contains no Promise */
|
|
36
|
+
type runNoPromise<T> =
|
|
37
|
+
[ T ] extends [ Promise<any> ] ? never : T
|
|
38
|
+
|
|
39
|
+
/* run a synchronous or asynchronous action */
|
|
40
|
+
export function run<T, X extends runNoPromise<T> | never> (
|
|
41
|
+
action: () => X,
|
|
42
|
+
oncatch?: (error: Error) => X | never,
|
|
43
|
+
onfinally?: () => void
|
|
44
|
+
): X
|
|
45
|
+
export function run<T, X extends runNoPromise<T> | never> (
|
|
46
|
+
description: string,
|
|
47
|
+
action: () => X,
|
|
48
|
+
oncatch?: (error: Error) => X | never,
|
|
49
|
+
onfinally?: () => void
|
|
50
|
+
): X
|
|
51
|
+
export function run<T, X extends (T | Promise<T>)> (
|
|
52
|
+
action: () => X,
|
|
53
|
+
oncatch?: (error: Error) => X,
|
|
54
|
+
onfinally?: () => void
|
|
55
|
+
): Promise<T>
|
|
56
|
+
export function run<T, X extends (T | Promise<T>)> (
|
|
57
|
+
description: string,
|
|
58
|
+
action: () => X,
|
|
59
|
+
oncatch?: (error: Error) => X,
|
|
60
|
+
onfinally?: () => void
|
|
61
|
+
): Promise<T>
|
|
62
|
+
export function run<T> (
|
|
63
|
+
...args: any[]
|
|
64
|
+
): T | Promise<T> | never {
|
|
65
|
+
/* support overloaded signatures */
|
|
66
|
+
let description: string | undefined
|
|
67
|
+
let action: () => T | Promise<T> | never
|
|
68
|
+
let oncatch: (error: Error) => T | Promise<T> | never
|
|
69
|
+
let onfinally: () => void
|
|
70
|
+
if (typeof args[0] === "string") {
|
|
71
|
+
description = args[0]
|
|
72
|
+
action = args[1]
|
|
73
|
+
oncatch = args[2]
|
|
74
|
+
onfinally = args[3]
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
action = args[0]
|
|
78
|
+
oncatch = args[1]
|
|
79
|
+
onfinally = args[2]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* perform the action */
|
|
83
|
+
let result: T | Promise<T>
|
|
84
|
+
try {
|
|
85
|
+
result = action()
|
|
86
|
+
}
|
|
87
|
+
catch (arg: unknown) {
|
|
88
|
+
/* synchronous case (error branch) */
|
|
89
|
+
let error = ensureError(arg, description)
|
|
90
|
+
if (oncatch) {
|
|
91
|
+
try {
|
|
92
|
+
result = oncatch(error)
|
|
93
|
+
}
|
|
94
|
+
catch (arg: unknown) {
|
|
95
|
+
error = ensureError(arg, description)
|
|
96
|
+
runFinally(onfinally)
|
|
97
|
+
throw error
|
|
98
|
+
}
|
|
99
|
+
runFinally(onfinally)
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
runFinally(onfinally)
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
if (result instanceof Promise) {
|
|
106
|
+
/* asynchronous case (result or error branch) */
|
|
107
|
+
return result.catch((arg: unknown) => {
|
|
108
|
+
/* asynchronous case (error branch) */
|
|
109
|
+
let error = ensureError(arg, description)
|
|
110
|
+
if (oncatch) {
|
|
111
|
+
try {
|
|
112
|
+
return oncatch(error)
|
|
113
|
+
}
|
|
114
|
+
catch (arg: unknown) {
|
|
115
|
+
error = ensureError(arg, description)
|
|
116
|
+
throw error
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw error
|
|
120
|
+
}).finally(() => {
|
|
121
|
+
/* asynchronous case (result and error branch) */
|
|
122
|
+
runFinally(onfinally)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
/* synchronous case (result branch) */
|
|
127
|
+
runFinally(onfinally)
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* run a synchronous or asynchronous action */
|
|
133
|
+
/* eslint @typescript-eslint/unified-signatures: off */
|
|
134
|
+
export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
|
|
135
|
+
action: F,
|
|
136
|
+
oncatch?: (error: Error) => X | never,
|
|
137
|
+
onfinally?: () => void
|
|
138
|
+
): F
|
|
139
|
+
export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
|
|
140
|
+
description: string,
|
|
141
|
+
action: F,
|
|
142
|
+
oncatch?: (error: Error) => X | never,
|
|
143
|
+
onfinally?: () => void
|
|
144
|
+
): F
|
|
145
|
+
export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
|
|
146
|
+
action: F,
|
|
147
|
+
oncatch?: (error: Error) => X,
|
|
148
|
+
onfinally?: () => void
|
|
149
|
+
): F
|
|
150
|
+
export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
|
|
151
|
+
description: string,
|
|
152
|
+
action: F,
|
|
153
|
+
oncatch?: (error: Error) => X,
|
|
154
|
+
onfinally?: () => void
|
|
155
|
+
): F
|
|
156
|
+
export function runner<T> (
|
|
157
|
+
...args: any[]
|
|
158
|
+
): (...args: any[]) => T | Promise<T> | never {
|
|
159
|
+
/* support overloaded signatures */
|
|
160
|
+
let description: string | undefined
|
|
161
|
+
let action: (...args: any[]) => T | Promise<T> | never
|
|
162
|
+
let oncatch: (error: Error) => T | Promise<T> | never
|
|
163
|
+
let onfinally: () => void
|
|
164
|
+
if (typeof args[0] === "string") {
|
|
165
|
+
description = args[0]
|
|
166
|
+
action = args[1]
|
|
167
|
+
oncatch = args[2]
|
|
168
|
+
onfinally = args[3]
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
action = args[0]
|
|
172
|
+
oncatch = args[1]
|
|
173
|
+
onfinally = args[2]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* wrap the "run" operation on "action" into function
|
|
177
|
+
which exposes the signature of "action" */
|
|
178
|
+
return (...args: any[]) => {
|
|
179
|
+
if (description)
|
|
180
|
+
return run(description, () => action(...args), oncatch, onfinally)
|
|
181
|
+
else
|
|
182
|
+
return run(() => action(...args), oncatch, onfinally)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
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 { EventEmitter } from "node:events"
|
|
9
|
+
import { type, type Type } from "arktype"
|
|
10
|
+
|
|
11
|
+
/* external dependencies */
|
|
12
|
+
import { Duration } from "luxon"
|
|
13
|
+
import * as IntervalTree from "node-interval-tree"
|
|
14
|
+
|
|
15
|
+
/* internal dependencies */
|
|
16
|
+
import * as util from "./speechflow-util"
|
|
17
|
+
|
|
18
|
+
/* import an object with parsing and strict error handling */
|
|
19
|
+
export function importObject<T>(name: string, arg: object | string, validator: Type<T, {}>): T {
|
|
20
|
+
const obj: object = typeof arg === "string" ?
|
|
21
|
+
util.run(`${name}: parsing JSON`, () => JSON.parse(arg)) :
|
|
22
|
+
arg
|
|
23
|
+
const result = validator(obj)
|
|
24
|
+
if (result instanceof type.errors)
|
|
25
|
+
throw new Error(`${name}: validation: ${result.summary}`)
|
|
26
|
+
return result as T
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* helper class for single item queue */
|
|
30
|
+
export class SingleQueue<T> extends EventEmitter {
|
|
31
|
+
private queue = new Array<T>()
|
|
32
|
+
write (item: T) {
|
|
33
|
+
this.queue.unshift(item)
|
|
34
|
+
this.emit("dequeue")
|
|
35
|
+
}
|
|
36
|
+
read () {
|
|
37
|
+
return new Promise<T>((resolve, reject) => {
|
|
38
|
+
const consume = () =>
|
|
39
|
+
this.queue.length > 0 ? this.queue.pop()! : null
|
|
40
|
+
const tryToConsume = () => {
|
|
41
|
+
const item = consume()
|
|
42
|
+
if (item !== null)
|
|
43
|
+
resolve(item)
|
|
44
|
+
else
|
|
45
|
+
this.once("dequeue", tryToConsume)
|
|
46
|
+
}
|
|
47
|
+
tryToConsume()
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* helper class for double-item queue */
|
|
53
|
+
export class DoubleQueue<T0, T1> extends EventEmitter {
|
|
54
|
+
private queue0 = new Array<T0>()
|
|
55
|
+
private queue1 = new Array<T1>()
|
|
56
|
+
private notify () {
|
|
57
|
+
if (this.queue0.length > 0 && this.queue1.length > 0)
|
|
58
|
+
this.emit("dequeue")
|
|
59
|
+
}
|
|
60
|
+
write0 (item: T0) {
|
|
61
|
+
this.queue0.unshift(item)
|
|
62
|
+
this.notify()
|
|
63
|
+
}
|
|
64
|
+
write1 (item: T1) {
|
|
65
|
+
this.queue1.unshift(item)
|
|
66
|
+
this.notify()
|
|
67
|
+
}
|
|
68
|
+
read () {
|
|
69
|
+
return new Promise<[ T0, T1 ]>((resolve, reject) => {
|
|
70
|
+
const consume = (): [ T0, T1 ] | null => {
|
|
71
|
+
if (this.queue0.length > 0 && this.queue1.length > 0) {
|
|
72
|
+
const item0 = this.queue0.pop() as T0
|
|
73
|
+
const item1 = this.queue1.pop() as T1
|
|
74
|
+
return [ item0, item1 ]
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
const tryToConsume = () => {
|
|
79
|
+
const items = consume()
|
|
80
|
+
if (items !== null)
|
|
81
|
+
resolve(items)
|
|
82
|
+
else
|
|
83
|
+
this.once("dequeue", tryToConsume)
|
|
84
|
+
}
|
|
85
|
+
tryToConsume()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* queue element */
|
|
91
|
+
export type QueueElement = { type: string }
|
|
92
|
+
|
|
93
|
+
/* queue pointer */
|
|
94
|
+
export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
95
|
+
/* internal state */
|
|
96
|
+
private index = 0
|
|
97
|
+
|
|
98
|
+
/* construction */
|
|
99
|
+
constructor (
|
|
100
|
+
private name: string,
|
|
101
|
+
private queue: Queue<T>
|
|
102
|
+
) {
|
|
103
|
+
super()
|
|
104
|
+
this.setMaxListeners(100)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* positioning operations */
|
|
108
|
+
maxPosition () {
|
|
109
|
+
return this.queue.elements.length
|
|
110
|
+
}
|
|
111
|
+
position (index?: number): number {
|
|
112
|
+
if (index !== undefined) {
|
|
113
|
+
this.index = Math.max(0, Math.min(index, this.queue.elements.length))
|
|
114
|
+
this.emit("position", this.index)
|
|
115
|
+
}
|
|
116
|
+
return this.index
|
|
117
|
+
}
|
|
118
|
+
walk (num: number) {
|
|
119
|
+
const indexOld = this.index
|
|
120
|
+
if (num > 0)
|
|
121
|
+
this.index = Math.min(this.index + num, this.queue.elements.length)
|
|
122
|
+
else if (num < 0)
|
|
123
|
+
this.index = Math.max(this.index + num, 0)
|
|
124
|
+
if (this.index !== indexOld)
|
|
125
|
+
this.emit("position", { start: this.index })
|
|
126
|
+
}
|
|
127
|
+
walkForwardUntil (type: T["type"]) {
|
|
128
|
+
while (this.index < this.queue.elements.length
|
|
129
|
+
&& this.queue.elements[this.index].type !== type)
|
|
130
|
+
this.index++
|
|
131
|
+
this.emit("position", { start: this.index })
|
|
132
|
+
}
|
|
133
|
+
walkBackwardUntil (type: T["type"]) {
|
|
134
|
+
while (this.index > 0
|
|
135
|
+
&& this.queue.elements[this.index].type !== type)
|
|
136
|
+
this.index--
|
|
137
|
+
this.emit("position", { start: this.index })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* search operations */
|
|
141
|
+
searchForward (type: T["type"]) {
|
|
142
|
+
let position = this.index
|
|
143
|
+
while (position < this.queue.elements.length
|
|
144
|
+
&& this.queue.elements[position].type !== type)
|
|
145
|
+
position++
|
|
146
|
+
this.emit("search", { start: this.index, end: position })
|
|
147
|
+
return position
|
|
148
|
+
}
|
|
149
|
+
searchBackward (type: T["type"]) {
|
|
150
|
+
let position = this.index
|
|
151
|
+
while (position > 0
|
|
152
|
+
&& this.queue.elements[position].type !== type)
|
|
153
|
+
position--
|
|
154
|
+
this.emit("search", { start: position, end: this.index })
|
|
155
|
+
return position
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* reading operations */
|
|
159
|
+
peek (position?: number) {
|
|
160
|
+
if (position === undefined)
|
|
161
|
+
position = this.index
|
|
162
|
+
position = Math.max(0, Math.min(position, this.queue.elements.length))
|
|
163
|
+
const element = this.queue.elements[position]
|
|
164
|
+
this.queue.emit("read", { start: position, end: position })
|
|
165
|
+
return element
|
|
166
|
+
}
|
|
167
|
+
read () {
|
|
168
|
+
const element = this.queue.elements[this.index]
|
|
169
|
+
if (this.index < this.queue.elements.length)
|
|
170
|
+
this.index++
|
|
171
|
+
this.queue.emit("read", { start: this.index - 1, end: this.index - 1 })
|
|
172
|
+
return element
|
|
173
|
+
}
|
|
174
|
+
slice (size?: number) {
|
|
175
|
+
let slice: T[]
|
|
176
|
+
const start = this.index
|
|
177
|
+
if (size !== undefined) {
|
|
178
|
+
size = Math.max(0, Math.min(size, this.queue.elements.length - this.index))
|
|
179
|
+
slice = this.queue.elements.slice(this.index, this.index + size)
|
|
180
|
+
this.index += size
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
slice = this.queue.elements.slice(this.index)
|
|
184
|
+
this.index = this.queue.elements.length
|
|
185
|
+
}
|
|
186
|
+
this.queue.emit("read", { start, end: this.index })
|
|
187
|
+
return slice
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* writing operations */
|
|
191
|
+
touch () {
|
|
192
|
+
if (this.index >= this.queue.elements.length)
|
|
193
|
+
throw new Error("cannot touch after last element")
|
|
194
|
+
this.queue.emit("write", { start: this.index, end: this.index + 1 })
|
|
195
|
+
}
|
|
196
|
+
append (element: T) {
|
|
197
|
+
this.queue.elements.push(element)
|
|
198
|
+
this.index = this.queue.elements.length
|
|
199
|
+
this.queue.emit("write", { start: this.index - 1, end: this.index - 1 })
|
|
200
|
+
}
|
|
201
|
+
insert (element: T) {
|
|
202
|
+
this.queue.elements.splice(this.index, 0, element)
|
|
203
|
+
this.queue.emit("write", { start: this.index - 1, end: this.index })
|
|
204
|
+
}
|
|
205
|
+
delete () {
|
|
206
|
+
if (this.index >= this.queue.elements.length)
|
|
207
|
+
throw new Error("cannot delete after last element")
|
|
208
|
+
this.queue.elements.splice(this.index, 1)
|
|
209
|
+
this.queue.emit("write", { start: this.index, end: this.index })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* queue */
|
|
214
|
+
export class Queue<T extends QueueElement> extends EventEmitter {
|
|
215
|
+
public elements: T[] = []
|
|
216
|
+
private pointers = new Map<string, QueuePointer<T>>()
|
|
217
|
+
constructor () {
|
|
218
|
+
super()
|
|
219
|
+
this.setMaxListeners(100)
|
|
220
|
+
}
|
|
221
|
+
pointerUse (name: string): QueuePointer<T> {
|
|
222
|
+
if (!this.pointers.has(name))
|
|
223
|
+
this.pointers.set(name, new QueuePointer<T>(name, this))
|
|
224
|
+
return this.pointers.get(name)!
|
|
225
|
+
}
|
|
226
|
+
pointerDelete (name: string): void {
|
|
227
|
+
if (!this.pointers.has(name))
|
|
228
|
+
throw new Error("pointer not exists")
|
|
229
|
+
this.pointers.delete(name)
|
|
230
|
+
}
|
|
231
|
+
trim (): void {
|
|
232
|
+
/* determine minimum pointer position */
|
|
233
|
+
let min = this.elements.length
|
|
234
|
+
for (const pointer of this.pointers.values())
|
|
235
|
+
if (min > pointer.position())
|
|
236
|
+
min = pointer.position()
|
|
237
|
+
|
|
238
|
+
/* trim the maximum amount of first elements */
|
|
239
|
+
if (min > 0) {
|
|
240
|
+
this.elements.splice(0, min)
|
|
241
|
+
|
|
242
|
+
/* shift all pointers */
|
|
243
|
+
for (const pointer of this.pointers.values())
|
|
244
|
+
pointer.position(pointer.position() - min)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* meta store */
|
|
250
|
+
interface TimeStoreInterval<T> extends IntervalTree.Interval {
|
|
251
|
+
item: T
|
|
252
|
+
}
|
|
253
|
+
export class TimeStore<T> extends EventEmitter {
|
|
254
|
+
private tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
|
|
255
|
+
store (start: Duration, end: Duration, item: T): void {
|
|
256
|
+
this.tree.insert({ low: start.toMillis(), high: end.toMillis(), item })
|
|
257
|
+
}
|
|
258
|
+
fetch (start: Duration, end: Duration): T[] {
|
|
259
|
+
const intervals = this.tree.search(start.toMillis(), end.toMillis())
|
|
260
|
+
return intervals.map((interval) => interval.item)
|
|
261
|
+
}
|
|
262
|
+
prune (_before: Duration): void {
|
|
263
|
+
const before = _before.toMillis()
|
|
264
|
+
const intervals = this.tree.search(0, before - 1)
|
|
265
|
+
for (const interval of intervals)
|
|
266
|
+
if (interval.low < before && interval.high < before)
|
|
267
|
+
this.tree.remove(interval)
|
|
268
|
+
}
|
|
269
|
+
clear (): void {
|
|
270
|
+
this.tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* asynchronous queue */
|
|
275
|
+
export class AsyncQueue<T> {
|
|
276
|
+
private queue: Array<T | null> = []
|
|
277
|
+
private resolvers: ((v: T | null) => void)[] = []
|
|
278
|
+
write (v: T | null) {
|
|
279
|
+
const resolve = this.resolvers.shift()
|
|
280
|
+
if (resolve)
|
|
281
|
+
resolve(v)
|
|
282
|
+
else
|
|
283
|
+
this.queue.push(v)
|
|
284
|
+
}
|
|
285
|
+
async read () {
|
|
286
|
+
if (this.queue.length > 0)
|
|
287
|
+
return this.queue.shift()!
|
|
288
|
+
else
|
|
289
|
+
return new Promise<T | null>((resolve) => this.resolvers.push(resolve))
|
|
290
|
+
}
|
|
291
|
+
destroy () {
|
|
292
|
+
for (const resolve of this.resolvers)
|
|
293
|
+
resolve(null)
|
|
294
|
+
this.resolvers = []
|
|
295
|
+
this.queue = []
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* cached regular expression class */
|
|
300
|
+
export class CachedRegExp {
|
|
301
|
+
private cache = new Map<string, RegExp>()
|
|
302
|
+
compile (pattern: string): RegExp | null {
|
|
303
|
+
if (this.cache.has(pattern))
|
|
304
|
+
return this.cache.get(pattern)!
|
|
305
|
+
try {
|
|
306
|
+
const regex = new RegExp(pattern)
|
|
307
|
+
this.cache.set(pattern, regex)
|
|
308
|
+
return regex
|
|
309
|
+
}
|
|
310
|
+
catch (_error) {
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
clear (): void {
|
|
315
|
+
this.cache.clear()
|
|
316
|
+
}
|
|
317
|
+
size (): number {
|
|
318
|
+
return this.cache.size
|
|
319
|
+
}
|
|
320
|
+
}
|