speechflow 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.claude/settings.local.json +3 -0
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +82 -45
  4. package/etc/speechflow.yaml +19 -14
  5. package/package.json +5 -5
  6. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +2 -1
  7. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  8. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +2 -1
  9. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  10. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +3 -2
  11. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  12. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +11 -8
  13. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  14. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +4 -1
  15. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  16. package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -0
  17. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +22 -2
  18. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  19. package/speechflow-cli/dst/speechflow-node-xio-device.d.ts +3 -0
  20. package/speechflow-cli/dst/speechflow-node-xio-device.js +99 -84
  21. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  22. package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -0
  23. package/speechflow-cli/dst/speechflow-util-error.js +7 -0
  24. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
  25. package/speechflow-cli/etc/stx.conf +6 -10
  26. package/speechflow-cli/package.json +13 -13
  27. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +2 -1
  28. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +2 -1
  29. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +3 -2
  30. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +11 -8
  31. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +4 -1
  32. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +25 -2
  33. package/speechflow-cli/src/speechflow-node-xio-device.ts +108 -89
  34. package/speechflow-cli/src/speechflow-util-error.ts +7 -0
  35. package/speechflow-ui-db/dst/index.js +24 -33
  36. package/speechflow-ui-db/package.json +12 -10
  37. package/speechflow-ui-db/src/app.vue +16 -2
  38. package/speechflow-ui-st/.claude/settings.local.json +3 -0
  39. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.eot +0 -0
  40. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.ttf +0 -0
  41. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-BoldIt.woff +0 -0
  42. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.eot +0 -0
  43. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.ttf +0 -0
  44. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-RegularIt.woff +0 -0
  45. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.eot +0 -0
  46. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.ttf +0 -0
  47. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-Semibold.woff +0 -0
  48. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.eot +0 -0
  49. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.ttf +0 -0
  50. package/speechflow-ui-st/dst/app-font-TypoPRO-SourceSansPro-SemiboldIt.woff +0 -0
  51. package/speechflow-ui-st/dst/index.css +2 -2
  52. package/speechflow-ui-st/dst/index.js +26 -28
  53. package/speechflow-ui-st/package.json +11 -10
  54. package/speechflow-ui-st/src/app.vue +142 -48
  55. package/speechflow-ui-st/src/index.ts +4 -0
@@ -33,12 +33,16 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
33
33
  /* declare node configuration parameters */
34
34
  this.configure({
35
35
  interval: { type: "number", pos: 0, val: 250 },
36
+ mode: { type: "string", pos: 1, val: "filter", match: /^(?:filter|sink)$/ },
36
37
  dashboard: { type: "string", val: "" }
37
38
  })
38
39
 
39
40
  /* declare node input/output format */
40
41
  this.input = "audio"
41
- this.output = "audio"
42
+ if (this.params.mode === "filter")
43
+ this.output = "audio"
44
+ else if (this.params.mode === "sink")
45
+ this.output = "none"
42
46
  }
43
47
 
44
48
  /* open node */
@@ -53,7 +57,7 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
53
57
  /* internal state */
54
58
  const sampleWindowDuration = 3 /* LUFS-S requires 3s */
55
59
  const sampleWindowSize = Math.floor(this.config.audioSampleRate * sampleWindowDuration)
56
- let sampleWindow = new Float32Array(sampleWindowSize)
60
+ const sampleWindow = new Float32Array(sampleWindowSize)
57
61
  sampleWindow.fill(0, 0, sampleWindowSize)
58
62
  let lufss = -60
59
63
  let rms = -60
@@ -66,10 +70,8 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
66
70
  /* define chunk processing function */
