speechflow 1.6.4 → 1.6.6

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 (178) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +28 -3
  3. package/etc/speechflow.yaml +15 -13
  4. package/etc/stx.conf +5 -0
  5. package/package.json +5 -5
  6. package/speechflow-cli/dst/speechflow-main-api.js +3 -7
  7. package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
  8. package/speechflow-cli/dst/speechflow-main-graph.js +1 -1
  9. package/speechflow-cli/dst/speechflow-main.js +6 -0
  10. package/speechflow-cli/dst/speechflow-main.js.map +1 -1
  11. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +1 -21
  12. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  13. package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +1 -1
  14. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +12 -11
  15. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  16. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -21
  17. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
  18. package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +1 -1
  19. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +12 -11
  20. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  21. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +4 -10
  22. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  23. package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +1 -1
  24. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +18 -16
  25. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
  26. package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +1 -1
  27. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +8 -8
  28. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
  29. package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +1 -1
  30. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +70 -60
  31. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  32. package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -1
  33. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +58 -42
  34. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  35. package/speechflow-cli/dst/speechflow-node-a2a-mute.d.ts +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +44 -10
  37. package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
  38. package/speechflow-cli/dst/speechflow-node-a2a-pitch.d.ts +13 -0
  39. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +213 -0
  40. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -0
  41. package/speechflow-cli/dst/speechflow-node-a2a-pitch2-wt.js +149 -0
  42. package/speechflow-cli/dst/speechflow-node-a2a-pitch2-wt.js.map +1 -0
  43. package/speechflow-cli/dst/speechflow-node-a2a-pitch2.d.ts +13 -0
  44. package/speechflow-cli/dst/speechflow-node-a2a-pitch2.js +202 -0
  45. package/speechflow-cli/dst/speechflow-node-a2a-pitch2.js.map +1 -0
  46. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +1 -1
  47. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +13 -11
  48. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
  49. package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +1 -1
  50. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +13 -12
  51. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-a2a-vad.d.ts +1 -1
  53. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +26 -25
  54. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  55. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +35 -7
  56. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  57. package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +1 -1
  58. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +16 -16
  59. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
  60. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +1 -1
  61. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -16
  62. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  63. package/speechflow-cli/dst/speechflow-node-a2t-openai.d.ts +1 -2
  64. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +15 -21
  65. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
  66. package/speechflow-cli/dst/speechflow-node-t2a-amazon.d.ts +1 -2
  67. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +9 -15
  68. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
  69. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.d.ts +1 -2
  70. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +13 -18
  71. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  72. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +0 -1
  73. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +4 -10
  74. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  75. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +3 -3
  76. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
  77. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +2 -2
  78. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  79. package/speechflow-cli/dst/speechflow-node-t2t-format.js +36 -2
  80. package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-google.js +2 -2
  82. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
  83. package/speechflow-cli/dst/speechflow-node-t2t-modify.js +5 -5
  84. package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -1
  85. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +3 -3
  86. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  87. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +2 -2
  88. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-t2t-sentence.d.ts +1 -1
  90. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +13 -13
  91. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  92. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +8 -8
  93. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  94. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +2 -2
  95. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +2 -2
  97. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  98. package/speechflow-cli/dst/speechflow-node-x2x-trace.d.ts +1 -1
  99. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +42 -8
  100. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-node-xio-device.js +6 -4
  102. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  103. package/speechflow-cli/dst/speechflow-node-xio-file.js +19 -18
  104. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  105. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +13 -13
  106. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  107. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +8 -8
  108. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  109. package/speechflow-cli/dst/speechflow-node.js +6 -6
  110. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  111. package/speechflow-cli/dst/speechflow-util-audio.d.ts +1 -0
  112. package/speechflow-cli/dst/speechflow-util-audio.js +22 -1
  113. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
  114. package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
  115. package/speechflow-cli/dst/speechflow-util-error.js +7 -1
  116. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
  117. package/speechflow-cli/dst/speechflow-util-stream.d.ts +2 -1
  118. package/speechflow-cli/dst/speechflow-util-stream.js +23 -3
  119. package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
  120. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  121. package/speechflow-cli/etc/tsconfig.json +1 -0
  122. package/speechflow-cli/package.json +20 -20
  123. package/speechflow-cli/src/speechflow-main-api.ts +6 -13
  124. package/speechflow-cli/src/speechflow-main-graph.ts +1 -1
  125. package/speechflow-cli/src/speechflow-main.ts +4 -0
  126. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +1 -29
  127. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +13 -12
  128. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -29
  129. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +13 -12
  130. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +4 -10
  131. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +19 -17
  132. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +8 -8
  133. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +83 -72
  134. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +66 -46
  135. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +11 -10
  136. package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +221 -0
  137. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +14 -12
  138. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +14 -13
  139. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +26 -25
  140. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +2 -7
  141. package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +16 -16
  142. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +16 -16
  143. package/speechflow-cli/src/speechflow-node-a2t-openai.ts +15 -21
  144. package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +9 -15
  145. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +13 -18
  146. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +4 -10
  147. package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +3 -3
  148. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +2 -2
  149. package/speechflow-cli/src/speechflow-node-t2t-format.ts +3 -2
  150. package/speechflow-cli/src/speechflow-node-t2t-google.ts +2 -2
  151. package/speechflow-cli/src/speechflow-node-t2t-modify.ts +6 -6
  152. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +3 -3
  153. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +2 -2
  154. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +13 -13
  155. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +12 -16
  156. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +2 -2
  157. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -2
  158. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +10 -9
  159. package/speechflow-cli/src/speechflow-node-xio-device.ts +7 -5
  160. package/speechflow-cli/src/speechflow-node-xio-file.ts +20 -19
  161. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +14 -14
  162. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +11 -11
  163. package/speechflow-cli/src/speechflow-node.ts +6 -6
  164. package/speechflow-cli/src/speechflow-util-audio.ts +31 -1
  165. package/speechflow-cli/src/speechflow-util-error.ts +9 -3
  166. package/speechflow-cli/src/speechflow-util-stream.ts +31 -6
  167. package/speechflow-ui-db/dst/index.js +25 -25
  168. package/speechflow-ui-db/package.json +11 -11
  169. package/speechflow-ui-db/src/app.vue +14 -5
  170. package/speechflow-ui-st/dst/index.js +460 -25
  171. package/speechflow-ui-st/package.json +13 -13
  172. package/speechflow-ui-st/src/app.vue +8 -3
  173. package/speechflow-cli/dst/speechflow-util-webaudio-wt.js +0 -124
  174. package/speechflow-cli/dst/speechflow-util-webaudio-wt.js.map +0 -1
  175. package/speechflow-cli/dst/speechflow-util-webaudio.d.ts +0 -13
  176. package/speechflow-cli/dst/speechflow-util-webaudio.js +0 -137
  177. package/speechflow-cli/dst/speechflow-util-webaudio.js.map +0 -1
  178. /package/speechflow-cli/dst/{speechflow-util-webaudio-wt.d.ts → speechflow-node-a2a-pitch2-wt.d.ts} +0 -0
