speechflow 1.6.0 → 1.6.1
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 +7 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/speechflow-cli/dst/speechflow-main-api.d.ts +12 -0
- package/speechflow-cli/dst/speechflow-main-api.js +319 -0
- package/speechflow-cli/dst/speechflow-main-api.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-cli.d.ts +28 -0
- package/speechflow-cli/dst/speechflow-main-cli.js +271 -0
- package/speechflow-cli/dst/speechflow-main-cli.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-config.d.ts +9 -0
- package/speechflow-cli/dst/speechflow-main-config.js +27 -0
- package/speechflow-cli/dst/speechflow-main-config.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-graph.d.ts +34 -0
- package/speechflow-cli/dst/speechflow-main-graph.js +367 -0
- package/speechflow-cli/dst/speechflow-main-graph.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-nodes.d.ts +10 -0
- package/speechflow-cli/dst/speechflow-main-nodes.js +60 -0
- package/speechflow-cli/dst/speechflow-main-nodes.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main-status.d.ts +11 -0
- package/speechflow-cli/dst/speechflow-main-status.js +60 -0
- package/speechflow-cli/dst/speechflow-main-status.js.map +1 -0
- package/speechflow-cli/dst/speechflow-main.d.ts +7 -0
- package/speechflow-cli/dst/speechflow-main.js +127 -0
- package/speechflow-cli/dst/speechflow-main.js.map +1 -0
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-compressor-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js +5 -6
- package/speechflow-cli/dst/speechflow-node-a2a-compressor.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js +5 -5
- package/speechflow-cli/dst/speechflow-node-a2a-expander-wt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js +5 -6
- package/speechflow-cli/dst/speechflow-node-a2a-expander.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js +5 -5
- package/speechflow-cli/dst/speechflow-node-a2a-ffmpeg.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js +3 -3
- package/speechflow-cli/dst/speechflow-node-a2a-filler.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-gain.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-rnnoise.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-speex.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js +6 -6
- package/speechflow-cli/dst/speechflow-node-a2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js +4 -4
- package/speechflow-cli/dst/speechflow-node-a2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2a-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-amazon.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-google.js +5 -5
- package/speechflow-cli/dst/speechflow-node-t2t-google.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-modify.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js +2 -2
- package/speechflow-cli/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js +2 -2
- package/speechflow-cli/dst/speechflow-node-x2x-filter.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-device.js +5 -5
- package/speechflow-cli/dst/speechflow-node-xio-device.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-file.js +27 -27
- package/speechflow-cli/dst/speechflow-node-xio-file.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js +4 -4
- package/speechflow-cli/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js +7 -7
- package/speechflow-cli/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/speechflow-cli/dst/{speechflow-utils-audio-wt.js → speechflow-util-audio-wt.js} +1 -1
- package/speechflow-cli/dst/speechflow-util-audio-wt.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-audio.d.ts +22 -0
- package/speechflow-cli/dst/speechflow-util-audio.js +251 -0
- package/speechflow-cli/dst/speechflow-util-audio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-error.d.ts +14 -0
- package/speechflow-cli/dst/speechflow-util-error.js +131 -0
- package/speechflow-cli/dst/speechflow-util-error.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-queue.d.ts +68 -0
- package/speechflow-cli/dst/speechflow-util-queue.js +338 -0
- package/speechflow-cli/dst/speechflow-util-queue.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-stream.d.ts +18 -0
- package/speechflow-cli/dst/speechflow-util-stream.js +219 -0
- package/speechflow-cli/dst/speechflow-util-stream.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util-webaudio-wt.js +124 -0
- package/speechflow-cli/dst/speechflow-util-webaudio-wt.js.map +1 -0
- package/speechflow-cli/dst/{speechflow-utils-audio.js → speechflow-util-webaudio.js} +2 -2
- package/speechflow-cli/dst/speechflow-util-webaudio.js.map +1 -0
- package/speechflow-cli/dst/speechflow-util.d.ts +4 -0
- package/speechflow-cli/dst/speechflow-util.js +26 -0
- package/speechflow-cli/dst/speechflow-util.js.map +1 -0
- package/speechflow-cli/dst/speechflow.js +3 -912
- package/speechflow-cli/dst/speechflow.js.map +1 -1
- package/speechflow-cli/etc/oxlint.jsonc +4 -1
- package/speechflow-cli/package.json +1 -0
- package/speechflow-cli/src/speechflow-main-api.ts +315 -0
- package/speechflow-cli/src/speechflow-main-cli.ts +259 -0
- package/speechflow-cli/src/speechflow-main-config.ts +17 -0
- package/speechflow-cli/src/speechflow-main-graph.ts +372 -0
- package/speechflow-cli/src/speechflow-main-nodes.ts +61 -0
- package/speechflow-cli/src/speechflow-main-status.ts +70 -0
- package/speechflow-cli/src/speechflow-main.ts +106 -0
- package/speechflow-cli/src/speechflow-node-a2a-compressor-wt.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-compressor.ts +5 -6
- package/speechflow-cli/src/speechflow-node-a2a-expander-wt.ts +5 -5
- package/speechflow-cli/src/speechflow-node-a2a-expander.ts +5 -6
- package/speechflow-cli/src/speechflow-node-a2a-ffmpeg.ts +5 -5
- package/speechflow-cli/src/speechflow-node-a2a-filler.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-gain.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-gender.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-meter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-a2a-rnnoise.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-speex.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2a-vad.ts +4 -4
- package/speechflow-cli/src/speechflow-node-a2t-amazon.ts +7 -7
- package/speechflow-cli/src/speechflow-node-a2t-deepgram.ts +5 -5
- package/speechflow-cli/src/speechflow-node-a2t-openai.ts +5 -5
- package/speechflow-cli/src/speechflow-node-t2a-amazon.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2a-kokoro.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-amazon.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-deepl.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-google.ts +5 -5
- package/speechflow-cli/src/speechflow-node-t2t-modify.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-ollama.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-openai.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-sentence.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-subtitle.ts +2 -2
- package/speechflow-cli/src/speechflow-node-t2t-transformers.ts +2 -2
- package/speechflow-cli/src/speechflow-node-x2x-filter.ts +2 -2
- package/speechflow-cli/src/speechflow-node-xio-device.ts +5 -5
- package/speechflow-cli/src/speechflow-node-xio-file.ts +9 -10
- package/speechflow-cli/src/speechflow-node-xio-mqtt.ts +5 -5
- package/speechflow-cli/src/speechflow-node-xio-websocket.ts +7 -7
- package/speechflow-cli/src/{speechflow-utils-audio.ts → speechflow-util-audio.ts} +131 -1
- package/speechflow-cli/src/speechflow-util-error.ts +184 -0
- package/speechflow-cli/src/speechflow-util-queue.ts +320 -0
- package/speechflow-cli/src/speechflow-util-stream.ts +197 -0
- package/speechflow-cli/src/speechflow-util.ts +10 -0
- package/speechflow-cli/src/speechflow.ts +3 -953
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js +0 -208
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics-wt.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.d.ts +0 -15
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js +0 -312
- package/speechflow-cli/dst/speechflow-node-a2a-dynamics.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.d.ts +0 -18
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js +0 -312
- package/speechflow-cli/dst/speechflow-node-a2t-awstranscribe.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.d.ts +0 -19
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js +0 -351
- package/speechflow-cli/dst/speechflow-node-a2t-openaitranscribe.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.d.ts +0 -16
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js +0 -204
- package/speechflow-cli/dst/speechflow-node-t2a-awspolly.js.map +0 -1
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.d.ts +0 -13
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js +0 -175
- package/speechflow-cli/dst/speechflow-node-t2t-awstranslate.js.map +0 -1
- package/speechflow-cli/dst/speechflow-utils-audio-wt.js.map +0 -1
- package/speechflow-cli/dst/speechflow-utils-audio.js.map +0 -1
- package/speechflow-cli/dst/speechflow-utils.d.ts +0 -108
- package/speechflow-cli/dst/speechflow-utils.js +0 -746
- package/speechflow-cli/dst/speechflow-utils.js.map +0 -1
- package/speechflow-cli/src/speechflow-utils.ts +0 -810
- /package/speechflow-cli/dst/{speechflow-node-a2a-dynamics-wt.d.ts → speechflow-util-audio-wt.d.ts} +0 -0
- /package/speechflow-cli/dst/{speechflow-utils-audio-wt.d.ts → speechflow-util-webaudio-wt.d.ts} +0 -0
- /package/speechflow-cli/dst/{speechflow-utils-audio.d.ts → speechflow-util-webaudio.d.ts} +0 -0
- /package/speechflow-cli/src/{speechflow-utils-audio-wt.ts → speechflow-util-audio-wt.ts} +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
|
|
10
|
+
/* external dependencies */
|
|
11
|
+
import CLIio from "cli-io"
|
|
12
|
+
import yargs from "yargs"
|
|
13
|
+
import { hideBin } from "yargs/helpers"
|
|
14
|
+
import jsYAML from "js-yaml"
|
|
15
|
+
import dotenvx from "@dotenvx/dotenvx"
|
|
16
|
+
import syspath from "syspath"
|
|
17
|
+
import chalk from "chalk"
|
|
18
|
+
|
|
19
|
+
/* internal dependencies */
|
|
20
|
+
import * as util from "./speechflow-util"
|
|
21
|
+
import pkg from "../../package.json"
|
|
22
|
+
|
|
23
|
+
/* command-line options */
|
|
24
|
+
export interface CLIOptions {
|
|
25
|
+
V: boolean
|
|
26
|
+
S: boolean
|
|
27
|
+
v: string
|
|
28
|
+
a: string
|
|
29
|
+
p: number
|
|
30
|
+
C: string
|
|
31
|
+
d: string
|
|
32
|
+
o: string
|
|
33
|
+
e: string
|
|
34
|
+
f: string
|
|
35
|
+
c: string
|
|
36
|
+
_: (string | number)[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class CLIContext {
|
|
40
|
+
public cli: CLIio | null = null
|
|
41
|
+
public args: CLIOptions | null = null
|
|
42
|
+
public config: string | null = null
|
|
43
|
+
public debug = false
|
|
44
|
+
|
|
45
|
+
/* type guard for initialization */
|
|
46
|
+
isInitialized (): this is CLIContext & { cli: CLIio; args: CLIOptions; config: string } {
|
|
47
|
+
return this.cli !== null && this.args !== null && this.config !== null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* initialization of CLI */
|
|
51
|
+
async init (): Promise<void> {
|
|
52
|
+
/* determine system paths */
|
|
53
|
+
const { dataDir } = syspath({
|
|
54
|
+
appName: "speechflow",
|
|
55
|
+
dataDirAutoCreate: true
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/* parse command-line arguments */
|
|
59
|
+
const coerce = (arg: string) => Array.isArray(arg) ? arg[arg.length - 1] : arg
|
|
60
|
+
this.args = await yargs()
|
|
61
|
+
/* eslint @stylistic/indent: off */
|
|
62
|
+
.usage(
|
|
63
|
+
"Usage: $0 " +
|
|
64
|
+
"[-h|--help] " +
|
|
65
|
+
"[-V|--version] " +
|
|
66
|
+
"[-S|--status] " +
|
|
67
|
+
"[-v|--verbose <level>] " +
|
|
68
|
+
"[-a|--address <ip-address>] " +
|
|
69
|
+
"[-p|--port <tcp-port>] " +
|
|
70
|
+
"[-C|--cache <directory>] " +
|
|
71
|
+
"[-d|--dashboard <type>:<id>:<name>[,...]] " +
|
|
72
|
+
"[-o|--osc <ip-address>:<udp-port> " +
|
|
73
|
+
"[-e|--expression <expression>] " +
|
|
74
|
+
"[-f|--file <file>] " +
|
|
75
|
+
"[-c|--config <id>@<yaml-config-file>] " +
|
|
76
|
+
"[<argument> [...]]"
|
|
77
|
+
)
|
|
78
|
+
.version(false)
|
|
79
|
+
.option("V", {
|
|
80
|
+
alias: "version",
|
|
81
|
+
type: "boolean",
|
|
82
|
+
array: false,
|
|
83
|
+
coerce,
|
|
84
|
+
default: false,
|
|
85
|
+
describe: "show program version information"
|
|
86
|
+
})
|
|
87
|
+
.option("S", {
|
|
88
|
+
alias: "status",
|
|
89
|
+
type: "boolean",
|
|
90
|
+
array: false,
|
|
91
|
+
coerce,
|
|
92
|
+
default: false,
|
|
93
|
+
describe: "show one-time status of nodes"
|
|
94
|
+
})
|
|
95
|
+
.option("v", {
|
|
96
|
+
alias: "log-level",
|
|
97
|
+
type: "string",
|
|
98
|
+
array: false,
|
|
99
|
+
coerce,
|
|
100
|
+
nargs: 1,
|
|
101
|
+
default: "warning",
|
|
102
|
+
describe: "level for verbose logging ('none', 'error', 'warning', 'info', 'debug')"
|
|
103
|
+
})
|
|
104
|
+
.option("a", {
|
|
105
|
+
alias: "address",
|
|
106
|
+
type: "string",
|
|
107
|
+
array: false,
|
|
108
|
+
coerce,
|
|
109
|
+
nargs: 1,
|
|
110
|
+
default: "0.0.0.0",
|
|
111
|
+
describe: "IP address for REST/WebSocket API"
|
|
112
|
+
})
|
|
113
|
+
.option("p", {
|
|
114
|
+
alias: "port",
|
|
115
|
+
type: "number",
|
|
116
|
+
array: false,
|
|
117
|
+
coerce,
|
|
118
|
+
nargs: 1,
|
|
119
|
+
default: 8484,
|
|
120
|
+
describe: "TCP port for REST/WebSocket API"
|
|
121
|
+
})
|
|
122
|
+
.option("C", {
|
|
123
|
+
alias: "cache",
|
|
124
|
+
type: "string",
|
|
125
|
+
array: false,
|
|
126
|
+
coerce,
|
|
127
|
+
nargs: 1,
|
|
128
|
+
default: path.join(dataDir, "cache"),
|
|
129
|
+
describe: "directory for cached files (primarily AI model files)"
|
|
130
|
+
})
|
|
131
|
+
.option("d", {
|
|
132
|
+
alias: "dashboard",
|
|
133
|
+
type: "string",
|
|
134
|
+
array: false,
|
|
135
|
+
coerce,
|
|
136
|
+
nargs: 1,
|
|
137
|
+
default: "",
|
|
138
|
+
describe: "list of dashboard block types and names"
|
|
139
|
+
})
|
|
140
|
+
.option("o", {
|
|
141
|
+
alias: "osc",
|
|
142
|
+
type: "string",
|
|
143
|
+
array: false,
|
|
144
|
+
coerce,
|
|
145
|
+
nargs: 1,
|
|
146
|
+
default: "",
|
|
147
|
+
describe: "OSC/UDP endpoint to send dashboard information"
|
|
148
|
+
})
|
|
149
|
+
.option("e", {
|
|
150
|
+
alias: "expression",
|
|
151
|
+
type: "string",
|
|
152
|
+
array: false,
|
|
153
|
+
coerce,
|
|
154
|
+
nargs: 1,
|
|
155
|
+
default: "",
|
|
156
|
+
describe: "FlowLink expression string"
|
|
157
|
+
})
|
|
158
|
+
.option("f", {
|
|
159
|
+
alias: "file",
|
|
160
|
+
type: "string",
|
|
161
|
+
array: false,
|
|
162
|
+
coerce,
|
|
163
|
+
nargs: 1,
|
|
164
|
+
default: "",
|
|
165
|
+
describe: "FlowLink expression file"
|
|
166
|
+
})
|
|
167
|
+
.option("c", {
|
|
168
|
+
alias: "config",
|
|
169
|
+
type: "string",
|
|
170
|
+
array: false,
|
|
171
|
+
coerce,
|
|
172
|
+
nargs: 1,
|
|
173
|
+
default: "",
|
|
174
|
+
describe: "FlowLink expression reference into YAML file (in format <id>@<file>)"
|
|
175
|
+
})
|
|
176
|
+
.help("h", "show usage help")
|
|
177
|
+
.alias("h", "help")
|
|
178
|
+
.showHelpOnFail(true)
|
|
179
|
+
.strict()
|
|
180
|
+
.demand(0)
|
|
181
|
+
.parse(hideBin(process.argv)) as CLIOptions
|
|
182
|
+
|
|
183
|
+
/* short-circuit version request */
|
|
184
|
+
if (this.args.V) {
|
|
185
|
+
process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
|
|
186
|
+
process.stderr.write(`${pkg.description}\n`)
|
|
187
|
+
process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
|
|
188
|
+
process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
|
|
189
|
+
process.exit(0)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* establish CLI environment */
|
|
193
|
+
this.cli = new CLIio({
|
|
194
|
+
encoding: "utf8",
|
|
195
|
+
logLevel: this.args.v,
|
|
196
|
+
logTime: true,
|
|
197
|
+
logPrefix: pkg.name
|
|
198
|
+
})
|
|
199
|
+
if (this.args.v.match(/^(?:info|debug)$/))
|
|
200
|
+
this.debug = true
|
|
201
|
+
|
|
202
|
+
/* provide startup information */
|
|
203
|
+
this.cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
|
|
204
|
+
|
|
205
|
+
/* load .env files */
|
|
206
|
+
const result = dotenvx.config({
|
|
207
|
+
encoding: "utf8",
|
|
208
|
+
ignore: [ "MISSING_ENV_FILE" ],
|
|
209
|
+
quiet: true
|
|
210
|
+
})
|
|
211
|
+
if (result?.parsed !== undefined)
|
|
212
|
+
for (const key of Object.keys(result.parsed))
|
|
213
|
+
this.cli.log("info", `loaded environment variable "${key}" from ".env" files`)
|
|
214
|
+
|
|
215
|
+
/* sanity check configuration situation */
|
|
216
|
+
let n = 0
|
|
217
|
+
if (typeof this.args.e === "string" && this.args.e !== "") n++
|
|
218
|
+
if (typeof this.args.f === "string" && this.args.f !== "") n++
|
|
219
|
+
if (typeof this.args.c === "string" && this.args.c !== "") n++
|
|
220
|
+
if (n === 0)
|
|
221
|
+
throw new Error("need at least one FlowLink specification source (use one of the options -e, -f or -c)")
|
|
222
|
+
else if (n !== 1)
|
|
223
|
+
throw new Error("cannot use more than one FlowLink specification source (use only one of the options -e, -f or -c)")
|
|
224
|
+
|
|
225
|
+
/* read configuration */
|
|
226
|
+
if (typeof this.args.e === "string" && this.args.e !== "")
|
|
227
|
+
this.config = this.args.e
|
|
228
|
+
else if (typeof this.args.f === "string" && this.args.f !== "")
|
|
229
|
+
this.config = await this.cli.input(this.args.f, { encoding: "utf8" })
|
|
230
|
+
else if (typeof this.args.c === "string" && this.args.c !== "") {
|
|
231
|
+
const m = this.args.c.match(/^(.+?)@(.+)$/)
|
|
232
|
+
if (m === null)
|
|
233
|
+
throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
|
|
234
|
+
const [ , id, file ] = m
|
|
235
|
+
const yaml = await this.cli.input(file, { encoding: "utf8" })
|
|
236
|
+
const obj: any = util.run("parsing YAML configuration", () => jsYAML.load(yaml))
|
|
237
|
+
if (obj[id] === undefined)
|
|
238
|
+
throw new Error(`no such id "${id}" found in configuration file "${file}"`)
|
|
239
|
+
this.config = obj[id] as string
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* utility function for handling a top-level error */
|
|
244
|
+
handleTopLevelError (err: Error): never {
|
|
245
|
+
if (this.cli !== null) {
|
|
246
|
+
if (this.debug)
|
|
247
|
+
this.cli.log("error", `${err.message}\n${err.stack}`)
|
|
248
|
+
else
|
|
249
|
+
this.cli.log("error", `${err.message}`)
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
if (this.debug)
|
|
253
|
+
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
|
|
254
|
+
else
|
|
255
|
+
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}`)
|
|
256
|
+
}
|
|
257
|
+
process.exit(1)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
/* the node configuration */
|
|
8
|
+
export class NodeConfig {
|
|
9
|
+
constructor (
|
|
10
|
+
public readonly audioChannels = 1,
|
|
11
|
+
public readonly audioBitDepth = 16,
|
|
12
|
+
public readonly audioLittleEndian = true,
|
|
13
|
+
public readonly audioSampleRate = 48000,
|
|
14
|
+
public readonly textEncoding = "utf8",
|
|
15
|
+
public cacheDir = ""
|
|
16
|
+
) {}
|
|
17
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** SpeechFlow - Speech Processing Flow Graph
|
|
3
|
+
** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* standard dependencies */
|
|
8
|
+
import Stream from "node:stream"
|
|
9
|
+
import { EventEmitter } from "node:events"
|
|
10
|
+
|
|
11
|
+
/* external dependencies */
|
|
12
|
+
import { DateTime } from "luxon"
|
|
13
|
+
import CLIio from "cli-io"
|
|
14
|
+
import FlowLink from "flowlink"
|
|
15
|
+
import objectPath from "object-path"
|
|
16
|
+
|
|
17
|
+
/* internal dependencies */
|
|
18
|
+
import SpeechFlowNode from "./speechflow-node"
|
|
19
|
+
import { NodeConfig } from "./speechflow-main-config"
|
|
20
|
+
import { CLIOptions } from "./speechflow-main-cli"
|
|
21
|
+
import { APIServer } from "./speechflow-main-api"
|
|
22
|
+
import * as util from "./speechflow-util"
|
|
23
|
+
|
|
24
|
+
/* the SpeechFlow node graph management */
|
|
25
|
+
export class NodeGraph {
|
|
26
|
+
/* internal state */
|
|
27
|
+
private graphNodes = new Set<SpeechFlowNode>()
|
|
28
|
+
private activeNodes = new Set<SpeechFlowNode>()
|
|
29
|
+
private finishEvents = new EventEmitter()
|
|
30
|
+
private timeZero: DateTime | null = null
|
|
31
|
+
private shuttingDown = false
|
|
32
|
+
|
|
33
|
+
/* simple construction */
|
|
34
|
+
constructor (
|
|
35
|
+
private cli: CLIio,
|
|
36
|
+
private debug = false
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/* get all graph nodes */
|
|
40
|
+
getGraphNodes (): Set<SpeechFlowNode> {
|
|
41
|
+
return this.graphNodes
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* find particular graph node */
|
|
45
|
+
findGraphNode (name: string): SpeechFlowNode | undefined {
|
|
46
|
+
return Array.from(this.graphNodes).find((node) => node.id === name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* graph establishment: PASS 1: parse DSL and create and connect nodes */
|
|
50
|
+
async createAndConnectNodes (
|
|
51
|
+
config: string,
|
|
52
|
+
nodes: { [ id: string ]: typeof SpeechFlowNode },
|
|
53
|
+
cfg: NodeConfig,
|
|
54
|
+
variables: { argv: any[], env: any },
|
|
55
|
+
accessBus: (name: string) => EventEmitter
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
const flowlink = new FlowLink<SpeechFlowNode>({
|
|
58
|
+
trace: (msg: string) => {
|
|
59
|
+
this.cli.log("debug", msg)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
const nodeNums = new Map<typeof SpeechFlowNode, number>()
|
|
63
|
+
let ast: unknown
|
|
64
|
+
try {
|
|
65
|
+
ast = flowlink.compile(config)
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const errorMsg = err instanceof Error && err.name === "FlowLinkError"
|
|
69
|
+
? err.toString() : (err instanceof Error ? err.message : "internal error")
|
|
70
|
+
this.cli.log("error", `failed to parse SpeechFlow configuration: ${errorMsg}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
flowlink.execute(ast, {
|
|
75
|
+
resolveVariable: (id: string) => {
|
|
76
|
+
if (!objectPath.has(variables, id))
|
|
77
|
+
throw new Error(`failed to resolve variable "${id}"`)
|
|
78
|
+
const value = objectPath.get(variables, id)
|
|
79
|
+
this.cli.log("info", `resolve variable: "${id}" -> "${value}"`)
|
|
80
|
+
return value
|
|
81
|
+
},
|
|
82
|
+
createNode: (id: string, opts: { [ id: string ]: any }, args: any[]) => {
|
|
83
|
+
if (nodes[id] === undefined)
|
|
84
|
+
throw new Error(`unknown node <${id}>`)
|
|
85
|
+
let node: SpeechFlowNode
|
|
86
|
+
try {
|
|
87
|
+
const NodeClass = nodes[id]
|
|
88
|
+
let num = nodeNums.get(NodeClass) ?? 0
|
|
89
|
+
nodeNums.set(NodeClass, ++num)
|
|
90
|
+
const name = num === 1 ? id : `${id}:${num}`
|
|
91
|
+
node = new NodeClass(name, cfg, opts, args)
|
|
92
|
+
node._accessBus = accessBus
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
/* fatal error */
|
|
96
|
+
if (err instanceof Error)
|
|
97
|
+
this.cli.log("error", `creation of node <${id}> failed: ${err.message}`)
|
|
98
|
+
else
|
|
99
|
+
this.cli.log("error", `creation of node <${id}> failed: ${err}`)
|
|
100
|
+
process.exit(1)
|
|
101
|
+
}
|
|
102
|
+
const params = Object.keys(node.params).map((key) => {
|
|
103
|
+
if (key.match(/key/))
|
|
104
|
+
return `${key}: [...]`
|
|
105
|
+
else
|
|
106
|
+
return `${key}: ${JSON.stringify(node.params[key])}`
|
|
107
|
+
}).join(", ")
|
|
108
|
+
this.cli.log("info", `create node <${node.id}> (${params})`)
|
|
109
|
+
this.graphNodes.add(node)
|
|
110
|
+
return node
|
|
111
|
+
},
|
|
112
|
+
connectNodes: (node1: SpeechFlowNode, node2: SpeechFlowNode) => {
|
|
113
|
+
this.cli.log("info", `connect node <${node1.id}> to node <${node2.id}>`)
|
|
114
|
+
node1.connect(node2)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const errorMsg = err instanceof Error && err.name === "FlowLinkError"
|
|
120
|
+
? err.toString() : (err instanceof Error ? err.message : "internal error")
|
|
121
|
+
this.cli.log("error", `failed to materialize SpeechFlow configuration: ${errorMsg}`)
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* graph establishment: PASS 2: prune connections of nodes */
|
|
127
|
+
async pruneConnections () {
|
|
128
|
+
for (const node of this.graphNodes) {
|
|
129
|
+
/* determine connections */
|
|
130
|
+
let connectionsIn = Array.from(node.connectionsIn)
|
|
131
|
+
let connectionsOut = Array.from(node.connectionsOut)
|
|
132
|
+
|
|
133
|
+
/* ensure necessary incoming links */
|
|
134
|
+
if (node.input !== "none" && connectionsIn.length === 0)
|
|
135
|
+
throw new Error(`node <${node.id}> requires input but has no input nodes connected`)
|
|
136
|
+
|
|
137
|
+
/* prune unnecessary incoming links */
|
|
138
|
+
if (node.input === "none" && connectionsIn.length > 0)
|
|
139
|
+
connectionsIn.forEach((other) => { other.disconnect(node) })
|
|
140
|
+
|
|
141
|
+
/* ensure necessary outgoing links */
|
|
142
|
+
if (node.output !== "none" && connectionsOut.length === 0)
|
|
143
|
+
throw new Error(`node <${node.id}> requires output but has no output nodes connected`)
|
|
144
|
+
|
|
145
|
+
/* prune unnecessary outgoing links */
|
|
146
|
+
if (node.output === "none" && connectionsOut.length > 0)
|
|
147
|
+
connectionsOut.forEach((other) => { node.disconnect(other) })
|
|
148
|
+
|
|
149
|
+
/* check for payload compatibility */
|
|
150
|
+
connectionsIn = Array.from(node.connectionsIn)
|
|
151
|
+
connectionsOut = Array.from(node.connectionsOut)
|
|
152
|
+
for (const other of connectionsOut)
|
|
153
|
+
if (other.input !== node.output)
|
|
154
|
+
throw new Error(`${node.output} output node <${node.id}> cannot be ` +
|
|
155
|
+
`connected to ${other.input} input node <${other.id}> (payload is incompatible)`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* graph establishment: PASS 3: open nodes */
|
|
160
|
+
async openNodes(): Promise<void> {
|
|
161
|
+
this.timeZero = DateTime.now()
|
|
162
|
+
for (const node of this.graphNodes) {
|
|
163
|
+
/* connect node events */
|
|
164
|
+
node.on("log", (level: string, msg: string, data?: any) => {
|
|
165
|
+
let str = `<${node.id}>: ${msg}`
|
|
166
|
+
if (data !== undefined)
|
|
167
|
+
str += ` (${JSON.stringify(data)})`
|
|
168
|
+
this.cli.log(level, str)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
/* open node */
|
|
172
|
+
this.cli.log("info", `open node <${node.id}>`)
|
|
173
|
+
node.setTimeZero(this.timeZero)
|
|
174
|
+
await Promise.race<void>([
|
|
175
|
+
node.open(),
|
|
176
|
+
new Promise<never>((resolve, reject) => setTimeout(() =>
|
|
177
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
178
|
+
]).catch((err: Error) => {
|
|
179
|
+
this.cli.log("error", `<${node.id}>: failed to open node <${node.id}>: ${err.message}`)
|
|
180
|
+
throw new Error(`failed to open node <${node.id}>: ${err.message}`)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* graph establishment: PASS 4: connect node streams */
|
|
186
|
+
async connectStreams() {
|
|
187
|
+
for (const node of this.graphNodes) {
|
|
188
|
+
if (node.stream === null)
|
|
189
|
+
throw new Error(`stream of node <${node.id}> still not initialized`)
|
|
190
|
+
for (const other of Array.from(node.connectionsOut)) {
|
|
191
|
+
if (other.stream === null)
|
|
192
|
+
throw new Error(`stream of incoming node <${other.id}> still not initialized`)
|
|
193
|
+
this.cli.log("info", `connect stream of node <${node.id}> to stream of node <${other.id}>`)
|
|
194
|
+
if (!( node.stream instanceof Stream.Readable
|
|
195
|
+
|| node.stream instanceof Stream.Duplex ))
|
|
196
|
+
throw new Error(`stream of output node <${node.id}> is neither of Readable nor Duplex type`)
|
|
197
|
+
if (!( other.stream instanceof Stream.Writable
|
|
198
|
+
|| other.stream instanceof Stream.Duplex ))
|
|
199
|
+
throw new Error(`stream of input node <${other.id}> is neither of Writable nor Duplex type`)
|
|
200
|
+
node.stream.pipe(other.stream)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* graph establishment: PASS 5: track stream finishing */
|
|
206
|
+
trackFinishing(args: CLIOptions, api: APIServer): void {
|
|
207
|
+
this.finishEvents.removeAllListeners()
|
|
208
|
+
this.finishEvents.setMaxListeners(this.graphNodes.size + 10)
|
|
209
|
+
for (const node of this.graphNodes) {
|
|
210
|
+
if (node.stream === null)
|
|
211
|
+
throw new Error(`stream of node <${node.id}> still not initialized`)
|
|
212
|
+
this.cli.log("info", `observe stream of node <${node.id}> for finish event`)
|
|
213
|
+
this.activeNodes.add(node)
|
|
214
|
+
const deactivateNode = (node: SpeechFlowNode, msg: string) => {
|
|
215
|
+
if (this.activeNodes.has(node))
|
|
216
|
+
this.activeNodes.delete(node)
|
|
217
|
+
this.cli.log("info", `${msg} (${this.activeNodes.size} active nodes remaining)`)
|
|
218
|
+
if (this.activeNodes.size === 0) {
|
|
219
|
+
const timeFinished = DateTime.now()
|
|
220
|
+
const duration = timeFinished.diff(this.timeZero!)
|
|
221
|
+
this.cli.log("info", "**** everything finished -- stream processing in SpeechFlow graph stops " +
|
|
222
|
+
`(total duration: ${duration.toFormat("hh:mm:ss.SSS")}) ****`)
|
|
223
|
+
this.finishEvents.emit("finished")
|
|
224
|
+
this.shutdown("finished", args, api)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
node.stream.on("end", () => {
|
|
228
|
+
deactivateNode(node, `readable stream side of node <${node.id}> raised "end" event`)
|
|
229
|
+
})
|
|
230
|
+
node.stream.on("finish", () => {
|
|
231
|
+
deactivateNode(node, `writable stream side of node <${node.id}> raised "finish" event`)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* start of internal stream processing */
|
|
236
|
+
this.cli.log("info", "**** everything established -- stream processing in SpeechFlow graph starts ****")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* graph destruction: PASS 1: disconnect node streams */
|
|
240
|
+
async disconnectStreams(): Promise<void> {
|
|
241
|
+
for (const node of this.graphNodes) {
|
|
242
|
+
if (node.stream === null) {
|
|
243
|
+
this.cli.log("warning", `stream of node <${node.id}> no longer initialized`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
for (const other of Array.from(node.connectionsOut)) {
|
|
247
|
+
if (other.stream === null) {
|
|
248
|
+
this.cli.log("warning", `stream of incoming node <${other.id}> no longer initialized`)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (!( node.stream instanceof Stream.Readable
|
|
252
|
+
|| node.stream instanceof Stream.Duplex )) {
|
|
253
|
+
this.cli.log("warning", `stream of output node <${node.id}> is neither of Readable nor Duplex type`)
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
if (!( other.stream instanceof Stream.Writable
|
|
257
|
+
|| other.stream instanceof Stream.Duplex )) {
|
|
258
|
+
this.cli.log("warning", `stream of input node <${other.id}> is neither of Writable nor Duplex type`)
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
this.cli.log("info", `disconnect stream of node <${node.id}> from stream of node <${other.id}>`)
|
|
262
|
+
node.stream.unpipe(other.stream)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* graph destruction: PASS 2: close nodes */
|
|
268
|
+
async closeNodes(): Promise<void> {
|
|
269
|
+
for (const node of this.graphNodes) {
|
|
270
|
+
this.cli.log("info", `close node <${node.id}>`)
|
|
271
|
+
await Promise.race<void>([
|
|
272
|
+
node.close(),
|
|
273
|
+
new Promise<never>((resolve, reject) => setTimeout(() =>
|
|
274
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
275
|
+
]).catch((err: Error) => {
|
|
276
|
+
this.cli.log("warning", `node <${node.id}> failed to close: ${err.message}`)
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* graph destruction: PASS 3: disconnect nodes */
|
|
282
|
+
disconnectNodes(): void {
|
|
283
|
+
for (const node of this.graphNodes) {
|
|
284
|
+
this.cli.log("info", `disconnect node <${node.id}>`)
|
|
285
|
+
const connectionsIn = Array.from(node.connectionsIn)
|
|
286
|
+
const connectionsOut = Array.from(node.connectionsOut)
|
|
287
|
+
connectionsIn.forEach((other) => { other.disconnect(node) })
|
|
288
|
+
connectionsOut.forEach((other) => { node.disconnect(other) })
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* graph destruction: PASS 4: destroy nodes */
|
|
293
|
+
destroyNodes(): void {
|
|
294
|
+
for (const node of this.graphNodes) {
|
|
295
|
+
this.cli.log("info", `destroy node <${node.id}>`)
|
|
296
|
+
this.graphNodes.delete(node)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* setup signal handling for shutdown */
|
|
301
|
+
setupSignalHandlers(args: CLIOptions, api: APIServer): void {
|
|
302
|
+
/* internal helper functions */
|
|
303
|
+
const shutdownHandler = (signal: string) =>
|
|
304
|
+
this.shutdown(signal, args, api)
|
|
305
|
+
const logError = (error: Error) => {
|
|
306
|
+
if (this.debug)
|
|
307
|
+
this.cli.log("error", `uncaught exception: ${error.message}\n${error.stack}`)
|
|
308
|
+
else
|
|
309
|
+
this.cli.log("error", `uncaught exception: ${error.message}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* hook into process signals */
|
|
313
|
+
process.on("SIGINT", () => { shutdownHandler("SIGINT") })
|
|
314
|
+
process.on("SIGUSR1", () => { shutdownHandler("SIGUSR1") })
|
|
315
|
+
process.on("SIGUSR2", () => { shutdownHandler("SIGUSR2") })
|
|
316
|
+
process.on("SIGTERM", () => { shutdownHandler("SIGTERM") })
|
|
317
|
+
|
|
318
|
+
/* re-hook into uncaught exception handler */
|
|
319
|
+
process.removeAllListeners("uncaughtException")
|
|
320
|
+
process.on("uncaughtException", (err) => {
|
|
321
|
+
const error = util.ensureError(err, "uncaught exception")
|
|
322
|
+
logError(error)
|
|
323
|
+
shutdownHandler("exception")
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
/* re-hook into unhandled promise rejection handler */
|
|
327
|
+
process.removeAllListeners("unhandledRejection")
|
|
328
|
+
process.on("unhandledRejection", (reason) => {
|
|
329
|
+
const error = util.ensureError(reason, "unhandled promise rejection")
|
|
330
|
+
logError(error)
|
|
331
|
+
shutdownHandler("exception")
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* shutdown procedure */
|
|
336
|
+
async shutdown(signal: string, args: CLIOptions, api: APIServer): Promise<void> {
|
|
337
|
+
if (this.shuttingDown)
|
|
338
|
+
return
|
|
339
|
+
this.shuttingDown = true
|
|
340
|
+
if (signal === "finished")
|
|
341
|
+
this.cli.log("info", "**** streams of all nodes finished -- shutting down service ****")
|
|
342
|
+
else if (signal === "exception")
|
|
343
|
+
this.cli.log("warning", "**** exception occurred -- shutting down service ****")
|
|
344
|
+
else
|
|
345
|
+
this.cli.log("warning", `**** received signal ${signal} -- shutting down service ****`)
|
|
346
|
+
|
|
347
|
+
/* shutdown API service */
|
|
348
|
+
await api.stop(args)
|
|
349
|
+
|
|
350
|
+
/* disconnect, close and destroy nodes */
|
|
351
|
+
await this.disconnectStreams()
|
|
352
|
+
await this.closeNodes()
|
|
353
|
+
this.disconnectNodes()
|
|
354
|
+
this.destroyNodes()
|
|
355
|
+
|
|
356
|
+
/* clear event emitters */
|
|
357
|
+
this.finishEvents.removeAllListeners()
|
|
358
|
+
|
|
359
|
+
/* clear active nodes */
|
|
360
|
+
this.activeNodes.clear()
|
|
361
|
+
|
|
362
|
+
/* terminate process */
|
|
363
|
+
if (signal === "finished") {
|
|
364
|
+
this.cli.log("info", "terminate process (exit code 0)")
|
|
365
|
+
process.exit(0)
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.cli.log("info", "terminate process (exit code 1)")
|
|
369
|
+
process.exit(1)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|