speechflow 0.9.0 → 0.9.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.
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 +24 -10
  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 +142 -46
  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
@@ -4,25 +4,26 @@
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
7
+ /* standard dependencies */
7
8
  import Stream from "node:stream"
8
9
 
10
+ /* external dependencies */
9
11
  import CLIio from "cli-io"
10
12
  import yargs from "yargs"
11
13
  import jsYAML from "js-yaml"
12
14
  import FlowLink from "flowlink"
13
15
  import objectPath from "object-path"
16
+ import installedPackages from "installed-packages"
17
+ import dotenvx from "@dotenvx/dotenvx"
14
18
 
19
+ /* internal dependencies */
15
20
  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
21
  import pkg from "../package.json"
24
22
 
23
+ /* central CLI context */
25
24
  let cli: CLIio | null = null
25
+
26
+ /* establish asynchronous environment */
26
27
  ;(async () => {
27
28
  /* parse command-line arguments */
28
29
  const args = await yargs()
@@ -57,7 +58,7 @@ let cli: CLIio | null = null
57
58
 
58
59
  /* short-circuit version request */
59
60
  if (args.version) {
60
- process.stderr.write(`${pkg.name} ${pkg.version} <${pkg.homepage}>\n`)
61
+ process.stderr.write(`SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]}) <${pkg.homepage}>\n`)
61
62
  process.stderr.write(`${pkg.description}\n`)
62
63
  process.stderr.write(`Copyright (c) 2024-2025 ${pkg.author.name} <${pkg.author.url}>\n`)
63
64
  process.stderr.write(`Licensed under ${pkg.license} <http://spdx.org/licenses/${pkg.license}.html>\n`)
@@ -72,6 +73,15 @@ let cli: CLIio | null = null
72
73
  logPrefix: pkg.name
73
74
  })
74
75
 
76
+ /* provide startup information */
77
+ cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
78
+
79
+ /* load .env files */
80
+ const result = dotenvx.config({ encoding: "utf8", quiet: true })
81
+ if (result?.parsed !== undefined)
82
+ for (const key of Object.keys(result.parsed))
83
+ cli.log("info", `loaded environment variable "${key}" from ".env" files`)
84
+
75
85
  /* handle uncaught exceptions */
76
86
  process.on("uncaughtException", async (err: Error) => {
77
87
  cli!.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`)
@@ -113,17 +123,50 @@ let cli: CLIio | null = null
113
123
  config = obj[key] as string
114
124
  }
115
125
 
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
126
+ /* track the available SpeechFlow nodes */
127
+ const nodes: { [ id: string ]: typeof SpeechFlowNode } = {}
128
+
129
+ /* load internal SpeechFlow nodes */
130
+ const pkgsI = [
131
+ "./speechflow-node-file.js",
132
+ "./speechflow-node-device.js",
133
+ "./speechflow-node-websocket.js",
134
+ "./speechflow-node-ffmpeg.js",
135
+ "./speechflow-node-deepgram.js",
136
+ "./speechflow-node-deepl.js",
137
+ "./speechflow-node-elevenlabs.js",
138
+ "./speechflow-node-gemma.js",
139
+ ]
140
+ for (const pkg of pkgsI) {
141
+ let node: any = await import(pkg)
142
+ while (node.default !== undefined)
143
+ node = node.default
144
+ if (typeof node === "function" && typeof node.name === "string") {
145
+ cli.log("info", `loading SpeechFlow node "${node.name}" from internal module`)
146
+ nodes[node.name] = node as typeof SpeechFlowNode
147
+ }
148
+ }
149
+
150
+ /* load external SpeechFlow nodes */
151
+ const pkgsE = await installedPackages()
152
+ for (const pkg of pkgsE) {
153
+ if (pkg.match(/^(?:@[^/]+\/)?speechflow-node-.+$/)) {
154
+ let node: any = await import(pkg)
155
+ while (node.default !== undefined)
156
+ node = node.default
157
+ if (typeof node === "function" && typeof node.name === "string") {
158
+ if (nodes[node.name] !== undefined) {
159
+ cli.log("warning", `failed loading SpeechFlow node "${node.name}" ` +
160
+ `from external module "${pkg}" -- node already exists`)
161
+ continue
162
+ }
163
+ cli.log("info", `loading SpeechFlow node "${node.name}" from external module "${pkg}"`)
164
+ nodes[node.name] = node as typeof SpeechFlowNode
165
+ }
166
+ }
124
167
  }
125
168
 
126
- /* parse configuration into node graph */
169
+ /* graph processing: PASS 1: parse DSL and create and connect nodes */
127
170
  const flowlink = new FlowLink<SpeechFlowNode>({
128
171
  trace: (msg: string) => {
129
172
  cli!.log("debug", msg)
@@ -142,37 +185,22 @@ let cli: CLIio | null = null
142
185
  },
143
186
  createNode (id: string, opts: { [ id: string ]: any }, args: any[]) {
144
187
  if (nodes[id] === undefined)
145
- throw new Error(`unknown SpeechFlow node "${id}"`)
188
+ throw new Error(`unknown node "${id}"`)
146
189
  const node = new nodes[id](`${id}[${nodenum++}]`, opts, args)
147
- graphNodes.add(node)
148
190
  const params = Object.keys(node.params)
149
191
  .map((key) => `${key}: ${JSON.stringify(node.params[key])}`).join(", ")
150
- cli!.log("info", `created SpeechFlow node "${node.id}" (${params})`)
192
+ cli!.log("info", `create node "${node.id}" (${params})`)
193
+ graphNodes.add(node)
151
194
  return node
152
195
  },
153
196
  connectNode (node1: SpeechFlowNode, node2: SpeechFlowNode) {
154
- cli!.log("info", `connect SpeechFlow node "${node1.id}" to node "${node2.id}"`)
197
+ cli!.log("info", `connect node "${node1.id}" to node "${node2.id}"`)
155
198
  node1.connect(node2)
156
199
  }
157
200
  })
158
201
 
159
- /* graph processing: PASS 1: activate and sanity check nodes */
202
+ /* graph processing: PASS 2: prune connections of nodes */
160
203
  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
204
  /* determine connections */
177
205
  const connectionsIn = Array.from(node.connectionsIn)
178
206
  const connectionsOut = Array.from(node.connectionsOut)
@@ -192,20 +220,47 @@ let cli: CLIio | null = null
192
220
  /* prune unnecessary outgoing links */
193
221
  if (node.output === "none" && connectionsOut.length > 0)
194
222
  connectionsOut.forEach((other) => { node.disconnect(other) })
223
+
224
+ /* check for payload compatibility */
225
+ for (const other of connectionsOut)
226
+ if (other.input !== node.output)
227
+ throw new Error(`${node.output} output node "${node.id}" cannot be ` +
228
+ `connected to ${other.input} input node "${other.id}" (payload is incompatible)`)
195
229
  }
196
230
 
197
- /* graph processing: PASS 2: activate streams */
231
+ /* graph processing: PASS 3: open nodes */
232
+ for (const node of graphNodes) {
233
+ /* connect node events */
234
+ node.on("log", (level: string, msg: string, data?: any) => {
235
+ let str = `[${node.id}]: ${msg}`
236
+ if (data !== undefined)
237
+ str += ` (${JSON.stringify(data)})`
238
+ cli!.log(level, str)
239
+ })
240
+
241
+ /* open node */
242
+ cli!.log("info", `open node "${node.id}"`)
243
+ await node.open().catch((err: Error) => {
244
+ cli!.log("error", `[${node.id}]: ${err.message}`)
245
+ throw new Error(`failed to open node "${node.id}"`)
246
+ })
247
+ }
248
+
249
+ /* graph processing: PASS 4: connect node streams */
198
250
  for (const node of graphNodes) {
199
251
  if (node.stream === null)
200
- throw new Error(`stream of outgoing node "${node.id}" still not initialized`)
252
+ throw new Error(`stream of node "${node.id}" still not initialized`)
201
253
  for (const other of Array.from(node.connectionsOut)) {
202
254
  if (other.stream === null)
203
255
  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)
256
+ cli!.log("info", `connect stream of node "${node.id}" to stream of node "${other.id}"`)
257
+ if (!( node.stream instanceof Stream.Readable
258
+ || node.stream instanceof Stream.Duplex ))
259
+ throw new Error(`stream of output node "${node.id}" is neither of Readable nor Duplex type`)
260
+ if (!( other.stream instanceof Stream.Writable
261
+ || other.stream instanceof Stream.Duplex ))
262
+ throw new Error(`stream of input node "${other.id}" is neither of Writable nor Duplex type`)
263
+ node.stream.pipe(other.stream)
209
264
  }
210
265
  }
211
266
 
@@ -216,14 +271,55 @@ let cli: CLIio | null = null
216
271
  return
217
272
  shuttingDown = true
218
273
  cli!.log("warning", `received signal ${signal} -- shutting down service`)
274
+
275
+ /* graph processing: PASS 1: disconnect node streams */
276
+ for (const node of graphNodes) {
277
+ if (node.stream === null) {
278
+ cli!.log("warning", `stream of node "${node.id}" no longer initialized`)
279
+ continue
280
+ }
281
+ for (const other of Array.from(node.connectionsOut)) {
282
+ if (other.stream === null) {
283
+ cli!.log("warning", `stream of incoming node "${other.id}" no longer initialized`)
284
+ continue
285
+ }
286
+ if (!( node.stream instanceof Stream.Readable
287
+ || node.stream instanceof Stream.Duplex )) {
288
+ cli!.log("warning", `stream of output node "${node.id}" is neither of Readable nor Duplex type`)
289
+ continue
290
+ }
291
+ if (!( other.stream instanceof Stream.Writable
292
+ || other.stream instanceof Stream.Duplex )) {
293
+ cli!.log("warning", `stream of input node "${other.id}" is neither of Writable nor Duplex type`)
294
+ continue
295
+ }
296
+ cli!.log("info", `disconnect stream of node "${node.id}" from stream of node "${other.id}"`)
297
+ node.stream.unpipe(other.stream)
298
+ }
299
+ }
300
+
301
+ /* graph processing: PASS 2: close nodes */
219
302
  for (const node of graphNodes) {
220
- cli!.log("info", `closing node "${node.id}"`)
303
+ cli!.log("info", `close node "${node.id}"`)
304
+ await node.close()
305
+ }
306
+
307
+ /* graph processing: PASS 3: disconnect nodes */
308
+ for (const node of graphNodes) {
309
+ cli!.log("info", `disconnect node "${node.id}"`)
221
310
  const connectionsIn = Array.from(node.connectionsIn)
222
311
  const connectionsOut = Array.from(node.connectionsOut)
223
312
  connectionsIn.forEach((other) => { other.disconnect(node) })
224
313
  connectionsOut.forEach((other) => { node.disconnect(other) })
225
- await node.close()
226
314
  }
315
+
316
+ /* graph processing: PASS 4: shutdown nodes */
317
+ for (const node of graphNodes) {
318
+ cli!.log("info", `destroy node "${node.id}"`)
319
+ graphNodes.delete(node)
320
+ }
321
+
322
+ /* terminate process */
227
323
  process.exit(1)
228
324
  }
229
325
  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
-