@livekit/agents 1.0.24 → 1.0.27

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 (184) hide show
  1. package/dist/inference/llm.cjs +1 -2
  2. package/dist/inference/llm.cjs.map +1 -1
  3. package/dist/inference/llm.d.ts.map +1 -1
  4. package/dist/inference/llm.js +1 -2
  5. package/dist/inference/llm.js.map +1 -1
  6. package/dist/inference/stt.cjs +1 -1
  7. package/dist/inference/stt.cjs.map +1 -1
  8. package/dist/inference/stt.d.ts.map +1 -1
  9. package/dist/inference/stt.js +1 -1
  10. package/dist/inference/stt.js.map +1 -1
  11. package/dist/inference/tts.cjs +4 -4
  12. package/dist/inference/tts.cjs.map +1 -1
  13. package/dist/inference/tts.d.cts +0 -1
  14. package/dist/inference/tts.d.ts +0 -1
  15. package/dist/inference/tts.d.ts.map +1 -1
  16. package/dist/inference/tts.js +4 -4
  17. package/dist/inference/tts.js.map +1 -1
  18. package/dist/ipc/job_proc_lazy_main.cjs +1 -1
  19. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  20. package/dist/ipc/job_proc_lazy_main.js +1 -1
  21. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  22. package/dist/job.cjs +29 -2
  23. package/dist/job.cjs.map +1 -1
  24. package/dist/job.d.cts +6 -0
  25. package/dist/job.d.ts +6 -0
  26. package/dist/job.d.ts.map +1 -1
  27. package/dist/job.js +19 -2
  28. package/dist/job.js.map +1 -1
  29. package/dist/llm/llm.cjs +2 -1
  30. package/dist/llm/llm.cjs.map +1 -1
  31. package/dist/llm/llm.d.cts +1 -1
  32. package/dist/llm/llm.d.ts +1 -1
  33. package/dist/llm/llm.d.ts.map +1 -1
  34. package/dist/llm/llm.js +2 -1
  35. package/dist/llm/llm.js.map +1 -1
  36. package/dist/stream/deferred_stream.cjs +12 -4
  37. package/dist/stream/deferred_stream.cjs.map +1 -1
  38. package/dist/stream/deferred_stream.d.cts +6 -1
  39. package/dist/stream/deferred_stream.d.ts +6 -1
  40. package/dist/stream/deferred_stream.d.ts.map +1 -1
  41. package/dist/stream/deferred_stream.js +12 -4
  42. package/dist/stream/deferred_stream.js.map +1 -1
  43. package/dist/stream/deferred_stream.test.cjs +2 -2
  44. package/dist/stream/deferred_stream.test.cjs.map +1 -1
  45. package/dist/stream/deferred_stream.test.js +2 -2
  46. package/dist/stream/deferred_stream.test.js.map +1 -1
  47. package/dist/stt/stream_adapter.cjs +15 -8
  48. package/dist/stt/stream_adapter.cjs.map +1 -1
  49. package/dist/stt/stream_adapter.d.cts +7 -3
  50. package/dist/stt/stream_adapter.d.ts +7 -3
  51. package/dist/stt/stream_adapter.d.ts.map +1 -1
  52. package/dist/stt/stream_adapter.js +15 -8
  53. package/dist/stt/stream_adapter.js.map +1 -1
  54. package/dist/stt/stt.cjs +8 -3
  55. package/dist/stt/stt.cjs.map +1 -1
  56. package/dist/stt/stt.d.cts +9 -3
  57. package/dist/stt/stt.d.ts +9 -3
  58. package/dist/stt/stt.d.ts.map +1 -1
  59. package/dist/stt/stt.js +9 -4
  60. package/dist/stt/stt.js.map +1 -1
  61. package/dist/telemetry/traces.cjs +23 -2
  62. package/dist/telemetry/traces.cjs.map +1 -1
  63. package/dist/telemetry/traces.d.ts.map +1 -1
  64. package/dist/telemetry/traces.js +23 -2
  65. package/dist/telemetry/traces.js.map +1 -1
  66. package/dist/tts/stream_adapter.cjs +10 -7
  67. package/dist/tts/stream_adapter.cjs.map +1 -1
  68. package/dist/tts/stream_adapter.d.cts +6 -3
  69. package/dist/tts/stream_adapter.d.ts +6 -3
  70. package/dist/tts/stream_adapter.d.ts.map +1 -1
  71. package/dist/tts/stream_adapter.js +10 -7
  72. package/dist/tts/stream_adapter.js.map +1 -1
  73. package/dist/tts/tts.cjs +27 -16
  74. package/dist/tts/tts.cjs.map +1 -1
  75. package/dist/tts/tts.d.cts +12 -5
  76. package/dist/tts/tts.d.ts +12 -5
  77. package/dist/tts/tts.d.ts.map +1 -1
  78. package/dist/tts/tts.js +28 -17
  79. package/dist/tts/tts.js.map +1 -1
  80. package/dist/types.cjs +21 -32
  81. package/dist/types.cjs.map +1 -1
  82. package/dist/types.d.cts +41 -10
  83. package/dist/types.d.ts +41 -10
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/types.js +18 -30
  86. package/dist/types.js.map +1 -1
  87. package/dist/voice/agent.cjs +54 -19
  88. package/dist/voice/agent.cjs.map +1 -1
  89. package/dist/voice/agent.d.ts.map +1 -1
  90. package/dist/voice/agent.js +54 -19
  91. package/dist/voice/agent.js.map +1 -1
  92. package/dist/voice/agent_activity.cjs +0 -3
  93. package/dist/voice/agent_activity.cjs.map +1 -1
  94. package/dist/voice/agent_activity.d.ts.map +1 -1
  95. package/dist/voice/agent_activity.js +0 -3
  96. package/dist/voice/agent_activity.js.map +1 -1
  97. package/dist/voice/agent_session.cjs +107 -27
  98. package/dist/voice/agent_session.cjs.map +1 -1
  99. package/dist/voice/agent_session.d.cts +16 -2
  100. package/dist/voice/agent_session.d.ts +16 -2
  101. package/dist/voice/agent_session.d.ts.map +1 -1
  102. package/dist/voice/agent_session.js +110 -27
  103. package/dist/voice/agent_session.js.map +1 -1
  104. package/dist/voice/events.cjs.map +1 -1
  105. package/dist/voice/events.d.cts +4 -4
  106. package/dist/voice/events.d.ts +4 -4
  107. package/dist/voice/events.d.ts.map +1 -1
  108. package/dist/voice/events.js.map +1 -1
  109. package/dist/voice/generation.cjs +6 -7
  110. package/dist/voice/generation.cjs.map +1 -1
  111. package/dist/voice/generation.d.ts.map +1 -1
  112. package/dist/voice/generation.js +7 -8
  113. package/dist/voice/generation.js.map +1 -1
  114. package/dist/voice/io.cjs +16 -0
  115. package/dist/voice/io.cjs.map +1 -1
  116. package/dist/voice/io.d.cts +8 -0
  117. package/dist/voice/io.d.ts +8 -0
  118. package/dist/voice/io.d.ts.map +1 -1
  119. package/dist/voice/io.js +16 -0
  120. package/dist/voice/io.js.map +1 -1
  121. package/dist/voice/recorder_io/index.cjs +23 -0
  122. package/dist/voice/recorder_io/index.cjs.map +1 -0
  123. package/dist/voice/recorder_io/index.d.cts +2 -0
  124. package/dist/voice/recorder_io/index.d.ts +2 -0
  125. package/dist/voice/recorder_io/index.d.ts.map +1 -0
  126. package/dist/voice/recorder_io/index.js +2 -0
  127. package/dist/voice/recorder_io/index.js.map +1 -0
  128. package/dist/voice/recorder_io/recorder_io.cjs +542 -0
  129. package/dist/voice/recorder_io/recorder_io.cjs.map +1 -0
  130. package/dist/voice/recorder_io/recorder_io.d.cts +100 -0
  131. package/dist/voice/recorder_io/recorder_io.d.ts +100 -0
  132. package/dist/voice/recorder_io/recorder_io.d.ts.map +1 -0
  133. package/dist/voice/recorder_io/recorder_io.js +508 -0
  134. package/dist/voice/recorder_io/recorder_io.js.map +1 -0
  135. package/dist/voice/report.cjs +7 -2
  136. package/dist/voice/report.cjs.map +1 -1
  137. package/dist/voice/report.d.cts +11 -1
  138. package/dist/voice/report.d.ts +11 -1
  139. package/dist/voice/report.d.ts.map +1 -1
  140. package/dist/voice/report.js +7 -2
  141. package/dist/voice/report.js.map +1 -1
  142. package/dist/voice/room_io/_input.cjs +2 -1
  143. package/dist/voice/room_io/_input.cjs.map +1 -1
  144. package/dist/voice/room_io/_input.d.ts.map +1 -1
  145. package/dist/voice/room_io/_input.js +2 -1
  146. package/dist/voice/room_io/_input.js.map +1 -1
  147. package/dist/voice/room_io/_output.cjs +8 -7
  148. package/dist/voice/room_io/_output.cjs.map +1 -1
  149. package/dist/voice/room_io/_output.d.cts +2 -1
  150. package/dist/voice/room_io/_output.d.ts +2 -1
  151. package/dist/voice/room_io/_output.d.ts.map +1 -1
  152. package/dist/voice/room_io/_output.js +8 -7
  153. package/dist/voice/room_io/_output.js.map +1 -1
  154. package/dist/worker.cjs +4 -3
  155. package/dist/worker.cjs.map +1 -1
  156. package/dist/worker.js +4 -3
  157. package/dist/worker.js.map +1 -1
  158. package/package.json +1 -1
  159. package/src/inference/llm.ts +0 -1
  160. package/src/inference/stt.ts +1 -2
  161. package/src/inference/tts.ts +5 -4
  162. package/src/ipc/job_proc_lazy_main.ts +1 -1
  163. package/src/job.ts +21 -2
  164. package/src/llm/llm.ts +2 -2
  165. package/src/stream/deferred_stream.test.ts +3 -3
  166. package/src/stream/deferred_stream.ts +22 -5
  167. package/src/stt/stream_adapter.ts +18 -8
  168. package/src/stt/stt.ts +19 -6
  169. package/src/telemetry/traces.ts +25 -3
  170. package/src/tts/stream_adapter.ts +15 -7
  171. package/src/tts/tts.ts +46 -21
  172. package/src/types.ts +57 -33
  173. package/src/voice/agent.ts +59 -19
  174. package/src/voice/agent_activity.ts +0 -3
  175. package/src/voice/agent_session.ts +142 -35
  176. package/src/voice/events.ts +6 -3
  177. package/src/voice/generation.ts +10 -8
  178. package/src/voice/io.ts +19 -0
  179. package/src/voice/recorder_io/index.ts +4 -0
  180. package/src/voice/recorder_io/recorder_io.ts +690 -0
  181. package/src/voice/report.ts +20 -3
  182. package/src/voice/room_io/_input.ts +2 -1
  183. package/src/voice/room_io/_output.ts +10 -7
  184. package/src/worker.ts +1 -1
