speechflow 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +46 -11
  3. package/dst/speechflow-node-a2a-gender.d.ts +17 -0
  4. package/dst/speechflow-node-a2a-gender.js +272 -0
  5. package/dst/speechflow-node-a2a-gender.js.map +1 -0
  6. package/dst/speechflow-node-a2a-meter.js +7 -3
  7. package/dst/speechflow-node-a2a-meter.js.map +1 -1
  8. package/dst/speechflow-node-a2a-mute.js +1 -0
  9. package/dst/speechflow-node-a2a-mute.js.map +1 -1
  10. package/dst/speechflow-node-a2a-vad.js +47 -63
  11. package/dst/speechflow-node-a2a-vad.js.map +1 -1
  12. package/dst/speechflow-node-a2a-wav.js +145 -122
  13. package/dst/speechflow-node-a2a-wav.js.map +1 -1
  14. package/dst/speechflow-node-a2t-deepgram.d.ts +3 -0
  15. package/dst/speechflow-node-a2t-deepgram.js +29 -4
  16. package/dst/speechflow-node-a2t-deepgram.js.map +1 -1
  17. package/dst/speechflow-node-t2a-elevenlabs.d.ts +3 -0
  18. package/dst/speechflow-node-t2a-elevenlabs.js +18 -6
  19. package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
  20. package/dst/speechflow-node-t2a-kokoro.js.map +1 -1
  21. package/dst/speechflow-node-t2t-deepl.d.ts +3 -0
  22. package/dst/speechflow-node-t2t-deepl.js +8 -1
  23. package/dst/speechflow-node-t2t-deepl.js.map +1 -1
  24. package/dst/speechflow-node-t2t-format.js.map +1 -1
  25. package/dst/speechflow-node-t2t-ollama.js.map +1 -1
  26. package/dst/speechflow-node-t2t-openai.js +1 -1
  27. package/dst/speechflow-node-t2t-openai.js.map +1 -1
  28. package/dst/speechflow-node-t2t-subtitle.js.map +1 -1
  29. package/dst/speechflow-node-t2t-transformers.js.map +1 -1
  30. package/dst/speechflow-node-x2x-filter.d.ts +11 -0
  31. package/dst/speechflow-node-x2x-filter.js +113 -0
  32. package/dst/speechflow-node-x2x-filter.js.map +1 -0
  33. package/dst/speechflow-node-x2x-trace.js +25 -11
  34. package/dst/speechflow-node-x2x-trace.js.map +1 -1
  35. package/dst/speechflow-node-xio-device.js +17 -6
  36. package/dst/speechflow-node-xio-device.js.map +1 -1
  37. package/dst/speechflow-node-xio-file.js +61 -28
  38. package/dst/speechflow-node-xio-file.js.map +1 -1
  39. package/dst/speechflow-node-xio-mqtt.js +7 -5
  40. package/dst/speechflow-node-xio-mqtt.js.map +1 -1
  41. package/dst/speechflow-node-xio-websocket.js +5 -5
  42. package/dst/speechflow-node-xio-websocket.js.map +1 -1
  43. package/dst/speechflow-node.d.ts +5 -1
  44. package/dst/speechflow-node.js +9 -2
  45. package/dst/speechflow-node.js.map +1 -1
  46. package/dst/speechflow-utils.d.ts +14 -1
  47. package/dst/speechflow-utils.js +110 -2
  48. package/dst/speechflow-utils.js.map +1 -1
  49. package/dst/speechflow.js +73 -14
  50. package/dst/speechflow.js.map +1 -1
  51. package/etc/speechflow.yaml +53 -26
  52. package/package.json +12 -10
  53. package/src/speechflow-node-a2a-gender.ts +272 -0
  54. package/src/speechflow-node-a2a-meter.ts +8 -4
  55. package/src/speechflow-node-a2a-mute.ts +1 -0
  56. package/src/speechflow-node-a2a-vad.ts +58 -68
  57. package/src/speechflow-node-a2a-wav.ts +128 -91
  58. package/src/speechflow-node-a2t-deepgram.ts +32 -5
  59. package/src/speechflow-node-t2a-elevenlabs.ts +21 -8
  60. package/src/speechflow-node-t2a-kokoro.ts +3 -3
  61. package/src/speechflow-node-t2t-deepl.ts +11 -3
  62. package/src/speechflow-node-t2t-format.ts +2 -2
  63. package/src/speechflow-node-t2t-ollama.ts +2 -2
  64. package/src/speechflow-node-t2t-openai.ts +3 -3
  65. package/src/speechflow-node-t2t-subtitle.ts +1 -1
  66. package/src/speechflow-node-t2t-transformers.ts +2 -2
  67. package/src/speechflow-node-x2x-filter.ts +122 -0
  68. package/src/speechflow-node-x2x-trace.ts +29 -12
  69. package/src/speechflow-node-xio-device.ts +24 -9
  70. package/src/speechflow-node-xio-file.ts +76 -36
  71. package/src/speechflow-node-xio-mqtt.ts +11 -9
  72. package/src/speechflow-node-xio-websocket.ts +7 -7
  73. package/src/speechflow-node.ts +11 -2
  74. package/src/speechflow-utils.ts +81 -2
  75. package/src/speechflow.ts +96 -35
