speechflow 0.9.0 → 0.9.2

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.
Files changed (35) hide show
  1. package/README.md +30 -0
  2. package/dst/speechflow-node-deepgram.d.ts +10 -0
  3. package/dst/speechflow-node-deepgram.js +44 -23
  4. package/dst/speechflow-node-deepl.d.ts +10 -0
  5. package/dst/speechflow-node-deepl.js +30 -12
  6. package/dst/speechflow-node-device.d.ts +11 -0
  7. package/dst/speechflow-node-device.js +73 -14
  8. package/dst/speechflow-node-elevenlabs.d.ts +10 -0
  9. package/dst/speechflow-node-elevenlabs.js +14 -2
  10. package/dst/speechflow-node-ffmpeg.d.ts +11 -0
  11. package/dst/speechflow-node-ffmpeg.js +114 -0
  12. package/dst/speechflow-node-file.d.ts +9 -0
  13. package/dst/speechflow-node-file.js +71 -13
  14. package/dst/speechflow-node-gemma.d.ts +11 -0
  15. package/dst/speechflow-node-gemma.js +152 -0
  16. package/dst/speechflow-node-websocket.d.ts +11 -0
  17. package/dst/speechflow-node-websocket.js +34 -6
  18. package/dst/speechflow-node.d.ts +38 -0
  19. package/dst/speechflow-node.js +28 -10
  20. package/dst/speechflow.d.ts +1 -0
  21. package/dst/speechflow.js +128 -43
  22. package/etc/tsconfig.json +2 -0
  23. package/package.json +25 -11
  24. package/src/speechflow-node-deepgram.ts +55 -24
  25. package/src/speechflow-node-deepl.ts +38 -16
  26. package/src/speechflow-node-device.ts +88 -14
  27. package/src/speechflow-node-elevenlabs.ts +19 -2
  28. package/src/speechflow-node-ffmpeg.ts +122 -0
  29. package/src/speechflow-node-file.ts +76 -14
  30. package/src/speechflow-node-gemma.ts +169 -0
  31. package/src/speechflow-node-websocket.ts +52 -13
  32. package/src/speechflow-node.ts +43 -21
  33. package/src/speechflow.ts +144 -47
  34. package/dst/speechflow-util.js +0 -37
  35. package/src/speechflow-util.ts +0 -36
@@ -4,26 +4,57 @@
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
7
- import Stream from "node:stream"
8
- import ws from "ws"
9
- import ReconnWebsocket, { ErrorEvent } from "@opensumi/reconnecting-websocket"
10
- import SpeechFlowNode from "./speechflow-node"
7
+ /* standard dependencies */
8
+ import Stream from "node:stream"
11
9
 
10
+ /* external dependencies */
11
+ import ws from "ws"
12
+ import ReconnWebsocket, { ErrorEvent } from "@opensumi/reconnecting-websocket"
13
+
14
+ /* internal dependencies */
15
+ import SpeechFlowNode from "./speechflow-node"
16
+
17
+ /* SpeechFlow node for Websocket networking */
12
18
  export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
19
+ /* declare official node name */
20
+ public static name = "websocket"
21
+
22
+ /* internal state */
13
23
  private server: ws.WebSocketServer | null = null
14
- private client: WebSocket | null = null
24
+ private client: WebSocket | null = null
25
+
26
+ /* construct node */
15
27
  constructor (id: string, opts: { [ id: string ]: any }, args: any[]) {
16
28
  super(id, opts, args)
29
+
30
+ /* declare node configuration parameters */
17
31
  this.configure({
18
32
  listen: { type: "string", val: "", match: /^(?:|ws:\/\/(.+?):(\d+))$/ },
19
33
  connect: { type: "string", val: "", match: /^(?:|ws:\/\/(.+?):(\d+)(?:\/.*)?)$/ },
20
34
  type: { type: "string", val: "text", match: /^(?:audio|text)$/ }
21
35
  })
36
+
37
+ /* sanity check usage */
38
+ if (this.params.listen !== "" && this.params.connect !== "")
39
+ throw new Error("Websocket node cannot listen and connect at the same time")
40
+ else if (this.params.listen === "" && this.params.connect === "")
41
+ throw new Error("Websocket node requires either listen or connect mode")
42
+
43
+ /* declare node input/output format */
44
+ if (this.params.listen !== "") {
45
+ this.input = "none"
46
+ this.output = this.params.type
47
+ }
48
+ else if (this.params.connect !== "") {
49
+ this.input = this.params.type
50
+ this.output = "none"
51
+ }
22
52
  }
