speechflow 1.4.5 → 1.5.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.
Files changed (176) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +242 -7
  3. package/etc/claude.md +70 -0
  4. package/etc/speechflow.yaml +13 -11
  5. package/etc/stx.conf +7 -0
  6. package/package.json +7 -6
  7. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.d.ts +1 -0
  8. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +155 -0
  9. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -0
  10. package/speechflow-cli/dst/speechflow-node-a2a-compressor.d.ts +15 -0
  11. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +287 -0
  12. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -0
  13. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.d.ts +1 -0
  14. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +208 -0
  15. package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +1 -0
  16. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +15 -0
  17. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +312 -0
  18. package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +1 -0
  19. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.d.ts +1 -0
  20. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +161 -0
  21. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -0
  22. package/speechflow-cli/dst/speechflow-node-a2a-expander.d.ts +13 -0
  23. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +208 -0
  24. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -0
  25. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +3 -3
  26. package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
  27. package/speechflow-cli/dst/speechflow-node-a2a-filler.d.ts +14 -0
  28. package/speechflow-cli/dst/speechflow-node-a2a-filler.js +233 -0
  29. package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -0
  30. package/speechflow-cli/dst/speechflow-node-a2a-gain.d.ts +12 -0
  31. package/speechflow-cli/dst/speechflow-node-a2a-gain.js +125 -0
  32. package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -0
  33. package/speechflow-cli/dst/speechflow-node-a2a-gender.d.ts +0 -1
  34. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +28 -12
  35. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  36. package/speechflow-cli/dst/speechflow-node-a2a-meter.d.ts +1 -0
  37. package/speechflow-cli/dst/speechflow-node-a2a-meter.js +12 -8
  38. package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
  39. package/speechflow-cli/dst/speechflow-node-a2a-mute.js +2 -1
  40. package/speechflow-cli/dst/speechflow-node-a2a-mute.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.d.ts +1 -0
  42. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js +55 -0
  43. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise-wt.js.map +1 -0
  44. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.d.ts +14 -0
  45. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +184 -0
  46. package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -0
  47. package/speechflow-cli/dst/speechflow-node-a2a-speex.d.ts +14 -0
  48. package/speechflow-cli/dst/speechflow-node-a2a-speex.js +156 -0
  49. package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -0
  50. package/speechflow-cli/dst/speechflow-node-a2a-vad.js +3 -3
  51. package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
  52. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +22 -17
  53. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  54. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +18 -0
  55. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +312 -0
  56. package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +1 -0
  57. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +16 -14
  58. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  59. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +19 -0
  60. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +351 -0
  61. package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +1 -0
  62. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +16 -0
  63. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +204 -0
  64. package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +1 -0
  65. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +19 -14
  66. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  67. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +47 -8
  68. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  69. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +13 -0
  70. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +175 -0
  71. package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +1 -0
  72. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +14 -15
  73. package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
  74. package/speechflow-cli/dst/speechflow-node-t2t-format.js +10 -15
  75. package/speechflow-cli/dst/speechflow-node-t2t-format.js.map +1 -1
  76. package/speechflow-cli/dst/speechflow-node-t2t-google.d.ts +13 -0
  77. package/speechflow-cli/dst/speechflow-node-t2t-google.js +153 -0
  78. package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -0
  79. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +80 -33
  80. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
  81. package/speechflow-cli/dst/speechflow-node-t2t-openai.js +78 -45
  82. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
  83. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +8 -8
  84. package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
  85. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +13 -14
  86. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  87. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +23 -27
  88. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
  89. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
  90. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +50 -15
  91. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  92. package/speechflow-cli/dst/speechflow-node-x2x-trace.js +17 -18
  93. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  94. package/speechflow-cli/dst/speechflow-node-xio-device.js +13 -21
  95. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-node-xio-mqtt.d.ts +1 -0
  97. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +22 -16
  98. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  99. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +19 -19
  100. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-node.d.ts +6 -3
  102. package/speechflow-cli/dst/speechflow-node.js +13 -2
  103. package/speechflow-cli/dst/speechflow-node.js.map +1 -1
  104. package/speechflow-cli/dst/speechflow-utils-audio-wt.d.ts +1 -0
  105. package/speechflow-cli/dst/speechflow-utils-audio-wt.js +124 -0
  106. package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +1 -0
  107. package/speechflow-cli/dst/speechflow-utils-audio.d.ts +13 -0
  108. package/speechflow-cli/dst/speechflow-utils-audio.js +137 -0
  109. package/speechflow-cli/dst/speechflow-utils-audio.js.map +1 -0
  110. package/speechflow-cli/dst/speechflow-utils.d.ts +34 -0
  111. package/speechflow-cli/dst/speechflow-utils.js +256 -35
  112. package/speechflow-cli/dst/speechflow-utils.js.map +1 -1
  113. package/speechflow-cli/dst/speechflow.js +75 -26
  114. package/speechflow-cli/dst/speechflow.js.map +1 -1
  115. package/speechflow-cli/etc/biome.jsonc +2 -1
  116. package/speechflow-cli/etc/oxlint.jsonc +113 -11
  117. package/speechflow-cli/etc/stx.conf +2 -2
  118. package/speechflow-cli/etc/tsconfig.json +1 -1
  119. package/speechflow-cli/package.d/@shiguredo+rnnoise-wasm+2025.1.5.patch +25 -0
  120. package/speechflow-cli/package.json +103 -94
  121. package/speechflow-cli/src/lib.d.ts +24 -0
  122. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +151 -0
  123. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +303 -0
  124. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +158 -0
  125. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +212 -0
  126. package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +3 -3
  127. package/speechflow-cli/src/speechflow-node-a2a-filler.ts +223 -0
  128. package/speechflow-cli/src/speechflow-node-a2a-gain.ts +98 -0
  129. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +31 -17
  130. package/speechflow-cli/src/speechflow-node-a2a-meter.ts +13 -9
  131. package/speechflow-cli/src/speechflow-node-a2a-mute.ts +3 -2
  132. package/speechflow-cli/src/speechflow-node-a2a-rnnoise-wt.ts +62 -0
  133. package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +164 -0
  134. package/speechflow-cli/src/speechflow-node-a2a-speex.ts +137 -0
  135. package/speechflow-cli/src/speechflow-node-a2a-vad.ts +3 -3
  136. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +20 -13
  137. package/speechflow-cli/src/speechflow-node-a2t-awstranscribe.ts +306 -0
  138. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +17 -15
  139. package/speechflow-cli/src/speechflow-node-a2t-openaitranscribe.ts +337 -0
  140. package/speechflow-cli/src/speechflow-node-t2a-awspolly.ts +187 -0
  141. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +19 -14
  142. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +15 -9
  143. package/speechflow-cli/src/speechflow-node-t2t-awstranslate.ts +153 -0
  144. package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +14 -15
  145. package/speechflow-cli/src/speechflow-node-t2t-format.ts +10 -15
  146. package/speechflow-cli/src/speechflow-node-t2t-google.ts +133 -0
  147. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +58 -44
  148. package/speechflow-cli/src/speechflow-node-t2t-openai.ts +59 -58
  149. package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +10 -10
  150. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +18 -18
  151. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +28 -32
  152. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +20 -16
  153. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +20 -19
  154. package/speechflow-cli/src/speechflow-node-xio-device.ts +15 -23
  155. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +23 -16
  156. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +19 -19
  157. package/speechflow-cli/src/speechflow-node.ts +21 -8
  158. package/speechflow-cli/src/speechflow-utils-audio-wt.ts +172 -0
  159. package/speechflow-cli/src/speechflow-utils-audio.ts +147 -0
  160. package/speechflow-cli/src/speechflow-utils.ts +314 -32
  161. package/speechflow-cli/src/speechflow.ts +84 -33
  162. package/speechflow-ui-db/dst/app-font-fa-brands-400.woff2 +0 -0
  163. package/speechflow-ui-db/dst/app-font-fa-regular-400.woff2 +0 -0
  164. package/speechflow-ui-db/dst/app-font-fa-solid-900.woff2 +0 -0
  165. package/speechflow-ui-db/dst/app-font-fa-v4compatibility.woff2 +0 -0
  166. package/speechflow-ui-db/dst/index.css +2 -2
  167. package/speechflow-ui-db/dst/index.js +37 -38
  168. package/speechflow-ui-db/etc/eslint.mjs +0 -1
  169. package/speechflow-ui-db/etc/tsc-client.json +3 -3
  170. package/speechflow-ui-db/package.json +12 -11
  171. package/speechflow-ui-db/src/app.vue +20 -6
  172. package/speechflow-ui-st/dst/index.js +26 -26
  173. package/speechflow-ui-st/etc/eslint.mjs +0 -1
  174. package/speechflow-ui-st/etc/tsc-client.json +3 -3
  175. package/speechflow-ui-st/package.json +12 -11
  176. package/speechflow-ui-st/src/app.vue +5 -12
