speechflow 0.9.9 → 1.1.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 +16 -0
- package/README.md +57 -9
- package/dst/speechflow-node-a2a-ffmpeg.js +1 -0
- package/dst/speechflow-node-a2a-ffmpeg.js.map +1 -0
- package/dst/{speechflow-node-gemma.d.ts → speechflow-node-a2a-meter.d.ts} +2 -3
- package/dst/speechflow-node-a2a-meter.js +151 -0
- package/dst/speechflow-node-a2a-meter.js.map +1 -0
- package/dst/speechflow-node-a2a-mute.d.ts +16 -0
- package/dst/speechflow-node-a2a-mute.js +90 -0
- package/dst/speechflow-node-a2a-mute.js.map +1 -0
- package/dst/speechflow-node-a2a-vad.js +130 -289
- package/dst/speechflow-node-a2a-vad.js.map +1 -0
- package/dst/speechflow-node-a2a-wav.js +1 -0
- package/dst/speechflow-node-a2a-wav.js.map +1 -0
- package/dst/speechflow-node-a2t-deepgram.d.ts +3 -0
- package/dst/speechflow-node-a2t-deepgram.js +18 -2
- package/dst/speechflow-node-a2t-deepgram.js.map +1 -0
- package/dst/speechflow-node-t2a-elevenlabs.d.ts +3 -0
- package/dst/speechflow-node-t2a-elevenlabs.js +9 -1
- package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -0
- package/dst/speechflow-node-t2a-kokoro.js +1 -0
- package/dst/speechflow-node-t2a-kokoro.js.map +1 -0
- package/dst/speechflow-node-t2t-deepl.d.ts +3 -0
- package/dst/speechflow-node-t2t-deepl.js +9 -1
- package/dst/speechflow-node-t2t-deepl.js.map +1 -0
- package/dst/speechflow-node-t2t-format.js +1 -0
- package/dst/speechflow-node-t2t-format.js.map +1 -0
- package/dst/speechflow-node-t2t-ollama.js +1 -0
- package/dst/speechflow-node-t2t-ollama.js.map +1 -0
- package/dst/speechflow-node-t2t-openai.js +2 -1
- package/dst/speechflow-node-t2t-openai.js.map +1 -0
- package/dst/speechflow-node-t2t-subtitle.js +1 -0
- package/dst/speechflow-node-t2t-subtitle.js.map +1 -0
- package/dst/speechflow-node-t2t-transformers.js +10 -6
- package/dst/speechflow-node-t2t-transformers.js.map +1 -0
- package/dst/speechflow-node-x2x-trace.js +2 -1
- package/dst/speechflow-node-x2x-trace.js.map +1 -0
- package/dst/speechflow-node-xio-device.js +4 -1
- package/dst/speechflow-node-xio-device.js.map +1 -0
- package/dst/speechflow-node-xio-file.js +4 -1
- package/dst/speechflow-node-xio-file.js.map +1 -0
- package/dst/speechflow-node-xio-mqtt.js +8 -5
- package/dst/speechflow-node-xio-mqtt.js.map +1 -0
- package/dst/speechflow-node-xio-websocket.js +6 -5
- package/dst/speechflow-node-xio-websocket.js.map +1 -0
- package/dst/speechflow-node.d.ts +8 -1
- package/dst/speechflow-node.js +18 -2
- package/dst/speechflow-node.js.map +1 -0
- package/dst/speechflow-utils.d.ts +33 -0
- package/dst/speechflow-utils.js +183 -1
- package/dst/speechflow-utils.js.map +1 -0
- package/dst/speechflow.js +259 -16
- package/dst/speechflow.js.map +1 -0
- package/etc/speechflow.yaml +9 -7
- package/etc/stx.conf +1 -1
- package/etc/tsconfig.json +2 -2
- package/package.json +19 -12
- package/src/speechflow-node-a2a-meter.ts +129 -0
- package/src/speechflow-node-a2a-mute.ts +101 -0
- package/src/speechflow-node-a2a-vad.ts +266 -0
- package/src/speechflow-node-a2t-deepgram.ts +18 -2
- package/src/speechflow-node-t2a-elevenlabs.ts +9 -1
- package/src/speechflow-node-t2t-deepl.ts +9 -1
- package/src/speechflow-node-t2t-openai.ts +1 -1
- package/src/speechflow-node-t2t-transformers.ts +12 -7
- package/src/speechflow-node-x2x-trace.ts +1 -1
- package/src/speechflow-node-xio-device.ts +4 -1
- package/src/speechflow-node-xio-file.ts +3 -1
- package/src/speechflow-node-xio-mqtt.ts +8 -6
- package/src/speechflow-node-xio-websocket.ts +11 -11
- package/src/speechflow-node.ts +21 -2
- package/src/speechflow-utils.ts +195 -0
- package/src/speechflow.ts +245 -16
- package/dst/speechflow-node-deepgram.d.ts +0 -12
- package/dst/speechflow-node-deepgram.js +0 -220
- package/dst/speechflow-node-deepl.d.ts +0 -12
- package/dst/speechflow-node-deepl.js +0 -128
- package/dst/speechflow-node-device.d.ts +0 -13
- package/dst/speechflow-node-device.js +0 -205
- package/dst/speechflow-node-elevenlabs.d.ts +0 -13
- package/dst/speechflow-node-elevenlabs.js +0 -182
- package/dst/speechflow-node-ffmpeg.d.ts +0 -13
- package/dst/speechflow-node-ffmpeg.js +0 -152
- package/dst/speechflow-node-file.d.ts +0 -11
- package/dst/speechflow-node-file.js +0 -176
- package/dst/speechflow-node-format.d.ts +0 -11
- package/dst/speechflow-node-format.js +0 -80
- package/dst/speechflow-node-gemma.js +0 -213
- package/dst/speechflow-node-mqtt.d.ts +0 -13
- package/dst/speechflow-node-mqtt.js +0 -181
- package/dst/speechflow-node-opus.d.ts +0 -12
- package/dst/speechflow-node-opus.js +0 -135
- package/dst/speechflow-node-subtitle.d.ts +0 -12
- package/dst/speechflow-node-subtitle.js +0 -96
- package/dst/speechflow-node-t2t-gemma.d.ts +0 -13
- package/dst/speechflow-node-t2t-gemma.js +0 -233
- package/dst/speechflow-node-t2t-opus.d.ts +0 -12
- package/dst/speechflow-node-t2t-opus.js +0 -135
- package/dst/speechflow-node-trace.d.ts +0 -11
- package/dst/speechflow-node-trace.js +0 -88
- package/dst/speechflow-node-wav.d.ts +0 -11
- package/dst/speechflow-node-wav.js +0 -170
- package/dst/speechflow-node-websocket.d.ts +0 -13
- package/dst/speechflow-node-websocket.js +0 -275
- package/dst/speechflow-node-whisper-common.d.ts +0 -34
- package/dst/speechflow-node-whisper-common.js +0 -7
- package/dst/speechflow-node-whisper-ggml.d.ts +0 -1
- package/dst/speechflow-node-whisper-ggml.js +0 -97
- package/dst/speechflow-node-whisper-onnx.d.ts +0 -1
- package/dst/speechflow-node-whisper-onnx.js +0 -131
- package/dst/speechflow-node-whisper-worker-ggml.d.ts +0 -1
- package/dst/speechflow-node-whisper-worker-ggml.js +0 -97
- package/dst/speechflow-node-whisper-worker-onnx.d.ts +0 -1
- package/dst/speechflow-node-whisper-worker-onnx.js +0 -131
- package/dst/speechflow-node-whisper-worker.d.ts +0 -1
- package/dst/speechflow-node-whisper-worker.js +0 -116
- package/dst/speechflow-node-whisper-worker2.d.ts +0 -1
- package/dst/speechflow-node-whisper-worker2.js +0 -82
- package/dst/speechflow-node-whisper.d.ts +0 -19
- package/dst/speechflow-node-whisper.js +0 -604
package/src/speechflow-utils.ts
CHANGED
|
@@ -31,6 +31,33 @@ export function audioBufferDuration (
|
|
|
31
31
|
return totalSamples / sampleRate
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* calculate duration of an audio array */
|
|
35
|
+
export function audioArrayDuration (
|
|
36
|
+
arr: Float32Array,
|
|
37
|
+
sampleRate = 48000,
|
|
38
|
+
channels = 1
|
|
39
|
+
) {
|
|
40
|
+
const totalSamples = arr.length / channels
|
|
41
|
+
return totalSamples / sampleRate
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* helper function: convert Buffer in PCM/I16 to Float32Array in PCM/F32 format */
|
|
45
|
+
export function convertBufToF32 (buf: Buffer, littleEndian = true) {
|
|
46
|
+
const dataView = new DataView(buf.buffer)
|
|
47
|
+
const arr = new Float32Array(buf.length / 2)
|
|
48
|
+
for (let i = 0; i < arr.length; i++)
|
|
49
|
+
arr[i] = dataView.getInt16(i * 2, littleEndian) / 32768
|
|
50
|
+
return arr
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* helper function: convert Float32Array in PCM/F32 to Buffer in PCM/I16 format */
|
|
54
|
+
export function convertF32ToBuf (arr: Float32Array) {
|
|
55
|
+
const int16Array = new Int16Array(arr.length)
|
|
56
|
+
for (let i = 0; i < arr.length; i++)
|
|
57
|
+
int16Array[i] = Math.max(-32768, Math.min(32767, Math.round(arr[i] * 32768)))
|
|
58
|
+
return Buffer.from(int16Array.buffer)
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
/* create a Duplex/Transform stream which has
|
|
35
62
|
object-mode on Writable side and buffer/string-mode on Readable side */
|
|
36
63
|
export function createTransformStreamForWritableSide () {
|
|
@@ -210,3 +237,171 @@ export class DoubleQueue<T0, T1> extends EventEmitter {
|
|
|
210
237
|
})
|
|
211
238
|
}
|
|
212
239
|
}
|
|
240
|
+
|
|
241
|
+
/* queue element */
|
|
242
|
+
export type QueueElement = { type: string }
|
|
243
|
+
|
|
244
|
+
/* queue pointer */
|
|
245
|
+
export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
246
|
+
/* internal state */
|
|
247
|
+
private index = 0
|
|
248
|
+
|
|
249
|
+
/* construction */
|
|
250
|
+
constructor (
|
|
251
|
+
private name: string,
|
|
252
|
+
private queue: Queue<T>
|
|
253
|
+
) {
|
|
254
|
+
super()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* positioning operations */
|
|
258
|
+
maxPosition () {
|
|
259
|
+
return this.queue.elements.length
|
|
260
|
+
}
|
|
261
|
+
position (index?: number): number {
|
|
262
|
+
if (index !== undefined) {
|
|
263
|
+
this.index = index
|
|
264
|
+
if (this.index < 0)
|
|
265
|
+
this.index = 0
|
|
266
|
+
else if (this.index >= this.queue.elements.length)
|
|
267
|
+
this.index = this.queue.elements.length
|
|
268
|
+
this.emit("position", this.index)
|
|
269
|
+
}
|
|
270
|
+
return this.index
|
|
271
|
+
}
|
|
272
|
+
walk (num: number) {
|
|
273
|
+
if (num > 0) {
|
|
274
|
+
for (let i = 0; i < num && this.index < this.queue.elements.length; i++)
|
|
275
|
+
this.index++
|
|
276
|
+
this.emit("position", { start: this.index })
|
|
277
|
+
}
|
|
278
|
+
else if (num < 0) {
|
|
279
|
+
for (let i = 0; i < Math.abs(num) && this.index > 0; i++)
|
|
280
|
+
this.index--
|
|
281
|
+
this.emit("position", { start: this.index })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
walkForwardUntil (type: T["type"]) {
|
|
285
|
+
while (this.index < this.queue.elements.length
|
|
286
|
+
&& this.queue.elements[this.index].type !== type)
|
|
287
|
+
this.index++
|
|
288
|
+
this.emit("position", { start: this.index })
|
|
289
|
+
}
|
|
290
|
+
walkBackwardUntil (type: T["type"]) {
|
|
291
|
+
while (this.index > 0
|
|
292
|
+
&& this.queue.elements[this.index].type !== type)
|
|
293
|
+
this.index--
|
|
294
|
+
this.emit("position", { start: this.index })
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* search operations */
|
|
298
|
+
searchForward (type: T["type"]) {
|
|
299
|
+
let position = this.index
|
|
300
|
+
while (position < this.queue.elements.length
|
|
301
|
+
&& this.queue.elements[position].type !== type)
|
|
302
|
+
position++
|
|
303
|
+
this.emit("search", { start: this.index, end: position })
|
|
304
|
+
return position
|
|
305
|
+
}
|
|
306
|
+
searchBackward (type: T["type"]) {
|
|
307
|
+
let position = this.index
|
|
308
|
+
while (position > 0
|
|
309
|
+
&& this.queue.elements[position].type !== type)
|
|
310
|
+
position--
|
|
311
|
+
this.emit("search", { start: position, end: this.index })
|
|
312
|
+
return position
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* reading operations */
|
|
316
|
+
peek (position?: number) {
|
|
317
|
+
if (position === undefined)
|
|
318
|
+
position = this.index
|
|
319
|
+
else {
|
|
320
|
+
if (position < 0)
|
|
321
|
+
position = 0
|
|
322
|
+
else if (position > this.queue.elements.length)
|
|
323
|
+
position = this.queue.elements.length
|
|
324
|
+
}
|
|
325
|
+
const element = this.queue.elements[position]
|
|
326
|
+
this.queue.emit("read", { start: position, end: position })
|
|
327
|
+
return element
|
|
328
|
+
}
|
|
329
|
+
read () {
|
|
330
|
+
const element = this.queue.elements[this.index]
|
|
331
|
+
if (this.index < this.queue.elements.length)
|
|
332
|
+
this.index++
|
|
333
|
+
this.queue.emit("read", { start: this.index - 1, end: this.index - 1 })
|
|
334
|
+
return element
|
|
335
|
+
}
|
|
336
|
+
slice (size?: number) {
|
|
337
|
+
let slice: T[]
|
|
338
|
+
const start = this.index
|
|
339
|
+
if (size !== undefined) {
|
|
340
|
+
if (size < 0)
|
|
341
|
+
size = 0
|
|
342
|
+
else if (size > this.queue.elements.length - this.index)
|
|
343
|
+
size = this.queue.elements.length - this.index
|
|
344
|
+
slice = this.queue.elements.slice(this.index, size)
|
|
345
|
+
this.index += size
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
slice = this.queue.elements.slice(this.index)
|
|
349
|
+
this.index = this.queue.elements.length
|
|
350
|
+
}
|
|
351
|
+
this.queue.emit("read", { start, end: this.index })
|
|
352
|
+
return slice
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* writing operations */
|
|
356
|
+
touch () {
|
|
357
|
+
if (this.index >= this.queue.elements.length)
|
|
358
|
+
throw new Error("cannot touch after last element")
|
|
359
|
+
this.queue.emit("write", { start: this.index, end: this.index + 1 })
|
|
360
|
+
}
|
|
361
|
+
append (element: T) {
|
|
362
|
+
this.queue.elements.push(element)
|
|
363
|
+
this.index = this.queue.elements.length
|
|
364
|
+
this.queue.emit("write", { start: this.index - 1, end: this.index - 1 })
|
|
365
|
+
}
|
|
366
|
+
insert (element: T) {
|
|
367
|
+
this.queue.elements.splice(this.index++, 0, element)
|
|
368
|
+
this.queue.emit("write", { start: this.index - 1, end: this.index })
|
|
369
|
+
}
|
|
370
|
+
delete () {
|
|
371
|
+
if (this.index >= this.queue.elements.length)
|
|
372
|
+
throw new Error("cannot delete after last element")
|
|
373
|
+
this.queue.elements.splice(this.index, 1)
|
|
374
|
+
this.queue.emit("write", { start: this.index, end: this.index })
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* queue */
|
|
379
|
+
export class Queue<T extends QueueElement> extends EventEmitter {
|
|
380
|
+
public elements: T[] = []
|
|
381
|
+
private pointers = new Map<string, QueuePointer<T>>()
|
|
382
|
+
pointerUse (name: string): QueuePointer<T> {
|
|
383
|
+
if (!this.pointers.has(name))
|
|
384
|
+
this.pointers.set(name, new QueuePointer<T>(name, this))
|
|
385
|
+
return this.pointers.get(name)!
|
|
386
|
+
}
|
|
387
|
+
pointerDelete (name: string): void {
|
|
388
|
+
if (!this.pointers.has(name))
|
|
389
|
+
throw new Error("pointer not exists")
|
|
390
|
+
this.pointers.delete(name)
|
|
391
|
+
}
|
|
392
|
+
trim (): void {
|
|
393
|
+
/* determine minimum pointer position */
|
|
394
|
+
let min = this.elements.length
|
|
395
|
+
for (const pointer of this.pointers.values())
|
|
396
|
+
if (min > pointer.position())
|
|
397
|
+
min = pointer.position()
|
|
398
|
+
|
|
399
|
+
/* trim the maximum amount of first elements */
|
|
400
|
+
this.elements.splice(0, min)
|
|
401
|
+
|
|
402
|
+
/* shift all pointers */
|
|
403
|
+
for (const pointer of this.pointers.values())
|
|
404
|
+
pointer.position(pointer.position() - min)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
package/src/speechflow.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
import path from "node:path"
|
|
9
9
|
import Stream from "node:stream"
|
|
10
10
|
import { EventEmitter } from "node:events"
|
|
11
|
+
import http from "node:http"
|
|
12
|
+
import * as HAPI from "@hapi/hapi"
|
|
13
|
+
import WebSocket from "ws"
|
|
14
|
+
import HAPIWebSocket from "hapi-plugin-websocket"
|
|
15
|
+
import HAPIHeader from "hapi-plugin-header"
|
|
11
16
|
|
|
12
17
|
/* external dependencies */
|
|
13
18
|
import { DateTime } from "luxon"
|
|
@@ -20,6 +25,9 @@ import objectPath from "object-path"
|
|
|
20
25
|
import installedPackages from "installed-packages"
|
|
21
26
|
import dotenvx from "@dotenvx/dotenvx"
|
|
22
27
|
import syspath from "syspath"
|
|
28
|
+
import * as arktype from "arktype"
|
|
29
|
+
import Table from "cli-table3"
|
|
30
|
+
import chalk from "chalk"
|
|
23
31
|
|
|
24
32
|
/* internal dependencies */
|
|
25
33
|
import SpeechFlowNode from "./speechflow-node"
|
|
@@ -28,6 +36,15 @@ import pkg from "../package.json"
|
|
|
28
36
|
/* central CLI context */
|
|
29
37
|
let cli: CLIio | null = null
|
|
30
38
|
|
|
39
|
+
type wsPeerCtx = {
|
|
40
|
+
peer: string
|
|
41
|
+
}
|
|
42
|
+
type wsPeerInfo = {
|
|
43
|
+
ctx: wsPeerCtx
|
|
44
|
+
ws: WebSocket
|
|
45
|
+
req: http.IncomingMessage
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
/* establish asynchronous environment */
|
|
32
49
|
;(async () => {
|
|
33
50
|
/* determine system paths */
|
|
@@ -45,12 +62,16 @@ let cli: CLIio | null = null
|
|
|
45
62
|
"[-h|--help] " +
|
|
46
63
|
"[-V|--version] " +
|
|
47
64
|
"[-v|--verbose <level>] " +
|
|
65
|
+
"[-a|--address <ip-address>] " +
|
|
66
|
+
"[-p|--port <tcp-port>] " +
|
|
48
67
|
"[-C|--cache <directory>] " +
|
|
68
|
+
"[-S|--status] " +
|
|
49
69
|
"[-e|--expression <expression>] " +
|
|
50
70
|
"[-f|--file <file>] " +
|
|
51
71
|
"[-c|--config <id>@<yaml-config-file>] " +
|
|
52
72
|
"[<argument> [...]]"
|
|
53
73
|
)
|
|
74
|
+
.version(false)
|
|
54
75
|
.option("V", {
|
|
55
76
|
alias: "version",
|
|
56
77
|
type: "boolean",
|
|
@@ -68,6 +89,24 @@ let cli: CLIio | null = null
|
|
|
68
89
|
default: "warning",
|
|
69
90
|
describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
|
|
70
91
|
})
|
|
92
|
+
.option("a", {
|
|
93
|
+
alias: "address",
|
|
94
|
+
type: "string",
|
|
95
|
+
array: false,
|
|
96
|
+
coerce,
|
|
97
|
+
nargs: 1,
|
|
98
|
+
default: "0.0.0.0",
|
|
99
|
+
describe: "IP address for REST/WebSocket API"
|
|
100
|
+
})
|
|
101
|
+
.option("p", {
|
|
102
|
+
alias: "port",
|
|
103
|
+
type: "number",
|
|
104
|
+
array: false,
|
|
105
|
+
coerce,
|
|
106
|
+
nargs: 1,
|
|
107
|
+
default: 8484,
|
|
108
|
+
describe: "TCP port for REST/WebSocket API"
|
|
109
|
+
})
|
|
71
110
|
.option("C", {
|
|
72
111
|
alias: "cache",
|
|
73
112
|
type: "string",
|
|
@@ -77,6 +116,14 @@ let cli: CLIio | null = null
|
|
|
77
116
|
default: path.join(dataDir, "cache"),
|
|
78
117
|
describe: "directory for cached files (primarily AI model files)"
|
|
79
118
|
})
|
|
119
|
+
.option("S", {
|
|
120
|
+
alias: "status",
|
|
121
|
+
type: "boolean",
|
|
122
|
+
array: false,
|
|
123
|
+
coerce,
|
|
124
|
+
default: false,
|
|
125
|
+
describe: "show one-time status of nodes"
|
|
126
|
+
})
|
|
80
127
|
.option("e", {
|
|
81
128
|
alias: "expression",
|
|
82
129
|
type: "string",
|
|
@@ -107,7 +154,6 @@ let cli: CLIio | null = null
|
|
|
107
154
|
.help("h", "show usage help")
|
|
108
155
|
.alias("h", "help")
|
|
109
156
|
.showHelpOnFail(true)
|
|
110
|
-
.version(false)
|
|
111
157
|
.strict()
|
|
112
158
|
.demand(0)
|
|
113
159
|
.parse(hideBin(process.argv))
|
|
@@ -186,6 +232,9 @@ let cli: CLIio | null = null
|
|
|
186
232
|
const pkgsI = [
|
|
187
233
|
"./speechflow-node-a2a-ffmpeg.js",
|
|
188
234
|
"./speechflow-node-a2a-wav.js",
|
|
235
|
+
"./speechflow-node-a2a-mute.js",
|
|
236
|
+
"./speechflow-node-a2a-meter.js",
|
|
237
|
+
"./speechflow-node-a2a-vad.js",
|
|
189
238
|
"./speechflow-node-a2t-deepgram.js",
|
|
190
239
|
"./speechflow-node-t2a-elevenlabs.js",
|
|
191
240
|
"./speechflow-node-t2a-kokoro.js",
|
|
@@ -193,7 +242,6 @@ let cli: CLIio | null = null
|
|
|
193
242
|
"./speechflow-node-t2t-openai.js",
|
|
194
243
|
"./speechflow-node-t2t-ollama.js",
|
|
195
244
|
"./speechflow-node-t2t-transformers.js",
|
|
196
|
-
"./speechflow-node-t2t-opus.js",
|
|
197
245
|
"./speechflow-node-t2t-subtitle.js",
|
|
198
246
|
"./speechflow-node-t2t-format.js",
|
|
199
247
|
"./speechflow-node-x2x-trace.js",
|
|
@@ -231,15 +279,7 @@ let cli: CLIio | null = null
|
|
|
231
279
|
}
|
|
232
280
|
}
|
|
233
281
|
|
|
234
|
-
/*
|
|
235
|
-
const flowlink = new FlowLink<SpeechFlowNode>({
|
|
236
|
-
trace: (msg: string) => {
|
|
237
|
-
cli!.log("debug", msg)
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
let nodenum = 1
|
|
241
|
-
const variables = { argv: args._, env: process.env }
|
|
242
|
-
const graphNodes = new Set<SpeechFlowNode>()
|
|
282
|
+
/* static configuration */
|
|
243
283
|
const cfg = {
|
|
244
284
|
audioChannels: 1,
|
|
245
285
|
audioBitDepth: 16,
|
|
@@ -248,6 +288,45 @@ let cli: CLIio | null = null
|
|
|
248
288
|
textEncoding: "utf8",
|
|
249
289
|
cacheDir: args.C
|
|
250
290
|
}
|
|
291
|
+
|
|
292
|
+
/* handle one-time status query of nodes */
|
|
293
|
+
if (args.S) {
|
|
294
|
+
const table = new Table({
|
|
295
|
+
head: [
|
|
296
|
+
chalk.reset.bold("NODE"),
|
|
297
|
+
chalk.reset.bold("PROPERTY"),
|
|
298
|
+
chalk.reset.bold("VALUE")
|
|
299
|
+
],
|
|
300
|
+
colWidths: [ 15, 15, 50 - (2 * 2 + 2 * 3) ],
|
|
301
|
+
style: { "padding-left": 1, "padding-right": 1, border: [ "grey" ], compact: true },
|
|
302
|
+
chars: { "left-mid": "", mid: "", "mid-mid": "", "right-mid": "" }
|
|
303
|
+
})
|
|
304
|
+
for (const name of Object.keys(nodes)) {
|
|
305
|
+
cli!.log("info", `gathering status of node <${name}>`)
|
|
306
|
+
const node = new nodes[name](name, cfg, {}, [])
|
|
307
|
+
const status = await node.status()
|
|
308
|
+
if (Object.keys(status).length > 0) {
|
|
309
|
+
let first = true
|
|
310
|
+
for (const key of Object.keys(status)) {
|
|
311
|
+
table.push([ first ? chalk.bold(name) : "", key, chalk.blue(status[key]) ])
|
|
312
|
+
first = false
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const output = table.toString()
|
|
317
|
+
process.stdout.write(output + "\n")
|
|
318
|
+
process.exit(0)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* graph processing: PASS 1: parse DSL and create and connect nodes */
|
|
322
|
+
const flowlink = new FlowLink<SpeechFlowNode>({
|
|
323
|
+
trace: (msg: string) => {
|
|
324
|
+
cli!.log("debug", msg)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
const variables = { argv: args._, env: process.env }
|
|
328
|
+
const graphNodes = new Set<SpeechFlowNode>()
|
|
329
|
+
const nodeNums = new Map<typeof SpeechFlowNode, number>()
|
|
251
330
|
let ast: unknown
|
|
252
331
|
try {
|
|
253
332
|
ast = flowlink.compile(config)
|
|
@@ -275,17 +354,19 @@ let cli: CLIio | null = null
|
|
|
275
354
|
throw new Error(`unknown node "${id}"`)
|
|
276
355
|
let node: SpeechFlowNode
|
|
277
356
|
try {
|
|
278
|
-
|
|
357
|
+
let num = nodeNums.get(nodes[id]) ?? 0
|
|
358
|
+
nodeNums.set(nodes[id], ++num)
|
|
359
|
+
const name = num === 1 ? id : `${id}:${num}`
|
|
360
|
+
node = new nodes[id](name, cfg, opts, args)
|
|
279
361
|
}
|
|
280
362
|
catch (err) {
|
|
281
363
|
/* fatal error */
|
|
282
364
|
if (err instanceof Error)
|
|
283
|
-
cli!.log("error", `creation of
|
|
365
|
+
cli!.log("error", `creation of <${id}> node failed: ${err.message}`)
|
|
284
366
|
else
|
|
285
|
-
cli!.log("error", `creation of
|
|
367
|
+
cli!.log("error", `creation of <${id}> node failed: ${err}`)
|
|
286
368
|
process.exit(1)
|
|
287
369
|
}
|
|
288
|
-
nodenum++
|
|
289
370
|
const params = Object.keys(node.params)
|
|
290
371
|
.map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
|
|
291
372
|
cli!.log("info", `create node "${node.id}" (${params})`)
|
|
@@ -403,6 +484,150 @@ let cli: CLIio | null = null
|
|
|
403
484
|
})
|
|
404
485
|
}
|
|
405
486
|
|
|
487
|
+
/* define external request/response structure */
|
|
488
|
+
const requestValidator = arktype.type({
|
|
489
|
+
request: "string",
|
|
490
|
+
node: "string",
|
|
491
|
+
args: "unknown[]"
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
/* forward external request to target node in graph */
|
|
495
|
+
const consumeExternalRequest = async (_req: any) => {
|
|
496
|
+
const req = requestValidator(_req)
|
|
497
|
+
if (req instanceof arktype.type.errors)
|
|
498
|
+
throw new Error(`invalid request: ${req.summary}`)
|
|
499
|
+
if (req.request !== "COMMAND")
|
|
500
|
+
throw new Error("invalid external request (command expected)")
|
|
501
|
+
const name = req.node as string
|
|
502
|
+
const args = req.args as any[]
|
|
503
|
+
const foundNode = Array.from(graphNodes).find((node) => node.id === name)
|
|
504
|
+
if (foundNode === undefined)
|
|
505
|
+
cli!.log("warning", `external request failed: no such node <${name}>`)
|
|
506
|
+
else {
|
|
507
|
+
await foundNode.receiveRequest(args).catch((err: Error) => {
|
|
508
|
+
cli!.log("warning", `external request to node <${name}> failed: ${err}`)
|
|
509
|
+
throw new Error(`external request to node <${name}> failed: ${err}`)
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* establish REST/WebSocket API */
|
|
515
|
+
const wsPeers = new Map<string, wsPeerInfo>()
|
|
516
|
+
const hapi = new HAPI.Server({
|
|
517
|
+
address: args.a,
|
|
518
|
+
port: args.p
|
|
519
|
+
})
|
|
520
|
+
await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
|
|
521
|
+
await hapi.register({ plugin: HAPIWebSocket })
|
|
522
|
+
hapi.events.on("response", (request: HAPI.Request) => {
|
|
523
|
+
let protocol = `HTTP/${request.raw.req.httpVersion}`
|
|
524
|
+
const ws = request.websocket()
|
|
525
|
+
if (ws.mode === "websocket") {
|
|
526
|
+
const wsVersion = (ws.ws as any).protocolVersion ??
|
|
527
|
+
request.headers["sec-websocket-version"] ?? "13?"
|
|
528
|
+
protocol = `WebSocket/${wsVersion}+${protocol}`
|
|
529
|
+
}
|
|
530
|
+
const msg =
|
|
531
|
+
"remote=" + request.info.remoteAddress + ", " +
|
|
532
|
+
"method=" + request.method.toUpperCase() + ", " +
|
|
533
|
+
"url=" + request.url.pathname + ", " +
|
|
534
|
+
"protocol=" + protocol + ", " +
|
|
535
|
+
"response=" + ("statusCode" in request.response ? request.response.statusCode : "<unknown>")
|
|
536
|
+
cli!.log("info", `HAPI: request: ${msg}`)
|
|
537
|
+
})
|
|
538
|
+
hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
|
|
539
|
+
if (event.error instanceof Error)
|
|
540
|
+
cli!.log("error", `HAPI: request-error: ${event.error.message}`)
|
|
541
|
+
else
|
|
542
|
+
cli!.log("error", `HAPI: request-error: ${event.error}`)
|
|
543
|
+
})
|
|
544
|
+
hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
|
|
545
|
+
if (tags.error) {
|
|
546
|
+
const err = event.error
|
|
547
|
+
if (err instanceof Error)
|
|
548
|
+
cli!.log("error", `HAPI: log: ${err.message}`)
|
|
549
|
+
else
|
|
550
|
+
cli!.log("error", `HAPI: log: ${err}`)
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
hapi.route({
|
|
554
|
+
method: "GET",
|
|
555
|
+
path: "/api/{req}/{node}/{params*}",
|
|
556
|
+
options: {
|
|
557
|
+
},
|
|
558
|
+
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
559
|
+
const peer = request.info.remoteAddress
|
|
560
|
+
const req = {
|
|
561
|
+
request: request.params.req,
|
|
562
|
+
node: request.params.node,
|
|
563
|
+
args: (request.params.params as string ?? "").split("/").filter((seg) => seg !== "")
|
|
564
|
+
}
|
|
565
|
+
cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
|
|
566
|
+
return consumeExternalRequest(req).then(() => {
|
|
567
|
+
return h.response({ response: "OK" }).code(200)
|
|
568
|
+
}).catch((err) => {
|
|
569
|
+
return h.response({ response: "ERROR", data: err.message }).code(417)
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
hapi.route({
|
|
574
|
+
method: "POST",
|
|
575
|
+
path: "/api",
|
|
576
|
+
options: {
|
|
577
|
+
payload: {
|
|
578
|
+
output: "data",
|
|
579
|
+
parse: true,
|
|
580
|
+
allow: "application/json"
|
|
581
|
+
},
|
|
582
|
+
plugins: {
|
|
583
|
+
websocket: {
|
|
584
|
+
autoping: 30 * 1000,
|
|
585
|
+
connect: (args: any) => {
|
|
586
|
+
const ctx: wsPeerCtx = args.ctx
|
|
587
|
+
const ws: WebSocket = args.ws
|
|
588
|
+
const req: http.IncomingMessage = args.req
|
|
589
|
+
const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
|
|
590
|
+
ctx.peer = peer
|
|
591
|
+
wsPeers.set(peer, { ctx, ws, req })
|
|
592
|
+
cli!.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
|
|
593
|
+
},
|
|
594
|
+
disconnect: (args: any) => {
|
|
595
|
+
const ctx: wsPeerCtx = args.ctx
|
|
596
|
+
const peer = ctx.peer
|
|
597
|
+
wsPeers.delete(peer)
|
|
598
|
+
cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
604
|
+
/* on WebSocket message transfer */
|
|
605
|
+
const peer = request.info.remoteAddress
|
|
606
|
+
const req = requestValidator(request.payload)
|
|
607
|
+
if (req instanceof arktype.type.errors)
|
|
608
|
+
return h.response({ response: "ERROR", data: `invalid request: ${req.summary}` }).code(417)
|
|
609
|
+
cli!.log("info", `HAPI: peer ${peer}: POST: ${JSON.stringify(req)}`)
|
|
610
|
+
return consumeExternalRequest(req).then(() => {
|
|
611
|
+
return h.response({ response: "OK" }).code(200)
|
|
612
|
+
}).catch((err: Error) => {
|
|
613
|
+
return h.response({ response: "ERROR", data: err.message }).code(417)
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
await hapi.start()
|
|
618
|
+
cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.address}:${args.port}`)
|
|
619
|
+
|
|
620
|
+
/* hook for sendResponse method of nodes */
|
|
621
|
+
for (const node of graphNodes) {
|
|
622
|
+
node.on("send-response", (args: any[]) => {
|
|
623
|
+
const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
|
|
624
|
+
for (const [ peer, info ] of wsPeers.entries()) {
|
|
625
|
+
cli!.log("info", `HAPI: peer ${peer}: ${data}`)
|
|
626
|
+
info.ws.send(data)
|
|
627
|
+
}
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
406
631
|
/* start of internal stream processing */
|
|
407
632
|
cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
|
|
408
633
|
|
|
@@ -417,6 +642,10 @@ let cli: CLIio | null = null
|
|
|
417
642
|
else
|
|
418
643
|
cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
|
|
419
644
|
|
|
645
|
+
/* shutdown HAPI service */
|
|
646
|
+
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
|
|
647
|
+
await hapi.stop()
|
|
648
|
+
|
|
420
649
|
/* graph processing: PASS 1: disconnect node streams */
|
|
421
650
|
for (const node of graphNodes) {
|
|
422
651
|
if (node.stream === null) {
|
|
@@ -491,7 +720,7 @@ let cli: CLIio | null = null
|
|
|
491
720
|
if (cli !== null)
|
|
492
721
|
cli.log("error", err.message)
|
|
493
722
|
else
|
|
494
|
-
process.stderr.write(`${pkg.name}: ERROR: ${err.message}\n`)
|
|
723
|
+
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message} ${err.stack}\n`)
|
|
495
724
|
process.exit(1)
|
|
496
725
|
})
|
|
497
726
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import SpeechFlowNode from "./speechflow-node";
|
|
2
|
-
export default class SpeechFlowNodeDeepgram extends SpeechFlowNode {
|
|
3
|
-
static name: string;
|
|
4
|
-
private dg;
|
|
5
|
-
constructor(id: string, cfg: {
|
|
6
|
-
[id: string]: any;
|
|
7
|
-
}, opts: {
|
|
8
|
-
[id: string]: any;
|
|
9
|
-
}, args: any[]);
|
|
10
|
-
open(): Promise<void>;
|
|
11
|
-
close(): Promise<void>;
|
|
12
|
-
}
|