53
+
54
+ /* open node */
23
55
  async open () {
24
- this.input = this.params.type
25
- this.output = this.params.type
26
56
  if (this.params.listen !== "") {
57
+ /* listen locally on a Websocket port */
27
58
  const url = new URL(this.params.listen)
28
59
  let websocket: ws.WebSocket | null = null
29
60
  const server = new ws.WebSocketServer({
@@ -46,8 +77,9 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
46
77
  this.log("error", `error on URL ${this.params.listen}: ${error.message}`)
47
78
  websocket = null
48
79
  })
80
+ const textEncoding = this.config.textEncoding
49
81
  this.stream = new Stream.Duplex({
50
- write (chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void) {
82
+ write (chunk: Buffer, encoding, callback) {
51
83
  const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
52
84
  if (websocket !== null) {
53
85
  websocket.send(data, (error) => {
@@ -61,7 +93,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
61
93
  read (size: number) {
62
94
  if (websocket !== null) {
63
95
  websocket.once("message", (data, isBinary) => {
64
- this.push(data, isBinary ? "binary" : "utf8")
96
+ this.push(data, isBinary ? "binary" : textEncoding)
65
97
  })
66
98
  }
67
99
  else
@@ -70,6 +102,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
70
102
  })
71
103
  }
72
104
  else if (this.params.connect !== "") {
105
+ /* connect remotely to a Websocket port */
73
106
  this.client = new ReconnWebsocket(this.params.connect, [], {
74
107
  WebSocket: ws,
75
108
  WebSocketOptions: {},
@@ -90,8 +123,9 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
90
123
  })
91
124
  const client = this.client
92
125
  client.binaryType = "arraybuffer"
126
+ const textEncoding = this.config.textEncoding
93
127
  this.stream = new Stream.Duplex({
94
- write (chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void) {
128
+ write (chunk: Buffer, encoding, callback) {
95
129
  const data = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
96
130
  if (client.OPEN) {
97
131
  client.send(data)
@@ -106,7 +140,7 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
106
140
  if (ev.data instanceof ArrayBuffer)
107
141
  this.push(ev.data, "binary")
108
142
  else
109
- this.push(ev.data, "utf8")
143
+ this.push(ev.data, textEncoding)
110
144
  }, { once: true })
111
145
  }
112
146
  else
@@ -114,10 +148,11 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
114
148
  }
115
149
  })
116
150
  }
117
- else
118
- throw new Error("neither listen nor connect mode requested")
119
151
  }
152
+
153
+ /* close node */
120
154
  async close () {
155
+ /* close Websocket server */
121
156
  if (this.server !== null) {
122
157
  await new Promise<void>((resolve, reject) => {
123
158
  this.server!.close((error) => {
@@ -127,10 +162,14 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
127
162
  })
128
163
  this.server = null
129
164
  }
165
+
166
+ /* close Websocket client */
130
167
  if (this.client !== null) {
131
168
  this.client!.close()
132
169
  this.client = null
133
170
  }
171
+
172
+ /* close stream */
134
173
  if (this.stream !== null) {
135
174
  this.stream.destroy()
136
175
  this.stream = null
@@ -4,73 +4,95 @@
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
7
- import Events from "node:events"
8
- import Stream from "node:stream"
7
+ /* standard dependencies */
8
+ import Events from "node:events"
9
+ import Stream from "node:stream"
9
10
 
11
+ /* the base class for all SpeechFlow nodes */
10
12
  export default class SpeechFlowNode extends Events.EventEmitter {
11
- public config = {
13
+ /* general constant configuration (for reference) */
14
+ config = {
12
15
  audioChannels: 1, /* audio mono channel */
13
16
  audioBitDepth: 16, /* audio PCM 16-bit integer */
14
17
  audioLittleEndian: true, /* audio PCM little-endian */
15
18
  audioSampleRate: 48000, /* audio 48kHz sample rate */
16
19
  textEncoding: "utf8" /* UTF-8 text encoding */
17
20
  } as const
18
- public input = "none"
19
- public output = "none"
20
- public params = {} as { [ id: string ]: any }
21
- public stream: Stream.Writable | Stream.Readable | Stream.Duplex | null = null
22
21
 
23
- public connectionsIn = new Set<SpeechFlowNode>()
24
- public connectionsOut = new Set<SpeechFlowNode>()
22
+ /* announced information */
23
+ input = "none"
24
+ output = "none"
25
+ params: { [ id: string ]: any } = {}
26
+ stream: Stream.Writable | Stream.Readable | Stream.Duplex | null = null
27
+ connectionsIn = new Set<SpeechFlowNode>()
28
+ connectionsOut = new Set<SpeechFlowNode>()
25
29
 
30
+ /* the default constructor */
26
31
  constructor (
27
- public id: string,
32
+ public id: string,
28
33
  private opts: { [ id: string ]: any },
29
34
  private args: any[]
30
35
  ) {
31
36
  super()
32
37
  }
33
38
 
39
+ /* INTERNAL: utility function: create "params" attribute from constructor of sub-classes */
34
40
  configure (spec: { [ id: string ]: { type: string, pos?: number, val?: any, match?: RegExp } }) {
35
41
  for (const name of Object.keys(spec)) {
36
42
  if (this.opts[name] !== undefined) {
43
+ /* named parameter */
37
44
  if (typeof this.opts[name] !== spec[name].type)
38
- throw new Error(`invalid type of option "${name}"`)
39
- if ("match" in spec[name] && this.opts[name].match(spec[name].match) === null)
40
- throw new Error(`invalid value of option "${name}" (has to match ${spec[name].match})`)
45
+ throw new Error(`invalid type of named parameter "${name}" ` +
46
+ `(has to be ${spec[name].type})`)
47
+ if ("match" in spec[name]
48
+ && this.opts[name].match(spec[name].match) === null)
49
+ throw new Error(`invalid value of named parameter "${name}" ` +
50
+ `(has to match ${spec[name].match})`)
41
51
  this.params[name] = this.opts[name]
42
52
  }
43
53
  else if (this.opts[name] === undefined
44
54
  && "pos" in spec[name]
45
- && spec[name].pos! < this.args.length) {
55
+ && typeof spec[name].pos === "number"
56
+ && spec[name].pos < this.args.length) {
57
+ /* positional argument */
46
58
  if (typeof this.args[spec[name].pos!] !== spec[name].type)
47
- throw new Error(`invalid type of argument "${name}"`)
48
- if ("match" in spec[name] && this.args[spec[name].pos!].match(spec[name].match) === null)
49
- throw new Error(`invalid value of option "${name}" (has to match ${spec[name].match})`)
59
+ throw new Error(`invalid type of positional parameter "${name}" ` +
60
+ `(has to be ${spec[name].type})`)
61
+ if ("match" in spec[name]
62
+ && this.args[spec[name].pos!].match(spec[name].match) === null)
63
+ throw new Error(`invalid value of positional parameter "${name}" ` +
64
+ `(has to match ${spec[name].match})`)
50
65
  this.params[name] = this.args[spec[name].pos!]
51
66
  }
52
67
  else if ("val" in spec[name] && spec[name].val !== undefined)
68
+ /* default argument */
53
69
  this.params[name] = spec[name].val
54
70
  else
55
71
  throw new Error(`required parameter "${name}" not given`)
56
72
  }
57
73
  }
74
+
75
+ /* connect node to another one */
58
76
  connect (other: SpeechFlowNode) {
59
77
  this.connectionsOut.add(other)
60
78
  other.connectionsIn.add(this)
61
79
  }
80
+
81
+ /* disconnect node from another one */
62
82
  disconnect (other: SpeechFlowNode) {
63
83
  if (!this.connectionsOut.has(other))
64
84
  throw new Error("invalid node: not connected to this node")
65
85
  this.connectionsOut.delete(other)
66
86
  other.connectionsIn.delete(this)
67
87
  }
88
+
89
+ /* internal log function */
68
90
  log (level: string, msg: string, data?: any) {
69
91
  this.emit("log", level, msg, data)
70
92
  }
71
- async open () {
72
- }
73
- async close () {
74
- }
93
+
94
+ /* default implementation for open/close operations */
95
+ async open () {}
96
+ async close () {}
75
97
  }
76
98
 
package/src/speechflow.ts CHANGED
@@ -1,28 +1,30 @@
1
- /*
1
+ #!/usr/bin/env node
2
+ /*!
2
3
  ** SpeechFlow - Speech Processing Flow Graph
3
4
  ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
5
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
6
  */
6
7
 
8
+ /* standard dependencies */
7
9
  import Stream from "node:stream"
8
10
 
11
+ /* external dependencies */
9
12
  import CLIio from "cli-io"
10
13
  import yargs from "yargs"
11
14
  import jsYAML from "js-yaml"
12
15
  import FlowLink from "flowlink"
13
16
  import objectPath from "object-path"
17
+ import installedPackages from "installed-packages"
18
+ import dotenvx from "@dotenvx/dotenvx"
14
19
 
20
+ /* internal dependencies */
15
21
  import SpeechFlowNode from "./speechflow-node"
16
- import SpeechFlowNodeFile from "./speechflow-node-file"
17
- import SpeechFlowNodeDevice from "./speechflow-node-device"
18
- import SpeechFlowNodeWebsocket from "./speechflow-node-websocket"
19
- import SpeechFlowNodeDeepgram from "./speechflow-node-deepgram"
20
- import SpeechFlowNodeDeepL from "./speechflow-node-deepl"
21
- import SpeechFlowNodeElevenLabs from "./speechflow-node-elevenlabs"
22
-
23
22
  import pkg from "../package.json"
24
23
 
24
+ /* central CLI context */
25
25
  let cli: CLIio | null = null
26
+
27
+ /* establish asynchronous environment */
26
28
  ;(async () => {
27
29
  /* parse command-line arguments */
28
30
  const args = await yargs()
@@ -57,7 +59,7 @@ let cli: CLIio | null = null
57
59
 
58
60
  /* short-circuit version request */
59
61
  if (args.version) {
60
- process.stderr.write(`${pkg.name} ${pkg.version} <${pkg.homepage}>\n`)
62
+ process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
61
63
  process.stderr.write(`${pkg.description}\n`)
62
64
  process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
63
65
  process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
@@ -72,6 +74,15 @@ let cli: CLIio | null = null
72
74
  logPrefix: pkg.name
73
75
  })
74
76
 
77
+ /* provide startup information */
78
+ cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
79
+
80
+ /* load .env files */
81
+ const result = dotenvx.config({ encoding: "utf8", quiet: true })
82
+ if (result?.parsed !== undefined)
83
+ for (const key of Object.keys(result.parsed))
84
+ cli.log("info", `loaded environment variable "${key}" from ".env" files`)
85
+
75
86
  /* handle uncaught exceptions */
76
87
  process.on("uncaughtException", async (err: Error) => {
77
88
  cli!.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`)
@@ -113,17 +124,50 @@ let cli: CLIio | null = null
113
124
  config = obj[key] as string
114
125
  }
115
126
 
116
- /* configuration of nodes */
117
- const nodes: { [ id: string ]: typeof SpeechFlowNode } = {
118
- "file": SpeechFlowNodeFile,
119
- "device": SpeechFlowNodeDevice,
120
- "websocket": SpeechFlowNodeWebsocket,
121
- "deepgram": SpeechFlowNodeDeepgram,
122
- "deepl": SpeechFlowNodeDeepL,
123
- "elevenlabs": SpeechFlowNodeElevenLabs
127
+ /* track the available SpeechFlow nodes */
128
+ const nodes: { [ id: string ]: typeof SpeechFlowNode } = {}
129
+
130
+ /* load internal SpeechFlow nodes */
131
+ const pkgsI = [
132
+ "./speechflow-node-file.js",
133
+ "./speechflow-node-device.js",
134
+ "./speechflow-node-websocket.js",
135
+ "./speechflow-node-ffmpeg.js",
136
+ "./speechflow-node-deepgram.js",
137
+ "./speechflow-node-deepl.js",
138
+ "./speechflow-node-elevenlabs.js",
139
+ "./speechflow-node-gemma.js",
140
+ ]
141
+ for (const pkg of pkgsI) {
142
+ let node: any = await import(pkg)
143
+ while (node.default !== undefined)
144
+ node = node.default
145
+ if (typeof node === "function" && typeof node.name === "string") {
146
+ cli.log("info", `loading SpeechFlow node "${node.name}" from internal module`)
147
+ nodes[node.name] = node as typeof SpeechFlowNode
148
+ }
149
+ }
150
+
151
+ /* load external SpeechFlow nodes */
152
+ const pkgsE = await installedPackages()
153
+ for (const pkg of pkgsE) {
154
+ if (pkg.match(/^(?:@[^/]+\/)?speechflow-node-.+$/)) {
155
+ let node: any = await import(pkg)
156
+ while (node.default !== undefined)
157
+ node = node.default
158
+ if (typeof node === "function" && typeof node.name === "string") {
159
+ if (nodes[node.name] !== undefined) {
160
+ cli.log("warning", `failed loading SpeechFlow node "${node.name}" ` +
161
+ `from external module "${pkg}" -- node already exists`)
162
+ continue
163
+ }
164
+ cli.log("info", `loading SpeechFlow node "${node.name}" from external module "${pkg}"`)
165
+ nodes[node.name] = node as typeof SpeechFlowNode
166
+ }
167
+ }
124
168
  }
125
169
 
126
- /* parse configuration into node graph */
170
+ /* graph processing: PASS 1: parse DSL and create and connect nodes */
127
171
  const flowlink = new FlowLink<SpeechFlowNode>({
128
172
  trace: (msg: string) => {
129
173
  cli!.log("debug", msg)
@@ -142,37 +186,22 @@ let cli: CLIio | null = null
142
186
  },
143
187
  createNode (id: string, opts: { [ id: string ]: any }, args: any[]) {
144
188
  if (nodes[id] === undefined)
145
- throw new Error(`unknown SpeechFlow node "${id}"`)
189
+ throw new Error(`unknown node "${id}"`)
146
190
  const node = new nodes[id](`${id}[${nodenum++}]`, opts, args)
147
- graphNodes.add(node)
148
191
  const params = Object.keys(node.params)
149
192
  .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
150
- cli!.log("info", `created SpeechFlow node "${node.id}" (${params})`)
193
+ cli!.log("info", `create node "${node.id}" (${params})`)
194
+ graphNodes.add(node)
151
195
  return node
152
196
  },
153
197
  connectNode (node1: SpeechFlowNode, node2: SpeechFlowNode) {
154
- cli!.log("info", `connect SpeechFlow node "${node1.id}" to node "${node2.id}"`)
198
+ cli!.log("info", `connect node "${node1.id}" to node "${node2.id}"`)
155
199
  node1.connect(node2)
156
200
  }
157
201
  })
158
202
 
159
- /* graph processing: PASS 1: activate and sanity check nodes */
203
+ /* graph processing: PASS 2: prune connections of nodes */
160
204
  for (const node of graphNodes) {
161
- /* connect node events */
162
- node.on("log", (level: string, msg: string, data?: any) => {
163
- let str = `[${node.id}]: ${msg}`
164
- if (data !== undefined)
165
- str += ` (${JSON.stringify(data)})`
166
- cli!.log(level, str)
167
- })
168
-
169
- /* open node */
170
- cli!.log("info", `opening node "${node.id}"`)
171
- await node.open().catch((err: Error) => {
172
- cli!.log("error", `[${node.id}]: ${err.message}`)
173
- throw new Error(`failed to open node "${node.id}"`)
174
- })
175
-
176
205
  /* determine connections */
177
206
  const connectionsIn = Array.from(node.connectionsIn)
178
207
  const connectionsOut = Array.from(node.connectionsOut)
@@ -192,20 +221,47 @@ let cli: CLIio | null = null
192
221
  /* prune unnecessary outgoing links */
193
222
  if (node.output === "none" && connectionsOut.length > 0)
194
223
  connectionsOut.forEach((other) => { node.disconnect(other) })
224
+
225
+ /* check for payload compatibility */
226
+ for (const other of connectionsOut)
227
+ if (other.input !== node.output)
228
+ throw new Error(`${node.output} output node "${node.id}" cannot be ` +
229
+ `connected to ${other.input} input node "${other.id}" (payload is incompatible)`)
195
230
  }
196
231
 
197
- /* graph processing: PASS 2: activate streams */
232
+ /* graph processing: PASS 3: open nodes */
233
+ for (const node of graphNodes) {
234
+ /* connect node events */
235
+ node.on("log", (level: string, msg: string, data?: any) => {
236
+ let str = `[${node.id}]: ${msg}`
237
+ if (data !== undefined)
238
+ str += ` (${JSON.stringify(data)})`
239
+ cli!.log(level, str)
240
+ })
241
+
242
+ /* open node */
243
+ cli!.log("info", `open node "${node.id}"`)
244
+ await node.open().catch((err: Error) => {
245
+ cli!.log("error", `[${node.id}]: ${err.message}`)
246
+ throw new Error(`failed to open node "${node.id}"`)
247
+ })
248
+ }
249
+
250
+ /* graph processing: PASS 4: connect node streams */
198
251
  for (const node of graphNodes) {
199
252
  if (node.stream === null)
200
- throw new Error(`stream of outgoing node "${node.id}" still not initialized`)
253
+ throw new Error(`stream of node "${node.id}" still not initialized`)
201
254
  for (const other of Array.from(node.connectionsOut)) {
202
255
  if (other.stream === null)
203
256
  throw new Error(`stream of incoming node "${other.id}" still not initialized`)
204
- if (node.output !== other.input)
205
- throw new Error(`${node.output} output node "${node.id}" cannot be " +
206
- "connected to ${other.input} input node "${other.id}" (payload is incompatible)`)
207
- cli!.log("info", `connecting stream of node "${node.id}" to stream of node "${other.id}"`)
208
- node.stream.pipe(other.stream as Stream.Writable)
257
+ cli!.log("info", `connect stream of node "${node.id}" to stream of node "${other.id}"`)
258
+ if (!( node.stream instanceof Stream.Readable
259
+ || node.stream instanceof Stream.Duplex ))
260
+ throw new Error(`stream of output node "${node.id}" is neither of Readable nor Duplex type`)
261
+ if (!( other.stream instanceof Stream.Writable
262
+ || other.stream instanceof Stream.Duplex ))
263
+ throw new Error(`stream of input node "${other.id}" is neither of Writable nor Duplex type`)
264
+ node.stream.pipe(other.stream)
209
265
  }
210
266
  }
211
267
 
@@ -216,14 +272,55 @@ let cli: CLIio | null = null
216
272
  return
217
273
  shuttingDown = true
218
274
  cli!.log("warning", `received signal ${signal} -- shutting down service`)
275
+
276
+ /* graph processing: PASS 1: disconnect node streams */
277
+ for (const node of graphNodes) {
278
+ if (node.stream === null) {
279
+ cli!.log("warning", `stream of node "${node.id}" no longer initialized`)
280
+ continue
281
+ }
282
+ for (const other of Array.from(node.connectionsOut)) {
283
+ if (other.stream === null) {
284
+ cli!.log("warning", `stream of incoming node "${other.id}" no longer initialized`)
285
+ continue
286
+ }
287
+ if (!( node.stream instanceof Stream.Readable
288
+ || node.stream instanceof Stream.Duplex )) {
289
+ cli!.log("warning", `stream of output node "${node.id}" is neither of Readable nor Duplex type`)
290
+ continue
291
+ }
292
+ if (!( other.stream instanceof Stream.Writable
293
+ || other.stream instanceof Stream.Duplex )) {
294
+ cli!.log("warning", `stream of input node "${other.id}" is neither of Writable nor Duplex type`)
295
+ continue
296
+ }
297
+ cli!.log("info", `disconnect stream of node "${node.id}" from stream of node "${other.id}"`)
298
+ node.stream.unpipe(other.stream)
299
+ }
300
+ }
301
+
302
+ /* graph processing: PASS 2: close nodes */
219
303
  for (const node of graphNodes) {
220
- cli!.log("info", `closing node "${node.id}"`)
304
+ cli!.log("info", `close node "${node.id}"`)
305
+ await node.close()
306
+ }
307
+
308
+ /* graph processing: PASS 3: disconnect nodes */
309
+ for (const node of graphNodes) {
310
+ cli!.log("info", `disconnect node "${node.id}"`)
221
311
  const connectionsIn = Array.from(node.connectionsIn)
222
312
  const connectionsOut = Array.from(node.connectionsOut)
223
313
  connectionsIn.forEach((other) => { other.disconnect(node) })
224
314
  connectionsOut.forEach((other) => { node.disconnect(other) })
225
- await node.close()
226
315
  }
316
+
317
+ /* graph processing: PASS 4: shutdown nodes */
318
+ for (const node of graphNodes) {
319
+ cli!.log("info", `destroy node "${node.id}"`)
320
+ graphNodes.delete(node)
321
+ }
322
+
323
+ /* terminate process */
227
324
  process.exit(1)
228
325
  }
229
326
  process.on("SIGINT", () => {
@@ -1,37 +0,0 @@
1
- "use strict";
2
- /*
3
- ** SpeechFlow - Speech Processing Flow Graph
4
- ** Copyright (c) 2024-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
- ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
- */
7
- var __importDefault = (this && this.__importDefault) || function (mod) {
8
- return (mod && mod.__esModule) ? mod : { "default": mod };
9
- };
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- const naudiodon_1 = __importDefault(require("@gpeng/naudiodon"));
12
- class SpeechFlowUtil {
13
- static audioDeviceFromURL(mode, url) {
14
- const m = url.match(/^(.+?):(.+)$/);
15
- if (m === null)
16
- throw new Error(`invalid audio device URL "${url}"`);
17
- const [, type, name] = m;
18
- const apis = naudiodon_1.default.getHostAPIs();
19
- const api = apis.HostAPIs.find((api) => api.type.toLowerCase() === type.toLowerCase());
20
- if (!api)
21
- throw new Error(`invalid audio device type "${type}"`);
22
- const devices = naudiodon_1.default.getDevices();
23
- console.log(devices);
24
- const device = devices.find((device) => {
25
- return (((mode === "r" && device.maxInputChannels > 0)
26
- || (mode === "w" && device.maxOutputChannels > 0)
27
- || (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
28
- || (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
29
- && device.name.match(name)
30
- && device.hostAPIName === api.name);
31
- });
32
- if (!device)
33
- throw new Error(`invalid audio device name "${name}" (of audio type "${type}")`);
34
- return device;
35
- }
36
- }
37
- exports.default = SpeechFlowUtil;
@@ -1,36 +0,0 @@
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
- import PortAudio from "@gpeng/naudiodon"
8
-
9
- export default class SpeechFlowUtil {
10
- static audioDeviceFromURL (mode: "any" | "r" | "w" | "rw", url: string) {
11
- const m = url.match(/^(.+?):(.+)$/)
12
- if (m === null)
13
- throw new Error(`invalid audio device URL "${url}"`)
14
- const [ , type, name ] = m
15
- const apis = PortAudio.getHostAPIs()
16
- const api = apis.HostAPIs.find((api) => api.type.toLowerCase() === type.toLowerCase())
17
- if (!api)
18
- throw new Error(`invalid audio device type "${type}"`)
19
- const devices = PortAudio.getDevices()
20
- console.log(devices)
21
- const device = devices.find((device) => {
22
- return (
23
- ( ( mode === "r" && device.maxInputChannels > 0)
24
- || (mode === "w" && device.maxOutputChannels > 0)
25
- || (mode === "rw" && device.maxInputChannels > 0 && device.maxOutputChannels > 0)
26
- || (mode === "any" && (device.maxInputChannels > 0 || device.maxOutputChannels > 0)))
27
- && device.name.match(name)
28
- && device.hostAPIName === api.name
29
- )
30
- })
31
- if (!device)
32
- throw new Error(`invalid audio device name "${name}" (of audio type "${type}")`)
33
- return device
34
- }
35
- }
36
-