@@ -7,15 +7,204 @@
7
7
  /* standard dependencies */
8
8
  import Stream from "node:stream"
9
9
  import { EventEmitter } from "node:events"
10
+ import { type, type Type } from "arktype"
10
11
 
11
12
  /* external dependencies */
12
13
  import { DateTime, Duration } from "luxon"
13
- import CBOR from "cbor2"
14
+ import * as CBOR from "cbor2"
14
15
  import * as IntervalTree from "node-interval-tree"
15
16
 
16
17
  /* internal dependencies */
17
18
  import { SpeechFlowChunk } from "./speechflow-node"
18
19
 
20
+ /* helper function for retrieving an Error object */
21
+ export function ensureError (error: unknown, prefix?: string): Error {
22
+ if (error instanceof Error && prefix === undefined)
23
+ return error
24
+ let msg = error instanceof Error ?
25
+ error.message : String(error)
26
+ if (prefix)
27
+ msg = `${prefix}: ${msg}`
28
+ return new Error(msg, { cause: error })
29
+ }
30
+
31
+ /* helper function for retrieving a Promise object */
32
+ export function ensurePromise<T> (arg: T | Promise<T>): Promise<T> {
33
+ if (!(arg instanceof Promise))
34
+ arg = Promise.resolve(arg)
35
+ return arg
36
+ }
37
+
38
+ /* helper function for running the finally code of "run" */
39
+ function runFinally (onfinally?: () => void) {
40
+ if (!onfinally)
41
+ return
42
+ try { onfinally() }
43
+ catch (_arg: unknown) { /* ignored */ }
44
+ }
45
+
46
+ /* helper type for ensuring T contains no Promise */
47
+ type runNoPromise<T> =
48
+ [ T ] extends [ Promise<any> ] ? never : T
49
+
50
+ /* run a synchronous or asynchronous action */
51
+ export function run<T, X extends runNoPromise<T> | never> (
52
+ action: () => X,
53
+ oncatch?: (error: Error) => X | never,
54
+ onfinally?: () => void
55
+ ): X
56
+ export function run<T, X extends runNoPromise<T> | never> (
57
+ description: string,
58
+ action: () => X,
59
+ oncatch?: (error: Error) => X | never,
60
+ onfinally?: () => void
61
+ ): X
62
+ export function run<T, X extends (T | Promise<T>)> (
63
+ action: () => X,
64
+ oncatch?: (error: Error) => X,
65
+ onfinally?: () => void
66
+ ): Promise<T>
67
+ export function run<T, X extends (T | Promise<T>)> (
68
+ description: string,
69
+ action: () => X,
70
+ oncatch?: (error: Error) => X,
71
+ onfinally?: () => void
72
+ ): Promise<T>
73
+ export function run<T> (
74
+ ...args: any[]
75
+ ): T | Promise<T> | never {
76
+ /* support overloaded signatures */
77
+ let description: string | undefined
78
+ let action: () => T | Promise<T> | never
79
+ let oncatch: (error: Error) => T | Promise<T> | never
80
+ let onfinally: () => void
81
+ if (typeof args[0] === "string") {
82
+ description = args[0]
83
+ action = args[1]
84
+ oncatch = args[2]
85
+ onfinally = args[3]
86
+ }
87
+ else {
88
+ action = args[0]
89
+ oncatch = args[1]
90
+ onfinally = args[2]
91
+ }
92
+
93
+ /* perform the action */
94
+ let result: T | Promise<T>
95
+ try {
96
+ result = action()
97
+ }
98
+ catch (arg: unknown) {
99
+ /* synchronous case (error branch) */
100
+ let error = ensureError(arg, description)
101
+ if (oncatch) {
102
+ try {
103
+ result = oncatch(error)
104
+ }
105
+ catch (arg: unknown) {
106
+ error = ensureError(arg, description)
107
+ runFinally(onfinally)
108
+ throw error
109
+ }
110
+ runFinally(onfinally)
111
+ return result
112
+ }
113
+ runFinally(onfinally)
114
+ throw error
115
+ }
116
+ if (result instanceof Promise) {
117
+ /* asynchronous case (result or error branch) */
118
+ return result.catch((arg: unknown) => {
119
+ /* asynchronous case (error branch) */
120
+ let error = ensureError(arg, description)
121
+ if (oncatch) {
122
+ try {
123
+ return ensurePromise(oncatch(error))
124
+ }
125
+ catch (arg: unknown) {
126
+ error = ensureError(arg, description)
127
+ return Promise.reject(error)
128
+ }
129
+ }
130
+ return Promise.reject(error)
131
+ }).finally(() => {
132
+ /* asynchronous case (result and error branch) */
133
+ runFinally(onfinally)
134
+ })
135
+ }
136
+ else {
137
+ /* synchronous case (result branch) */
138
+ runFinally(onfinally)
139
+ return result
140
+ }
141
+ }
142
+
143
+ /* run a synchronous or asynchronous action */
144
+ /* eslint @typescript-eslint/unified-signatures: off */
145
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
146
+ action: F,
147
+ oncatch?: (error: Error) => X | never,
148
+ onfinally?: () => void
149
+ ): F
150
+ export function runner<T, X extends runNoPromise<T> | never, F extends (...args: any[]) => X> (
151
+ description: string,
152
+ action: F,
153
+ oncatch?: (error: Error) => X | never,
154
+ onfinally?: () => void
155
+ ): F
156
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
157
+ action: F,
158
+ oncatch?: (error: Error) => X,
159
+ onfinally?: () => void
160
+ ): F
161
+ export function runner<T, X extends (T | Promise<T>), F extends (...args: any[]) => Promise<T>> (
162
+ description: string,
163
+ action: F,
164
+ oncatch?: (error: Error) => X,
165
+ onfinally?: () => void
166
+ ): F
167
+ export function runner<T> (
168
+ ...args: any[]
169
+ ): (...args: any[]) => T | Promise<T> | never {
170
+ /* support overloaded signatures */
171
+ let description: string | undefined
172
+ let action: (...args: any[]) => T | Promise<T> | never
173
+ let oncatch: (error: Error) => T | Promise<T> | never
174
+ let onfinally: () => void
175
+ if (typeof args[0] === "string") {
176
+ description = args[0]
177
+ action = args[1]
178
+ oncatch = args[2]
179
+ onfinally = args[3]
180
+ }
181
+ else {
182
+ action = args[0]
183
+ oncatch = args[1]
184
+ onfinally = args[2]
185
+ }
186
+
187
+ /* wrap the "run" operation on "action" into function
188
+ which exposes the signature of "action" */
189
+ return (...args: any[]) => {
190
+ if (description)
191
+ return run(description, () => action(...args), oncatch, onfinally)
192
+ else
193
+ return run(() => action(...args), oncatch, onfinally)
194
+ }
195
+ }
196
+
197
+ /* import an object with parsing and strict error handling */
198
+ export function importObject<T>(name: string, arg: object | string, validator: Type<T, {}>): T {
199
+ const obj: object = typeof arg === "string" ?
200
+ run(`${name}: parsing JSON`, () => JSON.parse(arg)) :
201
+ arg
202
+ const result = validator(obj)
203
+ if (result instanceof type.errors)
204
+ throw new Error(`${name}: validation: ${result.summary}`)
205
+ return result as T
206
+ }
207
+
19
208
  /* calculate duration of an audio buffer */