@@ -5,7 +5,9 @@
5
5
  */
6
6
 
7
7
  /* standard dependencies */
8
- import Stream from "node:stream"
8
+ import Stream from "node:stream"
9
+
10
+ /* external dependencies */
9
11
  import { Duration } from "luxon"
10
12
 
11
13
  /* internal dependencies */
@@ -23,7 +25,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
23
25
  /* declare node configuration parameters */
24
26
  this.configure({
25
27
  type: { type: "string", pos: 0, val: "audio", match: /^(?:audio|text)$/ },
26
- name: { type: "string", pos: 1 }
28
+ name: { type: "string", pos: 1, val: "trace" }
27
29
  })
28
30
 
29
31
  /* declare node input/output format */
@@ -41,7 +43,7 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
41
43
  this.log(level, msg)
42
44
  }
43
45
 
44
- /* provide Duplex stream and internally attach to Deepgram API */
46
+ /* provide Transform stream */
45
47
  const type = this.params.type
46
48
  this.stream = new Stream.Transform({
47
49
  writableObjectMode: true,
@@ -49,23 +51,38 @@ export default class SpeechFlowNodeTrace extends SpeechFlowNode {
49
51
  decodeStrings: false,
50
52
  transform (chunk: SpeechFlowChunk, encoding, callback) {
51
53
  let error: Error | undefined
52
- const fmt = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
54
+ const fmtTime = (t: Duration) => t.toFormat("hh:mm:ss.SSS")
55
+ const fmtMeta = (meta: Map<string, any>) => {
56
+ if (meta.size === 0)
57
+ return "none"
58
+ else
59
+ return `{ ${Array.from(meta.entries())
60
+ .map(([ k, v ]) => `${k}: ${JSON.stringify(v)}`)
61
+ .join(", ")
62
+ } }`
63
+ }
53
64
  if (Buffer.isBuffer(chunk.payload)) {
54
65
  if (type === "audio")
55
- log("debug", `writing ${type} chunk: start=${fmt(chunk.timestampStart)} ` +
56
- `end=${fmt(chunk.timestampEnd)} kind=${chunk.kind} type=${chunk.type} ` +
57
- `payload-type=Buffer payload-bytes=${chunk.payload.byteLength}`)
66
+ log("debug", `chunk: type=${chunk.type} ` +
67
+ `kind=${chunk.kind} ` +
68
+ `start=${fmtTime(chunk.timestampStart)} ` +
69
+ `end=${fmtTime(chunk.timestampEnd)} ` +
70
+ `payload-type=Buffer payload-length=${chunk.payload.byteLength} ` +
71
+ `meta=${fmtMeta(chunk.meta)}`)
58
72
  else
59
- error = new Error(`writing ${type} chunk: seen Buffer instead of String chunk type`)
73
+ error = new Error(`${type} chunk: seen Buffer instead of String chunk type`)
60
74
  }
61
75
  else {
62
76
  if (type === "text")
63
- log("debug", `writing ${type} chunk: start=${fmt(chunk.timestampStart)} ` +
64
- `end=${fmt(chunk.timestampEnd)} kind=${chunk.kind} type=${chunk.type}` +
77
+ log("debug", `${type} chunk: type=${chunk.type}` +
78
+ `kind=${chunk.kind} ` +
79
+ `start=${fmtTime(chunk.timestampStart)} ` +
80
+ `end=${fmtTime(chunk.timestampEnd)} ` +
65
81
  `payload-type=String payload-length=${chunk.payload.length} ` +
66
- `payload-encoding=${encoding} payload-content="${chunk.payload.toString()}"`)
82
+ `payload-encoding=${encoding} payload-content="${chunk.payload.toString()}" ` +
83
+ `meta=${fmtMeta(chunk.meta)}`)
67
84
  else
68
- error = new Error(`writing ${type} chunk: seen String instead of Buffer chunk type`)
85
+ error = new Error(`${type} chunk: seen String instead of Buffer chunk type`)
69
86
  }
70
87
  if (error !== undefined)
71
88
  callback(error)
@@ -31,8 +31,9 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
31
31
 
32
32
  /* declare node configuration parameters */
33
33
  this.configure({
34
- device: { type: "string", pos: 0, match: /^(.+?):(.+)$/ },
35
- mode: { type: "string", pos: 1, val: "rw", match: /^(?:r|w|rw)$/ }
34
+ device: { type: "string", pos: 0, val: "", match: /^(.+?):(.+)$/ },
35
+ mode: { type: "string", pos: 1, val: "rw", match: /^(?:r|w|rw)$/ },
36
+ chunk: { type: "number", pos: 2, val: 200, match: (n: number) => n >= 10 && n <= 1000 }
36
37
  })
37
38
 
38
39
  /* declare node input/output format */
@@ -86,6 +87,9 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
86
87
 
87
88
  /* open node */
88
89
  async open () {
90
+ if (this.params.device === "")
91
+ throw new Error("required parameter \"device\" has to be given")
92
+
89
93
  /* determine device */
90
94
  const device = this.audioDeviceFromURL(this.params.mode, this.params.device)
91
95
 
@@ -95,6 +99,13 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
95
99
  throw new Error(`audio device sample rate ${device.defaultSampleRate} is ` +
96
100
  `incompatible with required sample rate ${this.config.audioSampleRate}`)
97
101
 
102
+ /* determine how many bytes we need per chunk when
103
+ the chunk should be the requested duration */
104
+ const highwaterMark = (
105
+ this.config.audioSampleRate *
106
+ (this.config.audioBitDepth / 8)
107
+ ) / (1000 / this.params.chunk)
108
+
98
109
  /* establish device connection
99
110
  Notice: "naudion" actually implements Stream.{Readable,Writable,Duplex}, but
100
111
  declares just its sub-interface NodeJS.{Readable,Writable,Duplex}Stream,
@@ -112,13 +123,15 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
112
123
  deviceId: device.id,
113
124
  channelCount: this.config.audioChannels,
114
125
  sampleRate: this.config.audioSampleRate,
115
- sampleFormat: this.config.audioBitDepth
126
+ sampleFormat: this.config.audioBitDepth,
127
+ highwaterMark
116
128
  },
117
129
  outOptions: {
118
130
  deviceId: device.id,
119
131
  channelCount: this.config.audioChannels,
120
132
  sampleRate: this.config.audioSampleRate,
121
- sampleFormat: this.config.audioBitDepth
133
+ sampleFormat: this.config.audioBitDepth,
134
+ highwaterMark
122
135
  }
123
136
  })
124
137
  this.stream = this.io as unknown as Stream.Duplex
@@ -135,10 +148,11 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
135
148
  this.log("info", `resolved "${this.params.device}" to input device "${device.id}"`)
136
149
  this.io = PortAudio.AudioIO({
137
150
  inOptions: {
138
- deviceId: device.id,
139
- channelCount: this.config.audioChannels,
140
- sampleRate: this.config.audioSampleRate,
141
- sampleFormat: this.config.audioBitDepth
151
+ deviceId: device.id,
152
+ channelCount: this.config.audioChannels,
153
+ sampleRate: this.config.audioSampleRate,
154
+ sampleFormat: this.config.audioBitDepth,
155
+ highwaterMark
142
156
  }
143
157
  })
144
158
  this.stream = this.io as unknown as Stream.Readable
@@ -158,7 +172,8 @@ export default class SpeechFlowNodeDevice extends SpeechFlowNode {
158
172
  deviceId: device.id,
159
173
  channelCount: this.config.audioChannels,
160
174
  sampleRate: this.config.audioSampleRate,
161
- sampleFormat: this.config.audioBitDepth
175
+ sampleFormat: this.config.audioBitDepth,
176
+ highwaterMark
162
177
  }
163
178
  })
164
179
  this.stream = this.io as unknown as Stream.Writable
@@ -23,9 +23,11 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
23
23
 
24
24
  /* declare node configuration parameters */
25
25
  this.configure({
26
- path: { type: "string", pos: 0 },
27
- mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
28
- type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ }
26
+ path: { type: "string", pos: 0, val: "" },
27
+ mode: { type: "string", pos: 1, val: "r", match: /^(?:r|w|rw)$/ },
28
+ type: { type: "string", pos: 2, val: "audio", match: /^(?:audio|text)$/ },
29
+ chunka: { type: "number", val: 200, match: (n: number) => n >= 10 && n <= 1000 },
30
+ chunkt: { type: "number", val: 65536, match: (n: number) => n >= 1024 && n <= 131072 }
29
31
  })
30
32
 
31
33
  /* declare node input/output format */
@@ -45,90 +47,128 @@ export default class SpeechFlowNodeFile extends SpeechFlowNode {
45
47
 
46
48
  /* open node */
47
49
  async open () {
50
+ /* determine how many bytes we need per chunk when
51
+ the chunk should be of the required duration/size */
52
+ const highWaterMarkAudio = (
53
+ this.config.audioSampleRate *
54
+ (this.config.audioBitDepth / 8)
55
+ ) / (1000 / this.params.chunka)
56
+ const highWaterMarkText = this.params.chunkt
57
+
58
+ /* sanity check */
59
+ if (this.params.path === "")
60
+ throw new Error("required parameter \"path\" has to be given")
61
+
62
+ /* dispatch according to mode and path */
48
63
  if (this.params.mode === "rw") {
49
64
  if (this.params.path === "-") {
50
65
  /* standard I/O */
51
66
  if (this.params.type === "audio") {
52
67
  process.stdin.setEncoding()
53
68
  process.stdout.setEncoding()
69
+ const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
70
+ process.stdin.pipe(streamR)
71
+ const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
72
+ streamW.pipe(process.stdout)
73
+ this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
54
74
  }
55
75
  else {
56
76
  process.stdin.setEncoding(this.config.textEncoding)
57
77
  process.stdout.setEncoding(this.config.textEncoding)
78
+ const streamR = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
79
+ process.stdin.pipe(streamR)
80
+ const streamW = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
81
+ streamW.pipe(process.stdout)
82
+ this.stream = Stream.Duplex.from({ readable: streamR, writable: streamW })
58
83
  }
59
- this.stream = Stream.Duplex.from({
60
- readable: process.stdin,
61
- writable: process.stdout
62
- })
63
84
  }
64
85
  else {
65
86
  /* file I/O */
66
87
  if (this.params.type === "audio") {
67
88
  this.stream = Stream.Duplex.from({
68
- readable: fs.createReadStream(this.params.path),
69
- writable: fs.createWriteStream(this.params.path)
89
+ readable: fs.createReadStream(this.params.path,
90
+ { highWaterMark: highWaterMarkAudio }),
91
+ writable: fs.createWriteStream(this.params.path,
92
+ { highWaterMark: highWaterMarkAudio })
70
93
  })
71
94
  }
72
95
  else {
73
96
  this.stream = Stream.Duplex.from({
74
- readable: fs.createReadStream(this.params.path,
75
- { encoding: this.config.textEncoding }),
76
- writable: fs.createWriteStream(this.params.path,
77
- { encoding: this.config.textEncoding })
97
+ readable: fs.createReadStream(this.params.path, {
98
+ highWaterMark: highWaterMarkText,
99
+ encoding: this.config.textEncoding
100
+ }),
101
+ writable: fs.createWriteStream(this.params.path, {
102
+ highWaterMark: highWaterMarkText,
103
+ encoding: this.config.textEncoding
104
+ })
78
105
  })
79
106
  }
80
107
  }
81
108
 
82
109
  /* convert regular stream into object-mode stream */
83
110
  const wrapper1 = utils.createTransformStreamForWritableSide()
84
- const wrapper2 = utils.createTransformStreamForReadableSide(this.params.type, () => this.timeZero)
111
+ const wrapper2 = utils.createTransformStreamForReadableSide(
112
+ this.params.type, () => this.timeZero)
85
113
  this.stream = Stream.compose(wrapper1, this.stream, wrapper2)
86
114
  }
87
115
  else if (this.params.mode === "r") {
88
116
  if (this.params.path === "-") {
89
117
  /* standard I/O */
90
- if (this.params.type === "audio")
118
+ let chunker: Stream.PassThrough
119
+ if (this.params.type === "audio") {
91
120
  process.stdin.setEncoding()
92
- else
121
+ chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
122
+ }
123
+ else {
93
124
  process.stdin.setEncoding(this.config.textEncoding)
94
- this.stream = process.stdin
125
+ chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
126
+ }
127
+ const wrapper = utils.createTransformStreamForReadableSide(
128
+ this.params.type, () => this.timeZero)
129
+ this.stream = Stream.compose(process.stdin, chunker, wrapper)
95
130
  }
96
131
  else {
97
132
  /* file I/O */
133
+ let readable: Stream.Readable
98
134
  if (this.params.type === "audio")
99
- this.stream = fs.createReadStream(this.params.path)
135
+ readable = fs.createReadStream(this.params.path,
136
+ { highWaterMark: highWaterMarkAudio })
100
137
  else
101
- this.stream = fs.createReadStream(this.params.path,
102
- { encoding: this.config.textEncoding })
138
+ readable = fs.createReadStream(this.params.path,
139
+ { highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
140
+ const wrapper = utils.createTransformStreamForReadableSide(
141
+ this.params.type, () => this.timeZero)
142
+ this.stream = Stream.compose(readable, wrapper)
103
143
  }
104
-
105
- /* convert regular stream into object-mode stream */
106
- const wrapper = utils.createTransformStreamForReadableSide(this.params.type, () => this.timeZero)
107
- this.stream.pipe(wrapper)
108
- this.stream = wrapper
109
144
  }
110
145
  else if (this.params.mode === "w") {
111
146
  if (this.params.path === "-") {
112
147
  /* standard I/O */
113
- if (this.params.type === "audio")
148
+ let chunker: Stream.PassThrough
149
+ if (this.params.type === "audio") {
114
150
  process.stdout.setEncoding()
115
- else
151
+ chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkAudio })
152
+ }
153
+ else {
116
154
  process.stdout.setEncoding(this.config.textEncoding)
117
- this.stream = process.stdout
155
+ chunker = new Stream.PassThrough({ highWaterMark: highWaterMarkText })
156
+ }
157
+ const wrapper = utils.createTransformStreamForWritableSide()
158
+ this.stream = Stream.compose(wrapper, chunker, process.stdout)
118
159
  }
119
160
  else {
120
161
  /* file I/O */
162
+ let writable: Stream.Writable
121
163
  if (this.params.type === "audio")
122
- this.stream = fs.createWriteStream(this.params.path)
164
+ writable = fs.createWriteStream(this.params.path,
165
+ { highWaterMark: highWaterMarkAudio })
123
166
  else
124
- this.stream = fs.createWriteStream(this.params.path,
125
- { encoding: this.config.textEncoding })
167
+ writable = fs.createWriteStream(this.params.path,
168
+ { highWaterMark: highWaterMarkText, encoding: this.config.textEncoding })
169
+ const wrapper = utils.createTransformStreamForWritableSide()
170
+ this.stream = Stream.compose(wrapper, writable)
126
171
  }
127
-
128
- /* convert regular stream into object-mode stream */
129
- const wrapper = utils.createTransformStreamForWritableSide()
130
- wrapper.pipe(this.stream as Stream.Writable)
131
- this.stream = wrapper
132
172
  }
133
173
  else
134
174
  throw new Error(`invalid file mode "${this.params.mode}"`)
@@ -5,11 +5,11 @@
5
5
  */
6
6
 
7
7
  /* standard dependencies */
8
- import Stream from "node:stream"
8
+ import Stream from "node:stream"
9
9
 
10
10
  /* external dependencies */
11
- import MQTT from "mqtt"
12
- import UUID from "pure-uuid"
11
+ import MQTT from "mqtt"
12
+ import UUID from "pure-uuid"
13
13
 
14
14
  /* internal dependencies */
15
15
  import SpeechFlowNode, { SpeechFlowChunk } from "./speechflow-node"
@@ -39,12 +39,6 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
39
39
  type: { type: "string", pos: 6, val: "text", match: /^(?:audio|text)$/ }
40
40
  })
41
41
 
42
- /* logical parameter sanity check */
43
- if ((this.params.mode === "w" || this.params.mode === "rw") && this.params.topicWrite === "")
44
- throw new Error("writing to MQTT requires a topicWrite parameter")
45
- if ((this.params.mode === "r" || this.params.mode === "rw") && this.params.topicRead === "")
46
- throw new Error("reading from MQTT requires a topicRead parameter")
47
-
48
42
  /* declare node input/output format */
49
43
  if (this.params.mode === "rw") {
50
44
  this.input = this.params.type
@@ -62,6 +56,14 @@ export default class SpeechFlowNodeMQTT extends SpeechFlowNode {
62
56
 
63
57
  /* open node */
64
58
  async open () {
59
+ /* logical parameter sanity check */
60
+ if (this.params.url === "")
61
+ throw new Error("required parameter \"url\" has to be given")
62
+ if ((this.params.mode === "w" || this.params.mode === "rw") && this.params.topicWrite === "")
63
+ throw new Error("writing to MQTT requires a topicWrite parameter")
64
+ if ((this.params.mode === "r" || this.params.mode === "rw") && this.params.topicRead === "")
65
+ throw new Error("reading from MQTT requires a topicRead parameter")
66
+
65
67
  /* connect remotely to a MQTT broker */
66
68
  this.broker = MQTT.connect(this.params.url, {
67
69
  protocolId: "MQTT",
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /* standard dependencies */
8
- import Stream from "node:stream"
8
+ import Stream from "node:stream"
9
9
 
10
10
  /* external dependencies */
11
11
  import ws from "ws"
@@ -36,12 +36,6 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
36
36
  type: { type: "string", val: "text", match: /^(?:audio|text)$/ }
37
37
  })
38
38
 
39
- /* sanity check usage */
40
- if (this.params.listen !== "" && this.params.connect !== "")
41
- throw new Error("Websocket node cannot listen and connect at the same time")
42
- else if (this.params.listen === "" && this.params.connect === "")
43
- throw new Error("Websocket node requires either listen or connect mode")
44
-
45
39
  /* declare node input/output format */
46
40
  if (this.params.mode === "rw") {
47
41
  this.input = this.params.type
@@ -59,6 +53,12 @@ export default class SpeechFlowNodeWebsocket extends SpeechFlowNode {
59
53
 
60
54
  /* open node */
61
55
  async open () {
56
+ /* sanity check usage */
57
+ if (this.params.listen !== "" && this.params.connect !== "")
58
+ throw new Error("Websocket node cannot listen and connect at the same time")
59
+ else if (this.params.listen === "" && this.params.connect === "")
60
+ throw new Error("Websocket node requires either listen or connect mode")
61
+
62
62
  if (this.params.listen !== "") {
63
63
  /* listen locally on a Websocket port */
64
64
  const url = new URL(this.params.listen)
@@ -7,6 +7,8 @@
7
7
  /* standard dependencies */
8
8
  import Events from "node:events"
9
9
  import Stream from "node:stream"
10
+
11
+ /* external dependencies */
10
12
  import { DateTime, Duration } from "luxon"
11
13
 
12
14
  /* the definition of a single payload chunk passed through the SpeechFlow nodes */
@@ -16,7 +18,8 @@ export class SpeechFlowChunk {
16
18
  public timestampEnd: Duration,
17
19
  public kind: "intermediate" | "final",
18
20
  public type: "audio" | "text",
19
- public payload: Buffer | string
21
+ public payload: Buffer | string,
22
+ public meta = new Map<string, any>()
20
23
  ) {}
21
24
  clone () {
22
25
  let payload: Buffer | string
@@ -29,7 +32,8 @@ export class SpeechFlowChunk {
29
32
  Duration.fromMillis(this.timestampEnd.toMillis()),
30
33
  this.kind,
31
34
  this.type,
32
- payload
35
+ payload,
36
+ new Map(this.meta)
33
37
  )
34
38
  }
35
39
  }
@@ -168,6 +172,11 @@ export default class SpeechFlowNode extends Events.EventEmitter {
168
172
  this.emit("log", level, msg, data)
169
173
  }
170
174
 
175
+ /* default implementation for status operation */
176
+ async status (): Promise<{ [ key: string ]: string | number }> {
177
+ return {}
178
+ }
179
+
171
180
  /* default implementation for open/close operations */
172
181
  async open () {}
173
182
  async close () {}
@@ -4,14 +4,17 @@
4
4
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
5
  */
6
6
 
7
- /* external dependencies */
7
+ /* standard dependencies */
8
8
  import Stream from "node:stream"
9
9
  import { EventEmitter } from "node:events"
10
+
11
+ /* external dependencies */
10
12
  import { DateTime, Duration } from "luxon"
11
13
  import CBOR from "cbor2"
14
+ import * as IntervalTree from "node-interval-tree"
12
15
 
13
16
  /* internal dependencies */
14
- import { SpeechFlowChunk } from "./speechflow-node"
17
+ import { SpeechFlowChunk } from "./speechflow-node"
15
18
 
16
19
  /* calculate duration of an audio buffer */
17
20
  export function audioBufferDuration (
@@ -68,6 +71,10 @@ export function createTransformStreamForWritableSide () {
68
71
  transform (chunk: SpeechFlowChunk, encoding, callback) {
69
72
  this.push(chunk.payload)
70
73
  callback()
74
+ },
75
+ final (callback) {
76
+ this.push(null)
77
+ callback()
71
78
  }
72
79
  })
73
80
  }
@@ -90,6 +97,10 @@ export function createTransformStreamForReadableSide (type: "text" | "audio", ge
90
97
  const obj = new SpeechFlowChunk(start, end, "final", type, chunk)
91
98
  this.push(obj)
92
99
  callback()
100
+ },
101
+ final (callback) {
102
+ this.push(null)
103
+ callback()
93
104
  }
94
105
  })
95
106
  }
@@ -252,6 +263,7 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
252
263
  private queue: Queue<T>
253
264
  ) {
254
265
  super()
266
+ this.setMaxListeners(100)
255
267
  }
256
268
 
257
269
  /* positioning operations */
@@ -379,6 +391,10 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
379
391
  export class Queue<T extends QueueElement> extends EventEmitter {
380
392
  public elements: T[] = []
381
393
  private pointers = new Map<string, QueuePointer<T>>()
394
+ constructor () {
395
+ super()
396
+ this.setMaxListeners(100)
397
+ }
382
398
  pointerUse (name: string): QueuePointer<T> {
383
399
  if (!this.pointers.has(name))
384
400
  this.pointers.set(name, new QueuePointer<T>(name, this))
@@ -405,3 +421,66 @@ export class Queue<T extends QueueElement> extends EventEmitter {
405
421
  }
406
422
  }
407
423
 
424
+ /* utility class for wrapping a custom stream into a regular Transform stream */
425
+ export class StreamWrapper extends Stream.Transform {
426
+ private foreignStream: any
427
+ constructor (foreignStream: any, options: Stream.TransformOptions = {}) {
428
+ options.readableObjectMode = true
429
+ options.writableObjectMode = true
430
+ super(options)
431
+ this.foreignStream = foreignStream
432
+ this.foreignStream.on("data", (chunk: any) => {
433
+ this.push(chunk)
434
+ })
435
+ this.foreignStream.on("error", (err: Error) => {
436
+ this.emit("error", err)
437
+ })
438
+ this.foreignStream.on("end", () => {
439
+ this.push(null)
440
+ })
441
+ }
442
+ _transform (chunk: any, encoding: BufferEncoding, callback: Stream.TransformCallback): void {
443
+ try {
444
+ const canContinue = this.foreignStream.write(chunk)
445
+ if (canContinue)
446
+ callback()
447
+ else
448
+ this.foreignStream.once("drain", callback)
449
+ }
450
+ catch (err) {
451
+ callback(err as Error)
452
+ }
453
+ }
454
+ _flush (callback: Stream.TransformCallback): void {
455
+ try {
456
+ if (typeof this.foreignStream.end === "function")
457
+ this.foreignStream.end()
458
+ callback()
459
+ }
460
+ catch (err) {
461
+ callback(err as Error)
462
+ }
463
+ }
464
+ }
465
+
466
+ /* meta store */
467
+ interface TimeStoreInterval<T> extends IntervalTree.Interval {
468
+ item: T
469
+ }
470
+ export class TimeStore<T> extends EventEmitter {
471
+ private tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
472
+ store (start: Duration, end: Duration, item: T): void {
473
+ this.tree.insert({ low: start.toMillis(), high: end.toMillis(), item })
474
+ }
475
+ fetch (start: Duration, end: Duration): T[] {
476
+ const intervals = this.tree.search(start.toMillis(), end.toMillis())
477
+ return intervals.map((interval) => interval.item)
478
+ }
479
+ prune (_before: Duration): void {
480
+ const before = _before.toMillis()
481
+ const intervals = this.tree.search(0, before - 1)
482
+ for (const interval of intervals)
483
+ if (interval.low < before && interval.high < before)
484
+ this.tree.remove(interval)
485
+ }
486
+ }