speechflow 0.9.8 → 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 +18 -0
- package/LICENSE.txt +674 -0
- package/README.md +114 -17
- package/dst/speechflow-node-a2a-ffmpeg.js +1 -0
- package/dst/speechflow-node-a2a-ffmpeg.js.map +1 -0
- package/dst/{speechflow-node-deepl.d.ts → speechflow-node-a2a-meter.d.ts} +2 -2
- 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-whisper.d.ts → speechflow-node-a2a-vad.d.ts} +2 -5
- package/dst/speechflow-node-a2a-vad.js +272 -0
- 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-elevenlabs.d.ts → speechflow-node-t2a-kokoro.d.ts} +2 -2
- package/dst/speechflow-node-t2a-kokoro.js +148 -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-gemma.d.ts → speechflow-node-t2t-ollama.d.ts} +1 -1
- package/dst/{speechflow-node-gemma.js → speechflow-node-t2t-ollama.js} +41 -8
- package/dst/speechflow-node-t2t-ollama.js.map +1 -0
- package/dst/{speechflow-node-t2t-gemma.d.ts → speechflow-node-t2t-openai.d.ts} +2 -2
- package/dst/{speechflow-node-t2t-gemma.js → speechflow-node-t2t-openai.js} +43 -30
- 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-opus.d.ts → speechflow-node-t2t-transformers.d.ts} +3 -1
- package/dst/speechflow-node-t2t-transformers.js +264 -0
- package/dst/speechflow-node-t2t-transformers.js.map +1 -0
- package/dst/speechflow-node-x2x-trace.js +3 -2
- 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 +295 -46
- package/dst/speechflow.js.map +1 -0
- package/etc/speechflow.yaml +14 -5
- package/etc/stx.conf +1 -1
- package/etc/tsconfig.json +2 -2
- package/package.json +17 -10
- 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-t2a-kokoro.ts +160 -0
- package/src/{speechflow-node-t2t-gemma.ts → speechflow-node-t2t-ollama.ts} +44 -10
- package/src/speechflow-node-t2t-openai.ts +246 -0
- package/src/speechflow-node-t2t-transformers.ts +249 -0
- package/src/speechflow-node-x2x-trace.ts +2 -2
- 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 +279 -46
- package/dst/speechflow-node-deepgram.d.ts +0 -12
- package/dst/speechflow-node-deepgram.js +0 -220
- 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.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-mqtt.d.ts +0 -13
- package/dst/speechflow-node-mqtt.js +0 -181
- 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-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.js +0 -604
- package/src/speechflow-node-t2t-opus.ts +0 -111
package/src/speechflow.ts
CHANGED
|
@@ -8,17 +8,24 @@
|
|
|
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"
|
|
14
19
|
import CLIio from "cli-io"
|
|
15
20
|
import yargs from "yargs"
|
|
21
|
+
import { hideBin } from "yargs/helpers"
|
|
16
22
|
import jsYAML from "js-yaml"
|
|
17
23
|
import FlowLink from "flowlink"
|
|
18
24
|
import objectPath from "object-path"
|
|
19
25
|
import installedPackages from "installed-packages"
|
|
20
26
|
import dotenvx from "@dotenvx/dotenvx"
|
|
21
27
|
import syspath from "syspath"
|
|
28
|
+
import * as arktype from "arktype"
|
|
22
29
|
|
|
23
30
|
/* internal dependencies */
|
|
24
31
|
import SpeechFlowNode from "./speechflow-node"
|
|
@@ -27,6 +34,15 @@ import pkg from "../package.json"
|
|
|
27
34
|
/* central CLI context */
|
|
28
35
|
let cli: CLIio | null = null
|
|
29
36
|
|
|
37
|
+
type wsPeerCtx = {
|
|
38
|
+
peer: string
|
|
39
|
+
}
|
|
40
|
+
type wsPeerInfo = {
|
|
41
|
+
ctx: wsPeerCtx
|
|
42
|
+
ws: WebSocket
|
|
43
|
+
req: http.IncomingMessage
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
/* establish asynchronous environment */
|
|
31
47
|
;(async () => {
|
|
32
48
|
/* determine system paths */
|
|
@@ -36,6 +52,7 @@ let cli: CLIio | null = null
|
|
|
36
52
|
})
|
|
37
53
|
|
|
38
54
|
/* parse command-line arguments */
|
|
55
|
+
const coerce = (arg: string) => Array.isArray(arg) ? arg[arg.length - 1] : arg
|
|
39
56
|
const args = await yargs()
|
|
40
57
|
/* eslint @stylistic/indent: off */
|
|
41
58
|
.usage(
|
|
@@ -43,34 +60,95 @@ let cli: CLIio | null = null
|
|
|
43
60
|
"[-h|--help] " +
|
|
44
61
|
"[-V|--version] " +
|
|
45
62
|
"[-v|--verbose <level>] " +
|
|
63
|
+
"[-a|--address <ip-address>] " +
|
|
64
|
+
"[-p|--port <tcp-port>] " +
|
|
46
65
|
"[-C|--cache <directory>] " +
|
|
47
66
|
"[-e|--expression <expression>] " +
|
|
48
67
|
"[-f|--file <file>] " +
|
|
49
68
|
"[-c|--config <id>@<yaml-config-file>] " +
|
|
50
69
|
"[<argument> [...]]"
|
|
51
70
|
)
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
.option("V", {
|
|
72
|
+
alias: "version",
|
|
73
|
+
type: "boolean",
|
|
74
|
+
array: false,
|
|
75
|
+
coerce,
|
|
76
|
+
default: false,
|
|
77
|
+
describe: "show program version information"
|
|
78
|
+
})
|
|
79
|
+
.option("v", {
|
|
80
|
+
alias: "log-level",
|
|
81
|
+
type: "string",
|
|
82
|
+
array: false,
|
|
83
|
+
coerce,
|
|
84
|
+
nargs: 1,
|
|
85
|
+
default: "warning",
|
|
86
|
+
describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
|
|
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
|
+
})
|
|
106
|
+
.option("C", {
|
|
107
|
+
alias: "cache",
|
|
108
|
+
type: "string",
|
|
109
|
+
array: false,
|
|
110
|
+
coerce,
|
|
111
|
+
nargs: 1,
|
|
112
|
+
default: path.join(dataDir, "cache"),
|
|
113
|
+
describe: "directory for cached files (primarily AI model files)"
|
|
114
|
+
})
|
|
115
|
+
.option("e", {
|
|
116
|
+
alias: "expression",
|
|
117
|
+
type: "string",
|
|
118
|
+
array: false,
|
|
119
|
+
coerce,
|
|
120
|
+
nargs: 1,
|
|
121
|
+
default: "",
|
|
122
|
+
describe: "FlowLink expression string"
|
|
123
|
+
})
|
|
124
|
+
.option("f", {
|
|
125
|
+
alias: "file",
|
|
126
|
+
type: "string",
|
|
127
|
+
array: false,
|
|
128
|
+
coerce,
|
|
129
|
+
nargs: 1,
|
|
130
|
+
default: "",
|
|
131
|
+
describe: "FlowLink expression file"
|
|
132
|
+
})
|
|
133
|
+
.option("c", {
|
|
134
|
+
alias: "config",
|
|
135
|
+
type: "string",
|
|
136
|
+
array: false,
|
|
137
|
+
coerce,
|
|
138
|
+
nargs: 1,
|
|
139
|
+
default: "",
|
|
140
|
+
describe: "FlowLink expression reference into YAML file (in format <id>@<file>)"
|
|
141
|
+
})
|
|
142
|
+
.help("h", "show usage help")
|
|
143
|
+
.alias("h", "help")
|
|
144
|
+
.showHelpOnFail(true)
|
|
66
145
|
.version(false)
|
|
67
146
|
.strict()
|
|
68
|
-
.showHelpOnFail(true)
|
|
69
147
|
.demand(0)
|
|
70
|
-
.parse(process.argv
|
|
148
|
+
.parse(hideBin(process.argv))
|
|
71
149
|
|
|
72
150
|
/* short-circuit version request */
|
|
73
|
-
if (args.
|
|
151
|
+
if (args.V) {
|
|
74
152
|
process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
|
|
75
153
|
process.stderr.write(`${pkg.description}\n`)
|
|
76
154
|
process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
|
|
@@ -81,7 +159,7 @@ let cli: CLIio | null = null
|
|
|
81
159
|
/* establish CLI environment */
|
|
82
160
|
cli = new CLIio({
|
|
83
161
|
encoding: "utf8",
|
|
84
|
-
logLevel: args.
|
|
162
|
+
logLevel: args.v,
|
|
85
163
|
logTime: true,
|
|
86
164
|
logPrefix: pkg.name
|
|
87
165
|
})
|
|
@@ -112,28 +190,28 @@ let cli: CLIio | null = null
|
|
|
112
190
|
|
|
113
191
|
/* sanity check usage */
|
|
114
192
|
let n = 0
|
|
115
|
-
if (typeof args.
|
|
116
|
-
if (typeof args.
|
|
117
|
-
if (typeof args.
|
|
193
|
+
if (typeof args.e === "string" && args.e !== "") n++
|
|
194
|
+
if (typeof args.f === "string" && args.f !== "") n++
|
|
195
|
+
if (typeof args.c === "string" && args.c !== "") n++
|
|
118
196
|
if (n !== 1)
|
|
119
197
|
throw new Error("cannot use more than one FlowLink specification source (either option -e, -f or -c)")
|
|
120
198
|
|
|
121
199
|
/* read configuration */
|
|
122
200
|
let config = ""
|
|
123
|
-
if (typeof args.
|
|
124
|
-
config = args.
|
|
125
|
-
else if (typeof args.
|
|
126
|
-
config = await cli.input(args.
|
|
127
|
-
else if (typeof args.
|
|
128
|
-
const m = args.
|
|
201
|
+
if (typeof args.e === "string" && args.e !== "")
|
|
202
|
+
config = args.e
|
|
203
|
+
else if (typeof args.f === "string" && args.f !== "")
|
|
204
|
+
config = await cli.input(args.f, { encoding: "utf8" })
|
|
205
|
+
else if (typeof args.c === "string" && args.c !== "") {
|
|
206
|
+
const m = args.c.match(/^(.+?)@(.+)$/)
|
|
129
207
|
if (m === null)
|
|
130
|
-
throw new Error("invalid configuration file specification (expected \"<
|
|
131
|
-
const [ ,
|
|
208
|
+
throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
|
|
209
|
+
const [ , id, file ] = m
|
|
132
210
|
const yaml = await cli.input(file, { encoding: "utf8" })
|
|
133
211
|
const obj: any = jsYAML.load(yaml)
|
|
134
|
-
if (obj[
|
|
135
|
-
throw new Error(`no such
|
|
136
|
-
config = obj[
|
|
212
|
+
if (obj[id] === undefined)
|
|
213
|
+
throw new Error(`no such id "${id}" found in configuration file`)
|
|
214
|
+
config = obj[id] as string
|
|
137
215
|
}
|
|
138
216
|
|
|
139
217
|
/* track the available SpeechFlow nodes */
|
|
@@ -143,13 +221,18 @@ let cli: CLIio | null = null
|
|
|
143
221
|
const pkgsI = [
|
|
144
222
|
"./speechflow-node-a2a-ffmpeg.js",
|
|
145
223
|
"./speechflow-node-a2a-wav.js",
|
|
224
|
+
"./speechflow-node-a2a-mute.js",
|
|
225
|
+
"./speechflow-node-a2a-meter.js",
|
|
226
|
+
"./speechflow-node-a2a-vad.js",
|
|
146
227
|
"./speechflow-node-a2t-deepgram.js",
|
|
147
228
|
"./speechflow-node-t2a-elevenlabs.js",
|
|
229
|
+
"./speechflow-node-t2a-kokoro.js",
|
|
148
230
|
"./speechflow-node-t2t-deepl.js",
|
|
149
|
-
"./speechflow-node-t2t-
|
|
150
|
-
"./speechflow-node-t2t-
|
|
151
|
-
"./speechflow-node-t2t-
|
|
231
|
+
"./speechflow-node-t2t-openai.js",
|
|
232
|
+
"./speechflow-node-t2t-ollama.js",
|
|
233
|
+
"./speechflow-node-t2t-transformers.js",
|
|
152
234
|
"./speechflow-node-t2t-subtitle.js",
|
|
235
|
+
"./speechflow-node-t2t-format.js",
|
|
153
236
|
"./speechflow-node-x2x-trace.js",
|
|
154
237
|
"./speechflow-node-xio-device.js",
|
|
155
238
|
"./speechflow-node-xio-file.js",
|
|
@@ -191,16 +274,16 @@ let cli: CLIio | null = null
|
|
|
191
274
|
cli!.log("debug", msg)
|
|
192
275
|
}
|
|
193
276
|
})
|
|
194
|
-
let nodenum = 1
|
|
195
277
|
const variables = { argv: args._, env: process.env }
|
|
196
278
|
const graphNodes = new Set<SpeechFlowNode>()
|
|
279
|
+
const nodeNums = new Map<typeof SpeechFlowNode, number>()
|
|
197
280
|
const cfg = {
|
|
198
281
|
audioChannels: 1,
|
|
199
282
|
audioBitDepth: 16,
|
|
200
283
|
audioLittleEndian: true,
|
|
201
284
|
audioSampleRate: 48000,
|
|
202
285
|
textEncoding: "utf8",
|
|
203
|
-
cacheDir: args.
|
|
286
|
+
cacheDir: args.C
|
|
204
287
|
}
|
|
205
288
|
let ast: unknown
|
|
206
289
|
try {
|
|
@@ -229,17 +312,19 @@ let cli: CLIio | null = null
|
|
|
229
312
|
throw new Error(`unknown node "${id}"`)
|
|
230
313
|
let node: SpeechFlowNode
|
|
231
314
|
try {
|
|
232
|
-
|
|
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)
|
|
233
319
|
}
|
|
234
320
|
catch (err) {
|
|
235
321
|
/* fatal error */
|
|
236
322
|
if (err instanceof Error)
|
|
237
|
-
cli!.log("error", `creation of
|
|
323
|
+
cli!.log("error", `creation of <${id}> node failed: ${err.message}`)
|
|
238
324
|
else
|
|
239
|
-
cli!.log("error", `creation of
|
|
325
|
+
cli!.log("error", `creation of <${id}> node failed: ${err}`)
|
|
240
326
|
process.exit(1)
|
|
241
327
|
}
|
|
242
|
-
nodenum++
|
|
243
328
|
const params = Object.keys(node.params)
|
|
244
329
|
.map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
|
|
245
330
|
cli!.log("info", `create node "${node.id}" (${params})`)
|
|
@@ -254,9 +339,9 @@ let cli: CLIio | null = null
|
|
|
254
339
|
}
|
|
255
340
|
catch (err) {
|
|
256
341
|
if (err instanceof Error && err.name === "FlowLinkError")
|
|
257
|
-
cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.toString()}
|
|
342
|
+
cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.toString()}`)
|
|
258
343
|
else if (err instanceof Error)
|
|
259
|
-
cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.message}
|
|
344
|
+
cli!.log("error", `failed to materialize SpeechFlow configuration: ${err.message}`)
|
|
260
345
|
else
|
|
261
346
|
cli!.log("error", "failed to materialize SpeechFlow configuration: internal error")
|
|
262
347
|
process.exit(1)
|
|
@@ -357,8 +442,152 @@ let cli: CLIio | null = null
|
|
|
357
442
|
})
|
|
358
443
|
}
|
|
359
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
|
+
|
|
360
589
|
/* start of internal stream processing */
|
|
361
|
-
cli!.log("info", "everything established -- stream processing in SpeechFlow graph starts")
|
|
590
|
+
cli!.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
|
|
362
591
|
|
|
363
592
|
/* gracefully shutdown process */
|
|
364
593
|
let shuttingDown = false
|
|
@@ -367,9 +596,13 @@ let cli: CLIio | null = null
|
|
|
367
596
|
return
|
|
368
597
|
shuttingDown = true
|
|
369
598
|
if (signal === "finished")
|
|
370
|
-
cli!.log("info", "streams of all nodes finished -- shutting down service")
|
|
599
|
+
cli!.log("info", "**** streams of all nodes finished -- shutting down service ****")
|
|
371
600
|
else
|
|
372
|
-
cli!.log("warning",
|
|
601
|
+
cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
|
|
602
|
+
|
|
603
|
+
/* shutdown HAPI service */
|
|
604
|
+
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.address}:${args.port}`)
|
|
605
|
+
await hapi.stop()
|
|
373
606
|
|
|
374
607
|
/* graph processing: PASS 1: disconnect node streams */
|
|
375
608
|
for (const node of graphNodes) {
|
|
@@ -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
|
-
}
|