speechflow 0.9.9 → 1.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.
- package/CHANGELOG.md +8 -0
- package/README.md +48 -1
- 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 +147 -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.js +2 -1
- package/dst/speechflow-node-a2t-deepgram.js.map +1 -0
- package/dst/speechflow-node-t2a-elevenlabs.js +1 -0
- 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.js +1 -0
- 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 +1 -0
- 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 +1 -0
- package/dst/speechflow-node-x2x-trace.js.map +1 -0
- package/dst/speechflow-node-xio-device.js +1 -0
- package/dst/speechflow-node-xio-device.js.map +1 -0
- package/dst/speechflow-node-xio-file.js +1 -0
- package/dst/speechflow-node-xio-file.js.map +1 -0
- package/dst/speechflow-node-xio-mqtt.js +1 -0
- package/dst/speechflow-node-xio-mqtt.js.map +1 -0
- package/dst/speechflow-node-xio-websocket.js +1 -0
- package/dst/speechflow-node-xio-websocket.js.map +1 -0
- package/dst/speechflow-node.d.ts +3 -0
- package/dst/speechflow-node.js +10 -0
- 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 +209 -6
- package/dst/speechflow.js.map +1 -0
- package/etc/speechflow.yaml +5 -3
- package/etc/stx.conf +1 -1
- package/etc/tsconfig.json +2 -2
- package/package.json +14 -8
- package/src/speechflow-node-a2a-meter.ts +125 -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 +1 -1
- package/src/speechflow-node-t2t-transformers.ts +12 -7
- package/src/speechflow-node-xio-websocket.ts +5 -5
- package/src/speechflow-node.ts +12 -0
- package/src/speechflow-utils.ts +195 -0
- package/src/speechflow.ts +193 -6
- 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,7 @@ 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"
|
|
23
29
|
|
|
24
30
|
/* internal dependencies */
|
|
25
31
|
import SpeechFlowNode from "./speechflow-node"
|
|
@@ -28,6 +34,15 @@ import pkg from "../package.json"
|
|
|
28
34
|
/* central CLI context */
|
|
29
35
|
let cli: CLIio | null = null
|
|
30
36
|
|
|
37
|
+
type wsPeerCtx = {
|
|
38
|
+
peer: string
|
|
39
|
+
}
|
|
40
|
+
type wsPeerInfo = {
|
|
41
|
+
ctx: wsPeerCtx
|
|
42
|
+
ws: WebSocket
|
|
43
|
+
req: http.IncomingMessage
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/* establish asynchronous environment */
|
|
32
47
|
;(async () => {
|
|
33
48
|
/* determine system paths */
|
|
@@ -45,6 +60,8 @@ let cli: CLIio | null = null
|
|
|
45
60
|
"[-h|--help] " +
|
|
46
61
|
"[-V|--version] " +
|
|
47
62
|
"[-v|--verbose <level>] " +
|
|
63
|
+
"[-a|--address <ip-address>] " +
|
|
64
|
+
"[-p|--port <tcp-port>] " +
|
|
48
65
|
"[-C|--cache <directory>] " +
|
|
49
66
|
"[-e|--expression <expression>] " +
|
|
50
67
|
"[-f|--file <file>] " +
|
|
@@ -68,6 +85,24 @@ let cli: CLIio | null = null
|
|
|
68
85
|
default: "warning",
|
|
69
86
|
describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
|
|
70
87
|
})
|
|
88
|
+
.option("a", {
|
|
89
|
+
alias: "address",
|
|
90
|
+
type: "string",
|
|
91
|
+
array: false,
|
|
92
|
+
coerce,
|
|
93
|
+
nargs: 1,
|
|
94
|
+
default: "0.0.0.0",
|
|
95
|
+
describe: "IP address for REST/WebSocket API"
|
|
96
|
+
})
|
|
97
|
+
.option("p", {
|
|
98
|
+
alias: "port",
|
|
99
|
+
type: "number",
|
|
100
|
+
array: false,
|
|
101
|
+
coerce,
|
|
102
|
+
nargs: 1,
|
|
103
|
+
default: 8484,
|
|
104
|
+
describe: "TCP port for REST/WebSocket API"
|
|
105
|
+
})
|
|
71
106
|
.option("C", {
|
|
72
107
|
alias: "cache",
|
|
73
108
|
type: "string",
|
|
@@ -186,6 +221,9 @@ let cli: CLIio | null = null
|
|
|
186
221
|
const pkgsI = [
|
|
187
222
|
"./speechflow-node-a2a-ffmpeg.js",
|
|
188
223
|
"./speechflow-node-a2a-wav.js",
|
|
224
|
+
"./speechflow-node-a2a-mute.js",
|
|
225
|
+
"./speechflow-node-a2a-meter.js",
|
|
226
|
+
"./speechflow-node-a2a-vad.js",
|
|
189
227
|
"./speechflow-node-a2t-deepgram.js",
|
|
190
228
|
"./speechflow-node-t2a-elevenlabs.js",
|
|
191
229
|
"./speechflow-node-t2a-kokoro.js",
|
|
@@ -193,7 +231,6 @@ let cli: CLIio | null = null
|
|
|
193
231
|
"./speechflow-node-t2t-openai.js",
|
|
194
232
|
"./speechflow-node-t2t-ollama.js",
|
|
195
233
|
"./speechflow-node-t2t-transformers.js",
|
|
196
|
-
"./speechflow-node-t2t-opus.js",
|
|
197
234
|
"./speechflow-node-t2t-subtitle.js",
|
|
198
235
|
"./speechflow-node-t2t-format.js",
|
|
199
236
|
"./speechflow-node-x2x-trace.js",
|
|
@@ -237,9 +274,9 @@ let cli: CLIio | null = null
|
|
|
237
274
|
cli!.log("debug", msg)
|
|
238
275
|
}
|
|
239
276
|
})
|
|
240
|
-
let nodenum = 1
|
|
241
277
|
const variables = { argv: args._, env: process.env }
|
|
242
278
|
const graphNodes = new Set<SpeechFlowNode>()
|
|
279
|
+
const nodeNums = new Map<typeof SpeechFlowNode, number>()
|
|
243
280
|
const cfg = {
|
|
244
281
|
audioChannels: 1,
|
|
245
282
|
audioBitDepth: 16,
|
|
@@ -275,17 +312,19 @@ let cli: CLIio | null = null
|
|
|
275
312
|
throw new Error(`unknown node "${id}"`)
|
|
276
313
|
let node: SpeechFlowNode
|
|
277
314
|
try {
|
|
278
|
-
|
|
315
|
+
let num = nodeNums.get(nodes[id]) ?? 0
|
|
316
|
+
nodeNums.set(nodes[id], ++num)
|
|
317
|
+
const name = num === 1 ? id : `${id}:${num}`
|
|
318
|
+
node = new nodes[id](name, cfg, opts, args)
|
|
279
319
|
}
|
|
280
320
|
catch (err) {
|
|
281
321
|
/* fatal error */
|
|
282
322
|
if (err instanceof Error)
|
|
283
|
-
cli!.log("error", `creation of
|
|
323
|
+
cli!.log("error", `creation of <${id}> node failed: ${err.message}`)
|
|
284
324
|
else
|
|
285
|
-
cli!.log("error", `creation of
|
|
325
|
+
cli!.log("error", `creation of <${id}> node failed: ${err}`)
|
|
286
326
|
process.exit(1)
|
|
287
327
|
}
|
|
288
|
-
nodenum++
|
|
289
328
|
const params = Object.keys(node.params)
|
|
290
329
|
.map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
|
|
291
330
|
cli!.log("info", `create node "${node.id}" (${params})`)
|
|
@@ -403,6 +442,150 @@ let cli: CLIio | null = null
|
|
|
403
442
|
})
|
|
404
443
|
}
|
|
405
444
|
|
|
445
|
+
/* define external request/response structure */
|
|
446
|
+
const requestValidator = arktype.type({
|
|
447
|
+
request: "string",
|
|
448
|
+
node: "string",
|
|
449
|
+
args: "unknown[]"
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
/* forward external request to target node in graph */
|
|
453
|
+
const consumeExternalRequest = async (_req: any) => {
|
|
454
|
+
const req = requestValidator(_req)
|
|
455
|
+
if (req instanceof arktype.type.errors)
|
|
456
|
+
throw new Error(`invalid request: ${req.summary}`)
|
|
457
|
+
if (req.request !== "COMMAND")
|
|
458
|
+
throw new Error("invalid external request (command expected)")
|
|
459
|
+
const name = req.node as string
|
|
460
|
+
const args = req.args as any[]
|
|
461
|
+
const foundNode = Array.from(graphNodes).find((node) => node.id === name)
|
|
462
|
+
if (foundNode === undefined)
|
|
463
|
+
cli!.log("warning", `external request failed: no such node <${name}>`)
|
|
464
|
+
else {
|
|
465
|
+
await foundNode.receiveRequest(args).catch((err: Error) => {
|
|
466
|
+
cli!.log("warning", `external request to node <${name}> failed: ${err}`)
|
|
467
|
+
throw new Error(`external request to node <${name}> failed: ${err}`)
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* establish REST/WebSocket API */
|
|
473
|
+
const wsPeers = new Map<string, wsPeerInfo>()
|
|
474
|
+
const hapi = new HAPI.Server({
|
|
475
|
+
address: args.a,
|
|
476
|
+
port: args.p
|
|
477
|
+
})
|
|
478
|
+
await hapi.register({ plugin: HAPIHeader, options: { Server: `${pkg.name}/${pkg.version}` } })
|
|
479
|
+
await hapi.register({ plugin: HAPIWebSocket })
|
|
480
|
+
hapi.events.on("response", (request: HAPI.Request) => {
|
|
481
|
+
let protocol = `HTTP/${request.raw.req.httpVersion}`
|
|
482
|
+
const ws = request.websocket()
|
|
483
|
+
if (ws.mode === "websocket") {
|
|
484
|
+
const wsVersion = (ws.ws as any).protocolVersion ??
|
|
485
|
+
request.headers["sec-websocket-version"] ?? "13?"
|
|
486
|
+
protocol = `WebSocket/${wsVersion}+${protocol}`
|
|
487
|
+
}
|
|
488
|
+
const msg =
|
|
489
|
+
"remote=" + request.info.remoteAddress + ", " +
|
|
490
|
+
"method=" + request.method.toUpperCase() + ", " +
|
|
491
|
+
"url=" + request.url.pathname + ", " +
|
|
492
|
+
"protocol=" + protocol + ", " +
|
|
493
|
+
"response=" + ("statusCode" in request.response ? request.response.statusCode : "<unknown>")
|
|
494
|
+
cli!.log("info", `HAPI: request: ${msg}`)
|
|
495
|
+
})
|
|
496
|
+
hapi.events.on({ name: "request", channels: [ "error" ] }, (request: HAPI.Request, event: HAPI.RequestEvent, tags: { [key: string]: true }) => {
|
|
497
|
+
if (event.error instanceof Error)
|
|
498
|
+
cli!.log("error", `HAPI: request-error: ${event.error.message}`)
|
|
499
|
+
else
|
|
500
|
+
cli!.log("error", `HAPI: request-error: ${event.error}`)
|
|
501
|
+
})
|
|
502
|
+
hapi.events.on("log", (event: HAPI.LogEvent, tags: { [key: string]: true }) => {
|
|
503
|
+
if (tags.error) {
|
|
504
|
+
const err = event.error
|
|
505
|
+
if (err instanceof Error)
|
|
506
|
+
cli!.log("error", `HAPI: log: ${err.message}`)
|
|
507
|
+
else
|
|
508
|
+
cli!.log("error", `HAPI: log: ${err}`)
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
hapi.route({
|
|
512
|
+
method: "GET",
|
|
513
|
+
path: "/api/{req}/{node}/{params*}",
|
|
514
|
+
options: {
|
|
515
|
+
},
|
|
516
|
+
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
517
|
+
const peer = request.info.remoteAddress
|
|
518
|
+
const req = {
|
|
519
|
+
request: request.params.req,
|
|
520
|
+
node: request.params.node,
|
|
521
|
+
args: (request.params.params as string ?? "").split("/").filter((seg) => seg !== "")
|
|
522
|
+
}
|
|
523
|
+
cli!.log("info", `HAPI: peer ${peer}: GET: ${JSON.stringify(req)}`)
|
|
524
|
+
return consumeExternalRequest(req).then(() => {
|
|
525
|
+
return h.response({ response: "OK" }).code(200)
|
|
526
|
+
}).catch((err) => {
|
|
527
|
+
return h.response({ response: "ERROR", data: err.message }).code(417)
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
hapi.route({
|
|
532
|
+
method: "POST",
|
|
533
|
+
path: "/api",
|
|
534
|
+
options: {
|
|
535
|
+
payload: {
|
|
536
|
+
output: "data",
|
|
537
|
+
parse: true,
|
|
538
|
+
allow: "application/json"
|
|
539
|
+
},
|
|
540
|
+
plugins: {
|
|
541
|
+
websocket: {
|
|
542
|
+
autoping: 30 * 1000,
|
|
543
|
+
connect: (args: any) => {
|
|
544
|
+
const ctx: wsPeerCtx = args.ctx
|
|
545
|
+
const ws: WebSocket = args.ws
|
|
546
|
+
const req: http.IncomingMessage = args.req
|
|
547
|
+
const peer = `${req.socket.remoteAddress}:${req.socket.remotePort}`
|
|
548
|
+
ctx.peer = peer
|
|
549
|
+
wsPeers.set(peer, { ctx, ws, req })
|
|
550
|
+
cli!.log("info", `HAPI: WebSocket: connect: peer ${peer}`)
|
|
551
|
+
},
|
|
552
|
+
disconnect: (args: any) => {
|
|
553
|
+
const ctx: wsPeerCtx = args.ctx
|
|
554
|
+
const peer = ctx.peer
|
|
555
|
+
wsPeers.delete(peer)
|
|
556
|
+
cli!.log("info", `HAPI: WebSocket: disconnect: peer ${peer}`)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
handler: (request: HAPI.Request, h: HAPI.ResponseToolkit) => {
|
|
562
|
+
/* on WebSocket message transfer */
|
|
563
|
+
const peer = request.info.remoteAddress
|
|
564
|
+
const req = requestValidator(request.payload)
|
|
565
|
+
if (req instanceof arktype.type.errors)
|
|
566
|
+
return h.response({ response: "ERROR", data: `invalid request: ${req.summary}` }).code(417)
|
|
567
|
+
cli!.log("info", `HAPI: peer ${peer}: POST: ${JSON.stringify(req)}`)
|
|
568
|
+
return consumeExternalRequest(req).then(() => {
|
|
569
|
+
return h.response({ response: "OK" }).code(200)
|
|
570
|
+
}).catch((err: Error) => {
|
|
571
|
+
return h.response({ response: "ERROR", data: err.message }).code(417)
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
await hapi.start()
|
|
576
|
+
cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.address}:${args.port}`)
|
|
577
|
+
|
|
578
|
+
/* hook for sendResponse method of nodes */
|
|
579
|
+
for (const node of graphNodes) {
|
|
580
|
+
node.on("send-response", (args: any[]) => {
|
|
581
|
+
const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
|
|
582
|
+
for (const [ peer, info ] of wsPeers.entries()) {
|
|
583
|
+
cli!.log("info", `HAPI: peer ${peer}: ${data}`)
|
|
584
|
+
info.ws.send(data)
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
406
589
|
/* start of internal stream processing */
|
|
407
590
|
cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
|
|
408
591
|
|
|
@@ -417,6 +600,10 @@ let cli: CLIio | null = null
|
|
|
417
600
|
else
|
|
418
601
|
cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
|
|
419
602
|
|
|
603
|
+
/* shutdown HAPI service */
|
|
604
|
+
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
|
|
605
|
+
await hapi.stop()
|
|
606
|
+
|
|
420
607
|
/* graph processing: PASS 1: disconnect node streams */
|
|
421
608
|
for (const node of graphNodes) {
|
|
422
609
|
if (node.stream === null) {
|
|
@@ -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
|
-
}
|