@@ -0,0 +1,508 @@
1
+ import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
2
+ import { Mutex } from "@livekit/mutex";
3
+ import { AudioFrame, AudioResampler } from "@livekit/rtc-node";
4
+ import ffmpeg from "fluent-ffmpeg";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { PassThrough } from "node:stream";
8
+ import { TransformStream } from "node:stream/web";
9
+ import { log } from "../../log.js";
10
+ import { isStreamReaderReleaseError } from "../../stream/deferred_stream.js";
11
+ import { createStreamChannel } from "../../stream/stream_channel.js";
12
+ import { Future, Task, cancelAndWait, delay } from "../../utils.js";
13
+ import { AudioInput, AudioOutput } from "../io.js";
14
+ ffmpeg.setFfmpegPath(ffmpegInstaller.path);
15
+ const WRITE_INTERVAL_MS = 2500;
16
+ const DEFAULT_SAMPLE_RATE = 48e3;
17
+ class RecorderIO {
18
+ inRecord;
19
+ outRecord;
20
+ inChan = createStreamChannel();
21
+ outChan = createStreamChannel();
22
+ session;
23
+ sampleRate;
24
+ _outputPath;
25
+ forwardTask;
26
+ encodeTask;
27
+ closeFuture = new Future();
28
+ lock = new Mutex();
29
+ started = false;
30
+ // FFmpeg streaming state
31
+ pcmStream;
32
+ ffmpegPromise;
33
+ inResampler;
34
+ outResampler;
35
+ logger = log();
36
+ constructor(opts) {
37
+ const { agentSession, sampleRate = DEFAULT_SAMPLE_RATE } = opts;
38
+ this.session = agentSession;
39
+ this.sampleRate = sampleRate;
40
+ }
41
+ async start(outputPath) {
42
+ const unlock = await this.lock.lock();
43
+ try {
44
+ if (this.started) return;
45
+ if (!this.inRecord || !this.outRecord) {
46
+ throw new Error(
47
+ "RecorderIO not properly initialized: both `recordInput()` and `recordOutput()` must be called before starting the recorder."
48
+ );
49
+ }
50
+ this._outputPath = outputPath;
51
+ this.started = true;
52
+ this.closeFuture = new Future();
53
+ const dir = path.dirname(outputPath);
54
+ if (!fs.existsSync(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ }
57
+ this.forwardTask = Task.from(({ signal }) => this.forward(signal));
58
+ this.encodeTask = Task.from(() => this.encode(), void 0, "recorder_io_encode_task");
59
+ } finally {
60
+ unlock();
61
+ }
62
+ }
63
+ async close() {
64
+ const unlock = await this.lock.lock();
65
+ try {
66
+ if (!this.started) return;
67
+ await this.inChan.close();
68
+ await this.outChan.close();
69
+ await this.closeFuture.await;
70
+ await cancelAndWait([this.forwardTask, this.encodeTask]);
71
+ this.started = false;
72
+ } finally {
73
+ unlock();
74
+ }
75
+ }
76
+ recordInput(audioInput) {
77
+ this.inRecord = new RecorderAudioInput(this, audioInput);
78
+ return this.inRecord;
79
+ }
80
+ recordOutput(audioOutput) {
81
+ this.outRecord = new RecorderAudioOutput(this, audioOutput, (buf) => this.writeCb(buf));
82
+ return this.outRecord;
83
+ }
84
+ writeCb(buf) {
85
+ const inputBuf = this.inRecord.takeBuf();
86
+ this.inChan.write(inputBuf);
87
+ this.outChan.write(buf);
88
+ }
89
+ get recording() {
90
+ return this.started;
91
+ }
92
+ get outputPath() {
93
+ return this._outputPath;
94
+ }
95
+ get recordingStartedAt() {
96
+ return this.session._startedAt;
97
+ }
98
+ /**
99
+ * Forward task: periodically flush input buffer to encoder
100
+ */
101
+ async forward(signal) {
102
+ while (!signal.aborted) {
103
+ try {
104
+ await delay(WRITE_INTERVAL_MS, { signal });
105
+ } catch {
106
+ break;
107
+ }
108
+ if (this.outRecord.hasPendingData) {
109
+ continue;
110
+ }
111
+ const inputBuf = this.inRecord.takeBuf();
112
+ this.inChan.write(inputBuf).catch((err) => this.logger.error({ err }, "Error writing RecorderIO input buffer"));
113
+ this.outChan.write([]).catch((err) => this.logger.error({ err }, "Error writing RecorderIO output buffer"));
114
+ }
115
+ }
116
+ /**
117
+ * Start FFmpeg process for streaming encoding
118
+ */
119
+ startFFmpeg() {
120
+ if (this.pcmStream) return;
121
+ this.pcmStream = new PassThrough();
122
+ this.ffmpegPromise = new Promise((resolve, reject) => {
123
+ ffmpeg(this.pcmStream).inputFormat("s16le").inputOptions([`-ar ${this.sampleRate}`, "-ac 2"]).audioCodec("libopus").audioChannels(2).audioFrequency(this.sampleRate).format("ogg").output(this._outputPath).on("end", () => {
124
+ this.logger.debug("FFmpeg encoding finished");
125
+ resolve();
126
+ }).on("error", (err) => {
127
+ var _a, _b, _c, _d;
128
+ if (((_a = err.message) == null ? void 0 : _a.includes("Output stream closed")) || ((_b = err.message) == null ? void 0 : _b.includes("received signal 2")) || ((_c = err.message) == null ? void 0 : _c.includes("SIGKILL")) || ((_d = err.message) == null ? void 0 : _d.includes("SIGINT"))) {
129
+ resolve();
130
+ } else {
131
+ this.logger.error({ err }, "FFmpeg encoding error");
132
+ reject(err);
133
+ }
134
+ }).run();
135
+ });
136
+ }
137
+ /**
138
+ * Resample and mix frames to mono Float32
139
+ */
140
+ resampleAndMix(opts) {
141
+ const INV_INT16 = 1 / 32768;
142
+ const { frames, flush = false } = opts;
143
+ let { resampler } = opts;
144
+ if (frames.length === 0 && !flush) {
145
+ return { samples: new Float32Array(0), resampler };
146
+ }
147
+ if (!resampler && frames.length > 0) {
148
+ const firstFrame = frames[0];
149
+ resampler = new AudioResampler(firstFrame.sampleRate, this.sampleRate, firstFrame.channels);
150
+ }
151
+ const resampledFrames = [];
152
+ for (const frame of frames) {
153
+ if (resampler) {
154
+ resampledFrames.push(...resampler.push(frame));
155
+ }
156
+ }
157
+ if (flush && resampler) {
158
+ resampledFrames.push(...resampler.flush());
159
+ }
160
+ const totalSamples = resampledFrames.reduce((acc, frame) => acc + frame.samplesPerChannel, 0);
161
+ const samples = new Float32Array(totalSamples);
162
+ let pos = 0;
163
+ for (const frame of resampledFrames) {
164
+ const data = frame.data;
165
+ const numChannels = frame.channels;
166
+ for (let i = 0; i < frame.samplesPerChannel; i++) {
167
+ let sum = 0;
168
+ for (let ch = 0; ch < numChannels; ch++) {
169
+ sum += data[i * numChannels + ch];
170
+ }
171
+ samples[pos++] = sum / numChannels * INV_INT16;
172
+ }
173
+ }
174
+ return { samples, resampler };
175
+ }
176
+ /**
177
+ * Write PCM chunk to FFmpeg stream
178
+ */
179
+ writePCM(leftSamples, rightSamples) {
180
+ if (!this.pcmStream) {
181
+ this.startFFmpeg();
182
+ }
183
+ if (leftSamples.length !== rightSamples.length) {
184
+ const diff = Math.abs(leftSamples.length - rightSamples.length);
185
+ if (leftSamples.length < rightSamples.length) {
186
+ this.logger.warn(
187
+ `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`
188
+ );
189
+ const padded = new Float32Array(rightSamples.length);
190
+ padded.set(leftSamples, diff);
191
+ leftSamples = padded;
192
+ } else {
193
+ const padded = new Float32Array(leftSamples.length);
194
+ padded.set(rightSamples, diff);
195
+ rightSamples = padded;
196
+ }
197
+ }
198
+ const maxLen = Math.max(leftSamples.length, rightSamples.length);
199
+ if (maxLen <= 0) return;
200
+ const stereoData = new Int16Array(maxLen * 2);
201
+ for (let i = 0; i < maxLen; i++) {
202
+ stereoData[i * 2] = Math.max(
203
+ -32768,
204
+ Math.min(32767, Math.round((leftSamples[i] ?? 0) * 32768))
205
+ );
206
+ stereoData[i * 2 + 1] = Math.max(
207
+ -32768,
208
+ Math.min(32767, Math.round((rightSamples[i] ?? 0) * 32768))
209
+ );
210
+ }
211
+ this.pcmStream.write(Buffer.from(stereoData.buffer));
212
+ }
213
+ /**
214
+ * Encode task: read from channels, mix to stereo, stream to FFmpeg
215
+ */
216
+ async encode() {
217
+ if (!this._outputPath) return;
218
+ const inReader = this.inChan.stream().getReader();
219
+ const outReader = this.outChan.stream().getReader();
220
+ try {
221
+ while (true) {
222
+ const [inResult, outResult] = await Promise.all([inReader.read(), outReader.read()]);
223
+ if (inResult.done || outResult.done) {
224
+ break;
225
+ }
226
+ const inputBuf = inResult.value;
227
+ const outputBuf = outResult.value;
228
+ const inMixed = this.resampleAndMix({ frames: inputBuf, resampler: this.inResampler });
229
+ this.inResampler = inMixed.resampler;
230
+ const outMixed = this.resampleAndMix({
231
+ frames: outputBuf,
232
+ resampler: this.outResampler,
233
+ flush: outputBuf.length > 0
234
+ });
235
+ this.outResampler = outMixed.resampler;
236
+ this.writePCM(inMixed.samples, outMixed.samples);
237
+ }
238
+ if (this.pcmStream) {
239
+ this.pcmStream.end();
240
+ await this.ffmpegPromise;
241
+ }
242
+ } catch (err) {
243
+ this.logger.error({ err }, "Error in encode task");
244
+ } finally {
245
+ inReader.releaseLock();
246
+ outReader.releaseLock();
247
+ if (!this.closeFuture.done) {
248
+ this.closeFuture.resolve();
249
+ }
250
+ }
251
+ }
252
+ }
253
+ class RecorderAudioInput extends AudioInput {
254
+ source;
255
+ recorderIO;
256
+ accFrames = [];
257
+ _startedWallTime;
258
+ constructor(recorderIO, source) {
259
+ super();
260
+ this.recorderIO = recorderIO;
261
+ this.source = source;
262
+ this.deferredStream.setSource(this.createInterceptingStream());
263
+ }
264
+ /**
265
+ * Wall-clock time when the first frame was captured
266
+ */
267
+ get startedWallTime() {
268
+ return this._startedWallTime;
269
+ }
270
+ /**
271
+ * Take accumulated frames and clear the buffer
272
+ */
273
+ takeBuf() {
274
+ const frames = this.accFrames;
275
+ this.accFrames = [];
276
+ return frames;
277
+ }
278
+ /**
279
+ * Creates a stream that intercepts frames from the source,
280
+ * accumulates them when recording, and passes them through unchanged.
281
+ */
282
+ createInterceptingStream() {
283
+ const sourceStream = this.source.stream;
284
+ const reader = sourceStream.getReader();
285
+ const transform = new TransformStream({
286
+ transform: (frame, controller) => {
287
+ if (this.recorderIO.recording) {
288
+ if (this._startedWallTime === void 0) {
289
+ this._startedWallTime = Date.now();
290
+ }
291
+ this.accFrames.push(frame);
292
+ }
293
+ controller.enqueue(frame);
294
+ }
295
+ });
296
+ const pump = async () => {
297
+ const writer = transform.writable.getWriter();
298
+ let sourceError;
299
+ try {
300
+ while (true) {
301
+ const { done, value } = await reader.read();
302
+ if (done) break;
303
+ await writer.write(value);
304
+ }
305
+ } catch (e) {
306
+ if (isStreamReaderReleaseError(e)) return;
307
+ sourceError = e;
308
+ } finally {
309
+ if (sourceError) {
310
+ writer.abort(sourceError);
311
+ return;
312
+ }
313
+ writer.releaseLock();
314
+ try {
315
+ await transform.writable.close();
316
+ } catch {
317
+ }
318
+ }
319
+ };
320
+ pump();
321
+ return transform.readable;
322
+ }
323
+ onAttached() {
324
+ this.source.onAttached();
325
+ }
326
+ onDetached() {
327
+ this.source.onDetached();
328
+ }
329
+ }
330
+ class RecorderAudioOutput extends AudioOutput {
331
+ recorderIO;
332
+ writeFn;
333
+ accFrames = [];
334
+ _startedWallTime;
335
+ // Pause tracking
336
+ currentPauseStart;
337
+ pauseWallTimes = [];
338
+ // [start, end] pairs
339
+ constructor(recorderIO, audioOutput, writeFn) {
340
+ super(audioOutput.sampleRate, audioOutput);
341
+ this.recorderIO = recorderIO;
342
+ this.writeFn = writeFn;
343
+ }
344
+ get startedWallTime() {
345
+ return this._startedWallTime;
346
+ }
347
+ get hasPendingData() {
348
+ return this.accFrames.length > 0;
349
+ }
350
+ pause() {
351
+ if (this.currentPauseStart === void 0 && this.recorderIO.recording) {
352
+ this.currentPauseStart = Date.now();
353
+ }
354
+ if (this.nextInChain) {
355
+ this.nextInChain.pause();
356
+ }
357
+ }
358
+ /**
359
+ * Resume playback and record the pause interval
360
+ */
361
+ resume() {
362
+ if (this.currentPauseStart !== void 0 && this.recorderIO.recording) {
363
+ this.pauseWallTimes.push([this.currentPauseStart, Date.now()]);
364
+ this.currentPauseStart = void 0;
365
+ }
366
+ if (this.nextInChain) {
367
+ this.nextInChain.resume();
368
+ }
369
+ }
370
+ resetPauseState() {
371
+ this.currentPauseStart = void 0;
372
+ this.pauseWallTimes = [];
373
+ }
374
+ onPlaybackFinished(options) {
375
+ const finishTime = Date.now();
376
+ super.onPlaybackFinished(options);
377
+ if (!this.recorderIO.recording) {
378
+ return;
379
+ }
380
+ if (this.currentPauseStart !== void 0) {
381
+ this.pauseWallTimes.push([this.currentPauseStart, finishTime]);
382
+ this.currentPauseStart = void 0;
383
+ }
384
+ if (this.accFrames.length === 0) {
385
+ this.resetPauseState();
386
+ return;
387
+ }
388
+ const playbackPosition = options.playbackPosition;
389
+ const pauseEvents = [];
390
+ if (this.pauseWallTimes.length > 0) {
391
+ const totalPauseDuration = this.pauseWallTimes.reduce(
392
+ (sum, [start, end]) => sum + (end - start),
393
+ 0
394
+ );
395
+ const playbackStartTime = finishTime - playbackPosition * 1e3 - totalPauseDuration;
396
+ let accumulatedPause = 0;
397
+ for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {
398
+ let position = (pauseStart - playbackStartTime - accumulatedPause) / 1e3;
399
+ const duration = (pauseEnd - pauseStart) / 1e3;
400
+ position = Math.max(0, Math.min(position, playbackPosition));
401
+ pauseEvents.push([position, duration]);
402
+ accumulatedPause += pauseEnd - pauseStart;
403
+ }
404
+ }
405
+ const buf = [];
406
+ let accDur = 0;
407
+ const sampleRate = this.accFrames[0].sampleRate;
408
+ const numChannels = this.accFrames[0].channels;
409
+ let pauseIdx = 0;
410
+ let shouldBreak = false;
411
+ for (const frame of this.accFrames) {
412
+ let currentFrame = frame;
413
+ const frameDuration = frame.samplesPerChannel / frame.sampleRate;
414
+ if (frameDuration + accDur > playbackPosition) {
415
+ const [left] = splitFrame(currentFrame, playbackPosition - accDur);
416
+ currentFrame = left;
417
+ shouldBreak = true;
418
+ }
419
+ while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx][0] <= accDur) {
420
+ const [, pauseDur] = pauseEvents[pauseIdx];
421
+ buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));
422
+ pauseIdx++;
423
+ }
424
+ const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate;
425
+ while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx][0] < accDur + currentFrameDuration) {
426
+ const [pausePos, pauseDur] = pauseEvents[pauseIdx];
427
+ const [left, right] = splitFrame(currentFrame, pausePos - accDur);
428
+ buf.push(left);
429
+ accDur += left.samplesPerChannel / left.sampleRate;
430
+ buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));
431
+ currentFrame = right;
432
+ pauseIdx++;
433
+ }
434
+ buf.push(currentFrame);
435
+ accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate;
436
+ if (shouldBreak) {
437
+ break;
438
+ }
439
+ }
440
+ while (pauseIdx < pauseEvents.length) {
441
+ const [pausePos, pauseDur] = pauseEvents[pauseIdx];
442
+ if (pausePos <= playbackPosition) {
443
+ buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));
444
+ }
445
+ pauseIdx++;
446
+ }
447
+ if (buf.length > 0) {
448
+ this.writeFn(buf);
449
+ }
450
+ this.accFrames = [];
451
+ this.resetPauseState();
452
+ }
453
+ async captureFrame(frame) {
454
+ await super.captureFrame(frame);
455
+ if (this.recorderIO.recording) {
456
+ if (this._startedWallTime === void 0) {
457
+ this._startedWallTime = Date.now();
458
+ }
459
+ this.accFrames.push(frame);
460
+ }
461
+ if (this.nextInChain) {
462
+ await this.nextInChain.captureFrame(frame);
463
+ }
464
+ }
465
+ flush() {
466
+ super.flush();
467
+ if (this.nextInChain) {
468
+ this.nextInChain.flush();
469
+ }
470
+ }
471
+ clearBuffer() {
472
+ if (this.nextInChain) {
473
+ this.nextInChain.clearBuffer();
474
+ }
475
+ }
476
+ }
477
+ function createSilenceFrame(duration, sampleRate, numChannels) {
478
+ const samples = Math.floor(duration * sampleRate);
479
+ const data = new Int16Array(samples * numChannels);
480
+ return new AudioFrame(data, sampleRate, numChannels, samples);
481
+ }
482
+ function splitFrame(frame, position) {
483
+ if (position <= 0) {
484
+ const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);
485
+ return [emptyFrame, frame];
486
+ }
487
+ const frameDuration = frame.samplesPerChannel / frame.sampleRate;
488
+ if (position >= frameDuration) {
489
+ const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);
490
+ return [frame, emptyFrame];
491
+ }
492
+ const samplesNeeded = Math.floor(position * frame.sampleRate);
493
+ const numChannels = frame.channels;
494
+ const leftData = frame.data.slice(0, samplesNeeded * numChannels);
495
+ const rightData = frame.data.slice(samplesNeeded * numChannels);
496
+ const leftFrame = new AudioFrame(leftData, frame.sampleRate, frame.channels, samplesNeeded);
497
+ const rightFrame = new AudioFrame(
498
+ rightData,
499
+ frame.sampleRate,
500
+ frame.channels,
501
+ frame.samplesPerChannel - samplesNeeded
502
+ );
503
+ return [leftFrame, rightFrame];
504
+ }
505
+ export {
506
+ RecorderIO
507
+ };
508
+ //# sourceMappingURL=recorder_io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/voice/recorder_io/recorder_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { Mutex } from '@livekit/mutex';\nimport { AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PassThrough } from 'node:stream';\nimport type { ReadableStream } from 'node:stream/web';\nimport { TransformStream } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { isStreamReaderReleaseError } from '../../stream/deferred_stream.js';\nimport { type StreamChannel, createStreamChannel } from '../../stream/stream_channel.js';\nimport { Future, Task, cancelAndWait, delay } from '../../utils.js';\nimport type { AgentSession } from '../agent_session.js';\nimport { AudioInput, AudioOutput, type PlaybackFinishedEvent } from '../io.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nconst WRITE_INTERVAL_MS = 2500;\nconst DEFAULT_SAMPLE_RATE = 48000;\n\nexport interface RecorderOptions {\n agentSession: AgentSession;\n sampleRate?: number;\n}\n\ninterface ResampleAndMixOptions {\n frames: AudioFrame[];\n resampler: AudioResampler | undefined;\n flush?: boolean;\n}\n\nexport class RecorderIO {\n private inRecord?: RecorderAudioInput;\n private outRecord?: RecorderAudioOutput;\n\n private inChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n private outChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n\n private session: AgentSession;\n private sampleRate: number;\n\n private _outputPath?: string;\n private forwardTask?: Task<void>;\n private encodeTask?: Task<void>;\n\n private closeFuture: Future<void> = new Future();\n private lock: Mutex = new Mutex();\n private started: boolean = false;\n\n // FFmpeg streaming state\n private pcmStream?: PassThrough;\n private ffmpegPromise?: Promise<void>;\n private inResampler?: AudioResampler;\n private outResampler?: AudioResampler;\n\n private logger = log();\n\n constructor(opts: RecorderOptions) {\n const { agentSession, sampleRate = DEFAULT_SAMPLE_RATE } = opts;\n\n this.session = agentSession;\n this.sampleRate = sampleRate;\n }\n\n async start(outputPath: string): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (this.started) return;\n\n if (!this.inRecord || !this.outRecord) {\n throw new Error(\n 'RecorderIO not properly initialized: both `recordInput()` and `recordOutput()` must be called before starting the recorder.',\n );\n }\n\n this._outputPath = outputPath;\n this.started = true;\n this.closeFuture = new Future();\n\n // Ensure output directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n this.forwardTask = Task.from(({ signal }) => this.forward(signal));\n this.encodeTask = Task.from(() => this.encode(), undefined, 'recorder_io_encode_task');\n } finally {\n unlock();\n }\n }\n\n async close(): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (!this.started) return;\n\n await this.inChan.close();\n await this.outChan.close();\n await this.closeFuture.await;\n await cancelAndWait([this.forwardTask!, this.encodeTask!]);\n\n this.started = false;\n } finally {\n unlock();\n }\n }\n\n recordInput(audioInput: AudioInput): RecorderAudioInput {\n this.inRecord = new RecorderAudioInput(this, audioInput);\n return this.inRecord;\n }\n\n recordOutput(audioOutput: AudioOutput): RecorderAudioOutput {\n this.outRecord = new RecorderAudioOutput(this, audioOutput, (buf) => this.writeCb(buf));\n return this.outRecord;\n }\n\n private writeCb(buf: AudioFrame[]): void {\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan.write(inputBuf);\n this.outChan.write(buf);\n }\n\n get recording(): boolean {\n return this.started;\n }\n\n get outputPath(): string | undefined {\n return this._outputPath;\n }\n\n get recordingStartedAt(): number | undefined {\n // Use session start time to align with trace timestamps\n return this.session._startedAt;\n }\n\n /**\n * Forward task: periodically flush input buffer to encoder\n */\n private async forward(signal: AbortSignal): Promise<void> {\n while (!signal.aborted) {\n try {\n await delay(WRITE_INTERVAL_MS, { signal });\n } catch {\n // Aborted\n break;\n }\n\n if (this.outRecord!.hasPendingData) {\n // If the output is currently playing audio, wait for it to stay in sync\n continue;\n }\n\n // Flush input buffer\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan\n .write(inputBuf)\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO input buffer'));\n this.outChan\n .write([])\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO output buffer'));\n }\n }\n\n /**\n * Start FFmpeg process for streaming encoding\n */\n private startFFmpeg(): void {\n if (this.pcmStream) return;\n\n this.pcmStream = new PassThrough();\n\n this.ffmpegPromise = new Promise<void>((resolve, reject) => {\n ffmpeg(this.pcmStream!)\n .inputFormat('s16le')\n .inputOptions([`-ar ${this.sampleRate}`, '-ac 2'])\n .audioCodec('libopus')\n .audioChannels(2)\n .audioFrequency(this.sampleRate)\n .format('ogg')\n .output(this._outputPath!)\n .on('end', () => {\n this.logger.debug('FFmpeg encoding finished');\n resolve();\n })\n .on('error', (err) => {\n // Ignore errors from intentional stream closure or SIGINT during shutdown\n if (\n err.message?.includes('Output stream closed') ||\n err.message?.includes('received signal 2') ||\n err.message?.includes('SIGKILL') ||\n err.message?.includes('SIGINT')\n ) {\n resolve();\n } else {\n this.logger.error({ err }, 'FFmpeg encoding error');\n reject(err);\n }\n })\n .run();\n });\n }\n\n /**\n * Resample and mix frames to mono Float32\n */\n private resampleAndMix(opts: ResampleAndMixOptions): {\n samples: Float32Array;\n resampler: AudioResampler | undefined;\n } {\n const INV_INT16 = 1.0 / 32768.0;\n const { frames, flush = false } = opts;\n let { resampler } = opts;\n\n if (frames.length === 0 && !flush) {\n return { samples: new Float32Array(0), resampler };\n }\n\n if (!resampler && frames.length > 0) {\n const firstFrame = frames[0]!;\n resampler = new AudioResampler(firstFrame.sampleRate, this.sampleRate, firstFrame.channels);\n }\n\n const resampledFrames: AudioFrame[] = [];\n for (const frame of frames) {\n if (resampler) {\n resampledFrames.push(...resampler.push(frame));\n }\n }\n\n if (flush && resampler) {\n resampledFrames.push(...resampler.flush());\n }\n\n const totalSamples = resampledFrames.reduce((acc, frame) => acc + frame.samplesPerChannel, 0);\n const samples = new Float32Array(totalSamples);\n\n let pos = 0;\n for (const frame of resampledFrames) {\n const data = frame.data;\n const numChannels = frame.channels;\n for (let i = 0; i < frame.samplesPerChannel; i++) {\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += data[i * numChannels + ch]!;\n }\n samples[pos++] = (sum / numChannels) * INV_INT16;\n }\n }\n\n return { samples, resampler };\n }\n\n /**\n * Write PCM chunk to FFmpeg stream\n */\n private writePCM(leftSamples: Float32Array, rightSamples: Float32Array): void {\n if (!this.pcmStream) {\n this.startFFmpeg();\n }\n\n // Handle length mismatch by prepending silence\n if (leftSamples.length !== rightSamples.length) {\n const diff = Math.abs(leftSamples.length - rightSamples.length);\n if (leftSamples.length < rightSamples.length) {\n this.logger.warn(\n `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`,\n );\n const padded = new Float32Array(rightSamples.length);\n padded.set(leftSamples, diff);\n leftSamples = padded;\n } else {\n const padded = new Float32Array(leftSamples.length);\n padded.set(rightSamples, diff);\n rightSamples = padded;\n }\n }\n\n const maxLen = Math.max(leftSamples.length, rightSamples.length);\n if (maxLen <= 0) return;\n\n // Interleave stereo samples and convert back to Int16\n const stereoData = new Int16Array(maxLen * 2);\n for (let i = 0; i < maxLen; i++) {\n stereoData[i * 2] = Math.max(\n -32768,\n Math.min(32767, Math.round((leftSamples[i] ?? 0) * 32768)),\n );\n stereoData[i * 2 + 1] = Math.max(\n -32768,\n Math.min(32767, Math.round((rightSamples[i] ?? 0) * 32768)),\n );\n }\n\n this.pcmStream!.write(Buffer.from(stereoData.buffer));\n }\n\n /**\n * Encode task: read from channels, mix to stereo, stream to FFmpeg\n */\n private async encode(): Promise<void> {\n if (!this._outputPath) return;\n\n const inReader = this.inChan.stream().getReader();\n const outReader = this.outChan.stream().getReader();\n\n try {\n while (true) {\n const [inResult, outResult] = await Promise.all([inReader.read(), outReader.read()]);\n\n if (inResult.done || outResult.done) {\n break;\n }\n\n const inputBuf = inResult.value;\n const outputBuf = outResult.value;\n\n const inMixed = this.resampleAndMix({ frames: inputBuf, resampler: this.inResampler });\n this.inResampler = inMixed.resampler;\n\n const outMixed = this.resampleAndMix({\n frames: outputBuf,\n resampler: this.outResampler,\n flush: outputBuf.length > 0,\n });\n this.outResampler = outMixed.resampler;\n\n // Stream PCM data directly to FFmpeg\n this.writePCM(inMixed.samples, outMixed.samples);\n }\n\n // Close FFmpeg stream and wait for encoding to complete\n if (this.pcmStream) {\n this.pcmStream.end();\n await this.ffmpegPromise;\n }\n } catch (err) {\n this.logger.error({ err }, 'Error in encode task');\n } finally {\n inReader.releaseLock();\n outReader.releaseLock();\n\n if (!this.closeFuture.done) {\n this.closeFuture.resolve();\n }\n }\n }\n}\n\nclass RecorderAudioInput extends AudioInput {\n private source: AudioInput;\n private recorderIO: RecorderIO;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n constructor(recorderIO: RecorderIO, source: AudioInput) {\n super();\n this.recorderIO = recorderIO;\n this.source = source;\n\n // Set up the intercepting stream\n this.deferredStream.setSource(this.createInterceptingStream());\n }\n\n /**\n * Wall-clock time when the first frame was captured\n */\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n /**\n * Take accumulated frames and clear the buffer\n */\n takeBuf(): AudioFrame[] {\n const frames = this.accFrames;\n this.accFrames = [];\n return frames;\n }\n\n /**\n * Creates a stream that intercepts frames from the source,\n * accumulates them when recording, and passes them through unchanged.\n */\n private createInterceptingStream(): ReadableStream<AudioFrame> {\n const sourceStream = this.source.stream;\n const reader = sourceStream.getReader();\n\n const transform = new TransformStream<AudioFrame, AudioFrame>({\n transform: (frame, controller) => {\n // Accumulate frames when recording is active\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n controller.enqueue(frame);\n },\n });\n\n const pump = async () => {\n const writer = transform.writable.getWriter();\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await writer.write(value);\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n if (sourceError) {\n writer.abort(sourceError);\n return;\n }\n\n writer.releaseLock();\n\n try {\n await transform.writable.close();\n } catch {\n // ignore \"WritableStream is closed\" errors\n }\n }\n };\n\n pump();\n\n return transform.readable;\n }\n\n onAttached(): void {\n this.source.onAttached();\n }\n\n onDetached(): void {\n this.source.onDetached();\n }\n}\n\nclass RecorderAudioOutput extends AudioOutput {\n private recorderIO: RecorderIO;\n private writeFn: (buf: AudioFrame[]) => void;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n // Pause tracking\n private currentPauseStart?: number;\n private pauseWallTimes: Array<[number, number]> = []; // [start, end] pairs\n\n constructor(\n recorderIO: RecorderIO,\n audioOutput: AudioOutput,\n writeFn: (buf: AudioFrame[]) => void,\n ) {\n super(audioOutput.sampleRate, audioOutput);\n this.recorderIO = recorderIO;\n this.writeFn = writeFn;\n }\n\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n get hasPendingData(): boolean {\n return this.accFrames.length > 0;\n }\n\n pause(): void {\n if (this.currentPauseStart === undefined && this.recorderIO.recording) {\n this.currentPauseStart = Date.now();\n }\n\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume playback and record the pause interval\n */\n resume(): void {\n if (this.currentPauseStart !== undefined && this.recorderIO.recording) {\n this.pauseWallTimes.push([this.currentPauseStart, Date.now()]);\n this.currentPauseStart = undefined;\n }\n\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n\n private resetPauseState(): void {\n this.currentPauseStart = undefined;\n this.pauseWallTimes = [];\n }\n\n onPlaybackFinished(options: PlaybackFinishedEvent): void {\n const finishTime = Date.now();\n\n super.onPlaybackFinished(options);\n\n if (!this.recorderIO.recording) {\n return;\n }\n\n if (this.currentPauseStart !== undefined) {\n this.pauseWallTimes.push([this.currentPauseStart, finishTime]);\n this.currentPauseStart = undefined;\n }\n\n if (this.accFrames.length === 0) {\n this.resetPauseState();\n return;\n }\n\n const playbackPosition = options.playbackPosition;\n\n const pauseEvents: Array<[number, number]> = [];\n\n if (this.pauseWallTimes.length > 0) {\n const totalPauseDuration = this.pauseWallTimes.reduce(\n (sum, [start, end]) => sum + (end - start),\n 0,\n );\n // Convert playbackPosition from seconds to milliseconds for wall time calculations\n const playbackStartTime = finishTime - playbackPosition * 1000 - totalPauseDuration;\n\n let accumulatedPause = 0;\n for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {\n let position = (pauseStart - playbackStartTime - accumulatedPause) / 1000; // Convert to seconds\n const duration = (pauseEnd - pauseStart) / 1000; // Convert to seconds\n position = Math.max(0, Math.min(position, playbackPosition));\n pauseEvents.push([position, duration]);\n accumulatedPause += pauseEnd - pauseStart;\n }\n }\n\n const buf: AudioFrame[] = [];\n let accDur = 0;\n const sampleRate = this.accFrames[0]!.sampleRate;\n const numChannels = this.accFrames[0]!.channels;\n\n let pauseIdx = 0;\n let shouldBreak = false;\n\n for (const frame of this.accFrames) {\n let currentFrame = frame;\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (frameDuration + accDur > playbackPosition) {\n const [left] = splitFrame(currentFrame, playbackPosition - accDur);\n currentFrame = left;\n shouldBreak = true;\n }\n\n // Process any pauses before this frame starts\n while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx]![0] <= accDur) {\n const [, pauseDur] = pauseEvents[pauseIdx]!;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n pauseIdx++;\n }\n\n // Process any pauses within this frame\n const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate;\n while (\n pauseIdx < pauseEvents.length &&\n pauseEvents[pauseIdx]![0] < accDur + currentFrameDuration\n ) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n const [left, right] = splitFrame(currentFrame, pausePos - accDur);\n buf.push(left);\n accDur += left.samplesPerChannel / left.sampleRate;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n currentFrame = right;\n pauseIdx++;\n }\n\n buf.push(currentFrame);\n accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate;\n\n if (shouldBreak) {\n break;\n }\n }\n\n // Process remaining pauses\n while (pauseIdx < pauseEvents.length) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n if (pausePos <= playbackPosition) {\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n }\n pauseIdx++;\n }\n\n if (buf.length > 0) {\n this.writeFn(buf);\n }\n\n this.accFrames = [];\n this.resetPauseState();\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n await super.captureFrame(frame);\n\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n if (this.nextInChain) {\n await this.nextInChain.captureFrame(frame);\n }\n }\n\n flush(): void {\n super.flush();\n\n if (this.nextInChain) {\n this.nextInChain.flush();\n }\n }\n\n clearBuffer(): void {\n if (this.nextInChain) {\n this.nextInChain.clearBuffer();\n }\n }\n}\n\n/**\n * Create a silent audio frame with the given duration\n */\nfunction createSilenceFrame(duration: number, sampleRate: number, numChannels: number): AudioFrame {\n const samples = Math.floor(duration * sampleRate);\n const data = new Int16Array(samples * numChannels); // Zero-filled by default\n return new AudioFrame(data, sampleRate, numChannels, samples);\n}\n\n/**\n * Split an audio frame at the given position (in seconds)\n * Returns [left, right] frames\n */\nfunction splitFrame(frame: AudioFrame, position: number): [AudioFrame, AudioFrame] {\n if (position <= 0) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [emptyFrame, frame];\n }\n\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n if (position >= frameDuration) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [frame, emptyFrame];\n }\n\n // samplesNeeded is samples per channel (i.e., sample count in time)\n const samplesNeeded = Math.floor(position * frame.sampleRate);\n // Int16Array: each element is one sample, interleaved by channel\n // So total elements = samplesPerChannel * channels\n const numChannels = frame.channels;\n\n const leftData = frame.data.slice(0, samplesNeeded * numChannels);\n const rightData = frame.data.slice(samplesNeeded * numChannels);\n\n const leftFrame = new AudioFrame(leftData, frame.sampleRate, frame.channels, samplesNeeded);\n\n const rightFrame = new AudioFrame(\n rightData,\n frame.sampleRate,\n frame.channels,\n frame.samplesPerChannel - samplesNeeded,\n );\n\n return [leftFrame, rightFrame];\n}\n"],"mappings":"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","names":[]}
@@ -23,6 +23,8 @@ __export(report_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(report_exports);
25
25
  function createSessionReport(opts) {
26
+ const timestamp = opts.timestamp ?? Date.now();
27
+ const audioRecordingStartedAt = opts.audioRecordingStartedAt;
26
28
  return {
27
29
  jobId: opts.jobId,
28
30
  roomId: opts.roomId,
@@ -30,9 +32,12 @@ function createSessionReport(opts) {
30
32
  options: opts.options,
31
33
  events: opts.events,
32
34
  chatHistory: opts.chatHistory,
33
- enableRecording: opts.enableUserDataTraining ?? false,
35
+ enableRecording: opts.enableRecording ?? false,
34
36
  startedAt: opts.startedAt ?? Date.now(),
35
- timestamp: opts.timestamp ?? Date.now()
37
+ timestamp,
38
+ audioRecordingPath: opts.audioRecordingPath,
39
+ audioRecordingStartedAt,
40
+ duration: audioRecordingStartedAt !== void 0 ? timestamp - audioRecordingStartedAt : void 0
36
41
  };
37
42
  }
38
43
  function sessionReportToJSON(report) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/voice/report.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChatContext } from '../llm/chat_context.js';\nimport type { VoiceOptions } from './agent_session.js';\nimport type { AgentEvent } from './events.js';\n\nexport interface SessionReport {\n jobId: string;\n roomId: string;\n room: string;\n options: VoiceOptions;\n events: AgentEvent[];\n chatHistory: ChatContext;\n enableRecording: boolean;\n /** Timestamp when the session started (milliseconds) */\n startedAt: number;\n /** Timestamp when the session report was created (milliseconds), typically at the end of the session */\n timestamp: number;\n}\n\nexport interface SessionReportOptions {\n jobId: string;\n roomId: string;\n room: string;\n options: VoiceOptions;\n events: AgentEvent[];\n chatHistory: ChatContext;\n enableUserDataTraining?: boolean;\n /** Timestamp when the session started (milliseconds) */\n startedAt?: number;\n /** Timestamp when the session report was created (milliseconds) */\n timestamp?: number;\n}\n\nexport function createSessionReport(opts: SessionReportOptions): SessionReport {\n return {\n jobId: opts.jobId,\n roomId: opts.roomId,\n room: opts.room,\n options: opts.options,\n events: opts.events,\n chatHistory: opts.chatHistory,\n enableRecording: opts.enableUserDataTraining ?? false,\n startedAt: opts.startedAt ?? Date.now(),\n timestamp: opts.timestamp ?? Date.now(),\n };\n}\n\n// - header: protobuf MetricsRecordingHeader (room_id, duration, start_time)\n// - chat_history: JSON serialized chat history (use sessionReportToJSON)\n// - audio: audio recording file if available (ogg format)\n// - Uploads to LiveKit Cloud observability endpoint with JWT auth\nexport function sessionReportToJSON(report: SessionReport): Record<string, unknown> {\n const events: Record<string, unknown>[] = [];\n\n for (const event of report.events) {\n if (event.type === 'metrics_collected') {\n continue; // metrics are too noisy, Cloud is using the chat_history as the source of truth\n }\n\n events.push({ ...event });\n }\n\n return {\n job_id: report.jobId,\n room_id: report.roomId,\n room: report.room,\n events,\n options: {\n allow_interruptions: report.options.allowInterruptions,\n discard_audio_if_uninterruptible: report.options.discardAudioIfUninterruptible,\n min_interruption_duration: report.options.minInterruptionDuration,\n min_interruption_words: report.options.minInterruptionWords,\n min_endpointing_delay: report.options.minEndpointingDelay,\n max_endpointing_delay: report.options.maxEndpointingDelay,\n max_tool_steps: report.options.maxToolSteps,\n },\n chat_history: report.chatHistory.toJSON({ excludeTimestamp: false }),\n enable_user_data_training: report.enableRecording,\n timestamp: report.timestamp,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmCO,SAAS,oBAAoB,MAA2C;AAC7E,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,IACd,QAAQ,KAAK;AAAA,IACb,aAAa,KAAK;AAAA,IAClB,iBAAiB,KAAK,0BAA0B;AAAA,IAChD,WAAW,KAAK,aAAa,KAAK,IAAI;AAAA,IACtC,WAAW,KAAK,aAAa,KAAK,IAAI;AAAA,EACxC;AACF;AAMO,SAAS,oBAAoB,QAAgD;AAClF,QAAM,SAAoC,CAAC;AAE3C,aAAW,SAAS,OAAO,QAAQ;AACjC,QAAI,MAAM,SAAS,qBAAqB;AACtC;AAAA,IACF;AAEA,WAAO,KAAK,EAAE,GAAG,MAAM,CAAC;AAAA,EAC1B;AAEA,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,qBAAqB,OAAO,QAAQ;AAAA,MACpC,kCAAkC,OAAO,QAAQ;AAAA,MACjD,2BAA2B,OAAO,QAAQ;AAAA,MAC1C,wBAAwB,OAAO,QAAQ;AAAA,MACvC,uBAAuB,OAAO,QAAQ;AAAA,MACtC,uBAAuB,OAAO,QAAQ;AAAA,MACtC,gBAAgB,OAAO,QAAQ;AAAA,IACjC;AAAA,IACA,cAAc,OAAO,YAAY,OAAO,EAAE,kBAAkB,MAAM,CAAC;AAAA,IACnE,2BAA2B,OAAO;AAAA,IAClC,WAAW,OAAO;AAAA,EACpB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/voice/report.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { ChatContext } from '../llm/chat_context.js';\nimport type { VoiceOptions } from './agent_session.js';\nimport type { AgentEvent } from './events.js';\n\nexport interface SessionReport {\n jobId: string;\n roomId: string;\n room: string;\n options: VoiceOptions;\n events: AgentEvent[];\n chatHistory: ChatContext;\n enableRecording: boolean;\n /** Timestamp when the session started (milliseconds) */\n startedAt: number;\n /** Timestamp when the session report was created (milliseconds), typically at the end of the session */\n timestamp: number;\n /** Path to the audio recording file (if recording was enabled) */\n audioRecordingPath?: string;\n /** Timestamp when the audio recording started (milliseconds) */\n audioRecordingStartedAt?: number;\n /** Duration of the session in milliseconds */\n duration?: number;\n}\n\nexport interface SessionReportOptions {\n jobId: string;\n roomId: string;\n room: string;\n options: VoiceOptions;\n events: AgentEvent[];\n chatHistory: ChatContext;\n enableRecording?: boolean;\n /** Timestamp when the session started (milliseconds) */\n startedAt?: number;\n /** Timestamp when the session report was created (milliseconds) */\n timestamp?: number;\n /** Path to the audio recording file (if recording was enabled) */\n audioRecordingPath?: string;\n /** Timestamp when the audio recording started (milliseconds) */\n audioRecordingStartedAt?: number;\n}\n\nexport function createSessionReport(opts: SessionReportOptions): SessionReport {\n const timestamp = opts.timestamp ?? Date.now();\n const audioRecordingStartedAt = opts.audioRecordingStartedAt;\n\n return {\n jobId: opts.jobId,\n roomId: opts.roomId,\n room: opts.room,\n options: opts.options,\n events: opts.events,\n chatHistory: opts.chatHistory,\n enableRecording: opts.enableRecording ?? false,\n startedAt: opts.startedAt ?? Date.now(),\n timestamp,\n audioRecordingPath: opts.audioRecordingPath,\n audioRecordingStartedAt,\n duration:\n audioRecordingStartedAt !== undefined ? timestamp - audioRecordingStartedAt : undefined,\n };\n}\n\n// - header: protobuf MetricsRecordingHeader (room_id, duration, start_time)\n// - chat_history: JSON serialized chat history (use sessionReportToJSON)\n// - audio: audio recording file if available (ogg format)\n// - Uploads to LiveKit Cloud observability endpoint with JWT auth\nexport function sessionReportToJSON(report: SessionReport): Record<string, unknown> {\n const events: Record<string, unknown>[] = [];\n\n for (const event of report.events) {\n if (event.type === 'metrics_collected') {\n continue; // metrics are too noisy, Cloud is using the chat_history as the source of truth\n }\n\n events.push({ ...event });\n }\n\n return {\n job_id: report.jobId,\n room_id: report.roomId,\n room: report.room,\n events,\n options: {\n allow_interruptions: report.options.allowInterruptions,\n discard_audio_if_uninterruptible: report.options.discardAudioIfUninterruptible,\n min_interruption_duration: report.options.minInterruptionDuration,\n min_interruption_words: report.options.minInterruptionWords,\n min_endpointing_delay: report.options.minEndpointingDelay,\n max_endpointing_delay: report.options.maxEndpointingDelay,\n max_tool_steps: report.options.maxToolSteps,\n },\n chat_history: report.chatHistory.toJSON({ excludeTimestamp: false }),\n enable_user_data_training: report.enableRecording,\n timestamp: report.timestamp,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6CO,SAAS,oBAAoB,MAA2C;AAC7E,QAAM,YAAY,KAAK,aAAa,KAAK,IAAI;AAC7C,QAAM,0BAA0B,KAAK;AAErC,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,MAAM,KAAK;AAAA,IACX,SAAS,KAAK;AAAA,IACd,QAAQ,KAAK;AAAA,IACb,aAAa,KAAK;AAAA,IAClB,iBAAiB,KAAK,mBAAmB;AAAA,IACzC,WAAW,KAAK,aAAa,KAAK,IAAI;AAAA,IACtC;AAAA,IACA,oBAAoB,KAAK;AAAA,IACzB;AAAA,IACA,UACE,4BAA4B,SAAY,YAAY,0BAA0B;AAAA,EAClF;AACF;AAMO,SAAS,oBAAoB,QAAgD;AAClF,QAAM,SAAoC,CAAC;AAE3C,aAAW,SAAS,OAAO,QAAQ;AACjC,QAAI,MAAM,SAAS,qBAAqB;AACtC;AAAA,IACF;AAEA,WAAO,KAAK,EAAE,GAAG,MAAM,CAAC;AAAA,EAC1B;AAEA,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,MAAM,OAAO;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,qBAAqB,OAAO,QAAQ;AAAA,MACpC,kCAAkC,OAAO,QAAQ;AAAA,MACjD,2BAA2B,OAAO,QAAQ;AAAA,MAC1C,wBAAwB,OAAO,QAAQ;AAAA,MACvC,uBAAuB,OAAO,QAAQ;AAAA,MACtC,uBAAuB,OAAO,QAAQ;AAAA,MACtC,gBAAgB,OAAO,QAAQ;AAAA,IACjC;AAAA,IACA,cAAc,OAAO,YAAY,OAAO,EAAE,kBAAkB,MAAM,CAAC;AAAA,IACnE,2BAA2B,OAAO;AAAA,IAClC,WAAW,OAAO;AAAA,EACpB;AACF;","names":[]}
@@ -13,6 +13,12 @@ export interface SessionReport {
13
13
  startedAt: number;
14
14
  /** Timestamp when the session report was created (milliseconds), typically at the end of the session */
15
15
  timestamp: number;
16
+ /** Path to the audio recording file (if recording was enabled) */
17
+ audioRecordingPath?: string;
18
+ /** Timestamp when the audio recording started (milliseconds) */
19
+ audioRecordingStartedAt?: number;
20
+ /** Duration of the session in milliseconds */
21
+ duration?: number;
16
22
  }
17
23
  export interface SessionReportOptions {
18
24
  jobId: string;
@@ -21,11 +27,15 @@ export interface SessionReportOptions {
21
27
  options: VoiceOptions;
22
28
  events: AgentEvent[];
23
29
  chatHistory: ChatContext;
24
- enableUserDataTraining?: boolean;
30
+ enableRecording?: boolean;
25
31
  /** Timestamp when the session started (milliseconds) */
26
32
  startedAt?: number;
27
33
  /** Timestamp when the session report was created (milliseconds) */
28
34
  timestamp?: number;
35
+ /** Path to the audio recording file (if recording was enabled) */
36
+ audioRecordingPath?: string;
37
+ /** Timestamp when the audio recording started (milliseconds) */
38
+ audioRecordingStartedAt?: number;
29
39
  }
30
40
  export declare function createSessionReport(opts: SessionReportOptions): SessionReport;
31
41
  export declare function sessionReportToJSON(report: SessionReport): Record<string, unknown>;