speechflow 1.0.0 → 1.2.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/CHANGELOG.md +19 -0
- package/README.md +46 -11
- package/dst/speechflow-node-a2a-gender.d.ts +17 -0
- package/dst/speechflow-node-a2a-gender.js +272 -0
- package/dst/speechflow-node-a2a-gender.js.map +1 -0
- package/dst/speechflow-node-a2a-meter.js +7 -3
- package/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/dst/speechflow-node-a2a-mute.js +1 -0
- package/dst/speechflow-node-a2a-mute.js.map +1 -1
- package/dst/speechflow-node-a2a-vad.js +47 -63
- package/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/dst/speechflow-node-a2a-wav.js +145 -122
- package/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/dst/speechflow-node-a2t-deepgram.d.ts +3 -0
- package/dst/speechflow-node-a2t-deepgram.js +29 -4
- package/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/dst/speechflow-node-t2a-elevenlabs.d.ts +3 -0
- package/dst/speechflow-node-t2a-elevenlabs.js +18 -6
- package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/dst/speechflow-node-t2t-deepl.d.ts +3 -0
- package/dst/speechflow-node-t2t-deepl.js +8 -1
- package/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/dst/speechflow-node-t2t-format.js.map +1 -1
- package/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/dst/speechflow-node-t2t-openai.js +1 -1
- package/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/dst/speechflow-node-x2x-filter.d.ts +11 -0
- package/dst/speechflow-node-x2x-filter.js +113 -0
- package/dst/speechflow-node-x2x-filter.js.map +1 -0
- package/dst/speechflow-node-x2x-trace.js +25 -11
- package/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/dst/speechflow-node-xio-device.js +17 -6
- package/dst/speechflow-node-xio-device.js.map +1 -1
- package/dst/speechflow-node-xio-file.js +61 -28
- package/dst/speechflow-node-xio-file.js.map +1 -1
- package/dst/speechflow-node-xio-mqtt.js +7 -5
- package/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/dst/speechflow-node-xio-websocket.js +5 -5
- package/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/dst/speechflow-node.d.ts +5 -1
- package/dst/speechflow-node.js +9 -2
- package/dst/speechflow-node.js.map +1 -1
- package/dst/speechflow-utils.d.ts +14 -1
- package/dst/speechflow-utils.js +110 -2
- package/dst/speechflow-utils.js.map +1 -1
- package/dst/speechflow.js +73 -14
- package/dst/speechflow.js.map +1 -1
- package/etc/speechflow.yaml +53 -26
- package/package.json +12 -10
- package/src/speechflow-node-a2a-gender.ts +272 -0
- package/src/speechflow-node-a2a-meter.ts +8 -4
- package/src/speechflow-node-a2a-mute.ts +1 -0
- package/src/speechflow-node-a2a-vad.ts +58 -68
- package/src/speechflow-node-a2a-wav.ts +128 -91
- package/src/speechflow-node-a2t-deepgram.ts +32 -5
- package/src/speechflow-node-t2a-elevenlabs.ts +21 -8
- package/src/speechflow-node-t2a-kokoro.ts +3 -3
- package/src/speechflow-node-t2t-deepl.ts +11 -3
- package/src/speechflow-node-t2t-format.ts +2 -2
- package/src/speechflow-node-t2t-ollama.ts +2 -2
- package/src/speechflow-node-t2t-openai.ts +3 -3
- package/src/speechflow-node-t2t-subtitle.ts +1 -1
- package/src/speechflow-node-t2t-transformers.ts +2 -2
- package/src/speechflow-node-x2x-filter.ts +122 -0
- package/src/speechflow-node-x2x-trace.ts +29 -12
- package/src/speechflow-node-xio-device.ts +24 -9
- package/src/speechflow-node-xio-file.ts +76 -36
- package/src/speechflow-node-xio-mqtt.ts +11 -9
- package/src/speechflow-node-xio-websocket.ts +7 -7
- package/src/speechflow-node.ts +11 -2
- package/src/speechflow-utils.ts +81 -2
- package/src/speechflow.ts +96 -35
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/* standard dependencies */
|
|
8
|
-
import Stream
|
|
8
|
+
import Stream from "node:stream"
|
|
9
|
+
|
|
10
|
+
/* external dependencies */
|
|
9
11
|
import { Duration } from "luxon"
|
|
10
12
|
|
|
11
13
|
/* internal dependencies */
|
|
@@ -23,7 +25,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
23
25
|
/* declare node configuration parameters */
|
|
24
26
|
this.configure({
|
|
25
27
|
type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
|
|
26
|
-
name: { type: "string", pos: 1 }
|
|
28
|
+
name: { type: "string", pos: 1, val: "trace" }
|
|
27
29
|
})
|
|
28
30
|
|
|
29
31
|
/* declare node input/output format */
|
|
@@ -41,7 +43,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
41
43
|
this.log(level, msg)
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
/* provide
|
|
46
|
+
/* provide Transform stream */
|
|
45
47
|
const type = this.params.type
|
|
46
48
|
this.stream = new Stream.Transform({
|
|
47
49
|
writableObjectMode: true,
|
|
@@ -49,23 +51,38 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
49
51
|
decodeStrings: false,
|
|
50
52
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
51
53
|
let error: Error | undefined
|
|
52
|
-
const
|
|
54
|
+
const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
|
|
55
|
+
const fmtMeta = (meta: Map<string, any>) => {
|
|
56
|
+
if (meta.size === 0)
|
|
57
|
+
return "none"
|
|
58
|
+
else
|
|
59
|
+
return `{ ${Array.from(meta.entries())
|
|
60
|
+
.map(([ k, v ]) => `${k}: ${JSON.stringify(v)}`)
|
|
61
|
+
.join(", ")
|
|
62
|
+
} }`
|
|
63
|
+
}
|
|
53
64
|
if (Buffer.isBuffer(chunk.payload)) {
|
|
54
65
|
if (type === "audio")
|
|
55
|
-
log("debug", `
|
|
56
|
-
`
|
|
57
|
-
`
|
|
66
|
+
log("debug", `chunk: type=${chunk.type} ` +
|
|
67
|
+
`kind=${chunk.kind} ` +
|
|
68
|
+
`start=${fmtTime(chunk.timestampStart)} ` +
|
|
69
|
+
`end=${fmtTime(chunk.timestampEnd)} ` +
|
|
70
|
+
`payload-type=Buffer payload-length=${chunk.payload.byteLength} ` +
|
|
71
|
+
`meta=${fmtMeta(chunk.meta)}`)
|
|
58
72
|
else
|
|
59
|
-
error = new Error(
|
|
73
|
+
error = new Error(`${type} chunk: seen Buffer instead of String chunk type`)
|
|
60
74
|
}
|
|
61
75
|
else {
|
|
62
76
|
if (type === "text")
|
|
63
|
-
log("debug",
|
|
64
|
-
`
|
|
77
|
+
log("debug", `${type} chunk: type=${chunk.type}` +
|
|
78
|
+
`kind=${chunk.kind} ` +
|
|
79
|
+
`start=${fmtTime(chunk.timestampStart)} ` +
|
|
80
|
+
`end=${fmtTime(chunk.timestampEnd)} ` +
|
|
65
81
|
`payload-type=String payload-length=${chunk.payload.length} ` +
|
|
66
|
-
`payload-encoding=${encoding} payload-content="${chunk.payload.toString()}"`
|
|
82
|
+
`payload-encoding=${encoding} payload-content="${chunk.payload.toString()}" ` +
|
|
83
|
+
`meta=${fmtMeta(chunk.meta)}`)
|
|
67
84
|
else
|
|
68
|
-
error = new Error(
|
|
85
|
+
error = new Error(`${type} chunk: seen String instead of Buffer chunk type`)
|
|
69
86
|
}
|
|
70
87
|
if (error !== undefined)
|
|
71
88
|
callback(error)
|
|
@@ -31,8 +31,9 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
31
31
|
|
|
32
32
|
/* declare node configuration parameters */
|
|
33
33
|
this.configure({
|
|
34
|
-
device: { type: "string", pos: 0,
|
|
35
|
-
mode: { type: "string", pos: 1, val: "rw", match: /^(?:r|w|rw)$/ }
|
|
34
|
+
device: { type: "string", pos: 0, val: "", match: /^(.+?):(.+)$/ },
|
|
35
|
+
mode: { type: "string", pos: 1, val: "rw", match: /^(?:r|w|rw)$/ },
|
|
36
|
+
chunk: { type: "number", pos: 2, val: 200, match: (n: number) => n >= 10 && n <= 1000 }
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
/* declare node input/output format */
|
|
@@ -86,6 +87,9 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
86
87
|
|
|
87
88
|
/* open node */
|
|
88
89
|
async open () {
|
|
90
|
+
if (this.params.device === "")
|
|
91
|
+
throw new Error("required parameter \"device\" has to be given")
|
|
92
|
+
|
|
89
93
|
/* determine device */
|
|
90
94
|
const device = this.audioDeviceFromURL(this.params.mode, this.params.device)
|
|
91
95
|
|
|
@@ -95,6 +99,13 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
95
99
|
throw new Error(`audio device sample rate ${device.defaultSampleRate} is ` +
|
|
96
100
|
`incompatible with required sample rate ${this.config.audioSampleRate}`)
|
|
97
101
|
|
|
102
|
+
/* determine how many bytes we need per chunk when
|
|
103
|
+
the chunk should be the requested duration */
|
|
104
|
+
const highwaterMark = (
|
|
105
|
+
this.config.audioSampleRate *
|
|
106
|
+
(this.config.audioBitDepth / 8)
|
|
107
|
+
) / (1000 / this.params.chunk)
|
|
108
|
+
|
|
98
109
|
/* establish device connection
|
|
99
110
|
Notice: "naudion" actually implements Stream.{Readable,Writable,Duplex}, but
|
|
100
111
|
declares just its sub-interface NodeJS.{Readable,Writable,Duplex}Stream,
|
|
@@ -112,13 +123,15 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
112
123
|
deviceId: device.id,
|
|
113
124
|
channelCount: this.config.audioChannels,
|
|
114
125
|
sampleRate: this.config.audioSampleRate,
|
|
115
|
-
sampleFormat: this.config.audioBitDepth
|
|
126
|
+
sampleFormat: this.config.audioBitDepth,
|
|
127
|
+
highwaterMark
|
|
116
128
|
},
|
|
117
129
|
outOptions: {
|
|
118
130
|
deviceId: device.id,
|
|
119
131
|
channelCount: this.config.audioChannels,
|
|
120
132
|
sampleRate: this.config.audioSampleRate,
|
|
121
|
-
sampleFormat: this.config.audioBitDepth
|
|
133
|
+
sampleFormat: this.config.audioBitDepth,
|
|
134
|
+
highwaterMark
|
|
122
135
|
}
|
|
123
136
|
})
|
|
124
137
|
this.stream = this.io as unknown as Stream.Duplex
|
|
@@ -135,10 +148,11 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
135
148
|
this.log("info", `resolved "${this.params.device}" to input device "${device.id}"`)
|
|
136
149
|
this.io = PortAudio.AudioIO({
|
|
137
150
|
inOptions: {
|
|
138
|
-
deviceId:
|
|
139
|
-
channelCount:
|
|
140
|
-
sampleRate:
|
|
141
|
-
sampleFormat:
|
|
151
|
+
deviceId: device.id,
|
|
152
|
+
channelCount: this.config.audioChannels,
|
|
153
|
+
sampleRate: this.config.audioSampleRate,
|
|
154
|
+
sampleFormat: this.config.audioBitDepth,
|
|
155
|
+
highwaterMark
|
|
142
156
|
}
|
|
143
157
|
})
|
|
144
158
|
this.stream = this.io as unknown as Stream.Readable
|
|
@@ -158,7 +172,8 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
158
172
|
deviceId: device.id,
|
|
159
173
|
channelCount: this.config.audioChannels,
|
|
160
174
|
sampleRate: this.config.audioSampleRate,
|
|
161
|
-
sampleFormat: this.config.audioBitDepth
|
|
175
|
+
sampleFormat: this.config.audioBitDepth,
|
|
176
|
+
highwaterMark
|
|
162
177
|
}
|
|
163
178
|
})
|
|
164
179
|
this.stream = this.io as unknown as Stream.Writable
|
|
@@ -23,9 +23,11 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
|
|
|
23
23
|
|
|
24
24
|
/* declare node configuration parameters */
|
|
25
25
|
this.configure({
|
|
26
|
-
path:
|
|
27
|
-
mode:
|
|
28
|
-
type:
|
|
26
|
+
path: { type: "string", pos: 0, val: "" },
|
|
27
|
+
mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
|
|
28
|
+
type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
|
|
29
|
+
chunka: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
|
|
30
|
+
chunkt: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
/* declare node input/output format */
|
|
@@ -45,90 +47,128 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
|
|
|
45
47
|
|
|
46
48
|
/* open node */
|
|
47
49
|
async open () {
|
|
50
|
+
/* determine how many bytes we need per chunk when
|
|
51
|
+
the chunk should be of the required duration/size */
|
|
52
|
+
const highWaterMarkAudio = (
|
|
53
|
+
this.config.audioSampleRate *
|
|
54
|
+
(this.config.audioBitDepth / 8)
|
|
55
|
+
) / (1000 / this.params.chunka)
|
|
56
|
+
const highWaterMarkText = this.params.chunkt
|
|
57
|
+
|
|
58
|
+
/* sanity check */
|
|
59
|
+
if (this.params.path === "")
|
|
60
|
+
throw new Error("required parameter \"path\" has to be given")
|
|
61
|
+
|
|
62
|
+
/* dispatch according to mode and path */
|
|
48
63
|
if (this.params.mode === "rw") {
|
|
49
64
|
if (this.params.path === "-") {
|
|
50
65
|
/* standard I/O */
|
|
51
66
|
if (this.params.type === "audio") {
|
|
52
67
|
process.stdin.setEncoding()
|
|
53
68
|
process.stdout.setEncoding()
|
|
69
|
+
const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
|
|
70
|
+
process.stdin.pipe(streamR)
|
|
71
|
+
const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
|
|
72
|
+
streamW.pipe(process.stdout)
|
|
73
|
+
this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
|
|
54
74
|
}
|
|
55
75
|
else {
|
|
56
76
|
process.stdin.setEncoding(this.config.textEncoding)
|
|
57
77
|
process.stdout.setEncoding(this.config.textEncoding)
|
|
78
|
+
const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
79
|
+
process.stdin.pipe(streamR)
|
|
80
|
+
const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
81
|
+
streamW.pipe(process.stdout)
|
|
82
|
+
this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
|
|
58
83
|
}
|
|
59
|
-
this.stream = Stream.Duplex.from({
|
|
60
|
-
readable: process.stdin,
|
|
61
|
-
writable: process.stdout
|
|
62
|
-
})
|
|
63
84
|
}
|
|
64
85
|
else {
|
|
65
86
|
/* file I/O */
|
|
66
87
|
if (this.params.type === "audio") {
|
|
67
88
|
this.stream = Stream.Duplex.from({
|
|
68
|
-
readable: fs.createReadStream(this.params.path
|
|
69
|
-
|
|
89
|
+
readable: fs.createReadStream(this.params.path,
|
|
90
|
+
{ highWaterMark: highWaterMarkAudio }),
|
|
91
|
+
writable: fs.createWriteStream(this.params.path,
|
|
92
|
+
{ highWaterMark: highWaterMarkAudio })
|
|
70
93
|
})
|
|
71
94
|
}
|
|
72
95
|
else {
|
|
73
96
|
this.stream = Stream.Duplex.from({
|
|
74
|
-
readable: fs.createReadStream(this.params.path,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
readable: fs.createReadStream(this.params.path, {
|
|
98
|
+
highWaterMark: highWaterMarkText,
|
|
99
|
+
encoding: this.config.textEncoding
|
|
100
|
+
}),
|
|
101
|
+
writable: fs.createWriteStream(this.params.path, {
|
|
102
|
+
highWaterMark: highWaterMarkText,
|
|
103
|
+
encoding: this.config.textEncoding
|
|
104
|
+
})
|
|
78
105
|
})
|
|
79
106
|
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
/* convert regular stream into object-mode stream */
|
|
83
110
|
const wrapper1 = utils.createTransformStreamForWritableSide()
|
|
84
|
-
const wrapper2 = utils.createTransformStreamForReadableSide(
|
|
111
|
+
const wrapper2 = utils.createTransformStreamForReadableSide(
|
|
112
|
+
this.params.type, () => this.timeZero)
|
|
85
113
|
this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
|
|
86
114
|
}
|
|
87
115
|
else if (this.params.mode === "r") {
|
|
88
116
|
if (this.params.path === "-") {
|
|
89
117
|
/* standard I/O */
|
|
90
|
-
|
|
118
|
+
let chunker: Stream.PassThrough
|
|
119
|
+
if (this.params.type === "audio") {
|
|
91
120
|
process.stdin.setEncoding()
|
|
92
|
-
|
|
121
|
+
chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
93
124
|
process.stdin.setEncoding(this.config.textEncoding)
|
|
94
|
-
|
|
125
|
+
chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
126
|
+
}
|
|
127
|
+
const wrapper = utils.createTransformStreamForReadableSide(
|
|
128
|
+
this.params.type, () => this.timeZero)
|
|
129
|
+
this.stream = Stream.compose(process.stdin, chunker, wrapper)
|
|
95
130
|
}
|
|
96
131
|
else {
|
|
97
132
|
/* file I/O */
|
|
133
|
+
let readable: Stream.Readable
|
|
98
134
|
if (this.params.type === "audio")
|
|
99
|
-
|
|
135
|
+
readable = fs.createReadStream(this.params.path,
|
|
136
|
+
{ highWaterMark: highWaterMarkAudio })
|
|
100
137
|
else
|
|
101
|
-
|
|
102
|
-
{ encoding: this.config.textEncoding })
|
|
138
|
+
readable = fs.createReadStream(this.params.path,
|
|
139
|
+
{ highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
|
|
140
|
+
const wrapper = utils.createTransformStreamForReadableSide(
|
|
141
|
+
this.params.type, () => this.timeZero)
|
|
142
|
+
this.stream = Stream.compose(readable, wrapper)
|
|
103
143
|
}
|
|
104
|
-
|
|
105
|
-
/* convert regular stream into object-mode stream */
|
|
106
|
-
const wrapper = utils.createTransformStreamForReadableSide(this.params.type, () => this.timeZero)
|
|
107
|
-
this.stream.pipe(wrapper)
|
|
108
|
-
this.stream = wrapper
|
|
109
144
|
}
|
|
110
145
|
else if (this.params.mode === "w") {
|
|
111
146
|
if (this.params.path === "-") {
|
|
112
147
|
/* standard I/O */
|
|
113
|
-
|
|
148
|
+
let chunker: Stream.PassThrough
|
|
149
|
+
if (this.params.type === "audio") {
|
|
114
150
|
process.stdout.setEncoding()
|
|
115
|
-
|
|
151
|
+
chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
116
154
|
process.stdout.setEncoding(this.config.textEncoding)
|
|
117
|
-
|
|
155
|
+
chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
|
|
156
|
+
}
|
|
157
|
+
const wrapper = utils.createTransformStreamForWritableSide()
|
|
158
|
+
this.stream = Stream.compose(wrapper, chunker, process.stdout)
|
|
118
159
|
}
|
|
119
160
|
else {
|
|
120
161
|
/* file I/O */
|
|
162
|
+
let writable: Stream.Writable
|
|
121
163
|
if (this.params.type === "audio")
|
|
122
|
-
|
|
164
|
+
writable = fs.createWriteStream(this.params.path,
|
|
165
|
+
{ highWaterMark: highWaterMarkAudio })
|
|
123
166
|
else
|
|
124
|
-
|
|
125
|
-
{ encoding: this.config.textEncoding })
|
|
167
|
+
writable = fs.createWriteStream(this.params.path,
|
|
168
|
+
{ highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
|
|
169
|
+
const wrapper = utils.createTransformStreamForWritableSide()
|
|
170
|
+
this.stream = Stream.compose(wrapper, writable)
|
|
126
171
|
}
|
|
127
|
-
|
|
128
|
-
/* convert regular stream into object-mode stream */
|
|
129
|
-
const wrapper = utils.createTransformStreamForWritableSide()
|
|
130
|
-
wrapper.pipe(this.stream as Stream.Writable)
|
|
131
|
-
this.stream = wrapper
|
|
132
172
|
}
|
|
133
173
|
else
|
|
134
174
|
throw new Error(`invalid file mode "${this.params.mode}"`)
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/* standard dependencies */
|
|
8
|
-
import Stream
|
|
8
|
+
import Stream from "node:stream"
|
|
9
9
|
|
|
10
10
|
/* external dependencies */
|
|
11
|
-
import MQTT
|
|
12
|
-
import UUID
|
|
11
|
+
import MQTT from "mqtt"
|
|
12
|
+
import UUID from "pure-uuid"
|
|
13
13
|
|
|
14
14
|
/* internal dependencies */
|
|
15
15
|
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
@@ -39,12 +39,6 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
|
|
|
39
39
|
type: { type: "string", pos: 6, val: "text", match: /^(?:audio|text)$/ }
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
/* logical parameter sanity check */
|
|
43
|
-
if ((this.params.mode === "w" || this.params.mode === "rw") && this.params.topicWrite === "")
|
|
44
|
-
throw new Error("writing to MQTT requires a topicWrite parameter")
|
|
45
|
-
if ((this.params.mode === "r" || this.params.mode === "rw") && this.params.topicRead === "")
|
|
46
|
-
throw new Error("reading from MQTT requires a topicRead parameter")
|
|
47
|
-
|
|
48
42
|
/* declare node input/output format */
|
|
49
43
|
if (this.params.mode === "rw") {
|
|
50
44
|
this.input = this.params.type
|
|
@@ -62,6 +56,14 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
|
|
|
62
56
|
|
|
63
57
|
/* open node */
|
|
64
58
|
async open () {
|
|
59
|
+
/* logical parameter sanity check */
|
|
60
|
+
if (this.params.url === "")
|
|
61
|
+
throw new Error("required parameter \"url\" has to be given")
|
|
62
|
+
if ((this.params.mode === "w" || this.params.mode === "rw") && this.params.topicWrite === "")
|
|
63
|
+
throw new Error("writing to MQTT requires a topicWrite parameter")
|
|
64
|
+
if ((this.params.mode === "r" || this.params.mode === "rw") && this.params.topicRead === "")
|
|
65
|
+
throw new Error("reading from MQTT requires a topicRead parameter")
|
|
66
|
+
|
|
65
67
|
/* connect remotely to a MQTT broker */
|
|
66
68
|
this.broker = MQTT.connect(this.params.url, {
|
|
67
69
|
protocolId: "MQTT",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/* standard dependencies */
|
|
8
|
-
import Stream
|
|
8
|
+
import Stream from "node:stream"
|
|
9
9
|
|
|
10
10
|
/* external dependencies */
|
|
11
11
|
import ws from "ws"
|
|
@@ -36,12 +36,6 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
36
36
|
type: { type: "string", val: "text", match: /^(?:audio|text)$/ }
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
/* sanity check usage */
|
|
40
|
-
if (this.params.listen !== "" && this.params.connect !== "")
|
|
41
|
-
throw new Error("Websocket node cannot listen and connect at the same time")
|
|
42
|
-
else if (this.params.listen === "" && this.params.connect === "")
|
|
43
|
-
throw new Error("Websocket node requires either listen or connect mode")
|
|
44
|
-
|
|
45
39
|
/* declare node input/output format */
|
|
46
40
|
if (this.params.mode === "rw") {
|
|
47
41
|
this.input = this.params.type
|
|
@@ -59,6 +53,12 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
|
|
|
59
53
|
|
|
60
54
|
/* open node */
|
|
61
55
|
async open () {
|
|
56
|
+
/* sanity check usage */
|
|
57
|
+
if (this.params.listen !== "" && this.params.connect !== "")
|
|
58
|
+
throw new Error("Websocket node cannot listen and connect at the same time")
|
|
59
|
+
else if (this.params.listen === "" && this.params.connect === "")
|
|
60
|
+
throw new Error("Websocket node requires either listen or connect mode")
|
|
61
|
+
|
|
62
62
|
if (this.params.listen !== "") {
|
|
63
63
|
/* listen locally on a Websocket port */
|
|
64
64
|
const url = new URL(this.params.listen)
|
package/src/speechflow-node.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
/* standard dependencies */
|
|
8
8
|
import Events from "node:events"
|
|
9
9
|
import Stream from "node:stream"
|
|
10
|
+
|
|
11
|
+
/* external dependencies */
|
|
10
12
|
import { DateTime, Duration } from "luxon"
|
|
11
13
|
|
|
12
14
|
/* the definition of a single payload chunk passed through the SpeechFlow nodes */
|
|
@@ -16,7 +18,8 @@ export class SpeechFlowChunk {
|
|
|
16
18
|
public timestampEnd: Duration,
|
|
17
19
|
public kind: "intermediate" | "final",
|
|
18
20
|
public type: "audio" | "text",
|
|
19
|
-
public payload: Buffer | string
|
|
21
|
+
public payload: Buffer | string,
|
|
22
|
+
public meta = new Map<string, any>()
|
|
20
23
|
) {}
|
|
21
24
|
clone () {
|
|
22
25
|
let payload: Buffer | string
|
|
@@ -29,7 +32,8 @@ export class SpeechFlowChunk {
|
|
|
29
32
|
Duration.fromMillis(this.timestampEnd.toMillis()),
|
|
30
33
|
this.kind,
|
|
31
34
|
this.type,
|
|
32
|
-
payload
|
|
35
|
+
payload,
|
|
36
|
+
new Map(this.meta)
|
|
33
37
|
)
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -168,6 +172,11 @@ export default class SpeechFlowNode extends Events.EventEmitter {
|
|
|
168
172
|
this.emit("log", level, msg, data)
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
/* default implementation for status operation */
|
|
176
|
+
async status (): Promise<{ [ key: string ]: string | number }> {
|
|
177
|
+
return {}
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
/* default implementation for open/close operations */
|
|
172
181
|
async open () {}
|
|
173
182
|
async close () {}
|
package/src/speechflow-utils.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/*
|
|
7
|
+
/* standard dependencies */
|
|
8
8
|
import Stream from "node:stream"
|
|
9
9
|
import { EventEmitter } from "node:events"
|
|
10
|
+
|
|
11
|
+
/* external dependencies */
|
|
10
12
|
import { DateTime, Duration } from "luxon"
|
|
11
13
|
import CBOR from "cbor2"
|
|
14
|
+
import * as IntervalTree from "node-interval-tree"
|
|
12
15
|
|
|
13
16
|
/* internal dependencies */
|
|
14
|
-
import { SpeechFlowChunk }
|
|
17
|
+
import { SpeechFlowChunk } from "./speechflow-node"
|
|
15
18
|
|
|
16
19
|
/* calculate duration of an audio buffer */
|
|
17
20
|
export function audioBufferDuration (
|
|
@@ -68,6 +71,10 @@ export function createTransformStreamForWritableSide () {
|
|
|
68
71
|
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
69
72
|
this.push(chunk.payload)
|
|
70
73
|
callback()
|
|
74
|
+
},
|
|
75
|
+
final (callback) {
|
|
76
|
+
this.push(null)
|
|
77
|
+
callback()
|
|
71
78
|
}
|
|
72
79
|
})
|
|
73
80
|
}
|
|
@@ -90,6 +97,10 @@ export function createTransformStreamForReadableSide (type: "text" | "audio", ge
|
|
|
90
97
|
const obj = new SpeechFlowChunk(start, end, "final", type, chunk)
|
|
91
98
|
this.push(obj)
|
|
92
99
|
callback()
|
|
100
|
+
},
|
|
101
|
+
final (callback) {
|
|
102
|
+
this.push(null)
|
|
103
|
+
callback()
|
|
93
104
|
}
|
|
94
105
|
})
|
|
95
106
|
}
|
|
@@ -252,6 +263,7 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
|
252
263
|
private queue: Queue<T>
|
|
253
264
|
) {
|
|
254
265
|
super()
|
|
266
|
+
this.setMaxListeners(100)
|
|
255
267
|
}
|
|
256
268
|
|
|
257
269
|
/* positioning operations */
|
|
@@ -379,6 +391,10 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
|
379
391
|
export class Queue<T extends QueueElement> extends EventEmitter {
|
|
380
392
|
public elements: T[] = []
|
|
381
393
|
private pointers = new Map<string, QueuePointer<T>>()
|
|
394
|
+
constructor () {
|
|
395
|
+
super()
|
|
396
|
+
this.setMaxListeners(100)
|
|
397
|
+
}
|
|
382
398
|
pointerUse (name: string): QueuePointer<T> {
|
|
383
399
|
if (!this.pointers.has(name))
|
|
384
400
|
this.pointers.set(name, new QueuePointer<T>(name, this))
|
|
@@ -405,3 +421,66 @@ export class Queue<T extends QueueElement> extends EventEmitter {
|
|
|
405
421
|
}
|
|
406
422
|
}
|
|
407
423
|
|
|
424
|
+
/* utility class for wrapping a custom stream into a regular Transform stream */
|
|
425
|
+
export class StreamWrapper extends Stream.Transform {
|
|
426
|
+
private foreignStream: any
|
|
427
|
+
constructor (foreignStream: any, options: Stream.TransformOptions = {}) {
|
|
428
|
+
options.readableObjectMode = true
|
|
429
|
+
options.writableObjectMode = true
|
|
430
|
+
super(options)
|
|
431
|
+
this.foreignStream = foreignStream
|
|
432
|
+
this.foreignStream.on("data", (chunk: any) => {
|
|
433
|
+
this.push(chunk)
|
|
434
|
+
})
|
|
435
|
+
this.foreignStream.on("error", (err: Error) => {
|
|
436
|
+
this.emit("error", err)
|
|
437
|
+
})
|
|
438
|
+
this.foreignStream.on("end", () => {
|
|
439
|
+
this.push(null)
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
_transform (chunk: any, encoding: BufferEncoding, callback: Stream.TransformCallback): void {
|
|
443
|
+
try {
|
|
444
|
+
const canContinue = this.foreignStream.write(chunk)
|
|
445
|
+
if (canContinue)
|
|
446
|
+
callback()
|
|
447
|
+
else
|
|
448
|
+
this.foreignStream.once("drain", callback)
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
callback(err as Error)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
_flush (callback: Stream.TransformCallback): void {
|
|
455
|
+
try {
|
|
456
|
+
if (typeof this.foreignStream.end === "function")
|
|
457
|
+
this.foreignStream.end()
|
|
458
|
+
callback()
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
callback(err as Error)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* meta store */
|
|
467
|
+
interface TimeStoreInterval<T> extends IntervalTree.Interval {
|
|
468
|
+
item: T
|
|
469
|
+
}
|
|
470
|
+
export class TimeStore<T> extends EventEmitter {
|
|
471
|
+
private tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
|
|
472
|
+
store (start: Duration, end: Duration, item: T): void {
|
|
473
|
+
this.tree.insert({ low: start.toMillis(), high: end.toMillis(), item })
|
|
474
|
+
}
|
|
475
|
+
fetch (start: Duration, end: Duration): T[] {
|
|
476
|
+
const intervals = this.tree.search(start.toMillis(), end.toMillis())
|
|
477
|
+
return intervals.map((interval) => interval.item)
|
|
478
|
+
}
|
|
479
|
+
prune (_before: Duration): void {
|
|
480
|
+
const before = _before.toMillis()
|
|
481
|
+
const intervals = this.tree.search(0, before - 1)
|
|
482
|
+
for (const interval of intervals)
|
|
483
|
+
if (interval.low < before && interval.high < before)
|
|
484
|
+
this.tree.remove(interval)
|
|
485
|
+
}
|
|
486
|
+
}
|