speechflow 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/etc/stx.conf +54 -58
- package/package.json +25 -106
- package/{etc → speechflow-cli/etc}/eslint.mjs +1 -2
- package/speechflow-cli/etc/stx.conf +77 -0
- package/speechflow-cli/package.json +116 -0
- package/{src → speechflow-cli/src}/speechflow-node-a2a-gender.ts +148 -64
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +217 -0
- package/{src → speechflow-cli/src}/speechflow-node-a2a-mute.ts +39 -11
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +384 -0
- package/{src → speechflow-cli/src}/speechflow-node-a2a-wav.ts +27 -11
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +313 -0
- package/{src → speechflow-cli/src}/speechflow-node-t2a-elevenlabs.ts +59 -12
- package/{src → speechflow-cli/src}/speechflow-node-t2a-kokoro.ts +11 -4
- package/{src → speechflow-cli/src}/speechflow-node-t2t-deepl.ts +9 -4
- package/{src → speechflow-cli/src}/speechflow-node-t2t-format.ts +2 -2
- package/{src → speechflow-cli/src}/speechflow-node-t2t-ollama.ts +1 -1
- package/{src → speechflow-cli/src}/speechflow-node-t2t-openai.ts +1 -1
- package/{src → speechflow-cli/src}/speechflow-node-t2t-sentence.ts +37 -20
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +276 -0
- package/{src → speechflow-cli/src}/speechflow-node-t2t-transformers.ts +4 -3
- package/{src → speechflow-cli/src}/speechflow-node-x2x-filter.ts +9 -5
- package/{src → speechflow-cli/src}/speechflow-node-x2x-trace.ts +16 -8
- package/{src → speechflow-cli/src}/speechflow-node-xio-device.ts +12 -8
- package/{src → speechflow-cli/src}/speechflow-node-xio-file.ts +9 -3
- package/{src → speechflow-cli/src}/speechflow-node-xio-mqtt.ts +5 -2
- package/{src → speechflow-cli/src}/speechflow-node-xio-websocket.ts +12 -12
- package/{src → speechflow-cli/src}/speechflow-node.ts +7 -0
- package/{src → speechflow-cli/src}/speechflow-utils.ts +78 -44
- package/{src → speechflow-cli/src}/speechflow.ts +188 -53
- package/speechflow-ui-db/etc/eslint.mjs +106 -0
- package/speechflow-ui-db/etc/htmllint.json +55 -0
- package/speechflow-ui-db/etc/stx.conf +79 -0
- package/speechflow-ui-db/etc/stylelint.js +46 -0
- package/speechflow-ui-db/etc/stylelint.yaml +33 -0
- package/speechflow-ui-db/etc/tsc-client.json +30 -0
- package/speechflow-ui-db/etc/tsc.node.json +9 -0
- package/speechflow-ui-db/etc/vite-client.mts +63 -0
- package/speechflow-ui-db/package.d/htmllint-cli+0.0.7.patch +20 -0
- package/speechflow-ui-db/package.json +75 -0
- package/speechflow-ui-db/src/app-icon.ai +1989 -4
- package/speechflow-ui-db/src/app-icon.svg +26 -0
- package/speechflow-ui-db/src/app.styl +64 -0
- package/speechflow-ui-db/src/app.vue +221 -0
- package/speechflow-ui-db/src/index.html +23 -0
- package/speechflow-ui-db/src/index.ts +26 -0
- package/{dst/speechflow.d.ts → speechflow-ui-db/src/lib.d.ts} +5 -3
- package/speechflow-ui-db/src/tsconfig.json +3 -0
- package/speechflow-ui-st/etc/eslint.mjs +106 -0
- package/speechflow-ui-st/etc/htmllint.json +55 -0
- package/speechflow-ui-st/etc/stx.conf +79 -0
- package/speechflow-ui-st/etc/stylelint.js +46 -0
- package/speechflow-ui-st/etc/stylelint.yaml +33 -0
- package/speechflow-ui-st/etc/tsc-client.json +30 -0
- package/speechflow-ui-st/etc/tsc.node.json +9 -0
- package/speechflow-ui-st/etc/vite-client.mts +63 -0
- package/speechflow-ui-st/package.d/htmllint-cli+0.0.7.patch +20 -0
- package/speechflow-ui-st/package.json +79 -0
- package/speechflow-ui-st/src/app-icon.ai +1989 -4
- package/speechflow-ui-st/src/app-icon.svg +26 -0
- package/speechflow-ui-st/src/app.styl +64 -0
- package/speechflow-ui-st/src/app.vue +142 -0
- package/speechflow-ui-st/src/index.html +23 -0
- package/speechflow-ui-st/src/index.ts +26 -0
- package/speechflow-ui-st/src/lib.d.ts +9 -0
- package/speechflow-ui-st/src/tsconfig.json +3 -0
- package/dst/speechflow-node-a2a-ffmpeg.d.ts +0 -13
- package/dst/speechflow-node-a2a-ffmpeg.js +0 -153
- package/dst/speechflow-node-a2a-ffmpeg.js.map +0 -1
- package/dst/speechflow-node-a2a-gender.d.ts +0 -18
- package/dst/speechflow-node-a2a-gender.js +0 -271
- package/dst/speechflow-node-a2a-gender.js.map +0 -1
- package/dst/speechflow-node-a2a-meter.d.ts +0 -12
- package/dst/speechflow-node-a2a-meter.js +0 -155
- package/dst/speechflow-node-a2a-meter.js.map +0 -1
- package/dst/speechflow-node-a2a-mute.d.ts +0 -16
- package/dst/speechflow-node-a2a-mute.js +0 -91
- package/dst/speechflow-node-a2a-mute.js.map +0 -1
- package/dst/speechflow-node-a2a-vad.d.ts +0 -16
- package/dst/speechflow-node-a2a-vad.js +0 -285
- package/dst/speechflow-node-a2a-vad.js.map +0 -1
- package/dst/speechflow-node-a2a-wav.d.ts +0 -11
- package/dst/speechflow-node-a2a-wav.js +0 -195
- package/dst/speechflow-node-a2a-wav.js.map +0 -1
- package/dst/speechflow-node-a2t-deepgram.d.ts +0 -15
- package/dst/speechflow-node-a2t-deepgram.js +0 -255
- package/dst/speechflow-node-a2t-deepgram.js.map +0 -1
- package/dst/speechflow-node-t2a-elevenlabs.d.ts +0 -16
- package/dst/speechflow-node-t2a-elevenlabs.js +0 -195
- package/dst/speechflow-node-t2a-elevenlabs.js.map +0 -1
- package/dst/speechflow-node-t2a-kokoro.d.ts +0 -13
- package/dst/speechflow-node-t2a-kokoro.js +0 -149
- package/dst/speechflow-node-t2a-kokoro.js.map +0 -1
- package/dst/speechflow-node-t2t-deepl.d.ts +0 -15
- package/dst/speechflow-node-t2t-deepl.js +0 -142
- package/dst/speechflow-node-t2t-deepl.js.map +0 -1
- package/dst/speechflow-node-t2t-format.d.ts +0 -11
- package/dst/speechflow-node-t2t-format.js +0 -82
- package/dst/speechflow-node-t2t-format.js.map +0 -1
- package/dst/speechflow-node-t2t-ollama.d.ts +0 -13
- package/dst/speechflow-node-t2t-ollama.js +0 -247
- package/dst/speechflow-node-t2t-ollama.js.map +0 -1
- package/dst/speechflow-node-t2t-openai.d.ts +0 -13
- package/dst/speechflow-node-t2t-openai.js +0 -227
- package/dst/speechflow-node-t2t-openai.js.map +0 -1
- package/dst/speechflow-node-t2t-sentence.d.ts +0 -17
- package/dst/speechflow-node-t2t-sentence.js +0 -234
- package/dst/speechflow-node-t2t-sentence.js.map +0 -1
- package/dst/speechflow-node-t2t-subtitle.d.ts +0 -13
- package/dst/speechflow-node-t2t-subtitle.js +0 -278
- package/dst/speechflow-node-t2t-subtitle.js.map +0 -1
- package/dst/speechflow-node-t2t-transformers.d.ts +0 -14
- package/dst/speechflow-node-t2t-transformers.js +0 -265
- package/dst/speechflow-node-t2t-transformers.js.map +0 -1
- package/dst/speechflow-node-x2x-filter.d.ts +0 -11
- package/dst/speechflow-node-x2x-filter.js +0 -117
- package/dst/speechflow-node-x2x-filter.js.map +0 -1
- package/dst/speechflow-node-x2x-trace.d.ts +0 -11
- package/dst/speechflow-node-x2x-trace.js +0 -111
- package/dst/speechflow-node-x2x-trace.js.map +0 -1
- package/dst/speechflow-node-xio-device.d.ts +0 -13
- package/dst/speechflow-node-xio-device.js +0 -226
- package/dst/speechflow-node-xio-device.js.map +0 -1
- package/dst/speechflow-node-xio-file.d.ts +0 -11
- package/dst/speechflow-node-xio-file.js +0 -210
- package/dst/speechflow-node-xio-file.js.map +0 -1
- package/dst/speechflow-node-xio-mqtt.d.ts +0 -13
- package/dst/speechflow-node-xio-mqtt.js +0 -185
- package/dst/speechflow-node-xio-mqtt.js.map +0 -1
- package/dst/speechflow-node-xio-websocket.d.ts +0 -13
- package/dst/speechflow-node-xio-websocket.js +0 -278
- package/dst/speechflow-node-xio-websocket.js.map +0 -1
- package/dst/speechflow-node.d.ts +0 -65
- package/dst/speechflow-node.js +0 -180
- package/dst/speechflow-node.js.map +0 -1
- package/dst/speechflow-utils.d.ts +0 -69
- package/dst/speechflow-utils.js +0 -486
- package/dst/speechflow-utils.js.map +0 -1
- package/dst/speechflow.js +0 -768
- package/dst/speechflow.js.map +0 -1
- package/src/speechflow-node-a2a-meter.ts +0 -130
- package/src/speechflow-node-a2a-vad.ts +0 -285
- package/src/speechflow-node-a2t-deepgram.ts +0 -234
- package/src/speechflow-node-t2t-subtitle.ts +0 -149
- /package/{etc → speechflow-cli/etc}/biome.jsonc +0 -0
- /package/{etc → speechflow-cli/etc}/oxlint.jsonc +0 -0
- /package/{etc → speechflow-cli/etc}/speechflow.bat +0 -0
- /package/{etc → speechflow-cli/etc}/speechflow.sh +0 -0
- /package/{etc → speechflow-cli/etc}/speechflow.yaml +0 -0
- /package/{etc → speechflow-cli/etc}/tsconfig.json +0 -0
- /package/{package.d → speechflow-cli/package.d}/@ericedouard+vad-node-realtime+0.2.0.patch +0 -0
- /package/{src → speechflow-cli/src}/lib.d.ts +0 -0
- /package/{src → speechflow-cli/src}/speechflow-logo.ai +0 -0
- /package/{src → speechflow-cli/src}/speechflow-logo.svg +0 -0
- /package/{src → speechflow-cli/src}/speechflow-node-a2a-ffmpeg.ts +0 -0
- /package/{tsconfig.json → speechflow-cli/tsconfig.json} +0 -0
|
@@ -29,12 +29,12 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
29
29
|
public static name = "sentence"
|
|
30
30
|
|
|
31
31
|
/* internal state */
|
|
32
|
-
private static speexInitialized = false
|
|
33
32
|
private queue = new utils.Queue<TextQueueElement>()
|
|
34
33
|
private queueRecv = this.queue.pointerUse("recv")
|
|
35
34
|
private queueSplit = this.queue.pointerUse("split")
|
|
36
35
|
private queueSend = this.queue.pointerUse("send")
|
|
37
36
|
private destroyed = false
|
|
37
|
+
private workingOffTimer: ReturnType<typeof setTimeout> | null = null
|
|
38
38
|
|
|
39
39
|
/* construct node */
|
|
40
40
|
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
@@ -53,11 +53,7 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
53
53
|
/* clear destruction flag */
|
|
54
54
|
this.destroyed = false
|
|
55
55
|
|
|
56
|
-
/* pass-through logging */
|
|
57
|
-
const log = (level: string, msg: string) => { this.log(level, msg) }
|
|
58
|
-
|
|
59
56
|
/* work off queued audio frames */
|
|
60
|
-
let workingOffTimer: ReturnType<typeof setTimeout> | null = null
|
|
61
57
|
let workingOff = false
|
|
62
58
|
const workOffQueue = async () => {
|
|
63
59
|
if (this.destroyed)
|
|
@@ -67,14 +63,14 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
67
63
|
if (workingOff)
|
|
68
64
|
return
|
|
69
65
|
workingOff = true
|
|
70
|
-
if (workingOffTimer !== null) {
|
|
71
|
-
clearTimeout(workingOffTimer)
|
|
72
|
-
workingOffTimer = null
|
|
66
|
+
if (this.workingOffTimer !== null) {
|
|
67
|
+
clearTimeout(this.workingOffTimer)
|
|
68
|
+
this.workingOffTimer = null
|
|
73
69
|
}
|
|
74
70
|
this.queue.off("write", workOffQueue)
|
|
75
71
|
|
|
76
72
|
/* try to work off one or more chunks */
|
|
77
|
-
while (
|
|
73
|
+
while (!this.destroyed) {
|
|
78
74
|
const element = this.queueSplit.peek()
|
|
79
75
|
if (element === undefined)
|
|
80
76
|
break
|
|
@@ -136,10 +132,12 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
136
132
|
}
|
|
137
133
|
}
|
|
138
134
|
|
|
139
|
-
/* re-initiate working off round */
|
|
135
|
+
/* re-initiate working off round (if still not destroyed) */
|
|
140
136
|
workingOff = false
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
if (!this.destroyed) {
|
|
138
|
+
this.workingOffTimer = setTimeout(workOffQueue, 100)
|
|
139
|
+
this.queue.once("write", workOffQueue)
|
|
140
|
+
}
|
|
143
141
|
}
|
|
144
142
|
this.queue.once("write", workOffQueue)
|
|
145
143
|
|
|
@@ -153,12 +151,14 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
153
151
|
|
|
154
152
|
/* receive text chunk (writable side of stream) */
|
|
155
153
|
write (chunk: SpeechFlowChunk, encoding, callback) {
|
|
156
|
-
if (
|
|
154
|
+
if (self.destroyed)
|
|
155
|
+
callback(new Error("stream already destroyed"))
|
|
156
|
+
else if (Buffer.isBuffer(chunk.payload))
|
|
157
157
|
callback(new Error("expected text input as string chunks"))
|
|
158
158
|
else if (chunk.payload.length === 0)
|
|
159
159
|
callback()
|
|
160
160
|
else {
|
|
161
|
-
log("info", `received text: ${JSON.stringify(chunk.payload)}`)
|
|
161
|
+
self.log("info", `received text: ${JSON.stringify(chunk.payload)}`)
|
|
162
162
|
self.queueRecv.append({ type: "text-frame", chunk })
|
|
163
163
|
callback()
|
|
164
164
|
}
|
|
@@ -166,6 +166,10 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
166
166
|
|
|
167
167
|
/* receive no more text chunks (writable side of stream) */
|
|
168
168
|
final (callback) {
|
|
169
|
+
if (self.destroyed) {
|
|
170
|
+
callback()
|
|
171
|
+
return
|
|
172
|
+
}
|
|
169
173
|
/* signal end of file */
|
|
170
174
|
self.queueRecv.append({ type: "text-eof" })
|
|
171
175
|
callback()
|
|
@@ -173,8 +177,12 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
173
177
|
|
|
174
178
|
/* send text chunk(s) (readable side of stream) */
|
|
175
179
|
read (_size) {
|
|
176
|
-
/* flush pending
|
|
180
|
+
/* flush pending text chunks */
|
|
177
181
|
const flushPendingChunks = () => {
|
|
182
|
+
if (self.destroyed) {
|
|
183
|
+
this.push(null)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
178
186
|
const element = self.queueSend.peek()
|
|
179
187
|
if (element !== undefined
|
|
180
188
|
&& element.type === "text-eof") {
|
|
@@ -196,13 +204,13 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
196
204
|
else if (element.type === "text-frame"
|
|
197
205
|
&& element.complete !== true)
|
|
198
206
|
break
|
|
199
|
-
log("info", `send text: ${JSON.stringify(element.chunk.payload)}`)
|
|
207
|
+
self.log("info", `send text: ${JSON.stringify(element.chunk.payload)}`)
|
|
200
208
|
this.push(element.chunk)
|
|
201
209
|
self.queueSend.walk(+1)
|
|
202
210
|
self.queue.trim()
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
|
-
else
|
|
213
|
+
else if (!self.destroyed)
|
|
206
214
|
self.queue.once("write", flushPendingChunks)
|
|
207
215
|
}
|
|
208
216
|
flushPendingChunks()
|
|
@@ -212,13 +220,22 @@ export default class SpeechFlowNodeSentence extends SpeechFlowNode {
|
|
|
212
220
|
|
|
213
221
|
/* close node */
|
|
214
222
|
async close () {
|
|
223
|
+
/* indicate destruction */
|
|
224
|
+
this.destroyed = true
|
|
225
|
+
|
|
226
|
+
/* clean up timer */
|
|
227
|
+
if (this.workingOffTimer !== null) {
|
|
228
|
+
clearTimeout(this.workingOffTimer)
|
|
229
|
+
this.workingOffTimer = null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* remove any pending event listeners */
|
|
233
|
+
this.queue.removeAllListeners("write")
|
|
234
|
+
|
|
215
235
|
/* close stream */
|
|
216
236
|
if (this.stream !== null) {
|
|
217
237
|
this.stream.destroy()
|
|
218
238
|
this.stream = null
|
|
219
239
|
}
|
|
220
|
-
|
|
221
|
-
/* indicate destruction */
|
|
222
|
-
this.destroyed = true
|
|
223
240
|
}
|
|
224
241
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
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 path from "node:path"
|
|
9
|
+
import http from "node:http"
|
|
10
|
+
import Stream from "node:stream"
|
|
11
|
+
|
|
12
|
+
/* external dependencies */
|
|
13
|
+
import { Duration } from "luxon"
|
|
14
|
+
import * as HAPI from "@hapi/hapi"
|
|
15
|
+
import Inert from "@hapi/inert"
|
|
16
|
+
import WebSocket from "ws"
|
|
17
|
+
import HAPIWebSocket from "hapi-plugin-websocket"
|
|
18
|
+
|
|
19
|
+
/* internal dependencies */
|
|
20
|
+
import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
|
|
21
|
+
|
|
22
|
+
type wsPeerCtx = {
|
|
23
|
+
peer: string
|
|
24
|
+
}
|
|
25
|
+
type wsPeerInfo = {
|
|
26
|
+
ctx: wsPeerCtx
|
|
27
|
+
ws: WebSocket
|
|
28
|
+
req: http.IncomingMessage
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* SpeechFlow node for subtitle (text-to-text) "translations" */
|
|
32
|
+
export default class SpeechFlowNodeSubtitle extends SpeechFlowNode {
|
|
33
|
+
/* declare official node name */
|
|
34
|
+
public static name = "subtitle"
|
|
35
|
+
|
|
36
|
+
/* internal state */
|
|
37
|
+
private sequenceNo = 1
|
|
38
|
+
private hapi: HAPI.Server | null = null
|
|
39
|
+
|
|
40
|
+
/* construct node */
|
|
41
|
+
constructor (id: string, cfg: { [ id: string ]: any }, opts: { [ id: string ]: any }, args: any[]) {
|
|
42
|
+
super(id, cfg, opts, args)
|
|
43
|
+
|
|
44
|
+
/* declare node configuration parameters */
|
|
45
|
+
this.configure({
|
|
46
|
+
format: { type: "string", pos: 0, val: "srt", match: /^(?:srt|vtt)$/ },
|
|
47
|
+
words: { type: "boolean", val: false },
|
|
48
|
+
mode: { type: "string", val: "export", match: /^(?:export|render)$/ },
|
|
49
|
+
addr: { type: "string", val: "127.0.0.1" },
|
|
50
|
+
port: { type: "number", val: 8585 }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/* declare node input/output format */
|
|
54
|
+
this.input = "text"
|
|
55
|
+
this.output = this.params.mode === "export" ? "text" : "none"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* open node */
|
|
59
|
+
async open () {
|
|
60
|
+
if (this.params.mode === "export") {
|
|
61
|
+
this.sequenceNo = 1
|
|
62
|
+
|
|
63
|
+
/* provide text-to-subtitle conversion */
|
|
64
|
+
const convert = async (chunk: SpeechFlowChunk) => {
|
|
65
|
+
if (typeof chunk.payload !== "string")
|
|
66
|
+
throw new Error("chunk payload type must be string")
|
|
67
|
+
const convertSingle = (
|
|
68
|
+
start: Duration,
|
|
69
|
+
end: Duration,
|
|
70
|
+
text: string,
|
|
71
|
+
word?: string,
|
|
72
|
+
occurence?: number
|
|
73
|
+
) => {
|
|
74
|
+
if (word) {
|
|
75
|
+
occurence ??= 1
|
|
76
|
+
let match = 1
|
|
77
|
+
word = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
78
|
+
text = text.replaceAll(new RegExp(`\\b${word}\\b`, "g"), (m) => {
|
|
79
|
+
if (match++ === occurence)
|
|
80
|
+
return `<b>${m}</b>`
|
|
81
|
+
else
|
|
82
|
+
return m
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
if (this.params.format === "srt") {
|
|
86
|
+
const startFmt = start.toFormat("hh:mm:ss,SSS")
|
|
87
|
+
const endFmt = end.toFormat("hh:mm:ss,SSS")
|
|
88
|
+
text = `${this.sequenceNo++}\n` +
|
|
89
|
+
`${startFmt} --> ${endFmt}\n` +
|
|
90
|
+
`${text}\n\n`
|
|
91
|
+
}
|
|
92
|
+
else if (this.params.format === "vtt") {
|
|
93
|
+
const startFmt = start.toFormat("hh:mm:ss.SSS")
|
|
94
|
+
const endFmt = end.toFormat("hh:mm:ss.SSS")
|
|
95
|
+
text = `${startFmt} --> ${endFmt}\n` +
|
|
96
|
+
`${text}\n\n`
|
|
97
|
+
}
|
|
98
|
+
return text
|
|
99
|
+
}
|
|
100
|
+
let output = ""
|
|
101
|
+
if (this.params.words) {
|
|
102
|
+
output += convertSingle(chunk.timestampStart, chunk.timestampEnd, chunk.payload)
|
|
103
|
+
const words = (chunk.meta.get("words") ?? []) as
|
|
104
|
+
{ word: string, start: Duration, end: Duration }[]
|
|
105
|
+
const occurences = new Map<string, number>()
|
|
106
|
+
for (const word of words) {
|
|
107
|
+
let occurence = occurences.get(word.word) ?? 0
|
|
108
|
+
occurence++
|
|
109
|
+
occurences.set(word.word, occurence)
|
|
110
|
+
output += convertSingle(word.start, word.end, chunk.payload, word.word, occurence)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
output += convertSingle(chunk.timestampStart, chunk.timestampEnd, chunk.payload)
|
|
115
|
+
return output
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* establish a duplex stream */
|
|
119
|
+
const self = this
|
|
120
|
+
let firstChunk = true
|
|
121
|
+
this.stream = new Stream.Transform({
|
|
122
|
+
readableObjectMode: true,
|
|
123
|
+
writableObjectMode: true,
|
|
124
|
+
decodeStrings: false,
|
|
125
|
+
highWaterMark: 1,
|
|
126
|
+
transform (chunk: SpeechFlowChunk, encoding, callback) {
|
|
127
|
+
if (firstChunk && self.params.format === "vtt") {
|
|
128
|
+
this.push(new SpeechFlowChunk(
|
|
129
|
+
Duration.fromMillis(0), Duration.fromMillis(0),
|
|
130
|
+
"final", "text",
|
|
131
|
+
"WEBVTT\n\n"
|
|
132
|
+
))
|
|
133
|
+
firstChunk = false
|
|
134
|
+
}
|
|
135
|
+
if (Buffer.isBuffer(chunk.payload))
|
|
136
|
+
callback(new Error("invalid chunk payload type"))
|
|
137
|
+
else {
|
|
138
|
+
if (chunk.payload === "") {
|
|
139
|
+
this.push(chunk)
|
|
140
|
+
callback()
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
convert(chunk).then((payload) => {
|
|
144
|
+
const chunkNew = chunk.clone()
|
|
145
|
+
chunkNew.payload = payload
|
|
146
|
+
this.push(chunkNew)
|
|
147
|
+
callback()
|
|
148
|
+
}).catch((err) => {
|
|
149
|
+
callback(err)
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
final (callback) {
|
|
155
|
+
this.push(null)
|
|
156
|
+
callback()
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
else if (this.params.mode === "render") {
|
|
161
|
+
/* establish REST/WebSocket API */
|
|
162
|
+
const wsPeers = new Map<string, wsPeerInfo>()
|
|
163
|
+
this.hapi = new HAPI.Server({
|
|
164
|
+
address: this.params.addr,
|
|
165
|
+
port: this.params.port
|
|
166
|
+
})
|
|
167
|
+
await this.hapi.register({ plugin: Inert })
|
|
168
|
+
await this.hapi.register({ plugin: HAPIWebSocket })
|
|
169
|
+
this.hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
|
|
170
|
+
if (event.error instanceof Error)
|
|
171
|
+
this.log("error", `HAPI: request-error: ${event.error.message}`)
|
|
172
|
+
else
|
|
173
|
+
this.log("error", `HAPI: request-error: ${event.error}`)
|
|
174
|
+
})
|
|
175
|
+
this.hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
|
|
176
|
+
if (tags.error) {
|
|
177
|
+
const err = event.error
|
|
178
|
+
if (err instanceof Error)
|
|
179
|
+
this.log("error", `HAPI: log: ${err.message}`)
|
|
180
|
+
else
|
|
181
|
+
this.log("error", `HAPI: log: ${err}`)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
this.hapi.route({
|
|
185
|
+
method: "GET",
|
|
186
|
+
path: "/{param*}",
|
|
187
|
+
handler: {
|
|
188
|
+
directory: {
|
|
189
|
+
path: path.join(__dirname, "../../speechflow-ui-st/dst"),
|
|
190
|
+
redirectToSlash: true,
|
|
191
|
+
index: true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
this.hapi.route({
|
|
196
|
+
method: "POST",
|
|
197
|
+
path: "/api",
|
|
198
|
+
options: {
|
|
199
|
+
payload: {
|
|
200
|
+
output: "data",
|
|
201
|
+
parse: true,
|
|
202
|
+
allow: "application/json"
|
|
203
|
+
},
|
|
204
|
+
plugins: {
|
|
205
|
+
websocket: {
|
|
206
|
+
autoping: 30 * 1000,
|
|
207
|
+
connect: (args: any) => {
|
|
208
|
+
const ctx: wsPeerCtx = args.ctx
|
|
209
|
+
const ws: WebSocket = args.ws
|
|
210
|
+
const req: http.IncomingMessage = args.req
|
|
211
|
+
const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
|
|
212
|
+
ctx.peer = peer
|
|
213
|
+
wsPeers.set(peer, { ctx, ws, req })
|
|
214
|
+
this.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
|
|
215
|
+
},
|
|
216
|
+
disconnect: (args: any) => {
|
|
217
|
+
const ctx: wsPeerCtx = args.ctx
|
|
218
|
+
const peer = ctx.peer
|
|
219
|
+
wsPeers.delete(peer)
|
|
220
|
+
this.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
226
|
+
return h.response({}).code(204)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
await this.hapi.start()
|
|
231
|
+
this.log("info", `HAPI: started REST/WebSocket network service: http://${this.params.addr}:${this.params.port}`)
|
|
232
|
+
|
|
233
|
+
const emit = (chunk: SpeechFlowChunk) => {
|
|
234
|
+
const data = JSON.stringify(chunk)
|
|
235
|
+
for (const info of wsPeers.values())
|
|
236
|
+
info.ws.send(data)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.stream = new Stream.Writable({
|
|
240
|
+
objectMode: true,
|
|
241
|
+
decodeStrings: false,
|
|
242
|
+
highWaterMark: 1,
|
|
243
|
+
write (chunk: SpeechFlowChunk, encoding, callback) {
|
|
244
|
+
if (Buffer.isBuffer(chunk.payload))
|
|
245
|
+
callback(new Error("invalid chunk payload type"))
|
|
246
|
+
else {
|
|
247
|
+
if (chunk.payload === "")
|
|
248
|
+
callback()
|
|
249
|
+
else {
|
|
250
|
+
emit(chunk)
|
|
251
|
+
callback()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
final (callback) {
|
|
256
|
+
callback()
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* open node */
|
|
263
|
+
async close () {
|
|
264
|
+
/* close stream */
|
|
265
|
+
if (this.stream !== null) {
|
|
266
|
+
this.stream.destroy()
|
|
267
|
+
this.stream = null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* shutdown HAPI */
|
|
271
|
+
if (this.hapi !== null) {
|
|
272
|
+
await this.hapi.stop()
|
|
273
|
+
this.hapi = null
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -104,7 +104,7 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
|
|
|
104
104
|
|
|
105
105
|
/* open node */
|
|
106
106
|
async open () {
|
|
107
|
-
let model
|
|
107
|
+
let model = ""
|
|
108
108
|
|
|
109
109
|
/* track download progress when instantiating Transformers engine and model */
|
|
110
110
|
const progressState = new Map<string, number>()
|
|
@@ -138,7 +138,6 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
|
|
|
138
138
|
progress_callback: progressCallback
|
|
139
139
|
})
|
|
140
140
|
this.translator = await pipeline
|
|
141
|
-
clearInterval(interval)
|
|
142
141
|
if (this.translator === null)
|
|
143
142
|
throw new Error("failed to instantiate translator pipeline")
|
|
144
143
|
}
|
|
@@ -151,13 +150,15 @@ export default class SpeechFlowNodeTransformers extends SpeechFlowNode {
|
|
|
151
150
|
progress_callback: progressCallback
|
|
152
151
|
})
|
|
153
152
|
this.generator = await pipeline
|
|
154
|
-
clearInterval(interval)
|
|
155
153
|
if (this.generator === null)
|
|
156
154
|
throw new Error("failed to instantiate generator pipeline")
|
|
157
155
|
}
|
|
158
156
|
else
|
|
159
157
|
throw new Error("invalid model")
|
|
160
158
|
|
|
159
|
+
/* clear progress interval again */
|
|
160
|
+
clearInterval(interval)
|
|
161
|
+
|
|
161
162
|
/* provide text-to-text translation */
|
|
162
163
|
const translate = async (text: string) => {
|
|
163
164
|
if (this.params.model === "OPUS") {
|
|
@@ -23,7 +23,7 @@ export default class SpeechFlowNodeFilter extends SpeechFlowNode {
|
|
|
23
23
|
this.configure({
|
|
24
24
|
type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
|
|
25
25
|
name: { type: "string", pos: 1, val: "filter", match: /^.+?$/ },
|
|
26
|
-
var: { type: "string", pos: 2, val: "", match: /^(?:meta:.+|payload:(?:length|text)|time:(?:start|end))$/ },
|
|
26
|
+
var: { type: "string", pos: 2, val: "", match: /^(?:meta:.+|payload:(?:length|text)|time:(?:start|end)|kind|type)$/ },
|
|
27
27
|
op: { type: "string", pos: 3, val: "==", match: /^(?:<|<=|==|!=|~~|!~|>=|>)$/ },
|
|
28
28
|
val: { type: "string", pos: 4, val: "", match: /^.*$/ }
|
|
29
29
|
})
|
|
@@ -94,13 +94,17 @@ export default class SpeechFlowNodeFilter extends SpeechFlowNode {
|
|
|
94
94
|
const m = self.params.var.match(/^meta:(.+)$/)
|
|
95
95
|
if (m !== null)
|
|
96
96
|
val1 = chunk.meta.get(m[1]) ?? ""
|
|
97
|
-
else if (self.params.
|
|
97
|
+
else if (self.params.var === "kind")
|
|
98
|
+
val1 = chunk.kind
|
|
99
|
+
else if (self.params.var === "type")
|
|
100
|
+
val1 = chunk.type
|
|
101
|
+
else if (self.params.var === "payload:length")
|
|
98
102
|
val1 = chunk.payload.length
|
|
99
|
-
else if (self.params.
|
|
103
|
+
else if (self.params.var === "payload:text")
|
|
100
104
|
val1 = (self.params.type === "text" ? chunk.payload as string : "")
|
|
101
|
-
else if (self.params.
|
|
105
|
+
else if (self.params.var === "time:start")
|
|
102
106
|
val1 = chunk.timestampStart.toMillis()
|
|
103
|
-
else if (self.params.
|
|
107
|
+
else if (self.params.var === "time:end")
|
|
104
108
|
val1 = chunk.timestampEnd.toMillis()
|
|
105
109
|
if (comparison(val1, self.params.op, val2)) {
|
|
106
110
|
self.log("info", `[${self.params.name}]: passing through ${chunk.type} chunk`)
|
|
@@ -24,10 +24,15 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
24
24
|
|
|
25
25
|
/* declare node configuration parameters */
|
|
26
26
|
this.configure({
|
|
27
|
-
type:
|
|
28
|
-
name:
|
|
27
|
+
type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
|
|
28
|
+
name: { type: "string", pos: 1, val: "trace" },
|
|
29
|
+
dashboard: { type: "string", val: "" }
|
|
29
30
|
})
|
|
30
31
|
|
|
32
|
+
/* sanity check parameters */
|
|
33
|
+
if (this.params.dashboard !== "" && this.params.type === "audio")
|
|
34
|
+
throw new Error("only trace nodes of type \"text\" can export to dashboard")
|
|
35
|
+
|
|
31
36
|
/* declare node input/output format */
|
|
32
37
|
this.input = this.params.type
|
|
33
38
|
this.output = this.params.type
|
|
@@ -44,7 +49,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/* provide Transform stream */
|
|
47
|
-
const
|
|
52
|
+
const self = this
|
|
48
53
|
this.stream = new Stream.Transform({
|
|
49
54
|
writableObjectMode: true,
|
|
50
55
|
readableObjectMode: true,
|
|
@@ -63,7 +68,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
63
68
|
} }`
|
|
64
69
|
}
|
|
65
70
|
if (Buffer.isBuffer(chunk.payload)) {
|
|
66
|
-
if (type === "audio")
|
|
71
|
+
if (self.params.type === "audio")
|
|
67
72
|
log("debug", `chunk: type=${chunk.type} ` +
|
|
68
73
|
`kind=${chunk.kind} ` +
|
|
69
74
|
`start=${fmtTime(chunk.timestampStart)} ` +
|
|
@@ -71,19 +76,22 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
|
|
|
71
76
|
`payload-type=Buffer payload-length=${chunk.payload.byteLength} ` +
|
|
72
77
|
`meta=${fmtMeta(chunk.meta)}`)
|
|
73
78
|
else
|
|
74
|
-
error = new Error(`${type} chunk: seen Buffer instead of String chunk type`)
|
|
79
|
+
error = new Error(`${self.params.type} chunk: seen Buffer instead of String chunk type`)
|
|
75
80
|
}
|
|
76
81
|
else {
|
|
77
|
-
if (type === "text")
|
|
78
|
-
log("debug",
|
|
82
|
+
if (self.params.type === "text") {
|
|
83
|
+
log("debug", `chunk: type=${chunk.type} ` +
|
|
79
84
|
`kind=${chunk.kind} ` +
|
|
80
85
|
`start=${fmtTime(chunk.timestampStart)} ` +
|
|
81
86
|
`end=${fmtTime(chunk.timestampEnd)} ` +
|
|
82
87
|
`payload-type=String payload-length=${chunk.payload.length} ` +
|
|
83
88
|
`payload-content="${chunk.payload.toString()}" ` +
|
|
84
89
|
`meta=${fmtMeta(chunk.meta)}`)
|
|
90
|
+
if (self.params.dashboard !== "")
|
|
91
|
+
self.dashboardInfo("text", self.params.dashboard, chunk.kind, chunk.payload.toString())
|
|
92
|
+
}
|
|
85
93
|
else
|
|
86
|
-
error = new Error(`${type} chunk: seen String instead of Buffer chunk type`)
|
|
94
|
+
error = new Error(`${self.params.type} chunk: seen String instead of Buffer chunk type`)
|
|
87
95
|
}
|
|
88
96
|
if (error !== undefined)
|
|
89
97
|
callback(error)
|
|
@@ -159,8 +159,7 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
159
159
|
|
|
160
160
|
/* convert regular stream into object-mode stream */
|
|
161
161
|
const wrapper = utils.createTransformStreamForReadableSide("audio", () => this.timeZero)
|
|
162
|
-
this.stream.
|
|
163
|
-
this.stream = wrapper
|
|
162
|
+
this.stream = Stream.compose(this.stream, wrapper)
|
|
164
163
|
}
|
|
165
164
|
else if (this.params.mode === "w") {
|
|
166
165
|
/* output device */
|
|
@@ -180,8 +179,7 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
180
179
|
|
|
181
180
|
/* convert regular stream into object-mode stream */
|
|
182
181
|
const wrapper = utils.createTransformStreamForWritableSide()
|
|
183
|
-
|
|
184
|
-
this.stream = wrapper
|
|
182
|
+
this.stream = Stream.compose(wrapper, this.stream)
|
|
185
183
|
}
|
|
186
184
|
else
|
|
187
185
|
throw new Error(`device "${device.id}" does not have any input or output channels`)
|
|
@@ -200,13 +198,19 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
|
|
|
200
198
|
/* shutdown PortAudio */
|
|
201
199
|
if (this.io !== null) {
|
|
202
200
|
await new Promise<void>((resolve, reject) => {
|
|
203
|
-
this.io!.abort(() => {
|
|
204
|
-
|
|
201
|
+
this.io!.abort((err?: Error) => {
|
|
202
|
+
if (err)
|
|
203
|
+
reject(err)
|
|
204
|
+
else
|
|
205
|
+
resolve()
|
|
205
206
|
})
|
|
206
207
|
})
|
|
207
208
|
await new Promise<void>((resolve, reject) => {
|
|
208
|
-
this.io!.quit(() => {
|
|
209
|
-
|
|
209
|
+
this.io!.quit((err?: Error) => {
|
|
210
|
+
if (err)
|
|
211
|
+
reject(err)
|
|
212
|
+
else
|
|
213
|
+
resolve()
|
|
210
214
|
})
|
|
211
215
|
})
|
|
212
216
|
this.io = null
|
|
@@ -178,9 +178,15 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
|
|
|
178
178
|
async close () {
|
|
179
179
|
/* shutdown stream */
|
|
180
180
|
if (this.stream !== null) {
|
|
181
|
-
await new Promise<void>((resolve) => {
|
|
182
|
-
if (this.stream instanceof Stream.Writable || this.stream instanceof Stream.Duplex)
|
|
183
|
-
this.stream.end(() => {
|
|
181
|
+
await new Promise<void>((resolve, reject) => {
|
|
182
|
+
if (this.stream instanceof Stream.Writable || this.stream instanceof Stream.Duplex) {
|
|
183
|
+
this.stream.end((err?: Error) => {
|
|
184
|
+
if (err)
|
|
185
|
+
reject(err)
|
|
186
|
+
else
|
|
187
|
+
resolve()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
184
190
|
else
|
|
185
191
|
resolve()
|
|
186
192
|
})
|
|
@@ -83,7 +83,10 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
|
|
|
83
83
|
this.broker.on("connect", (packet: MQTT.IConnackPacket) => {
|
|
84
84
|
this.log("info", `connection opened to MQTT ${this.params.url}`)
|
|
85
85
|
if (this.params.mode !== "w" && !packet.sessionPresent)
|
|
86
|
-
this.broker!.subscribe([ this.params.topicRead ], () => {
|
|
86
|
+
this.broker!.subscribe([ this.params.topicRead ], (err) => {
|
|
87
|
+
if (err)
|
|
88
|
+
this.log("error", `failed to subscribe to MQTT topic "${this.params.topicRead}": ${err.message}`)
|
|
89
|
+
})
|
|
87
90
|
})
|
|
88
91
|
this.broker.on("reconnect", () => {
|
|
89
92
|
this.log("info", `connection re-opened to MQTT ${this.params.url}`)
|
|
@@ -141,7 +144,7 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
|
|
|
141
144
|
|
|
142
145
|
/* close node */
|
|
143
146
|
async close () {
|
|
144
|
-
/* close
|
|
147
|
+
/* close MQTT broker */
|
|
145
148
|
if (this.broker !== null) {
|
|
146
149
|
if (this.broker.connected)
|
|
147
150
|
this.broker.end()
|