20
209
  export function audioBufferDuration (
21
210
  buffer: Buffer,
@@ -86,6 +275,31 @@ export function convertF32ToBuf (arr: Float32Array) {
86
275
  return Buffer.from(int16Array.buffer)
87
276
  }
88
277
 
278
+ /* helper function: convert Buffer in PCM/I16 to Int16Array */
279
+ export function convertBufToI16 (buf: Buffer, littleEndian = true) {
280
+ if (buf.length % 2 !== 0)
281
+ throw new Error("buffer length must be even for 16-bit samples")
282
+ const dataView = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
283
+ const arr = new Int16Array(buf.length / 2)
284
+ for (let i = 0; i < buf.length / 2; i++)
285
+ arr[i] = dataView.getInt16(i * 2, littleEndian)
286
+ return arr
287
+ }
288
+
289
+ /* helper function: convert In16Array in PCM/I16 to Buffer */
290
+ export function convertI16ToBuf (arr: Int16Array, littleEndian = true) {
291
+ if (arr.length === 0)
292
+ return Buffer.alloc(0)
293
+ const buf = Buffer.allocUnsafe(arr.length * 2)
294
+ for (let i = 0; i < arr.length; i++) {
295
+ if (littleEndian)
296
+ buf.writeInt16LE(arr[i], i * 2)
297
+ else
298
+ buf.writeInt16BE(arr[i], i * 2)
299
+ }
300
+ return buf
301
+ }
302
+
89
303
  /* create a Duplex/Transform stream which has
90
304
  object-mode on Writable side and buffer/string-mode on Readable side */
91
305
  export function createTransformStreamForWritableSide () {
@@ -209,25 +423,16 @@ export class SingleQueue<T> extends EventEmitter {
209
423
  }
210
424
  read () {
211
425
  return new Promise<T>((resolve, reject) => {
212
- const consume = () => {
213
- if (this.queue.length > 0)
214
- return this.queue.pop()!
426
+ const consume = () =>
427
+ this.queue.length > 0 ? this.queue.pop()! : null
428
+ const tryToConsume = () => {
429
+ const item = consume()
430
+ if (item !== null)
431
+ resolve(item)
215
432
  else
216
- return null
217
- }
218
- let item = consume()
219
- if (item !== null)
220
- resolve(item)
221
- else {
222
- const tryToConsume = () => {
223
- item = consume()
224
- if (item !== null)
225
- resolve(item)
226
- else
227
- this.once("dequeue", tryToConsume)
228
- }
229
- this.once("dequeue", tryToConsume)
433
+ this.once("dequeue", tryToConsume)
230
434
  }
435
+ tryToConsume()
231
436
  })
232
437
  }
233
438
  }
@@ -256,22 +461,16 @@ export class DoubleQueue<T0, T1> extends EventEmitter {
256
461
  const item1 = this.queue1.pop() as T1
257
462
  return [ item0, item1 ]
258
463
  }
259
- else
260
- return null
464
+ return null
261
465
  }
262
- let items = consume()
263
- if (items !== null)
264
- resolve(items)
265
- else {
266
- const tryToConsume = () => {
267
- items = consume()
268
- if (items !== null)
269
- resolve(items)
270
- else
271
- this.once("dequeue", tryToConsume)
272
- }
273
- this.once("dequeue", tryToConsume)
466
+ const tryToConsume = () => {
467
+ const items = consume()
468
+ if (items !== null)
469
+ resolve(items)
470
+ else
471
+ this.once("dequeue", tryToConsume)
274
472
  }
473
+ tryToConsume()
275
474
  })