67
71
  const processChunk = (chunkData: Float32Array) => {
68
72
  /* update internal audio sample sliding window */
69
- const newWindow = new Float32Array(sampleWindowSize)
70
- newWindow.set(sampleWindow.slice(chunkData.length), 0)
71
- newWindow.set(chunkData, sampleWindowSize - chunkData.length)
72
- sampleWindow = newWindow
73
+ sampleWindow.set(sampleWindow.subarray(chunkData.length), 0)
74
+ sampleWindow.set(chunkData, sampleWindowSize - chunkData.length)
73
75
 
74
76
  /* calculate the LUFS-S and RMS metric */
75
77
  const audioData = {
@@ -151,7 +153,8 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
151
153
  self.chunkBuffer = newBuffer
152
154
 
153
155
  /* pass-through original audio chunk */
154
- this.push(chunk)
156
+ if (self.params.mode === "filter")
157
+ this.push(chunk)
155
158
  callback()
156
159
  }
157
160
  catch (error) {
@@ -160,7 +163,7 @@ export default class SpeechFlowNodeA2AMeter extends SpeechFlowNode {
160
163
  }
161
164
  },
162
165
  final (callback) {
163
- if (self.destroyed) {
166
+ if (self.destroyed || self.params.mode === "sink") {
164
167
  callback()
165
168
  return
166
169
  }
@@ -123,7 +123,10 @@ export default class SpeechFlowNodeA2TDeepgram extends SpeechFlowNode {
123
123
  if (text === "")
124
124
  this.log("info", `empty/dummy text received (start: ${data.start}s, duration: ${data.duration.toFixed(2)}s)`)
125
125
  else {
126
- this.log("info", `text received (start: ${data.start}s, duration: ${data.duration.toFixed(2)}s): "${text}"`)
126
+ this.log("info", `text received (start: ${data.start}s, ` +
127
+ `duration: ${data.duration.toFixed(2)}s, ` +
128
+ `kind: ${isFinal ? "final" : "intermediate"}): ` +
129
+ `${text}"`)
127
130
  const start = Duration.fromMillis(data.start * 1000).plus(this.timeZeroOffset)
128
131
  const end = start.plus({ seconds: data.duration })
129
132
  const metas = metastore.fetch(start, end)
@@ -18,6 +18,9 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
18
18
  /* declare official node name */
19
19
  public static name = "x2x-trace"
20
20
 
21
+ /* internal state */
22
+ private destroyed = false
23
+
21
24
  /* construct node */
22
25
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
23
26
  super(id, cfg, opts, args)
@@ -26,6 +29,7 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
26
29
  this.configure({
27
30
  type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
28
31
  name: { type: "string", pos: 1, val: "trace" },
32
+ mode: { type: "string", pos: 2, val: "filter", match: /^(?:filter|sink)$/ },
29
33
  dashboard: { type: "string", val: "" }
30
34
  })
31
35
 
@@ -35,7 +39,10 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
35
39
 
36
40
  /* declare node input/output format */
37
41
  this.input = this.params.type
38
- this.output = this.params.type
42
+ if (this.params.mode === "filter")
43
+ this.output = this.params.type
44
+ else if (this.params.mode === "sink")
45
+ this.output = "none"
39
46
  }
40
47
 
41
48
  /* open node */
@@ -48,6 +55,9 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
48
55
  this.log(level, msg)
49
56
  }
50
57
 
58
+ /* clear destruction flag */
59
+ this.destroyed = false
60
+
51
61
  /* helper functions for formatting */
52
62
  const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
53
63
  const fmtMeta = (meta: Map<string, any>) => {
@@ -74,6 +84,10 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
74
84
  highWaterMark: 1,
75
85
  transform (chunk: SpeechFlowChunk, encoding, callback) {
76
86
  let error: Error | undefined
87
+ if (self.destroyed) {
88
+ callback(new Error("stream already destroyed"))
89
+ return
90
+ }
77
91
  if (Buffer.isBuffer(chunk.payload)) {
78
92
  if (self.params.type === "audio")
79
93
  log("debug", fmtChunkBase(chunk) +
@@ -94,7 +108,9 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
94
108
  else
95
109
  error = new Error(`${self.params.type} chunk: seen String instead of Buffer chunk type`)
96
110
  }
97
- if (error !== undefined)
111
+ if (self.params.mode === "sink")
112
+ callback()
113
+ else if (error !== undefined)
98
114
  callback(error)
99
115
  else {
100
116
  this.push(chunk, encoding)
@@ -102,6 +118,10 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
102
118
  }
103
119
  },
104
120
  final (callback) {
121
+ if (self.destroyed || self.params.mode === "sink") {
122
+ callback()
123
+ return
124
+ }
105
125
  this.push(null)
106
126
  callback()
107
127
  }
@@ -115,5 +135,8 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
115
135
  this.stream.destroy()
116
136
  this.stream = null
117
137
  }
138
+
139
+ /* indicate destruction */
140
+ this.destroyed = true
118
141
  }
119
142
  }
@@ -83,6 +83,84 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
83
83
  return device
84
84
  }
85
85
 
86
+ /* NOTICE: "naudion" actually implements Stream.{Readable,Writable,Duplex}, but
87
+ declares just its sub-interface NodeJS.{Readable,Writable,Duplex}Stream,
88
+ so it is correct to cast it back to Stream.{Readable,Writable,Duplex}
89
+ in the following device stream setup functions! */
90
+
91
+ /* INTERNAL: setup duplex stream */
92
+ private setupDuplexStream (device: PortAudio.DeviceInfo, highwaterMark: number) {
93
+ if (device.maxInputChannels === 0)
94
+ throw new Error(`device "${device.id}" does not have any input channels (required by read/write mode)`)
95
+ if (device.maxOutputChannels === 0)
96
+ throw new Error(`device "${device.id}" does not have any output channels (required by read/write mode)`)
97
+ this.log("info", `resolved "${this.params.device}" to duplex device "${device.id}"`)
98
+ this.io = PortAudio.AudioIO({
99
+ inOptions: {
100
+ deviceId: device.id,
101
+ channelCount: this.config.audioChannels,
102
+ sampleRate: this.config.audioSampleRate,
103
+ sampleFormat: this.config.audioBitDepth,
104
+ highwaterMark
105
+ },
106
+ outOptions: {
107
+ deviceId: device.id,
108
+ channelCount: this.config.audioChannels,
109
+ sampleRate: this.config.audioSampleRate,
110
+ sampleFormat: this.config.audioBitDepth,
111
+ highwaterMark
112
+ }
113
+ })
114
+ this.stream = this.io as unknown as Stream.Duplex
115
+
116
+ /* convert regular stream into object-mode stream */
117
+ const wrapper1 = util.createTransformStreamForWritableSide()
118
+ const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
119
+ this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
120
+ }
121
+
122
+ /* INTERNAL: setup input stream */
123
+ private setupInputStream (device: PortAudio.DeviceInfo, highwaterMark: number) {
124
+ if (device.maxInputChannels === 0)
125
+ throw new Error(`device "${device.id}" does not have any input channels (required by read mode)`)
126
+ this.log("info", `resolved "${this.params.device}" to input device "${device.id}"`)
127
+ this.io = PortAudio.AudioIO({
128
+ inOptions: {
129
+ deviceId: device.id,
130
+ channelCount: this.config.audioChannels,
131
+ sampleRate: this.config.audioSampleRate,
132
+ sampleFormat: this.config.audioBitDepth,
133
+ highwaterMark
134
+ }
135
+ })
136
+ this.stream = this.io as unknown as Stream.Readable
137
+
138
+ /* convert regular stream into object-mode stream */
139
+ const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
140
+ this.stream = Stream.compose(this.stream, wrapper)
141
+ }
142
+
143
+ /* INTERNAL: setup output stream */
144
+ private setupOutputStream (device: PortAudio.DeviceInfo, highwaterMark: number) {
145
+ if (device.maxOutputChannels === 0)
146
+ throw new Error(`device "${device.id}" does not have any output channels (required by write mode)`)
147
+ this.log("info", `resolved "${this.params.device}" to output device "${device.id}"`)
148
+ this.io = PortAudio.AudioIO({
149
+ outOptions: {
150
+ deviceId: device.id,
151
+ channelCount: this.config.audioChannels,
152
+ sampleRate: this.config.audioSampleRate,
153
+ sampleFormat: this.config.audioBitDepth,
154
+ highwaterMark
155
+ }
156
+ })
157
+ this.stream = this.io as unknown as Stream.Writable
158
+
159
+ /* convert regular stream into object-mode stream */
160
+ const wrapper = util.createTransformStreamForWritableSide()
161
+ this.stream = Stream.compose(wrapper, this.stream)
162
+ }
163
+
86
164
  /* open node */
87
165
  async open () {
88
166
  if (this.params.device === "")
@@ -104,107 +182,48 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
104
182
  (this.config.audioBitDepth / 8)
105
183
  ) / (1000 / this.params.chunk)
106
184
 
107
- /* establish device connection
108
- Notice: "naudion" actually implements Stream.{Readable,Writable,Duplex}, but
109
- declares just its sub-interface NodeJS.{Readable,Writable,Duplex}Stream,
110
- so it is correct to cast it back to Stream.{Readable,Writable,Duplex} */
111
- /* FIXME: the underlying PortAudio outputs verbose/debugging messages */
112
- if (this.params.mode === "rw") {
113
- /* input/output device */
114
- if (device.maxInputChannels === 0)
115
- throw new Error(`device "${device.id}" does not have any input channels (required by read/write mode)`)
116
- if (device.maxOutputChannels === 0)
117
- throw new Error(`device "${device.id}" does not have any output channels (required by read/write mode)`)
118
- this.log("info", `resolved "${this.params.device}" to duplex device "${device.id}"`)
119
- this.io = PortAudio.AudioIO({
120
- inOptions: {
121
- deviceId: device.id,
122
- channelCount: this.config.audioChannels,
123
- sampleRate: this.config.audioSampleRate,
124
- sampleFormat: this.config.audioBitDepth,
125
- highwaterMark
126
- },
127
- outOptions: {
128
- deviceId: device.id,
129
- channelCount: this.config.audioChannels,
130
- sampleRate: this.config.audioSampleRate,
131
- sampleFormat: this.config.audioBitDepth,
132
- highwaterMark
133
- }
134
- })
135
- this.stream = this.io as unknown as Stream.Duplex
136
-
137
- /* convert regular stream into object-mode stream */
138
- const wrapper1 = util.createTransformStreamForWritableSide()
139
- const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
140
- this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
141
- }
142
- else if (this.params.mode === "r") {
143
- /* input device */
144
- if (device.maxInputChannels === 0)
145
- throw new Error(`device "${device.id}" does not have any input channels (required by read mode)`)
146
- this.log("info", `resolved "${this.params.device}" to input device "${device.id}"`)
147
- this.io = PortAudio.AudioIO({
148
- inOptions: {
149
- deviceId: device.id,
150
- channelCount: this.config.audioChannels,
151
- sampleRate: this.config.audioSampleRate,
152
- sampleFormat: this.config.audioBitDepth,
153
- highwaterMark
154
- }
155
- })
156
- this.stream = this.io as unknown as Stream.Readable
157
-
158
- /* convert regular stream into object-mode stream */
159
- const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
160
- this.stream = Stream.compose(this.stream, wrapper)
161
- }
162
- else if (this.params.mode === "w") {
163
- /* output device */
164
- if (device.maxOutputChannels === 0)
165
- throw new Error(`device "${device.id}" does not have any output channels (required by write mode)`)
166
- this.log("info", `resolved "${this.params.device}" to output device "${device.id}"`)
167
- this.io = PortAudio.AudioIO({
168
- outOptions: {
169
- deviceId: device.id,
170
- channelCount: this.config.audioChannels,
171
- sampleRate: this.config.audioSampleRate,
172
- sampleFormat: this.config.audioBitDepth,
173
- highwaterMark
174
- }
175
- })
176
- this.stream = this.io as unknown as Stream.Writable
177
-
178
- /* convert regular stream into object-mode stream */
179
- const wrapper = util.createTransformStreamForWritableSide()
180
- this.stream = Stream.compose(wrapper, this.stream)
181
- }
182
- else
183
- throw new Error(`device "${device.id}" does not have any input or output channels`)
185
+ /* establish device stream */
186
+ if (this.params.mode === "rw")
187
+ this.setupDuplexStream(device, highwaterMark)
188
+ else if (this.params.mode === "r")
189
+ this.setupInputStream(device, highwaterMark)
190
+ else if (this.params.mode === "w")
191
+ this.setupOutputStream(device, highwaterMark)
184
192
 
185
193
  /* pass-through PortAudio errors */
186
- this.io.on("error", (err) => {
194
+ this.io!.on("error", (err) => {
187
195
  this.emit("error", err)
188
196
  })
189
197
 
190
198
  /* start PortAudio */
191
- this.io.start()
199
+ this.io!.start()
192
200
  }
193
201
 
194
202
  /* close node */
195
203
  async close () {
196
204
  /* shutdown PortAudio */
197
205
  if (this.io !== null) {
198
- await new Promise<void>((resolve) => {
199
- this.io!.abort(() => {
200
- resolve()
201
- })
202
- })
203
- await new Promise<void>((resolve) => {
204
- this.io!.quit(() => {
205
- resolve()
206
- })
207
- })
206
+ const catchHandler = (err: unknown) => {
207
+ const error = util.ensureError(err)
208
+ if (!error.message.match(/AudioIO Quit expects 1 argument/))
209
+ throw error
210
+ }
211
+ await Promise.race([
212
+ util.timeoutPromise(2 * 1000, "PortAudio abort timeout"),
213
+ new Promise<void>((resolve) => {
214
+ this.io!.abort(() => {
215
+ resolve()
216
+ })
217
+ }).catch(catchHandler)
218
+ ])
219
+ await Promise.race([
220
+ util.timeoutPromise(2 * 1000, "PortAudio quit timeout"),
221
+ new Promise<void>((resolve) => {
222
+ this.io!.quit(() => {
223
+ resolve()
224
+ })
225
+ }).catch(catchHandler)
226
+ ])
208
227
  this.io = null
209
228
  }
210
229
  }
@@ -4,6 +4,13 @@
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
7
+ /* helper function for promise-based timeout */
8
+ export function timeoutPromise (duration: number = 10 * 1000, info = "timeout") {
9
+ return new Promise<void>((resolve, reject) => {
10
+ setTimeout(() => { reject(new Error(info)) }, duration)
11
+ })
12
+ }
13
+
7
14
  /* helper function for retrieving an Error object */
8
15
  export function ensureError (error: unknown, prefix?: string, debug = false): Error {
9
16
  if (error instanceof Error && prefix === undefined && debug === false)