@@ -26,8 +26,7 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
26
26
 
27
27
  /* internal state */
28
28
  private client: PollyClient | null = null
29
- private static speexInitialized = false
30
- private destroyed = false
29
+ private closing = false
31
30
  private resampler: SpeexResampler | null = null
32
31
 
33
32
  /* construct node */
@@ -62,7 +61,7 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
62
61
  /* open node */
63
62
  async open () {
64
63
  /* clear destruction flag */
65
- this.destroyed = false
64
+ this.closing = false
66
65
 
67
66
  /* establish AWS Polly connection */
68
67
  this.client = new PollyClient({
@@ -114,11 +113,6 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
114
113
 
115
114
  /* establish resampler from AWS Polly's maximum 16Khz output
116
115
  (for PCM output) to our standard audio sample rate (48KHz) */
117
- if (!SpeechFlowNodeT2AAmazon.speexInitialized) {
118
- /* at least once initialize resampler */
119
- await SpeexResampler.initPromise
120
- SpeechFlowNodeT2AAmazon.speexInitialized = true
121
- }
122
116
  this.resampler = new SpeexResampler(1, 16000, this.config.audioSampleRate, 7)
123
117
 
124
118
  /* create transform stream and connect it to the AWS Polly API */
@@ -129,7 +123,7 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
129
123
  decodeStrings: false,
130
124
  highWaterMark: 1,
131
125
  transform (chunk: SpeechFlowChunk, encoding, callback) {
132
- if (self.destroyed) {
126
+ if (self.closing) {
133
127
  callback(new Error("stream already destroyed"))
134
128
  return
135
129
  }
@@ -138,7 +132,7 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
138
132
  else if (chunk.payload.length > 0) {
139
133
  self.log("debug", `send data (${chunk.payload.length} bytes): "${chunk.payload}"`)
140
134
  textToSpeech(chunk.payload as string).then((buffer) => {
141
- if (self.destroyed)
135
+ if (self.closing)
142
136
  throw new Error("stream destroyed during processing")
143
137
  const chunkNew = chunk.clone()
144
138
  chunkNew.type = "audio"
@@ -153,7 +147,7 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
153
147
  callback()
154
148
  },
155
149
  final (callback) {
156
- if (self.destroyed) {
150
+ if (self.closing) {
157
151
  callback()
158
152
  return
159
153
  }
@@ -165,8 +159,8 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
165
159
 
166
160
  /* close node */
167
161
  async close () {
168
- /* indicate destruction */
169
- this.destroyed = true
162
+ /* indicate closing */
163
+ this.closing = true
170
164
 
171
165
  /* destroy resampler */
172
166
  if (this.resampler !== null)
@@ -177,9 +171,9 @@ export default class SpeechFlowNodeT2AAmazon extends SpeechFlowNode {
177
171
  this.client.destroy()
178
172
  this.client = null
179
173
  }
180
- /* destroy stream */
174
+ /* shutdown stream */
181
175
  if (this.stream !== null) {
182
- this.stream.destroy()
176
+ await util.destroyStream(this.stream)
183
177
  this.stream = null
184
178
  }
185
179
  }
@@ -14,6 +14,7 @@ import SpeexResampler from "speex-resampler"
14
14
 
15
15
  /* internal dependencies */
16
16
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
17
+ import * as util from "./speechflow-util"
17
18
 
18
19
  /* SpeechFlow node for Elevenlabs text-to-speech conversion */
19
20
  export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
@@ -22,8 +23,7 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
22
23
 
23
24
  /* internal state */
24
25
  private elevenlabs: ElevenLabs.ElevenLabsClient | null = null
25
- private static speexInitialized = false
26
- private destroyed = false
26
+ private closing = false
27
27
  private resampler: SpeexResampler | null = null
28
28
 
29
29
  /* construct node */
@@ -68,7 +68,7 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
68
68
  /* open node */
69
69
  async open () {
70
70
  /* clear destruction flag */
71
- this.destroyed = false
71
+ this.closing = false
72
72
 
73
73
  /* establish ElevenLabs API connection */
74
74
  this.elevenlabs = new ElevenLabs.ElevenLabsClient({
@@ -117,7 +117,7 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
117
117
  modelId: model,
118
118
  languageCode: this.params.language,
119
119
  outputFormat: `pcm_${maxSampleRate}` as ElevenLabs.ElevenLabs.OutputFormat,
120
- seed: 815, /* arbitrary, but fixated by us */
120
+ seed: 815, /* arbitrary, but fixated by us */
121
121
  voiceSettings: {
122
122
  speed: this.params.speed,
123
123
  stability: this.params.stability,
@@ -131,11 +131,6 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
131
131
 
132
132
  /* establish resampler from ElevenLabs's maximum 24Khz
133
133
  output to our standard audio sample rate (48KHz) */
134
- if (!SpeechFlowNodeT2AElevenlabs.speexInitialized) {
135
- /* at least once initialize resampler */
136
- await SpeexResampler.initPromise
137
- SpeechFlowNodeT2AElevenlabs.speexInitialized = true
138
- }
139
134
  this.resampler = new SpeexResampler(1, maxSampleRate, this.config.audioSampleRate, 7)
140
135
 
141
136
  /* create transform stream and connect it to the ElevenLabs API */
@@ -146,7 +141,7 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
146
141
  decodeStrings: false,
147
142
  highWaterMark: 1,
148
143
  transform (chunk: SpeechFlowChunk, encoding, callback) {
149
- if (self.destroyed)
144
+ if (self.closing)
150
145
  callback(new Error("stream already destroyed"))
151
146
  else if (Buffer.isBuffer(chunk.payload))
152
147
  callback(new Error("invalid chunk payload type"))
@@ -163,14 +158,14 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
163
158
  }
164
159
  }
165
160
  try {
166
- if (self.destroyed) {
161
+ if (self.closing) {
167
162
  clearProcessTimeout()
168
163
  callback(new Error("stream destroyed during processing"))
169
164
  return
170
165
  }
171
166
  const stream = await speechStream(chunk.payload as string)
172
167
  const buffer = await getStreamAsBuffer(stream)
173
- if (self.destroyed) {
168
+ if (self.closing) {
174
169
  clearProcessTimeout()
175
170
  callback(new Error("stream destroyed during processing"))
176
171
  return
@@ -186,13 +181,13 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
186
181
  }
187
182
  catch (error) {
188
183
  clearProcessTimeout()
189
- callback(error instanceof Error ? error : new Error("ElevenLabs processing failed"))
184
+ callback(util.ensureError(error, "ElevenLabs processing failed"))
190
185
  }
191
186
  })()
192
187
  }
193
188
  },
194
189
  final (callback) {
195
- if (self.destroyed) {
190
+ if (self.closing) {
196
191
  callback()
197
192
  return
198
193
  }
@@ -204,12 +199,12 @@ export default class SpeechFlowNodeT2AElevenlabs extends SpeechFlowNode {
204
199
 
205
200
  /* close node */
206
201
  async close () {
207
- /* indicate destruction */
208
- this.destroyed = true
202
+ /* indicate closing */
203
+ this.closing = true
209
204
 
210
- /* destroy stream */
205
+ /* shutdown stream */
211
206
  if (this.stream !== null) {
212
- this.stream.destroy()
207
+ await util.destroyStream(this.stream)
213
208
  this.stream = null
214
209
  }
215
210
 
@@ -23,7 +23,6 @@ export default class SpeechFlowNodeT2AKokoro extends SpeechFlowNode {
23
23
  /* internal state */
24
24
  private kokoro: KokoroTTS | null = null
25
25
  private resampler: SpeexResampler | null = null
26
- private static speexInitialized = false
27
26
 
28
27
  /* construct node */
29
28
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -82,11 +81,6 @@ export default class SpeechFlowNodeT2AKokoro extends SpeechFlowNode {
82
81
 
83
82
  /* establish resampler from Kokoro's maximum 24Khz
84
83
  output to our standard audio sample rate (48KHz) */
85
- if (!SpeechFlowNodeT2AKokoro.speexInitialized) {
86
- /* at least once initialize resampler */
87
- SpeechFlowNodeT2AKokoro.speexInitialized = true
88
- await SpeexResampler.initPromise
89
- }
90
84
  this.resampler = new SpeexResampler(1, 24000, this.config.audioSampleRate, 7)
91
85
 
92
86
  /* determine voice for text-to-speech operation */
@@ -125,7 +119,7 @@ export default class SpeechFlowNodeT2AKokoro extends SpeechFlowNode {
125
119
  }
126
120
 
127
121
  /* create transform stream and connect it to the Kokoro API */
128
- const log = (level: string, msg: string) => { this.log(level, msg) }
122
+ const self = this
129
123
  this.stream = new Stream.Transform({
130
124
  writableObjectMode: true,
131
125
  readableObjectMode: true,
@@ -136,7 +130,7 @@ export default class SpeechFlowNodeT2AKokoro extends SpeechFlowNode {
136
130
  callback(new Error("invalid chunk payload type"))
137
131
  else {
138
132
  text2speech(chunk.payload).then((buffer) => {
139
- log("info", `Kokoro: received audio (buffer length: ${buffer.byteLength})`)
133
+ self.log("info", `Kokoro: received audio (buffer length: ${buffer.byteLength})`)
140
134
  chunk = chunk.clone()
141
135
  chunk.type = "audio"
142
136
  chunk.payload = buffer
@@ -156,9 +150,9 @@ export default class SpeechFlowNodeT2AKokoro extends SpeechFlowNode {
156
150
 
157
151
  /* close node */
158
152
  async close () {
159
- /* destroy stream */
153
+ /* shutdown stream */
160
154
  if (this.stream !== null) {
161
- this.stream.destroy()
155
+ await util.destroyStream(this.stream)
162
156
  this.stream = null
163
157
  }
164
158
 
@@ -101,7 +101,7 @@ export default class SpeechFlowNodeT2TAmazon extends SpeechFlowNode {
101
101
  await new Promise((resolve) => setTimeout(resolve, delayMs))
102
102
  }
103
103
  }
104
- throw lastError instanceof Error ? lastError : new Error(String(lastError))
104
+ throw util.ensureError(lastError)
105
105
  }
106
106
 
107
107
  /* establish a duplex stream and connect it to AWS Translate */
@@ -143,9 +143,9 @@ export default class SpeechFlowNodeT2TAmazon extends SpeechFlowNode {
143
143
  this.client = null
144
144
  }
145
145
 
146
- /* close stream */
146
+ /* shutdown stream */
147
147
  if (this.stream !== null) {
148
- this.stream.destroy()
148
+ await util.destroyStream(this.stream)
149
149
  this.stream = null
150
150
  }
151
151
  }
@@ -108,9 +108,9 @@ export default class SpeechFlowNodeT2TDeepL extends SpeechFlowNode {
108
108
 
109
109
  /* close node */
110
110
  async close () {
111
- /* close stream */
111
+ /* shutdown stream */
112
112
  if (this.stream !== null) {
113
- this.stream.destroy()
113
+ await util.destroyStream(this.stream)
114
114
  this.stream = null
115
115
  }
116
116
 
@@ -12,6 +12,7 @@ import wrapText from "wrap-text"
12
12
 
13
13
  /* internal dependencies */
14
14
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
15
+ import * as util from "./speechflow-util"
15
16
 
16
17
  /* SpeechFlow node for text-to-text formatting */
17
18
  export default class SpeechFlowNodeT2TFormat extends SpeechFlowNode {
@@ -71,9 +72,9 @@ export default class SpeechFlowNodeT2TFormat extends SpeechFlowNode {
71
72
 
72
73
  /* close node */
73
74
  async close () {
74
- /* close stream */
75
+ /* shutdown stream */
75
76
  if (this.stream !== null) {
76
- this.stream.destroy()
77
+ await util.destroyStream(this.stream)
77
78
  this.stream = null
78
79
  }
79
80
  }
@@ -118,9 +118,9 @@ export default class SpeechFlowNodeT2TGoogle extends SpeechFlowNode {
118
118
 
119
119
  /* close node */
120
120
  async close () {
121
- /* close stream */
121
+ /* shutdown stream */
122
122
  if (this.stream !== null) {
123
- this.stream.destroy()
123
+ await util.destroyStream(this.stream)
124
124
  this.stream = null
125
125
  }
126
126
 
@@ -26,6 +26,10 @@ export default class SpeechFlowNodeT2TModify extends SpeechFlowNode {
26
26
  replace: { type: "string", val: "" }
27
27
  })
28
28
 
29
+ /* sanity check parameters */
30
+ if (this.params.match === "")
31
+ throw new Error("match parameter cannot be empty")
32
+
29
33
  /* declare node input/output format */
30
34
  this.input = "text"
31
35
  this.output = "text"
@@ -33,10 +37,6 @@ export default class SpeechFlowNodeT2TModify extends SpeechFlowNode {
33
37
 
34
38
  /* open node */
35
39
  async open () {
36
- /* validate parameters */
37
- if (this.params.match === "")
38
- throw new Error("match parameter cannot be empty")
39
-
40
40
  /* compile regex pattern */
41
41
  const regex = util.run("compiling regex pattern",
42
42
  () => new RegExp(this.params.match, "g"))
@@ -75,9 +75,9 @@ export default class SpeechFlowNodeT2TModify extends SpeechFlowNode {
75
75
 
76
76
  /* close node */
77
77
  async close () {
78
- /* close stream */
78
+ /* shutdown stream */
79
79
  if (this.stream !== null) {
80
- this.stream.destroy()
80
+ await util.destroyStream(this.stream)
81
81
  this.stream = null
82
82
  }
83
83
  }
@@ -177,7 +177,7 @@ export default class SpeechFlowNodeT2TOllama extends SpeechFlowNode {
177
177
  models = await this.ollama.list()
178
178
  }
179
179
  catch (err) {
180
- throw new Error(`failed to connect to Ollama API at ${this.params.api}: ${err}`)
180
+ throw new Error(`failed to connect to Ollama API at ${this.params.api}: ${err}`, { cause: err })
181
181
  }
182
182
  const exists = models.models.some((m) => m.name === this.params.model)
183
183
  if (!exists) {
@@ -266,9 +266,9 @@ export default class SpeechFlowNodeT2TOllama extends SpeechFlowNode {
266
266
 
267
267
  /* close node */
268
268
  async close () {
269
- /* close stream */
269
+ /* shutdown stream */
270
270
  if (this.stream !== null) {
271
- this.stream.destroy()
271
+ await util.destroyStream(this.stream)
272
272
  this.stream = null
273
273
  }
274
274
 
@@ -234,9 +234,9 @@ export default class SpeechFlowNodeT2TOpenAI extends SpeechFlowNode {
234
234
 
235
235
  /* close node */
236
236
  async close () {
237
- /* close stream */
237
+ /* shutdown stream */
238
238
  if (this.stream !== null) {
239
- this.stream.destroy()
239
+ await util.destroyStream(this.stream)
240
240
  this.stream = null
241
241
  }
242
242
 
@@ -33,7 +33,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
33
33
  private queueRecv = this.queue.pointerUse("recv")
34
34
  private queueSplit = this.queue.pointerUse("split")
35
35
  private queueSend = this.queue.pointerUse("send")
36
- private destroyed = false
36
+ private closing = false
37
37
  private workingOffTimer: ReturnType<typeof setTimeout> | null = null
38
38
 
39
39
  /* construct node */
@@ -51,12 +51,12 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
51
51
  /* open node */
52
52
  async open () {
53
53
  /* clear destruction flag */
54
- this.destroyed = false
54
+ this.closing = false
55
55
 
56
56
  /* work off queued text frames */
57
57
  let workingOff = false
58
58
  const workOffQueue = async () => {
59
- if (this.destroyed)
59
+ if (this.closing)
60
60
  return
61
61
 
62
62
  /* control working off round */
@@ -70,7 +70,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
70
70
  this.queue.off("write", workOffQueue)
71
71
 
72
72
  /* try to work off one or more chunks */
73
- while (!this.destroyed) {
73
+ while (!this.closing) {
74
74
  const element = this.queueSplit.peek()
75
75
  if (element === undefined)
76
76
  break
@@ -134,7 +134,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
134
134
 
135
135
  /* re-initiate working off round (if still not destroyed) */
136
136
  workingOff = false
137
- if (!this.destroyed) {
137
+ if (!this.closing) {
138
138
  this.workingOffTimer = setTimeout(workOffQueue, 100)
139
139
  this.queue.once("write", workOffQueue)
140
140
  }
@@ -151,7 +151,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
151
151
 
152
152
  /* receive text chunk (writable side of stream) */
153
153
  write (chunk: SpeechFlowChunk, encoding, callback) {
154
- if (self.destroyed)
154
+ if (self.closing)
155
155
  callback(new Error("stream already destroyed"))
156
156
  else if (Buffer.isBuffer(chunk.payload))
157
157
  callback(new Error("expected text input as string chunks"))
@@ -166,7 +166,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
166
166
 
167
167
  /* receive no more text chunks (writable side of stream) */
168
168
  final (callback) {
169
- if (self.destroyed) {
169
+ if (self.closing) {
170
170
  callback()
171
171
  return
172
172
  }
@@ -179,7 +179,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
179
179
  read (_size) {
180
180
  /* flush pending text chunks */
181
181
  const flushPendingChunks = () => {
182
- if (self.destroyed) {
182
+ if (self.closing) {
183
183
  this.push(null)
184
184
  return
185
185
  }
@@ -210,7 +210,7 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
210
210
  self.queue.trim()
211
211
  }
212
212
  }
213
- else if (!self.destroyed)
213
+ else if (!self.closing)
214
214
  self.queue.once("write", flushPendingChunks)
215
215
  }
216
216
  flushPendingChunks()
@@ -220,8 +220,8 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
220
220
 
221
221
  /* close node */
222
222
  async close () {
223
- /* indicate destruction */
224
- this.destroyed = true
223
+ /* indicate closing */
224
+ this.closing = true
225
225
 
226
226
  /* clean up timer */
227
227
  if (this.workingOffTimer !== null) {
@@ -232,9 +232,9 @@ export default class SpeechFlowNodeT2TSentence extends SpeechFlowNode {
232
232
  /* remove any pending event listeners */
233
233
  this.queue.removeAllListeners("write")
234
234
 
235
- /* close stream */
235
+ /* shutdown stream */
236
236
  if (this.stream !== null) {
237
- this.stream.destroy()
237
+ await util.destroyStream(this.stream)
238
238
  this.stream = null
239
239
  }
240
240
  }
@@ -20,13 +20,10 @@ import HAPIWebSocket from "hapi-plugin-websocket"
20
20
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
21
21
  import * as util from "./speechflow-util"
22
22
 
23
- type wsPeerCtx = {
24
- peer: string
25
- }
26
- type wsPeerInfo = {
27
- ctx: wsPeerCtx
28
- ws: WebSocket
29
- req: http.IncomingMessage
23
+ type WSPeerInfo = {
24
+ ctx: Record<string, any>
25
+ ws: WebSocket
26
+ req: http.IncomingMessage
30
27
  }
31
28
 
32
29
  /* SpeechFlow node for subtitle (text-to-text) "translations" */
@@ -160,7 +157,7 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
160
157
  }
161
158
  else if (this.params.mode === "render") {
162
159
  /* establish REST/WebSocket API */
163
- const wsPeers = new Map<string, wsPeerInfo>()
160
+ const wsPeers = new Map<string, WSPeerInfo>()
164
161
  this.hapi = new HAPI.Server({
165
162
  address: this.params.addr,
166
163
  port: this.params.port
@@ -205,19 +202,18 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
205
202
  plugins: {
206
203
  websocket: {
207
204
  autoping: 30 * 1000,
208
- connect: (args: any) => {
209
- const ctx: wsPeerCtx = args.ctx
210
- const ws: WebSocket = args.ws
211
- const req: http.IncomingMessage = args.req
205
+ connect: ({ ctx, ws, req }) => {
212
206
  const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
213
207
  ctx.peer = peer
214
208
  wsPeers.set(peer, { ctx, ws, req })
215
209
  this.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
216
210
  },
217
- disconnect: (args: any) => {
218
- const ctx: wsPeerCtx = args.ctx
211
+ disconnect: ({ ctx, ws }) => {
219
212
  const peer = ctx.peer
220
213
  wsPeers.delete(peer)
214
+ ws.removeAllListeners()
215
+ if (ws.readyState === WebSocket.OPEN)
216
+ ws.close()
221
217
  this.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
222
218
  }
223
219
  }
@@ -261,9 +257,9 @@ export default class SpeechFlowNodeT2TSubtitle extends SpeechFlowNode {
261
257
 
262
258
  /* close node */
263
259
  async close () {
264
- /* close stream */
260
+ /* shutdown stream */
265
261
  if (this.stream !== null) {
266
- this.stream.destroy()
262
+ await util.destroyStream(this.stream)
267
263
  this.stream = null
268
264
  }
269
265
 
@@ -227,9 +227,9 @@ export default class SpeechFlowNodeT2TTransformers extends SpeechFlowNode {
227
227
 
228
228
  /* close node */
229
229
  async close () {
230
- /* close stream */
230
+ /* shutdown stream */
231
231
  if (this.stream !== null) {
232
- this.stream.destroy()
232
+ await util.destroyStream(this.stream)
233
233
  this.stream = null
234
234
  }
235
235
 
@@ -125,9 +125,9 @@ export default class SpeechFlowNodeX2XFilter extends SpeechFlowNode {
125
125
 
126
126
  /* close node */
127
127
  async close () {
128
- /* close stream */
128
+ /* shutdown stream */
129
129
  if (this.stream !== null) {
130
- this.stream.destroy()
130
+ await util.destroyStream(this.stream)
131
131
  this.stream = null
132
132
  }
133
133
  }
@@ -12,6 +12,7 @@ import { Duration } from "luxon"
12
12
 
13
13
  /* internal dependencies */
14
14
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
15
+ import * as util from "./speechflow-util"
15
16
 
16
17
  /* SpeechFlow node for data flow tracing */
17
18
  export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
@@ -19,7 +20,7 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
19
20
  public static name = "x2x-trace"
20
21
 
21
22
  /* internal state */
22
- private destroyed = false
23
+ private closing = false
23
24
 
24
25
  /* construct node */
25
26
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
@@ -56,7 +57,7 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
56
57
  }
57
58
 
58
59
  /* clear destruction flag */
59
- this.destroyed = false
60
+ this.closing = false
60
61
 
61
62
  /* helper functions for formatting */
62
63
  const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
@@ -84,7 +85,7 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
84
85
  highWaterMark: 1,
85
86
  transform (chunk: SpeechFlowChunk, encoding, callback) {
86
87
  let error: Error | undefined
87
- if (self.destroyed) {
88
+ if (self.closing) {
88
89
  callback(new Error("stream already destroyed"))
89
90
  return
90
91
  }
@@ -118,7 +119,7 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
118
119
  }
119
120
  },
120
121
  final (callback) {
121
- if (self.destroyed || self.params.mode === "sink") {
122
+ if (self.closing || self.params.mode === "sink") {
122
123
  callback()
123
124
  return
124
125
  }
@@ -130,13 +131,13 @@ export default class SpeechFlowNodeX2XTrace extends SpeechFlowNode {
130
131
 
131
132
  /* close node */
132
133
  async close () {
133
- /* close stream */
134
+ /* indicate closing */
135
+ this.closing = true
136
+
137
+ /* shutdown stream */
134
138
  if (this.stream !== null) {
135
- this.stream.destroy()
139
+ await util.destroyStream(this.stream)
136
140
  this.stream = null
137
141
  }
138
-
139
- /* indicate destruction */
140
- this.destroyed = true
141
142
  }
142
143
  }
@@ -36,6 +36,10 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
36
36
  chunk: { type: "number", pos: 2, val: 200, match: (n: number) => n >= 10 && n <= 1000 }
37
37
  })
38
38
 
39
+ /* sanity check parameters */
40
+ if (this.params.device === "")
41
+ throw new Error("required parameter \"device\" has to be given")
42
+
39
43
  /* declare node input/output format */
40
44
  if (this.params.mode === "rw") {
41
45
  this.input = "audio"
@@ -115,7 +119,7 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
115
119
 
116
120
  /* convert regular stream into object-mode stream */
117
121
  const wrapper1 = util.createTransformStreamForWritableSide()
118
- const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
122
+ const wrapper2 = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark)
119
123
  this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
120
124
  }
121
125
 
@@ -136,7 +140,7 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
136
140
  this.stream = this.io as unknown as Stream.Readable
137
141
 
138
142
  /* convert regular stream into object-mode stream */
139
- const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero)
143
+ const wrapper = util.createTransformStreamForReadableSide("audio", () => this.timeZero, highwaterMark)
140
144
  this.stream = Stream.compose(this.stream, wrapper)
141
145
  }
142
146
 
@@ -163,9 +167,6 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
163
167
 
164
168
  /* open node */
165
169
  async open () {
166
- if (this.params.device === "")
167
- throw new Error("required parameter \"device\" has to be given")
168
-
169
170
  /* determine device */
170
171
  const device = this.audioDeviceFromURL(this.params.mode, this.params.device)
171
172
 
@@ -193,6 +194,7 @@ export default class SpeechFlowNodeXIODevice extends SpeechFlowNode {
193
194
  /* pass-through PortAudio errors */
194
195
  this.io!.on("error", (err) => {
195
196
  this.emit("error", err)
197
+ this.stream?.emit("error", err)
196
198
  })
197
199
 
198
200
  /* start PortAudio */