speechflow 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +425 -146
  3. package/etc/claude.md +5 -5
  4. package/etc/speechflow.yaml +2 -2
  5. package/package.json +3 -3
  6. package/speechflow-cli/dst/speechflow-main-api.js +6 -5
  7. package/speechflow-cli/dst/speechflow-main-api.js.map +1 -1
  8. package/speechflow-cli/dst/speechflow-main-graph.d.ts +1 -0
  9. package/speechflow-cli/dst/speechflow-main-graph.js +35 -13
  10. package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -1
  11. package/speechflow-cli/dst/speechflow-main-status.js +3 -7
  12. package/speechflow-cli/dst/speechflow-main-status.js.map +1 -1
  13. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +3 -0
  14. package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
  15. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +4 -2
  16. package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
  17. package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +1 -1
  18. package/speechflow-cli/dst/speechflow-node-a2a-expander.js +4 -2
  19. package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
  20. package/speechflow-cli/dst/speechflow-node-a2a-gender.js +2 -2
  21. package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
  22. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js +1 -2
  23. package/speechflow-cli/dst/speechflow-node-a2a-pitch.js.map +1 -1
  24. package/speechflow-cli/dst/speechflow-node-a2a-wav.js +32 -5
  25. package/speechflow-cli/dst/speechflow-node-a2a-wav.js.map +1 -1
  26. package/speechflow-cli/dst/speechflow-node-a2t-amazon.d.ts +0 -1
  27. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +1 -6
  28. package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
  29. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.d.ts +0 -1
  30. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +9 -9
  31. package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  32. package/speechflow-cli/dst/speechflow-node-a2t-google.d.ts +17 -0
  33. package/speechflow-cli/dst/speechflow-node-a2t-google.js +320 -0
  34. package/speechflow-cli/dst/speechflow-node-a2t-google.js.map +1 -0
  35. package/speechflow-cli/dst/speechflow-node-a2t-openai.js +6 -4
  36. package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
  37. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +6 -11
  38. package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
  39. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js +6 -5
  40. package/speechflow-cli/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  41. package/speechflow-cli/dst/speechflow-node-t2a-google.d.ts +15 -0
  42. package/speechflow-cli/dst/speechflow-node-t2a-google.js +218 -0
  43. package/speechflow-cli/dst/speechflow-node-t2a-google.js.map +1 -0
  44. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.d.ts +2 -0
  45. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +19 -6
  46. package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  47. package/speechflow-cli/dst/speechflow-node-t2a-openai.d.ts +15 -0
  48. package/speechflow-cli/dst/speechflow-node-t2a-openai.js +195 -0
  49. package/speechflow-cli/dst/speechflow-node-t2a-openai.js.map +1 -0
  50. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.d.ts +17 -0
  51. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js +608 -0
  52. package/speechflow-cli/dst/speechflow-node-t2a-supertonic.js.map +1 -0
  53. package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
  54. package/speechflow-cli/dst/{speechflow-node-t2t-transformers.d.ts → speechflow-node-t2t-opus.d.ts} +1 -3
  55. package/speechflow-cli/dst/speechflow-node-t2t-opus.js +159 -0
  56. package/speechflow-cli/dst/speechflow-node-t2t-opus.js.map +1 -0
  57. package/speechflow-cli/dst/speechflow-node-t2t-profanity.d.ts +11 -0
  58. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js +118 -0
  59. package/speechflow-cli/dst/speechflow-node-t2t-profanity.js.map +1 -0
  60. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.d.ts +13 -0
  61. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js +220 -0
  62. package/speechflow-cli/dst/speechflow-node-t2t-punctuation.js.map +1 -0
  63. package/speechflow-cli/dst/{speechflow-node-t2t-openai.d.ts → speechflow-node-t2t-spellcheck.d.ts} +2 -2
  64. package/speechflow-cli/dst/{speechflow-node-t2t-openai.js → speechflow-node-t2t-spellcheck.js} +47 -99
  65. package/speechflow-cli/dst/speechflow-node-t2t-spellcheck.js.map +1 -0
  66. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +3 -6
  67. package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  68. package/speechflow-cli/dst/speechflow-node-t2t-summary.d.ts +16 -0
  69. package/speechflow-cli/dst/speechflow-node-t2t-summary.js +241 -0
  70. package/speechflow-cli/dst/speechflow-node-t2t-summary.js.map +1 -0
  71. package/speechflow-cli/dst/{speechflow-node-t2t-ollama.d.ts → speechflow-node-t2t-translate.d.ts} +2 -2
  72. package/speechflow-cli/dst/{speechflow-node-t2t-transformers.js → speechflow-node-t2t-translate.js} +53 -115
  73. package/speechflow-cli/dst/speechflow-node-t2t-translate.js.map +1 -0
  74. package/speechflow-cli/dst/speechflow-node-x2x-filter.d.ts +1 -0
  75. package/speechflow-cli/dst/speechflow-node-x2x-filter.js +10 -0
  76. package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
  77. package/speechflow-cli/dst/speechflow-node-x2x-trace.js.map +1 -1
  78. package/speechflow-cli/dst/speechflow-node-xio-device.js +3 -3
  79. package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
  80. package/speechflow-cli/dst/speechflow-node-xio-exec.d.ts +12 -0
  81. package/speechflow-cli/dst/speechflow-node-xio-exec.js +223 -0
  82. package/speechflow-cli/dst/speechflow-node-xio-exec.js.map +1 -0
  83. package/speechflow-cli/dst/speechflow-node-xio-file.d.ts +1 -0
  84. package/speechflow-cli/dst/speechflow-node-xio-file.js +80 -67
  85. package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
  86. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +2 -1
  87. package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
  88. package/speechflow-cli/dst/speechflow-node-xio-vban.d.ts +17 -0
  89. package/speechflow-cli/dst/speechflow-node-xio-vban.js +330 -0
  90. package/speechflow-cli/dst/speechflow-node-xio-vban.js.map +1 -0
  91. package/speechflow-cli/dst/speechflow-node-xio-webrtc.d.ts +39 -0
  92. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js +500 -0
  93. package/speechflow-cli/dst/speechflow-node-xio-webrtc.js.map +1 -0
  94. package/speechflow-cli/dst/speechflow-node-xio-websocket.js +2 -1
  95. package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
  96. package/speechflow-cli/dst/speechflow-util-audio.js +5 -6
  97. package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -1
  98. package/speechflow-cli/dst/speechflow-util-error.d.ts +1 -1
  99. package/speechflow-cli/dst/speechflow-util-error.js +5 -7
  100. package/speechflow-cli/dst/speechflow-util-error.js.map +1 -1
  101. package/speechflow-cli/dst/speechflow-util-llm.d.ts +35 -0
  102. package/speechflow-cli/dst/speechflow-util-llm.js +363 -0
  103. package/speechflow-cli/dst/speechflow-util-llm.js.map +1 -0
  104. package/speechflow-cli/dst/speechflow-util-misc.d.ts +1 -1
  105. package/speechflow-cli/dst/speechflow-util-misc.js +4 -4
  106. package/speechflow-cli/dst/speechflow-util-misc.js.map +1 -1
  107. package/speechflow-cli/dst/speechflow-util-queue.js +3 -3
  108. package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -1
  109. package/speechflow-cli/dst/speechflow-util-stream.js +4 -2
  110. package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -1
  111. package/speechflow-cli/dst/speechflow-util.d.ts +1 -0
  112. package/speechflow-cli/dst/speechflow-util.js +1 -0
  113. package/speechflow-cli/dst/speechflow-util.js.map +1 -1
  114. package/speechflow-cli/etc/oxlint.jsonc +2 -1
  115. package/speechflow-cli/package.json +34 -17
  116. package/speechflow-cli/src/lib.d.ts +5 -0
  117. package/speechflow-cli/src/speechflow-main-api.ts +6 -5
  118. package/speechflow-cli/src/speechflow-main-graph.ts +40 -13
  119. package/speechflow-cli/src/speechflow-main-status.ts +4 -8
  120. package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -0
  121. package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +4 -2
  122. package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +1 -1
  123. package/speechflow-cli/src/speechflow-node-a2a-expander.ts +4 -2
  124. package/speechflow-cli/src/speechflow-node-a2a-gender.ts +2 -2
  125. package/speechflow-cli/src/speechflow-node-a2a-pitch.ts +1 -2
  126. package/speechflow-cli/src/speechflow-node-a2a-wav.ts +33 -6
  127. package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +6 -11
  128. package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +13 -12
  129. package/speechflow-cli/src/speechflow-node-a2t-google.ts +322 -0
  130. package/speechflow-cli/src/speechflow-node-a2t-openai.ts +8 -4
  131. package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +7 -11
  132. package/speechflow-cli/src/speechflow-node-t2a-elevenlabs.ts +6 -5
  133. package/speechflow-cli/src/speechflow-node-t2a-google.ts +206 -0
  134. package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +22 -6
  135. package/speechflow-cli/src/speechflow-node-t2a-openai.ts +179 -0
  136. package/speechflow-cli/src/speechflow-node-t2a-supertonic.ts +701 -0
  137. package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -1
  138. package/speechflow-cli/src/speechflow-node-t2t-opus.ts +136 -0
  139. package/speechflow-cli/src/speechflow-node-t2t-profanity.ts +93 -0
  140. package/speechflow-cli/src/speechflow-node-t2t-punctuation.ts +201 -0
  141. package/speechflow-cli/src/{speechflow-node-t2t-openai.ts → speechflow-node-t2t-spellcheck.ts} +48 -107
  142. package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +3 -6
  143. package/speechflow-cli/src/speechflow-node-t2t-summary.ts +229 -0
  144. package/speechflow-cli/src/speechflow-node-t2t-translate.ts +181 -0
  145. package/speechflow-cli/src/speechflow-node-x2x-filter.ts +16 -3
  146. package/speechflow-cli/src/speechflow-node-x2x-trace.ts +3 -3
  147. package/speechflow-cli/src/speechflow-node-xio-device.ts +4 -7
  148. package/speechflow-cli/src/speechflow-node-xio-exec.ts +210 -0
  149. package/speechflow-cli/src/speechflow-node-xio-file.ts +93 -80
  150. package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +3 -2
  151. package/speechflow-cli/src/speechflow-node-xio-vban.ts +325 -0
  152. package/speechflow-cli/src/speechflow-node-xio-webrtc.ts +533 -0
  153. package/speechflow-cli/src/speechflow-node-xio-websocket.ts +2 -1
  154. package/speechflow-cli/src/speechflow-util-audio-wt.ts +4 -4
  155. package/speechflow-cli/src/speechflow-util-audio.ts +10 -10
  156. package/speechflow-cli/src/speechflow-util-error.ts +9 -7
  157. package/speechflow-cli/src/speechflow-util-llm.ts +367 -0
  158. package/speechflow-cli/src/speechflow-util-misc.ts +4 -4
  159. package/speechflow-cli/src/speechflow-util-queue.ts +4 -4
  160. package/speechflow-cli/src/speechflow-util-stream.ts +5 -3
  161. package/speechflow-cli/src/speechflow-util.ts +1 -0
  162. package/speechflow-ui-db/package.json +9 -9
  163. package/speechflow-ui-st/package.json +9 -9
  164. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +0 -293
  165. package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +0 -1
  166. package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +0 -1
  167. package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +0 -1
  168. package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +0 -281
  169. package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +0 -247