276
475
  }
277
476
  }
@@ -520,3 +719,86 @@ export class TimeStore<T> extends EventEmitter {
520
719
  this.tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
521
720
  }
522
721
  }
722
+
723
+ /* asynchronous queue */
724
+ export class AsyncQueue<T> {
725
+ private queue: Array<T | null> = []
726
+ private resolvers: ((v: T | null) => void)[] = []
727
+ write (v: T | null) {
728
+ const resolve = this.resolvers.shift()
729
+ if (resolve)
730
+ resolve(v)
731
+ else
732
+ this.queue.push(v)
733
+ }
734
+ async read () {
735
+ if (this.queue.length > 0)
736
+ return this.queue.shift()!
737
+ else
738
+ return new Promise<T | null>((resolve) => this.resolvers.push(resolve))
739
+ }
740
+ destroy () {
741
+ for (const resolve of this.resolvers)
742
+ resolve(null)
743
+ this.resolvers = []
744
+ this.queue = []
745
+ }
746
+ }
747
+
748
+ /* process Int16Array in fixed-size segments */
749
+ export async function processInt16ArrayInSegments (
750
+ data: Int16Array<ArrayBuffer>,
751
+ segmentSize: number,
752
+ processor: (segment: Int16Array<ArrayBuffer>) => Promise<Int16Array<ArrayBuffer>>
753
+ ): Promise<Int16Array<ArrayBuffer>> {
754
+ /* process full segments */
755
+ let i = 0
756
+ while ((i + segmentSize) <= data.length) {
757
+ const segment = data.slice(i, i + segmentSize)
758
+ const result = await processor(segment)
759
+ data.set(result, i)
760
+ i += segmentSize
761
+ }
762
+
763
+ /* process final partial segment if it exists */
764
+ if (i < data.length) {
765
+ const len = data.length - i
766
+ const segment = new Int16Array(segmentSize)
767
+ segment.set(data.slice(i), 0)
768
+ segment.fill(0, len, segmentSize)
769
+ const result = await processor(segment)
770
+ data.set(result.slice(0, len), i)
771
+ }
772
+ return data
773
+ }
774
+
775
+ /* cached regular expression class */
776
+ export class CachedRegExp {
777
+ private cache = new Map<string, RegExp>()
778
+ compile (pattern: string): RegExp | null {
779
+ if (this.cache.has(pattern))
780
+ return this.cache.get(pattern)!
781
+ try {
782
+ const regex = new RegExp(pattern)
783
+ this.cache.set(pattern, regex)
784
+ return regex
785
+ }
786
+ catch (_error) {
787
+ return null
788
+ }
789
+ }
790
+ clear (): void {
791
+ this.cache.clear()
792
+ }
793
+ size (): number {
794
+ return this.cache.size
795
+ }
796
+ }
797
+
798
+ /* helper functions for linear/decibel conversions */
799
+ export function lin2dB (x: number): number {
800
+ return 20 * Math.log10(Math.max(x, 1e-12))
801
+ }
802
+ export function dB2lin (db: number): number {
803
+ return Math.pow(10, db / 20)
804
+ }
@@ -15,6 +15,7 @@ import Inert from "@hapi/inert"
15
15
  import WebSocket from "ws"
16
16
  import HAPIWebSocket from "hapi-plugin-websocket"
17
17
  import HAPIHeader from "hapi-plugin-header"
18
+ import OSC from "osc-js"
18
19
 
19
20
  /* external dependencies */
20
21
  import { DateTime } from "luxon"
@@ -33,6 +34,7 @@ import chalk from "chalk"
33
34
 
34
35
  /* internal dependencies */
35
36
  import SpeechFlowNode from "./speechflow-node"
37
+ import * as utils from "./speechflow-utils"
36
38
  import pkg from "../../package.json"
37
39
 
38
40
  /* central CLI context */
@@ -70,6 +72,7 @@ let debug = false
70
72
  "[-p|--port <tcp-port>] " +
71
73
  "[-C|--cache <directory>] " +
72
74
  "[-d|--dashboard <type>:<id>:<name>[,...]] " +
75
+ "[-o|--osc <ip-address>:<udp-port> " +
73
76
  "[-e|--expression <expression>] " +
74
77
  "[-f|--file <file>] " +
75
78
  "[-c|--config <id>@<yaml-config-file>] " +
@@ -137,6 +140,15 @@ let debug = false
137
140
  default: "",
138
141
  describe: "list of dashboard block types and names"
139
142
  })
143
+ .option("o", {
144
+ alias: "osc",
145
+ type: "string",
146
+ array: false,
147
+ coerce,
148
+ nargs: 1,
149
+ default: "",
150
+ describe: "OSC/UDP endpoint to send dashboard information"
151
+ })
140
152
  .option("e", {
141
153
  alias: "expression",
142
154
  type: "string",
@@ -247,16 +259,7 @@ let debug = false
247
259
  throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
248
260
  const [ , id, file ] = m
249
261
  const yaml = await cli.input(file, { encoding: "utf8" })
250
- let obj: any
251
- try {
252
- obj = jsYAML.load(yaml)
253
- }
254
- catch (err) {
255
- if (err instanceof Error)
256
- throw new Error(`failed to parse YAML configuration: ${err.message}`)
257
- else
258
- throw new Error(`failed to parse YAML configuration: ${err}`)
259
- }
262
+ const obj: any = utils.run("parsing YAML configuration", () => jsYAML.load(yaml))
260
263
  if (obj[id] === undefined)
261
264
  throw new Error(`no such id "${id}" found in configuration file "${file}"`)
262
265
  config = obj[id] as string
@@ -267,17 +270,28 @@ let debug = false
267
270
 
268
271
  /* load internal SpeechFlow nodes */
269
272
  const pkgsI = [
273
+ "./speechflow-node-a2a-compressor.js",
274
+ "./speechflow-node-a2a-expander.js",
270
275
  "./speechflow-node-a2a-ffmpeg.js",
276
+ "./speechflow-node-a2a-filler.js",
277
+ "./speechflow-node-a2a-gain.js",
271
278
  "./speechflow-node-a2a-gender.js",
272
279
  "./speechflow-node-a2a-meter.js",
273
280
  "./speechflow-node-a2a-mute.js",
281
+ "./speechflow-node-a2a-rnnoise.js",
282
+ "./speechflow-node-a2a-speex.js",
274
283
  "./speechflow-node-a2a-vad.js",
275
284
  "./speechflow-node-a2a-wav.js",
285
+ "./speechflow-node-a2t-awstranscribe.js",
276
286
  "./speechflow-node-a2t-deepgram.js",
287
+ "./speechflow-node-a2t-openaitranscribe.js",
288
+ "./speechflow-node-t2a-awspolly.js",
277
289
  "./speechflow-node-t2a-elevenlabs.js",
278
290
  "./speechflow-node-t2a-kokoro.js",
291
+ "./speechflow-node-t2t-awstranslate.js",
279
292
  "./speechflow-node-t2t-deepl.js",
280
293
  "./speechflow-node-t2t-format.js",
294
+ "./speechflow-node-t2t-google.js",
281
295
  "./speechflow-node-t2t-ollama.js",
282
296
  "./speechflow-node-t2t-openai.js",
283
297
  "./speechflow-node-t2t-sentence.js",
@@ -329,6 +343,19 @@ let debug = false
329
343
  cacheDir: args.C
330
344
  }
331
345
 
346
+ /* provide access to internal communication busses */
347
+ const busses = new Map<string, EventEmitter>()
348
+ const accessBus = (name: string): EventEmitter => {
349
+ let bus: EventEmitter
350
+ if (busses.has(name))
351
+ bus = busses.get(name)!
352
+ else {
353
+ bus = new EventEmitter()
354
+ busses.set(name, bus)
355
+ }
356
+ return bus
357
+ }
358
+
332
359
  /* handle one-time status query of nodes */
333
360
  if (args.S) {
334
361
  const table = new Table({
@@ -344,6 +371,7 @@ let debug = false
344
371
  for (const name of Object.keys(nodes)) {
345
372
  cli!.log("info", `gathering status of node <${name}>`)
346
373
  const node = new nodes[name](name, cfg, {}, [])
374
+ node._accessBus = accessBus
347
375
  const status = await Promise.race<{ [ key: string ]: string | number }>([
348
376
  node.status(),
349
377
  new Promise<never>((resolve, reject) => setTimeout(() =>
@@ -403,6 +431,7 @@ let debug = false
403
431
  nodeNums.set(NodeClass, ++num)
404
432
  const name = num === 1 ? id : `${id}:${num}`
405
433
  node = new NodeClass(name, cfg, opts, args)
434
+ node._accessBus = accessBus
406
435
  }
407
436
  catch (err) {
408
437
  /* fatal error */
@@ -412,13 +441,14 @@ let debug = false
412
441
  cli!.log("error", `creation of node <${id}> failed: ${err}`)
413
442
  process.exit(1)
414
443
  }
415
- const params = Object.keys(node.params)
444
+ const params = Object.keys(node!.params)
445
+ .filter((key) => !key.match(/key/))
416
446
  .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
417
- cli!.log("info", `create node <${node.id}> (${params})`)
418
- graphNodes.add(node)
419
- return node
447
+ cli!.log("info", `create node <${node!.id}> (${params})`)
448
+ graphNodes.add(node!)
449
+ return node!
420
450
  },
421
- connectNode (node1: SpeechFlowNode, node2: SpeechFlowNode) {
451
+ connectNodes (node1: SpeechFlowNode, node2: SpeechFlowNode) {
422
452
  cli!.log("info", `connect node <${node1.id}> to node <${node2.id}>`)
423
453
  node1.connect(node2)
424
454
  }
@@ -481,7 +511,7 @@ let debug = false
481
511
  new Promise<never>((resolve, reject) => setTimeout(() =>
482
512
  reject(new Error("timeout")), 10 * 1000))
483
513
  ]).catch((err: Error) => {
484
- cli!.log("error", `[${node.id}]: failed to open node <${node.id}>: ${err.message}`)
514
+ cli!.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`)
485
515
  throw new Error(`failed to open node <${node.id}>: ${err.message}`)
486
516
  })
487
517
  }
@@ -631,8 +661,7 @@ let debug = false
631
661
  hapi.route({
632
662
  method: "GET",
633
663
  path: "/api/{req}/{node}/{params*}",
634
- options: {
635
- },
664
+ options: {},
636
665
  handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
637
666
  const peer = request.info.remoteAddress
638
667
  const params = request.params.params as string ?? ""
@@ -644,11 +673,9 @@ let debug = false
644
673
  args: params.split("/").filter((seg) => seg !== "")
645
674
  }
646
675
  cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
647
- return consumeExternalRequest(req).then(() => {
648
- return h.response({ response: "OK" }).code(200)
649
- }).catch((err) => {
650
- return h.response({ response: "ERROR", data: err.message }).code(417)
651
- })
676
+ return consumeExternalRequest(req)
677
+ .then(() => h.response({ response: "OK" }).code(200))
678
+ .catch((error: unknown) => h.response({ response: "ERROR", data: utils.ensureError(error).message }).code(417))
652
679
  }
653
680
  })
654
681
  hapi.route({
@@ -691,11 +718,9 @@ let debug = false
691
718
  if (req instanceof arktype.type.errors)
692
719
  return h.response({ response: "ERROR", data: `invalid request: ${req.summary}` }).code(417)
693
720
  cli!.log("info", `HAPI: peer ${peer}: POST: ${JSON.stringify(req)}`)
694
- return consumeExternalRequest(req).then(() => {
695
- return h.response({ response: "OK" }).code(200)
696
- }).catch((err: Error) => {
697
- return h.response({ response: "ERROR", data: err.message }).code(417)
698
- })
721
+ return consumeExternalRequest(req)
722
+ .then(() => h.response({ response: "OK" }).code(200))
723
+ .catch((err: Error) => h.response({ response: "ERROR", data: err.message }).code(417))
699
724
  }
700
725
  })
701
726
  await hapi.start()
@@ -713,10 +738,25 @@ let debug = false
713
738
  })
714
739
  }
715
740
 
716
- /* hook for dashboardInfo method of nodes */
741
+ /* establish OSC event emission */
742
+ let sendOSC: (url: string, ...args: any[]) => void
743
+ if (args.o !== "") {
744
+ const osc = new OSC({ plugin: new OSC.DatagramPlugin({ type: "udp4" }) })
745
+ const m = args.o.match(/^(.+?):(\d+)$/)
746
+ if (m === null)
747
+ throw new Error("invalid OSC/UDP endpoint (expected <ip-adress>:<udp-port>)")
748
+ const host = m[1]
749
+ const port = m[2]
750
+ sendOSC = (url: string, ...args: any[]) => {
751
+ const msg = new OSC.Message(url, ...args)
752
+ osc.send(msg, { host, port })
753
+ }
754
+ }
755
+
756
+ /* hook for send-dashboard method of nodes */
717
757
  for (const node of graphNodes) {
718
- node.on("dashboard-info", (info: {
719
- type: string,
758
+ node.on("send-dashboard", (info: {
759
+ type: "audio" | "text",
720
760
  id: string,
721
761
  kind: "final" | "intermediate",
722
762
  value: string | number
@@ -730,6 +770,17 @@ let debug = false
730
770
  cli!.log("debug", `HAPI: dashboard peer ${peer}: send ${data}`)
731
771
  info.ws.send(data)
732
772
  }
773
+ for (const node of graphNodes) {
774
+ Promise.race<void>([
775
+ node.receiveDashboard(info.type, info.id, info.kind, info.value),
776
+ new Promise<never>((resolve, reject) => setTimeout(() =>
777
+ reject(new Error("timeout")), 10 * 1000))
778
+ ]).catch((err: Error) => {
779
+ cli!.log("warning", `sending dashboard info to node <${node.id}> failed: ${err.message}`)
780
+ })
781
+ }
782
+ if (args.o !== "")
783
+ sendOSC("/speechflow/dashboard", info.type, info.id, info.kind, info.value)
733
784
  })
734
785
  }
735
786
 
@@ -777,8 +828,8 @@ let debug = false
777
828
  Promise.all(closePromises),
778
829
  new Promise((resolve, reject) =>
779
830
  setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
780
- ]).catch((err) => {
781
- cli!.log("warning", `HAPI: WebSockets failed to close: ${err}`)
831
+ ]).catch((error: unknown) => {
832
+ cli!.log("warning", `HAPI: WebSockets failed to close: ${utils.ensureError(error).message}`)
782
833
  })
783
834
  wsPeers.clear()
784
835
  }