@@ -0,0 +1,210 @@
1
+ /*
2
+ ** SpeechFlow - Speech Processing Flow Graph
3
+ ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+
7
+ /* standard dependencies */
8
+ import Stream from "node:stream"
9
+
10
+ /* external dependencies */
11
+ import { execa, type Subprocess, type Options } from "execa"
12
+ import shellParser from "shell-parser"
13
+
14
+ /* internal dependencies */
15
+ import SpeechFlowNode from "./speechflow-node"
16
+ import * as util from "./speechflow-util"
17
+
18
+ /* SpeechFlow node for external command execution */
19
+ export default class SpeechFlowNodeXIOExec extends SpeechFlowNode {
20
+ /* declare official node name */
21
+ public static name = "xio-exec"
22
+
23
+ /* internal state */
24
+ private subprocess: Subprocess | null = null
25
+
26
+ /* construct node */
27
+ constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
28
+ super(id, cfg, opts, args)
29
+
30
+ /* declare node configuration parameters */
31
+ this.configure({
32
+ command: { type: "string", pos: 0, val: "" },
33
+ mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
34
+ type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
35
+ chunkAudio: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
36
+ chunkText: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
37
+ })
38
+
39
+ /* sanity check parameters */
40
+ if (this.params.command === "")
41
+ throw new Error("required parameter \"command\" has to be given")
42
+
43
+ /* declare node input/output format */
44
+ if (this.params.mode === "rw") {
45
+ this.input = this.params.type
46
+ this.output = this.params.type
47
+ }
48
+ else if (this.params.mode === "r") {
49
+ this.input = "none"
50
+ this.output = this.params.type
51
+ }
52
+ else if (this.params.mode === "w") {
53
+ this.input = this.params.type
54
+ this.output = "none"
55
+ }
56
+ }
57
+
58
+ /* open node */
59
+ async open () {
60
+ /* determine how many bytes we need per chunk when
61
+ the chunk should be of the required duration/size */
62
+ const highWaterMarkAudio = (
63
+ this.config.audioSampleRate *
64
+ (this.config.audioBitDepth / 8)
65
+ ) / (1000 / this.params.chunkAudio)
66
+ const highWaterMarkText = this.params.chunkText
67
+
68
+ /* parse command into executable and arguments
69
+ (SECURITY: caller must ensure command parameter is properly validated
70
+ and does not contain untrusted user input to prevent command injection) */
71
+ const cmdParts = shellParser(this.params.command)
72
+ if (cmdParts.length === 0)
73
+ throw new Error("failed to parse command: no executable found")
74
+
75
+ /* warn about potentially dangerous shell metacharacters */
76
+ if (/[;&|`$()<>]/.test(this.params.command))
77
+ this.log("warning", "command contains shell metacharacters -- ensure input is trusted")
78
+ const executable = cmdParts[0]
79
+ const args = cmdParts.slice(1)
80
+
81
+ /* determine subprocess options */
82
+ const encoding = (this.params.type === "text" ?
83
+ this.config.textEncoding : "buffer") as Options["encoding"]
84
+
85
+ /* spawn subprocess */
86
+ this.log("info", `executing command: ${this.params.command}`)
87
+ this.subprocess = execa(executable, args, {
88
+ buffer: false,
89
+ encoding,
90
+ ...(this.params.mode === "rw" ? { stdin: "pipe", stdout: "pipe" } : {}),
91
+ ...(this.params.mode === "r" ? { stdin: "ignore", stdout: "pipe" } : {}),
92
+ ...(this.params.mode === "w" ? { stdin: "pipe", stdout: "ignore" } : {})
93
+ })
94
+
95
+ /* handle subprocess errors */
96
+ this.subprocess.on("error", (err) => {
97
+ this.log("error", `subprocess error: ${err.message}`)
98
+ this.emit("error", err)
99
+ if (this.stream !== null)
100
+ this.stream.emit("error", err)
101
+ })
102
+
103
+ /* handle subprocess exit */
104
+ this.subprocess.on("exit", (code, signal) => {
105
+ if (code !== 0 && code !== null)
106
+ this.log("warning", `subprocess exited with code ${code}`)
107
+ else if (signal)
108
+ this.log("warning", `subprocess terminated by signal ${signal}`)
109
+ else
110
+ this.log("info", "subprocess terminated gracefully")
111
+ })
112
+
113
+ /* determine high water mark based on type */
114
+ const highWaterMark = this.params.type === "audio" ? highWaterMarkAudio : highWaterMarkText
115
+
116
+ /* configure stream encoding */
117
+ if (this.subprocess.stdout && this.params.type === "text")
118
+ this.subprocess.stdout.setEncoding(this.config.textEncoding)
119
+ if (this.subprocess.stdin)
120
+ this.subprocess.stdin.setDefaultEncoding(this.params.type === "text" ?
121
+ this.config.textEncoding : "binary")
122
+
123
+ /* dispatch according to mode */
124
+ if (this.params.mode === "rw") {
125
+ /* bidirectional mode: both stdin and stdout */
126
+ this.stream = Stream.Duplex.from({
127
+ readable: this.subprocess.stdout,
128
+ writable: this.subprocess.stdin
129
+ })
130
+ const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, highWaterMark)
131
+ const wrapper2 = util.createTransformStreamForReadableSide(
132
+ this.params.type, () => this.timeZero, highWaterMark)
133
+ this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
134
+ }
135
+ else if (this.params.mode === "r") {
136
+ /* read-only mode: stdout only */
137
+ const wrapper = util.createTransformStreamForReadableSide(
138
+ this.params.type, () => this.timeZero, highWaterMark)
139
+ this.stream = Stream.compose(this.subprocess.stdout!, wrapper)
140
+ }
141
+ else if (this.params.mode === "w") {
142
+ /* write-only mode: stdin only */
143
+ const wrapper = util.createTransformStreamForWritableSide(
144
+ this.params.type, highWaterMark)
145
+ this.stream = Stream.compose(wrapper, this.subprocess.stdin!)
146
+ }
147
+ }
148
+
149
+ /* close node */
150
+ async close () {
151
+ /* terminate subprocess */
152
+ if (this.subprocess !== null) {
153
+ /* gracefully end stdin if in write or read/write mode */
154
+ if ((this.params.mode === "w" || this.params.mode === "rw") && this.subprocess.stdin &&
155
+ !this.subprocess.stdin.destroyed && !this.subprocess.stdin.writableEnded) {
156
+ await Promise.race([
157
+ new Promise<void>((resolve, reject) => {
158
+ this.subprocess!.stdin!.end((err?: Error) => {
159
+ if (err) reject(err)
160
+ else resolve()
161
+ })
162
+ }),
163
+ util.timeout(2000)
164
+ ]).catch((err: unknown) => {
165
+ const error = util.ensureError(err)
166
+ this.log("warning", `failed to gracefully close stdin: ${error.message}`)
167
+ })
168
+ }
169
+
170
+ /* wait for subprocess to exit gracefully */
171
+ await Promise.race([
172
+ this.subprocess,
173
+ util.timeout(5000, "subprocess exit timeout")
174
+ ]).catch(async (err: unknown) => {
175
+ /* force kill with SIGTERM */
176
+ const error = util.ensureError(err)
177
+ if (error.message.includes("timeout")) {
178
+ this.log("warning", "subprocess did not exit gracefully, forcing termination")
179
+ this.subprocess!.kill("SIGTERM")
180
+ return Promise.race([
181
+ this.subprocess,
182
+ util.timeout(2000)
183
+ ])
184
+ }
185
+ }).catch(async () => {
186
+ /* force kill with SIGKILL */
187
+ this.log("warning", "subprocess did not respond to SIGTERM, forcing SIGKILL")
188
+ this.subprocess!.kill("SIGKILL")
189
+ return Promise.race([
190
+ this.subprocess,
191
+ util.timeout(1000)
192
+ ])
193
+ }).catch(() => {
194
+ this.log("error", "subprocess did not terminate even after SIGKILL")
195
+ })
196
+
197
+ /* remove event listeners to prevent memory leaks */
198
+ this.subprocess.removeAllListeners("error")
199
+ this.subprocess.removeAllListeners("exit")
200
+
201
+ this.subprocess = null
202
+ }
203
+
204
+ /* shutdown stream */
205
+ if (this.stream !== null) {
206
+ await util.destroyStream(this.stream)
207
+ this.stream = null
208
+ }
209
+ }
210
+ }
@@ -9,37 +9,39 @@ import fs from "node:fs"
9
9
  import Stream from "node:stream"
10
10
 
11
11
  /* internal dependencies */
12
- import SpeechFlowNode from "./speechflow-node"
13
- import * as util from "./speechflow-util"
12
+ import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
13
+ import * as util from "./speechflow-util"
14
14
 
15
15
  /* SpeechFlow node for file access */
16
16
  export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
17
17
  /* declare official node name */
18
18
  public static name = "xio-file"
19
19
 
20
+ /* file descriptor for seekable write mode */
21
+ private fd: number | null = null
22
+
20
23
  /* construct node */
21
24
  constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
22
25
  super(id, cfg, opts, args)
23
26
 
24
27
  /* declare node configuration parameters */
25
28
  this.configure({
26
- path: { type: "string", pos: 0, val: "" },
27
- mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
28
- type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
29
- chunka: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
30
- chunkt: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
29
+ path: { type: "string", pos: 0, val: "" },
30
+ mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w)$/ },
31
+ type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
32
+ seekable: { type: "boolean", val: false },
33
+ chunkAudio: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
34
+ chunkText: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
31
35
  })
32
36
 
33
37
  /* sanity check parameters */
34
38
  if (this.params.path === "")
35
39
  throw new Error("required parameter \"path\" has to be given")
40
+ if (this.params.seekable && this.params.path === "-")
41
+ throw new Error("parameter \"seekable\" cannot be used with standard I/O")
36
42
 
37
43
  /* declare node input/output format */
38
- if (this.params.mode === "rw") {
39
- this.input = this.params.type
40
- this.output = this.params.type
41
- }
42
- else if (this.params.mode === "r") {
44
+ if (this.params.mode === "r") {
43
45
  this.input = "none"
44
46
  this.output = this.params.type
45
47
  }
@@ -56,8 +58,8 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
56
58
  const highWaterMarkAudio = (
57
59
  this.config.audioSampleRate *
58
60
  (this.config.audioBitDepth / 8)
59
- ) / (1000 / this.params.chunka)
60
- const highWaterMarkText = this.params.chunkt
61
+ ) / (1000 / this.params.chunkAudio)
62
+ const highWaterMarkText = this.params.chunkText
61
63
 
62
64
  /* utility function: create a writable stream as chunker that
63
65
  writes to process.stdout but properly handles finish events.
@@ -81,59 +83,7 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
81
83
  })
82
84
 
83
85
  /* dispatch according to mode and path */
84
- if (this.params.mode === "rw") {
85
- if (this.params.path === "-") {
86
- /* standard I/O */
87
- if (this.params.type === "audio") {
88
- process.stdin.setEncoding()
89
- process.stdout.setEncoding()
90
- const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
91
- process.stdin.pipe(streamR)
92
- const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
93
- streamW.pipe(process.stdout)
94
- this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
95
- }
96
- else {
97
- process.stdin.setEncoding(this.config.textEncoding)
98
- process.stdout.setEncoding(this.config.textEncoding)
99
- const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
100
- process.stdin.pipe(streamR)
101
- const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
102
- streamW.pipe(process.stdout)
103
- this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
104
- }
105
- }
106
- else {
107
- /* file I/O */
108
- if (this.params.type === "audio") {
109
- this.stream = Stream.Duplex.from({
110
- readable: fs.createReadStream(this.params.path,
111
- { highWaterMark: highWaterMarkAudio }),
112
- writable: fs.createWriteStream(this.params.path,
113
- { highWaterMark: highWaterMarkAudio })
114
- })
115
- }
116
- else {
117
- this.stream = Stream.Duplex.from({
118
- readable: fs.createReadStream(this.params.path, {
119
- highWaterMark: highWaterMarkText,
120
- encoding: this.config.textEncoding
121
- }),
122
- writable: fs.createWriteStream(this.params.path, {
123
- highWaterMark: highWaterMarkText,
124
- encoding: this.config.textEncoding
125
- })
126
- })
127
- }
128
- }
129
-
130
- /* convert regular stream into object-mode stream */
131
- const wrapper1 = util.createTransformStreamForWritableSide(this.params.type, 1)
132
- const wrapper2 = util.createTransformStreamForReadableSide(
133
- this.params.type, () => this.timeZero)
134
- this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
135
- }
136
- else if (this.params.mode === "r") {
86
+ if (this.params.mode === "r") {
137
87
  if (this.params.path === "-") {
138
88
  /* standard I/O */
139
89
  let chunker: Stream.PassThrough
@@ -176,15 +126,63 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
176
126
  }
177
127
  else {
178
128
  /* file I/O */
179
- let writable: Stream.Writable
180
- if (this.params.type === "audio")
181
- writable = fs.createWriteStream(this.params.path,
182
- { highWaterMark: highWaterMarkAudio })
183
- else
184
- writable = fs.createWriteStream(this.params.path,
185
- { highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
186
- const wrapper = util.createTransformStreamForWritableSide(this.params.type, 1)
187
- this.stream = Stream.compose(wrapper, writable)
129
+ if (this.params.seekable) {
130
+ /* seekable file I/O with file descriptor */
131
+ this.fd = fs.openSync(this.params.path, "w")
132
+ let writePosition = 0
133
+ const self = this
134
+ const writable = new Stream.Writable({
135
+ objectMode: true,
136
+ decodeStrings: false,
137
+ highWaterMark: 1,
138
+ write (chunk: SpeechFlowChunk, encoding, callback) {
139
+ const payload = Buffer.isBuffer(chunk.payload) ?
140
+ chunk.payload : Buffer.from(chunk.payload)
141
+ const seekPosition = chunk.meta.get("chunk:seek") as number | undefined
142
+ if (seekPosition !== undefined) {
143
+ /* seek to specified position and write (overload) */
144
+ fs.write(self.fd!, payload, 0, payload.byteLength, seekPosition, callback)
145
+ }
146
+ else {
147
+ /* append at current position */
148
+ fs.write(self.fd!, payload, 0, payload.byteLength, writePosition, (err) => {
149
+ if (err)
150
+ callback(err)
151
+ else {
152
+ writePosition += payload.byteLength
153
+ callback()
154
+ }
155
+ })
156
+ }
157
+ },
158
+ final (callback) {
159
+ callback()
160
+ },
161
+ destroy (err, callback) {
162
+ if (self.fd !== null) {
163
+ fs.close(self.fd, () => {
164
+ self.fd = null
165
+ callback(err)
166
+ })
167
+ }
168
+ else
169
+ callback(err)
170
+ }
171
+ })
172
+ this.stream = writable
173
+ }
174
+ else {
175
+ /* non-seekable file I/O with stream */
176
+ let writable: Stream.Writable
177
+ if (this.params.type === "audio")
178
+ writable = fs.createWriteStream(this.params.path,
179
+ { highWaterMark: highWaterMarkAudio })
180
+ else
181
+ writable = fs.createWriteStream(this.params.path,
182
+ { highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
183
+ const wrapper = util.createTransformStreamForWritableSide(this.params.type, 1)
184
+ this.stream = Stream.compose(wrapper, writable)
185
+ }
188
186
  }
189
187
  }
190
188
  else
@@ -202,20 +200,35 @@ export default class SpeechFlowNodeXIOFile extends SpeechFlowNode {
202
200
  /* for stdio streams, just end without destroying */
203
201
  const stream = this.stream
204
202
  if ((stream instanceof Stream.Writable || stream instanceof Stream.Duplex) &&
205
- (!stream.writableEnded && !stream.destroyed) ) {
203
+ (!stream.writableEnded && !stream.destroyed)) {
206
204
  await Promise.race([
207
205
  new Promise<void>((resolve, reject) => {
208
206
  stream.end((err?: Error) => {
209
- if (err) reject(err)
210
- else resolve()
207
+ if (err)
208
+ reject(err)
209
+ else
210
+ resolve()
211
211
  })
212
212
  }),
213
- util.timeoutPromise(5000)
213
+ util.timeout(5000)
214
214
  ])
215
215
  }
216
216
  }
217
217
  this.stream = null
218
218
  }
219
+
220
+ /* ensure file descriptor is closed */
221
+ if (this.fd !== null) {
222
+ await new Promise<void>((resolve, reject) => {
223
+ fs.close(this.fd!, (err) => {
224
+ this.fd = null
225
+ if (err)
226
+ reject(err)
227
+ else
228
+ resolve()
229
+ })
230
+ })
231
+ }
219
232
  }
220
233
  }
221
234
 
@@ -97,7 +97,8 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
97
97
  this.log("info", `connection re-opened to MQTT ${this.params.url}`)
98
98
  })
99
99
  this.broker.on("disconnect", (packet: MQTT.IDisconnectPacket) => {
100
- this.log("info", `connection closed to MQTT ${this.params.url}`)
100
+ const reasonCode = packet.reasonCode ?? 0
101
+ this.log("info", `connection closed to MQTT ${this.params.url} (reason code: ${reasonCode})`)
101
102
  })
102
103
  this.chunkQueue = new util.SingleQueue<SpeechFlowChunk>()
103
104
  this.broker.on("message", (topic: string, payload: Buffer, packet: MQTT.IPublishPacket) => {
@@ -107,7 +108,7 @@ export default class SpeechFlowNodeXIOMQTT extends SpeechFlowNode {
107
108
  const chunk = util.streamChunkDecode(payload)
108
109
  this.chunkQueue!.write(chunk)
109
110
  }
110
- catch (_err: any) {
111
+ catch (_err: unknown) {
111
112
  this.log("warning", `received invalid CBOR chunk from MQTT ${this.params.url}`)
112
113
  }
113
114
  })