@livekit/agents 1.0.34 → 1.0.36-dev.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 (187) hide show
  1. package/dist/cli.cjs.map +1 -1
  2. package/dist/index.cjs +3 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/inference/api_protos.d.cts +4 -4
  10. package/dist/inference/api_protos.d.ts +4 -4
  11. package/dist/inference/interruption/AdaptiveInterruptionDetector.cjs +152 -0
  12. package/dist/inference/interruption/AdaptiveInterruptionDetector.cjs.map +1 -0
  13. package/dist/inference/interruption/AdaptiveInterruptionDetector.d.cts +50 -0
  14. package/dist/inference/interruption/AdaptiveInterruptionDetector.d.ts +50 -0
  15. package/dist/inference/interruption/AdaptiveInterruptionDetector.d.ts.map +1 -0
  16. package/dist/inference/interruption/AdaptiveInterruptionDetector.js +125 -0
  17. package/dist/inference/interruption/AdaptiveInterruptionDetector.js.map +1 -0
  18. package/dist/inference/interruption/InterruptionStream.cjs +310 -0
  19. package/dist/inference/interruption/InterruptionStream.cjs.map +1 -0
  20. package/dist/inference/interruption/InterruptionStream.d.cts +57 -0
  21. package/dist/inference/interruption/InterruptionStream.d.ts +57 -0
  22. package/dist/inference/interruption/InterruptionStream.d.ts.map +1 -0
  23. package/dist/inference/interruption/InterruptionStream.js +288 -0
  24. package/dist/inference/interruption/InterruptionStream.js.map +1 -0
  25. package/dist/inference/interruption/defaults.cjs +76 -0
  26. package/dist/inference/interruption/defaults.cjs.map +1 -0
  27. package/dist/inference/interruption/defaults.d.cts +14 -0
  28. package/dist/inference/interruption/defaults.d.ts +14 -0
  29. package/dist/inference/interruption/defaults.d.ts.map +1 -0
  30. package/dist/inference/interruption/defaults.js +42 -0
  31. package/dist/inference/interruption/defaults.js.map +1 -0
  32. package/dist/inference/interruption/errors.cjs +2 -0
  33. package/dist/inference/interruption/errors.cjs.map +1 -0
  34. package/dist/inference/interruption/errors.d.cts +2 -0
  35. package/dist/inference/interruption/errors.d.ts +2 -0
  36. package/dist/inference/interruption/errors.d.ts.map +1 -0
  37. package/dist/inference/interruption/errors.js +1 -0
  38. package/dist/inference/interruption/errors.js.map +1 -0
  39. package/dist/inference/interruption/http_transport.cjs +57 -0
  40. package/dist/inference/interruption/http_transport.cjs.map +1 -0
  41. package/dist/inference/interruption/http_transport.d.cts +23 -0
  42. package/dist/inference/interruption/http_transport.d.ts +23 -0
  43. package/dist/inference/interruption/http_transport.d.ts.map +1 -0
  44. package/dist/inference/interruption/http_transport.js +33 -0
  45. package/dist/inference/interruption/http_transport.js.map +1 -0
  46. package/dist/inference/interruption/index.cjs +34 -0
  47. package/dist/inference/interruption/index.cjs.map +1 -0
  48. package/dist/inference/interruption/index.d.cts +5 -0
  49. package/dist/inference/interruption/index.d.ts +5 -0
  50. package/dist/inference/interruption/index.d.ts.map +1 -0
  51. package/dist/inference/interruption/index.js +7 -0
  52. package/dist/inference/interruption/index.js.map +1 -0
  53. package/dist/inference/interruption/interruption.cjs +85 -0
  54. package/dist/inference/interruption/interruption.cjs.map +1 -0
  55. package/dist/inference/interruption/interruption.d.cts +48 -0
  56. package/dist/inference/interruption/interruption.d.ts +48 -0
  57. package/dist/inference/interruption/interruption.d.ts.map +1 -0
  58. package/dist/inference/interruption/interruption.js +59 -0
  59. package/dist/inference/interruption/interruption.js.map +1 -0
  60. package/dist/inference/llm.cjs +30 -3
  61. package/dist/inference/llm.cjs.map +1 -1
  62. package/dist/inference/llm.d.cts +3 -1
  63. package/dist/inference/llm.d.ts +3 -1
  64. package/dist/inference/llm.d.ts.map +1 -1
  65. package/dist/inference/llm.js +30 -3
  66. package/dist/inference/llm.js.map +1 -1
  67. package/dist/inference/utils.cjs +15 -2
  68. package/dist/inference/utils.cjs.map +1 -1
  69. package/dist/inference/utils.d.cts +1 -0
  70. package/dist/inference/utils.d.ts +1 -0
  71. package/dist/inference/utils.d.ts.map +1 -1
  72. package/dist/inference/utils.js +13 -1
  73. package/dist/inference/utils.js.map +1 -1
  74. package/dist/inference/utils.test.cjs +20 -0
  75. package/dist/inference/utils.test.cjs.map +1 -0
  76. package/dist/inference/utils.test.js +19 -0
  77. package/dist/inference/utils.test.js.map +1 -0
  78. package/dist/ipc/inference_proc_executor.cjs.map +1 -1
  79. package/dist/ipc/job_proc_executor.cjs.map +1 -1
  80. package/dist/ipc/job_proc_lazy_main.cjs +1 -1
  81. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  82. package/dist/ipc/job_proc_lazy_main.js +1 -1
  83. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  84. package/dist/llm/chat_context.cjs +20 -2
  85. package/dist/llm/chat_context.cjs.map +1 -1
  86. package/dist/llm/chat_context.d.cts +9 -0
  87. package/dist/llm/chat_context.d.ts +9 -0
  88. package/dist/llm/chat_context.d.ts.map +1 -1
  89. package/dist/llm/chat_context.js +20 -2
  90. package/dist/llm/chat_context.js.map +1 -1
  91. package/dist/llm/llm.cjs.map +1 -1
  92. package/dist/llm/llm.d.cts +1 -0
  93. package/dist/llm/llm.d.ts +1 -0
  94. package/dist/llm/llm.d.ts.map +1 -1
  95. package/dist/llm/llm.js.map +1 -1
  96. package/dist/llm/provider_format/openai.cjs +43 -20
  97. package/dist/llm/provider_format/openai.cjs.map +1 -1
  98. package/dist/llm/provider_format/openai.d.ts.map +1 -1
  99. package/dist/llm/provider_format/openai.js +43 -20
  100. package/dist/llm/provider_format/openai.js.map +1 -1
  101. package/dist/llm/provider_format/openai.test.cjs +35 -0
  102. package/dist/llm/provider_format/openai.test.cjs.map +1 -1
  103. package/dist/llm/provider_format/openai.test.js +35 -0
  104. package/dist/llm/provider_format/openai.test.js.map +1 -1
  105. package/dist/llm/provider_format/utils.cjs +1 -1
  106. package/dist/llm/provider_format/utils.cjs.map +1 -1
  107. package/dist/llm/provider_format/utils.d.ts.map +1 -1
  108. package/dist/llm/provider_format/utils.js +1 -1
  109. package/dist/llm/provider_format/utils.js.map +1 -1
  110. package/dist/stream/stream_channel.cjs +3 -0
  111. package/dist/stream/stream_channel.cjs.map +1 -1
  112. package/dist/stream/stream_channel.d.cts +3 -2
  113. package/dist/stream/stream_channel.d.ts +3 -2
  114. package/dist/stream/stream_channel.d.ts.map +1 -1
  115. package/dist/stream/stream_channel.js +3 -0
  116. package/dist/stream/stream_channel.js.map +1 -1
  117. package/dist/telemetry/trace_types.cjs +15 -0
  118. package/dist/telemetry/trace_types.cjs.map +1 -1
  119. package/dist/telemetry/trace_types.d.cts +5 -0
  120. package/dist/telemetry/trace_types.d.ts +5 -0
  121. package/dist/telemetry/trace_types.d.ts.map +1 -1
  122. package/dist/telemetry/trace_types.js +10 -0
  123. package/dist/telemetry/trace_types.js.map +1 -1
  124. package/dist/utils/ws_transport.cjs +51 -0
  125. package/dist/utils/ws_transport.cjs.map +1 -0
  126. package/dist/utils/ws_transport.d.cts +9 -0
  127. package/dist/utils/ws_transport.d.ts +9 -0
  128. package/dist/utils/ws_transport.d.ts.map +1 -0
  129. package/dist/utils/ws_transport.js +17 -0
  130. package/dist/utils/ws_transport.js.map +1 -0
  131. package/dist/utils/ws_transport.test.cjs +212 -0
  132. package/dist/utils/ws_transport.test.cjs.map +1 -0
  133. package/dist/utils/ws_transport.test.js +211 -0
  134. package/dist/utils/ws_transport.test.js.map +1 -0
  135. package/dist/voice/agent_activity.cjs +49 -0
  136. package/dist/voice/agent_activity.cjs.map +1 -1
  137. package/dist/voice/agent_activity.d.cts +14 -0
  138. package/dist/voice/agent_activity.d.ts +14 -0
  139. package/dist/voice/agent_activity.d.ts.map +1 -1
  140. package/dist/voice/agent_activity.js +49 -0
  141. package/dist/voice/agent_activity.js.map +1 -1
  142. package/dist/voice/agent_session.cjs +12 -1
  143. package/dist/voice/agent_session.cjs.map +1 -1
  144. package/dist/voice/agent_session.d.cts +3 -0
  145. package/dist/voice/agent_session.d.ts +3 -0
  146. package/dist/voice/agent_session.d.ts.map +1 -1
  147. package/dist/voice/agent_session.js +12 -1
  148. package/dist/voice/agent_session.js.map +1 -1
  149. package/dist/voice/audio_recognition.cjs +124 -2
  150. package/dist/voice/audio_recognition.cjs.map +1 -1
  151. package/dist/voice/audio_recognition.d.cts +32 -1
  152. package/dist/voice/audio_recognition.d.ts +32 -1
  153. package/dist/voice/audio_recognition.d.ts.map +1 -1
  154. package/dist/voice/audio_recognition.js +127 -2
  155. package/dist/voice/audio_recognition.js.map +1 -1
  156. package/dist/voice/background_audio.cjs.map +1 -1
  157. package/dist/voice/generation.cjs +2 -1
  158. package/dist/voice/generation.cjs.map +1 -1
  159. package/dist/voice/generation.d.ts.map +1 -1
  160. package/dist/voice/generation.js +2 -1
  161. package/dist/voice/generation.js.map +1 -1
  162. package/package.json +2 -1
  163. package/src/index.ts +2 -0
  164. package/src/inference/interruption/AdaptiveInterruptionDetector.ts +166 -0
  165. package/src/inference/interruption/InterruptionStream.ts +397 -0
  166. package/src/inference/interruption/defaults.ts +33 -0
  167. package/src/inference/interruption/errors.ts +0 -0
  168. package/src/inference/interruption/http_transport.ts +61 -0
  169. package/src/inference/interruption/index.ts +4 -0
  170. package/src/inference/interruption/interruption.ts +88 -0
  171. package/src/inference/llm.ts +42 -3
  172. package/src/inference/utils.test.ts +31 -0
  173. package/src/inference/utils.ts +15 -0
  174. package/src/ipc/job_proc_lazy_main.ts +1 -1
  175. package/src/llm/chat_context.ts +32 -2
  176. package/src/llm/llm.ts +1 -0
  177. package/src/llm/provider_format/openai.test.ts +40 -0
  178. package/src/llm/provider_format/openai.ts +46 -19
  179. package/src/llm/provider_format/utils.ts +5 -1
  180. package/src/stream/stream_channel.ts +6 -2
  181. package/src/telemetry/trace_types.ts +7 -0
  182. package/src/utils/ws_transport.test.ts +282 -0
  183. package/src/utils/ws_transport.ts +22 -0
  184. package/src/voice/agent_activity.ts +61 -0
  185. package/src/voice/agent_session.ts +22 -2
  186. package/src/voice/audio_recognition.ts +161 -1
  187. package/src/voice/generation.ts +1 -0
@@ -1,5 +1,9 @@
1
1
  import { AudioFrame } from "@livekit/rtc-node";
2
2
  import { ReadableStream } from "node:stream/web";
3
+ import {
4
+ InterruptionStreamBase,
5
+ InterruptionStreamSentinel
6
+ } from "../inference/interruption/InterruptionStream.js";
3
7
  import {} from "../llm/chat_context.js";
4
8
  import { log } from "../log.js";
5
9
  import { DeferredReadableStream, isStreamReaderReleaseError } from "../stream/deferred_stream.js";
@@ -34,6 +38,7 @@ class AudioRecognition {
34
38
  userTurnSpan;
35
39
  vadInputStream;
36
40
  sttInputStream;
41
+ interruptionInputStream;
37
42
  silenceAudioTransform = new IdentityTransform();
38
43
  silenceAudioWriter;
39
44
  // all cancellable tasks
@@ -41,20 +46,30 @@ class AudioRecognition {
41
46
  commitUserTurnTask;
42
47
  vadTask;
43
48
  sttTask;
49
+ interruptionTask;
50
+ // interruption detection
51
+ interruptionDetector;
52
+ interruptionStream;
53
+ interruptionEnabled = false;
54
+ agentSpeaking = false;
44
55
  constructor(opts) {
45
56
  this.hooks = opts.recognitionHooks;
46
57
  this.stt = opts.stt;
47
58
  this.vad = opts.vad;
59
+ this.interruptionDetector = opts.interruptionDetector;
48
60
  this.turnDetector = opts.turnDetector;
49
61
  this.turnDetectionMode = opts.turnDetectionMode;
50
62
  this.minEndpointingDelay = opts.minEndpointingDelay;
51
63
  this.maxEndpointingDelay = opts.maxEndpointingDelay;
52
64
  this.lastLanguage = void 0;
53
65
  this.rootSpanContext = opts.rootSpanContext;
66
+ this.interruptionEnabled = this.interruptionDetector !== void 0 && this.vad !== void 0;
54
67
  this.deferredInputStream = new DeferredReadableStream();
55
- const [vadInputStream, sttInputStream] = this.deferredInputStream.stream.tee();
68
+ const [vadInputStream, rest] = this.deferredInputStream.stream.tee();
69
+ const [sttInputStream, interruptionInputStream] = rest.tee();
56
70
  this.vadInputStream = vadInputStream;
57
71
  this.sttInputStream = mergeReadableStreams(sttInputStream, this.silenceAudioTransform.readable);
72
+ this.interruptionInputStream = interruptionInputStream;
58
73
  this.silenceAudioWriter = this.silenceAudioTransform.writable.getWriter();
59
74
  }
60
75
  /**
@@ -75,6 +90,14 @@ class AudioRecognition {
75
90
  this.sttTask.result.catch((err) => {
76
91
  this.logger.error(`Error running STT task: ${err}`);
77
92
  });
93
+ if (this.interruptionEnabled && this.interruptionDetector) {
94
+ this.interruptionTask = Task.from(
95
+ ({ signal }) => this.createInterruptionTask(this.interruptionDetector, signal)
96
+ );
97
+ this.interruptionTask.result.catch((err) => {
98
+ this.logger.error(`Error running interruption task: ${err}`);
99
+ });
100
+ }
78
101
  }
79
102
  async onSTTEvent(ev) {
80
103
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
@@ -409,6 +432,9 @@ class AudioRecognition {
409
432
  if (ev.frames.length > 0 && ev.frames[0]) {
410
433
  this.sampleRate = ev.frames[0].sampleRate;
411
434
  }
435
+ if (this.agentSpeaking && this.interruptionEnabled) {
436
+ this.onStartOfOverlapSpeech(ev.speechDuration, this.userTurnSpan);
437
+ }
412
438
  (_a = this.bounceEOUTask) == null ? void 0 : _a.cancel();
413
439
  break;
414
440
  case VADEventType.INFERENCE_DONE:
@@ -424,6 +450,9 @@ class AudioRecognition {
424
450
  this.logger.debug("VAD task: END_OF_SPEECH");
425
451
  this.hooks.onEndOfSpeech(ev);
426
452
  this.speaking = false;
453
+ if (this.agentSpeaking && this.interruptionEnabled) {
454
+ this.onEndOfOverlapSpeech();
455
+ }
427
456
  if (this.vadBaseTurnDetection || this.turnDetectionMode === "stt" && this.userTurnCommitted) {
428
457
  const chatCtx = this.hooks.retrieveChatCtx();
429
458
  this.runEOUDetection(chatCtx);
@@ -437,6 +466,100 @@ class AudioRecognition {
437
466
  this.logger.debug("VAD task closed");
438
467
  }
439
468
  }
469
+ async createInterruptionTask(interruptionDetector, signal) {
470
+ this.interruptionStream = interruptionDetector.createStream();
471
+ const reader = this.interruptionInputStream.getReader();
472
+ const forwardTask = (async () => {
473
+ var _a;
474
+ try {
475
+ while (!signal.aborted) {
476
+ const { done, value: frame } = await reader.read();
477
+ if (done) break;
478
+ await ((_a = this.interruptionStream) == null ? void 0 : _a.pushFrame(frame));
479
+ }
480
+ } catch (e) {
481
+ if (!signal.aborted) {
482
+ this.logger.error(e, "Error forwarding audio to interruption stream");
483
+ }
484
+ } finally {
485
+ reader.releaseLock();
486
+ }
487
+ })();
488
+ const eventStream = this.interruptionStream.stream;
489
+ const eventReader = eventStream.getReader();
490
+ const abortHandler = () => {
491
+ var _a;
492
+ eventReader.releaseLock();
493
+ (_a = this.interruptionStream) == null ? void 0 : _a.close();
494
+ signal.removeEventListener("abort", abortHandler);
495
+ };
496
+ signal.addEventListener("abort", abortHandler);
497
+ try {
498
+ while (!signal.aborted) {
499
+ const { done, value: ev } = await eventReader.read();
500
+ if (done) break;
501
+ this.logger.debug({ type: ev.type, probability: ev.probability }, "Interruption event");
502
+ this.hooks.onInterruption(ev);
503
+ }
504
+ } catch (e) {
505
+ if (!signal.aborted) {
506
+ this.logger.error(e, "Error in interruption task");
507
+ }
508
+ } finally {
509
+ this.logger.debug("Interruption task closed");
510
+ await forwardTask;
511
+ }
512
+ }
513
+ /**
514
+ * Called when the agent starts speaking.
515
+ * Enables interruption detection by sending the agent-speech-started sentinel.
516
+ */
517
+ onStartOfAgentSpeech() {
518
+ this.agentSpeaking = true;
519
+ if (!this.interruptionEnabled || !this.interruptionStream) {
520
+ return;
521
+ }
522
+ this.interruptionStream.pushFrame(InterruptionStreamSentinel.speechStarted());
523
+ }
524
+ /**
525
+ * Called when the agent stops speaking.
526
+ * Disables interruption detection by sending the agent-speech-ended sentinel.
527
+ */
528
+ onEndOfAgentSpeech() {
529
+ if (!this.interruptionEnabled || !this.interruptionStream) {
530
+ this.agentSpeaking = false;
531
+ return;
532
+ }
533
+ this.interruptionStream.pushFrame(InterruptionStreamSentinel.speechEnded());
534
+ if (this.agentSpeaking) {
535
+ this.onEndOfOverlapSpeech();
536
+ }
537
+ this.agentSpeaking = false;
538
+ }
539
+ /**
540
+ * Called when user starts speaking while agent is speaking (overlap speech).
541
+ * This triggers the interruption detection inference.
542
+ */
543
+ onStartOfOverlapSpeech(speechDuration, userSpeakingSpan) {
544
+ if (!this.interruptionEnabled || !this.interruptionStream) {
545
+ return;
546
+ }
547
+ if (this.agentSpeaking && userSpeakingSpan) {
548
+ this.interruptionStream.pushFrame(
549
+ InterruptionStreamSentinel.overlapSpeechStarted(speechDuration, userSpeakingSpan)
550
+ );
551
+ }
552
+ }
553
+ /**
554
+ * Called when user stops speaking during overlap.
555
+ * This ends the interruption detection inference for this overlap period.
556
+ */
557
+ onEndOfOverlapSpeech() {
558
+ if (!this.interruptionEnabled || !this.interruptionStream) {
559
+ return;
560
+ }
561
+ this.interruptionStream.pushFrame(InterruptionStreamSentinel.overlapSpeechEnded());
562
+ }
440
563
  setInputAudioStream(audioStream) {
441
564
  this.deferredInputStream.setSource(audioStream);
442
565
  }
@@ -487,13 +610,15 @@ class AudioRecognition {
487
610
  });
488
611
  }
489
612
  async close() {
490
- var _a, _b, _c, _d;
613
+ var _a, _b, _c, _d, _e, _f;
491
614
  this.detachInputAudioStream();
492
615
  this.silenceAudioWriter.releaseLock();
493
616
  await ((_a = this.commitUserTurnTask) == null ? void 0 : _a.cancelAndWait());
494
617
  await ((_b = this.sttTask) == null ? void 0 : _b.cancelAndWait());
495
618
  await ((_c = this.vadTask) == null ? void 0 : _c.cancelAndWait());
496
619
  await ((_d = this.bounceEOUTask) == null ? void 0 : _d.cancelAndWait());
620
+ await ((_e = this.interruptionTask) == null ? void 0 : _e.cancelAndWait());
621
+ await ((_f = this.interruptionStream) == null ? void 0 : _f.close());
497
622
  }
498
623
  _endUserTurnSpan({
499
624
  transcript,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/voice/audio_recognition.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { AudioFrame } from '@livekit/rtc-node';\nimport type { Context, Span } from '@opentelemetry/api';\nimport type { WritableStreamDefaultWriter } from 'node:stream/web';\nimport { ReadableStream } from 'node:stream/web';\nimport { type ChatContext } from '../llm/chat_context.js';\nimport { log } from '../log.js';\nimport { DeferredReadableStream, isStreamReaderReleaseError } from '../stream/deferred_stream.js';\nimport { IdentityTransform } from '../stream/identity_transform.js';\nimport { mergeReadableStreams } from '../stream/merge_readable_streams.js';\nimport { type SpeechEvent, SpeechEventType } from '../stt/stt.js';\nimport { traceTypes, tracer } from '../telemetry/index.js';\nimport { Task, delay } from '../utils.js';\nimport { type VAD, type VADEvent, VADEventType } from '../vad.js';\nimport type { TurnDetectionMode } from './agent_session.js';\nimport type { STTNode } from './io.js';\n\nexport interface EndOfTurnInfo {\n newTranscript: string;\n transcriptConfidence: number;\n transcriptionDelay: number;\n endOfUtteranceDelay: number;\n startedSpeakingAt: number | undefined;\n stoppedSpeakingAt: number | undefined;\n}\n\nexport interface PreemptiveGenerationInfo {\n newTranscript: string;\n transcriptConfidence: number;\n}\n\nexport interface RecognitionHooks {\n onStartOfSpeech: (ev: VADEvent) => void;\n onVADInferenceDone: (ev: VADEvent) => void;\n onEndOfSpeech: (ev: VADEvent) => void;\n onInterimTranscript: (ev: SpeechEvent) => void;\n onFinalTranscript: (ev: SpeechEvent) => void;\n onEndOfTurn: (info: EndOfTurnInfo) => Promise<boolean>;\n onPreemptiveGeneration: (info: PreemptiveGenerationInfo) => void;\n\n retrieveChatCtx: () => ChatContext;\n}\n\nexport interface _TurnDetector {\n unlikelyThreshold: (language?: string) => Promise<number | undefined>;\n supportsLanguage: (language?: string) => Promise<boolean>;\n predictEndOfTurn(chatCtx: ChatContext): Promise<number>;\n}\n\nexport interface AudioRecognitionOptions {\n recognitionHooks: RecognitionHooks;\n stt?: STTNode;\n vad?: VAD;\n turnDetector?: _TurnDetector;\n turnDetectionMode?: Exclude<TurnDetectionMode, _TurnDetector>;\n minEndpointingDelay: number;\n maxEndpointingDelay: number;\n rootSpanContext?: Context;\n}\n\nexport class AudioRecognition {\n private hooks: RecognitionHooks;\n private stt?: STTNode;\n private vad?: VAD;\n private turnDetector?: _TurnDetector;\n private turnDetectionMode?: Exclude<TurnDetectionMode, _TurnDetector>;\n private minEndpointingDelay: number;\n private maxEndpointingDelay: number;\n private lastLanguage?: string;\n private rootSpanContext?: Context;\n\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n private logger = log();\n private lastFinalTranscriptTime = 0;\n private audioTranscript = '';\n private audioInterimTranscript = '';\n private audioPreflightTranscript = '';\n private finalTranscriptConfidence: number[] = [];\n private lastSpeakingTime: number | undefined;\n private speechStartTime: number | undefined;\n private userTurnCommitted = false;\n private speaking = false;\n private sampleRate?: number;\n\n private userTurnSpan?: Span;\n\n private vadInputStream: ReadableStream<AudioFrame>;\n private sttInputStream: ReadableStream<AudioFrame>;\n private silenceAudioTransform = new IdentityTransform<AudioFrame>();\n private silenceAudioWriter: WritableStreamDefaultWriter<AudioFrame>;\n\n // all cancellable tasks\n private bounceEOUTask?: Task<void>;\n private commitUserTurnTask?: Task<void>;\n private vadTask?: Task<void>;\n private sttTask?: Task<void>;\n\n constructor(opts: AudioRecognitionOptions) {\n this.hooks = opts.recognitionHooks;\n this.stt = opts.stt;\n this.vad = opts.vad;\n this.turnDetector = opts.turnDetector;\n this.turnDetectionMode = opts.turnDetectionMode;\n this.minEndpointingDelay = opts.minEndpointingDelay;\n this.maxEndpointingDelay = opts.maxEndpointingDelay;\n this.lastLanguage = undefined;\n this.rootSpanContext = opts.rootSpanContext;\n\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n const [vadInputStream, sttInputStream] = this.deferredInputStream.stream.tee();\n this.vadInputStream = vadInputStream;\n this.sttInputStream = mergeReadableStreams(sttInputStream, this.silenceAudioTransform.readable);\n this.silenceAudioWriter = this.silenceAudioTransform.writable.getWriter();\n }\n\n /**\n * Current transcript of the user's speech, including interim transcript if available.\n */\n get currentTranscript(): string {\n if (this.audioInterimTranscript) {\n return `${this.audioTranscript} ${this.audioInterimTranscript}`.trim();\n }\n return this.audioTranscript;\n }\n\n async start() {\n this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal));\n this.vadTask.result.catch((err) => {\n this.logger.error(`Error running VAD task: ${err}`);\n });\n\n this.sttTask = Task.from(({ signal }) => this.createSttTask(this.stt, signal));\n this.sttTask.result.catch((err) => {\n this.logger.error(`Error running STT task: ${err}`);\n });\n }\n\n private async onSTTEvent(ev: SpeechEvent) {\n if (\n this.turnDetectionMode === 'manual' &&\n this.userTurnCommitted &&\n (this.bounceEOUTask === undefined ||\n this.bounceEOUTask.done ||\n ev.type == SpeechEventType.INTERIM_TRANSCRIPT)\n ) {\n // ignore stt event if user turn already committed and EOU task is done\n // or it's an interim transcript\n this.logger.debug(\n {\n userTurnCommitted: this.userTurnCommitted,\n eouTaskDone: this.bounceEOUTask?.done,\n evType: ev.type,\n turnDetectionMode: this.turnDetectionMode,\n },\n 'ignoring stt event',\n );\n return;\n }\n\n switch (ev.type) {\n case SpeechEventType.FINAL_TRANSCRIPT:\n this.hooks.onFinalTranscript(ev);\n const transcript = ev.alternatives?.[0]?.text;\n const confidence = ev.alternatives?.[0]?.confidence ?? 0;\n this.lastLanguage = ev.alternatives?.[0]?.language;\n\n if (!transcript) {\n // stt final transcript received but no transcript\n return;\n }\n\n this.logger.debug(\n {\n user_transcript: transcript,\n language: this.lastLanguage,\n },\n 'received user transcript',\n );\n\n this.lastFinalTranscriptTime = Date.now();\n this.audioTranscript += ` ${transcript}`;\n this.audioTranscript = this.audioTranscript.trimStart();\n this.finalTranscriptConfidence.push(confidence);\n const transcriptChanged = this.audioTranscript !== this.audioPreflightTranscript;\n this.audioInterimTranscript = '';\n this.audioPreflightTranscript = '';\n\n if (!this.vad || this.lastSpeakingTime === undefined) {\n // vad disabled, use stt timestamp\n // TODO: this would screw up transcription latency metrics\n // but we'll live with it for now.\n // the correct way is to ensure STT fires SpeechEventType.END_OF_SPEECH\n // and using that timestamp for lastSpeakingTime\n this.lastSpeakingTime = Date.now();\n }\n\n if (this.vadBaseTurnDetection || this.userTurnCommitted) {\n if (transcriptChanged) {\n this.logger.debug(\n { transcript: this.audioTranscript },\n 'triggering preemptive generation (FINAL_TRANSCRIPT)',\n );\n this.hooks.onPreemptiveGeneration({\n newTranscript: this.audioTranscript,\n transcriptConfidence:\n this.finalTranscriptConfidence.length > 0\n ? this.finalTranscriptConfidence.reduce((a, b) => a + b, 0) /\n this.finalTranscriptConfidence.length\n : 0,\n });\n }\n\n if (!this.speaking) {\n const chatCtx = this.hooks.retrieveChatCtx();\n this.logger.debug('running EOU detection on stt FINAL_TRANSCRIPT');\n this.runEOUDetection(chatCtx);\n }\n }\n break;\n case SpeechEventType.PREFLIGHT_TRANSCRIPT:\n this.hooks.onInterimTranscript(ev);\n const preflightTranscript = ev.alternatives?.[0]?.text ?? '';\n const preflightConfidence = ev.alternatives?.[0]?.confidence ?? 0;\n const preflightLanguage = ev.alternatives?.[0]?.language;\n\n const MIN_LANGUAGE_DETECTION_LENGTH = 5;\n if (\n !this.lastLanguage ||\n (preflightLanguage && preflightTranscript.length > MIN_LANGUAGE_DETECTION_LENGTH)\n ) {\n this.lastLanguage = preflightLanguage;\n }\n\n if (!preflightTranscript) {\n return;\n }\n\n this.logger.debug(\n {\n user_transcript: preflightTranscript,\n language: this.lastLanguage,\n },\n 'received user preflight transcript',\n );\n\n // still need to increment it as it's used for turn detection,\n this.lastFinalTranscriptTime = Date.now();\n // preflight transcript includes all pre-committed transcripts (including final transcript from the previous STT run)\n this.audioPreflightTranscript =\n `${this.audioTranscript} ${preflightTranscript}`.trimStart();\n this.audioInterimTranscript = preflightTranscript;\n\n if (!this.vad || this.lastSpeakingTime === undefined) {\n // vad disabled, use stt timestamp\n this.lastSpeakingTime = Date.now();\n }\n\n if (this.turnDetectionMode !== 'manual' || this.userTurnCommitted) {\n const confidenceVals = [...this.finalTranscriptConfidence, preflightConfidence];\n this.logger.debug(\n {\n transcript:\n this.audioPreflightTranscript.length > 100\n ? this.audioPreflightTranscript.slice(0, 100) + '...'\n : this.audioPreflightTranscript,\n },\n 'triggering preemptive generation (PREFLIGHT_TRANSCRIPT)',\n );\n this.hooks.onPreemptiveGeneration({\n newTranscript: this.audioPreflightTranscript,\n transcriptConfidence:\n confidenceVals.length > 0\n ? confidenceVals.reduce((a, b) => a + b, 0) / confidenceVals.length\n : 0,\n });\n }\n break;\n case SpeechEventType.INTERIM_TRANSCRIPT:\n this.logger.debug({ transcript: ev.alternatives?.[0]?.text }, 'interim transcript');\n this.hooks.onInterimTranscript(ev);\n this.audioInterimTranscript = ev.alternatives?.[0]?.text ?? '';\n break;\n case SpeechEventType.START_OF_SPEECH:\n if (this.turnDetectionMode !== 'stt') break;\n this.hooks.onStartOfSpeech({\n type: VADEventType.START_OF_SPEECH,\n samplesIndex: 0,\n timestamp: Date.now(),\n speechDuration: 0,\n silenceDuration: 0,\n frames: [],\n probability: 0,\n inferenceDuration: 0,\n speaking: true,\n rawAccumulatedSilence: 0,\n rawAccumulatedSpeech: 0,\n });\n this.speaking = true;\n this.lastSpeakingTime = Date.now();\n\n this.bounceEOUTask?.cancel();\n break;\n case SpeechEventType.END_OF_SPEECH:\n if (this.turnDetectionMode !== 'stt') break;\n this.hooks.onEndOfSpeech({\n type: VADEventType.END_OF_SPEECH,\n samplesIndex: 0,\n timestamp: Date.now(),\n speechDuration: 0,\n silenceDuration: 0,\n frames: [],\n probability: 0,\n inferenceDuration: 0,\n speaking: false,\n rawAccumulatedSilence: 0,\n rawAccumulatedSpeech: 0,\n });\n this.speaking = false;\n this.userTurnCommitted = true;\n this.lastSpeakingTime = Date.now();\n\n if (!this.speaking) {\n const chatCtx = this.hooks.retrieveChatCtx();\n this.logger.debug('running EOU detection on stt END_OF_SPEECH');\n this.runEOUDetection(chatCtx);\n }\n }\n }\n\n private runEOUDetection(chatCtx: ChatContext) {\n this.logger.debug(\n {\n stt: this.stt,\n audioTranscript: this.audioTranscript,\n turnDetectionMode: this.turnDetectionMode,\n },\n 'running EOU detection',\n );\n\n if (this.stt && !this.audioTranscript && this.turnDetectionMode !== 'manual') {\n // stt enabled but no transcript yet\n this.logger.debug('skipping EOU detection');\n return;\n }\n\n chatCtx = chatCtx.copy();\n chatCtx.addMessage({ role: 'user', content: this.audioTranscript });\n\n const turnDetector =\n // disable EOU model if manual turn detection enabled\n this.audioTranscript && this.turnDetectionMode !== 'manual' ? this.turnDetector : undefined;\n\n const bounceEOUTask =\n (\n lastSpeakingTime: number | undefined,\n lastFinalTranscriptTime: number,\n speechStartTime: number | undefined,\n ) =>\n async (controller: AbortController) => {\n let endpointingDelay = this.minEndpointingDelay;\n\n if (turnDetector) {\n await tracer.startActiveSpan(\n async (span) => {\n this.logger.debug('Running turn detector model');\n\n let endOfTurnProbability = 0.0;\n let unlikelyThreshold: number | undefined;\n\n if (!(await turnDetector.supportsLanguage(this.lastLanguage))) {\n this.logger.debug(`Turn detector does not support language ${this.lastLanguage}`);\n } else {\n try {\n endOfTurnProbability = await turnDetector.predictEndOfTurn(chatCtx);\n unlikelyThreshold = await turnDetector.unlikelyThreshold(this.lastLanguage);\n\n this.logger.debug(\n { endOfTurnProbability, unlikelyThreshold, language: this.lastLanguage },\n 'end of turn probability',\n );\n\n if (unlikelyThreshold && endOfTurnProbability < unlikelyThreshold) {\n endpointingDelay = this.maxEndpointingDelay;\n }\n } catch (error) {\n this.logger.error(error, 'Error predicting end of turn');\n }\n }\n\n span.setAttribute(\n traceTypes.ATTR_CHAT_CTX,\n JSON.stringify(chatCtx.toJSON({ excludeTimestamp: false })),\n );\n span.setAttribute(traceTypes.ATTR_EOU_PROBABILITY, endOfTurnProbability);\n span.setAttribute(traceTypes.ATTR_EOU_UNLIKELY_THRESHOLD, unlikelyThreshold ?? 0);\n span.setAttribute(traceTypes.ATTR_EOU_DELAY, endpointingDelay);\n span.setAttribute(traceTypes.ATTR_EOU_LANGUAGE, this.lastLanguage ?? '');\n },\n {\n name: 'eou_detection',\n context: this.rootSpanContext,\n },\n );\n }\n\n let extraSleep = endpointingDelay;\n if (lastSpeakingTime !== undefined) {\n extraSleep += lastSpeakingTime - Date.now();\n }\n\n if (extraSleep > 0) {\n // add delay to see if there's a potential upcoming EOU task that cancels this one\n await delay(Math.max(extraSleep, 0), { signal: controller.signal });\n }\n\n this.logger.debug({ transcript: this.audioTranscript }, 'end of user turn');\n\n const confidenceAvg =\n this.finalTranscriptConfidence.length > 0\n ? this.finalTranscriptConfidence.reduce((a, b) => a + b, 0) /\n this.finalTranscriptConfidence.length\n : 0;\n\n let startedSpeakingAt: number | undefined;\n let stoppedSpeakingAt: number | undefined;\n let transcriptionDelay: number | undefined;\n let endOfUtteranceDelay: number | undefined;\n\n // sometimes, we can't calculate the metrics because VAD was unreliable.\n // in this case, we just ignore the calculation, it's better than providing likely wrong values\n if (\n lastFinalTranscriptTime !== 0 &&\n lastSpeakingTime !== undefined &&\n speechStartTime !== undefined\n ) {\n startedSpeakingAt = speechStartTime;\n stoppedSpeakingAt = lastSpeakingTime;\n transcriptionDelay = Math.max(lastFinalTranscriptTime - lastSpeakingTime, 0);\n endOfUtteranceDelay = Date.now() - lastSpeakingTime;\n }\n\n const committed = await this.hooks.onEndOfTurn({\n newTranscript: this.audioTranscript,\n transcriptConfidence: confidenceAvg,\n transcriptionDelay: transcriptionDelay ?? 0,\n endOfUtteranceDelay: endOfUtteranceDelay ?? 0,\n startedSpeakingAt,\n stoppedSpeakingAt,\n });\n\n if (committed) {\n this._endUserTurnSpan({\n transcript: this.audioTranscript,\n confidence: confidenceAvg,\n transcriptionDelay: transcriptionDelay ?? 0,\n endOfUtteranceDelay: endOfUtteranceDelay ?? 0,\n });\n\n // clear the transcript if the user turn was committed\n this.audioTranscript = '';\n this.finalTranscriptConfidence = [];\n this.lastSpeakingTime = undefined;\n this.lastFinalTranscriptTime = 0;\n this.speechStartTime = undefined;\n }\n\n this.userTurnCommitted = false;\n };\n\n // cancel any existing EOU task\n this.bounceEOUTask?.cancel();\n // copy the values before awaiting (the values can change)\n this.bounceEOUTask = Task.from(\n bounceEOUTask(this.lastSpeakingTime, this.lastFinalTranscriptTime, this.speechStartTime),\n );\n\n this.bounceEOUTask.result\n .then(() => {\n this.logger.debug('EOU detection task completed');\n })\n .catch((err: unknown) => {\n if (err instanceof Error && err.message.includes('This operation was aborted')) {\n // ignore aborted errors\n return;\n }\n this.logger.error(err, 'Error in EOU detection task:');\n });\n }\n\n private async createSttTask(stt: STTNode | undefined, signal: AbortSignal) {\n if (!stt) return;\n\n this.logger.debug('createSttTask: create stt stream from stt node');\n\n const sttStream = await stt(this.sttInputStream, {});\n\n if (signal.aborted || sttStream === null) return;\n\n if (sttStream instanceof ReadableStream) {\n const reader = sttStream.getReader();\n\n signal.addEventListener('abort', async () => {\n try {\n reader.releaseLock();\n await sttStream?.cancel();\n } catch (e) {\n this.logger.debug('createSttTask: error during abort handler:', e);\n }\n });\n\n try {\n while (true) {\n if (signal.aborted) break;\n\n const { done, value: ev } = await reader.read();\n if (done) break;\n\n if (typeof ev === 'string') {\n throw new Error('STT node must yield SpeechEvent');\n } else {\n await this.onSTTEvent(ev);\n }\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) {\n return;\n }\n this.logger.error({ error: e }, 'createSttTask: error reading sttStream');\n } finally {\n reader.releaseLock();\n try {\n await sttStream.cancel();\n } catch (e) {\n this.logger.debug(\n 'createSttTask: error cancelling sttStream (may already be cancelled):',\n e,\n );\n }\n }\n }\n }\n\n private async createVadTask(vad: VAD | undefined, signal: AbortSignal) {\n if (!vad) return;\n\n const vadStream = vad.stream();\n vadStream.updateInputStream(this.vadInputStream);\n\n const abortHandler = () => {\n vadStream.detachInputStream();\n vadStream.close();\n signal.removeEventListener('abort', abortHandler);\n };\n signal.addEventListener('abort', abortHandler);\n\n try {\n for await (const ev of vadStream) {\n if (signal.aborted) break;\n\n switch (ev.type) {\n case VADEventType.START_OF_SPEECH:\n this.logger.debug('VAD task: START_OF_SPEECH');\n this.hooks.onStartOfSpeech(ev);\n this.speaking = true;\n\n if (!this.userTurnSpan) {\n this.userTurnSpan = tracer.startSpan({\n name: 'user_turn',\n context: this.rootSpanContext,\n });\n }\n\n // Capture sample rate from the first VAD event if not already set\n if (ev.frames.length > 0 && ev.frames[0]) {\n this.sampleRate = ev.frames[0].sampleRate;\n }\n\n this.bounceEOUTask?.cancel();\n break;\n case VADEventType.INFERENCE_DONE:\n this.hooks.onVADInferenceDone(ev);\n // for metrics, get the \"earliest\" signal of speech as possible\n if (ev.rawAccumulatedSpeech > 0.0) {\n this.lastSpeakingTime = Date.now();\n\n if (this.speechStartTime === undefined) {\n this.speechStartTime = Date.now();\n }\n }\n break;\n case VADEventType.END_OF_SPEECH:\n this.logger.debug('VAD task: END_OF_SPEECH');\n this.hooks.onEndOfSpeech(ev);\n\n // when VAD fires END_OF_SPEECH, it already waited for the silence_duration\n this.speaking = false;\n\n if (\n this.vadBaseTurnDetection ||\n (this.turnDetectionMode === 'stt' && this.userTurnCommitted)\n ) {\n const chatCtx = this.hooks.retrieveChatCtx();\n this.runEOUDetection(chatCtx);\n }\n break;\n }\n }\n } catch (e) {\n this.logger.error(e, 'Error in VAD task');\n } finally {\n this.logger.debug('VAD task closed');\n }\n }\n\n setInputAudioStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputAudioStream() {\n this.deferredInputStream.detachSource();\n }\n\n clearUserTurn() {\n this.audioTranscript = '';\n this.audioInterimTranscript = '';\n this.audioPreflightTranscript = '';\n this.finalTranscriptConfidence = [];\n this.userTurnCommitted = false;\n\n this.sttTask?.cancelAndWait().finally(() => {\n this.sttTask = Task.from(({ signal }) => this.createSttTask(this.stt, signal));\n this.sttTask.result.catch((err) => {\n this.logger.error(`Error running STT task: ${err}`);\n });\n });\n }\n\n commitUserTurn(audioDetached: boolean) {\n const commitUserTurnTask =\n (delayDuration: number = 500) =>\n async (controller: AbortController) => {\n if (Date.now() - this.lastFinalTranscriptTime > delayDuration) {\n // flush the stt by pushing silence\n if (audioDetached && this.sampleRate !== undefined) {\n const numSamples = Math.floor(this.sampleRate * 0.5);\n const silence = new Int16Array(numSamples * 2);\n const silenceFrame = new AudioFrame(silence, this.sampleRate, 1, numSamples);\n this.silenceAudioWriter.write(silenceFrame);\n }\n\n // wait for the final transcript to be available\n await delay(delayDuration, { signal: controller.signal });\n }\n\n if (this.audioInterimTranscript) {\n // append interim transcript in case the final transcript is not ready\n this.audioTranscript = `${this.audioTranscript} ${this.audioInterimTranscript}`.trim();\n }\n this.audioInterimTranscript = '';\n\n const chatCtx = this.hooks.retrieveChatCtx();\n this.logger.debug('running EOU detection on commitUserTurn');\n this.runEOUDetection(chatCtx);\n this.userTurnCommitted = true;\n };\n\n // cancel any existing commit user turn task\n this.commitUserTurnTask?.cancel();\n this.commitUserTurnTask = Task.from(commitUserTurnTask());\n\n this.commitUserTurnTask.result\n .then(() => {\n this.logger.debug('User turn committed');\n })\n .catch((err: unknown) => {\n this.logger.error(err, 'Error in user turn commit task:');\n });\n }\n\n async close() {\n this.detachInputAudioStream();\n this.silenceAudioWriter.releaseLock();\n await this.commitUserTurnTask?.cancelAndWait();\n await this.sttTask?.cancelAndWait();\n await this.vadTask?.cancelAndWait();\n await this.bounceEOUTask?.cancelAndWait();\n }\n\n private _endUserTurnSpan({\n transcript,\n confidence,\n transcriptionDelay,\n endOfUtteranceDelay,\n }: {\n transcript: string;\n confidence: number;\n transcriptionDelay: number;\n endOfUtteranceDelay: number;\n }): void {\n if (this.userTurnSpan) {\n this.userTurnSpan.setAttributes({\n [traceTypes.ATTR_USER_TRANSCRIPT]: transcript,\n [traceTypes.ATTR_TRANSCRIPT_CONFIDENCE]: confidence,\n [traceTypes.ATTR_TRANSCRIPTION_DELAY]: transcriptionDelay,\n [traceTypes.ATTR_END_OF_TURN_DELAY]: endOfUtteranceDelay,\n });\n this.userTurnSpan.end();\n this.userTurnSpan = undefined;\n }\n }\n\n private get vadBaseTurnDetection() {\n return ['vad', undefined].includes(this.turnDetectionMode);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/voice/audio_recognition.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { AudioFrame } from '@livekit/rtc-node';\nimport type { Context, Span } from '@opentelemetry/api';\nimport type { WritableStreamDefaultWriter } from 'node:stream/web';\nimport { ReadableStream } from 'node:stream/web';\nimport type { AdaptiveInterruptionDetector } from '../inference/interruption/AdaptiveInterruptionDetector.js';\nimport {\n InterruptionStreamBase,\n InterruptionStreamSentinel,\n} from '../inference/interruption/InterruptionStream.js';\nimport type { InterruptionEvent } from '../inference/interruption/interruption.js';\nimport { type ChatContext } from '../llm/chat_context.js';\nimport { log } from '../log.js';\nimport { DeferredReadableStream, isStreamReaderReleaseError } from '../stream/deferred_stream.js';\nimport { IdentityTransform } from '../stream/identity_transform.js';\nimport { mergeReadableStreams } from '../stream/merge_readable_streams.js';\nimport { type SpeechEvent, SpeechEventType } from '../stt/stt.js';\nimport { traceTypes, tracer } from '../telemetry/index.js';\nimport { Task, delay } from '../utils.js';\nimport { type VAD, type VADEvent, VADEventType } from '../vad.js';\nimport type { TurnDetectionMode } from './agent_session.js';\nimport type { STTNode } from './io.js';\n\nexport interface EndOfTurnInfo {\n newTranscript: string;\n transcriptConfidence: number;\n transcriptionDelay: number;\n endOfUtteranceDelay: number;\n startedSpeakingAt: number | undefined;\n stoppedSpeakingAt: number | undefined;\n}\n\nexport interface PreemptiveGenerationInfo {\n newTranscript: string;\n transcriptConfidence: number;\n}\n\nexport interface RecognitionHooks {\n onStartOfSpeech: (ev: VADEvent) => void;\n onVADInferenceDone: (ev: VADEvent) => void;\n onEndOfSpeech: (ev: VADEvent) => void;\n onInterimTranscript: (ev: SpeechEvent) => void;\n onFinalTranscript: (ev: SpeechEvent) => void;\n onEndOfTurn: (info: EndOfTurnInfo) => Promise<boolean>;\n onPreemptiveGeneration: (info: PreemptiveGenerationInfo) => void;\n onInterruption: (ev: InterruptionEvent) => void;\n\n retrieveChatCtx: () => ChatContext;\n}\n\nexport interface _TurnDetector {\n unlikelyThreshold: (language?: string) => Promise<number | undefined>;\n supportsLanguage: (language?: string) => Promise<boolean>;\n predictEndOfTurn(chatCtx: ChatContext): Promise<number>;\n}\n\nexport interface AudioRecognitionOptions {\n recognitionHooks: RecognitionHooks;\n stt?: STTNode;\n vad?: VAD;\n interruptionDetector?: AdaptiveInterruptionDetector;\n turnDetector?: _TurnDetector;\n turnDetectionMode?: Exclude<TurnDetectionMode, _TurnDetector>;\n minEndpointingDelay: number;\n maxEndpointingDelay: number;\n rootSpanContext?: Context;\n}\n\nexport class AudioRecognition {\n private hooks: RecognitionHooks;\n private stt?: STTNode;\n private vad?: VAD;\n private turnDetector?: _TurnDetector;\n private turnDetectionMode?: Exclude<TurnDetectionMode, _TurnDetector>;\n private minEndpointingDelay: number;\n private maxEndpointingDelay: number;\n private lastLanguage?: string;\n private rootSpanContext?: Context;\n\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n private logger = log();\n private lastFinalTranscriptTime = 0;\n private audioTranscript = '';\n private audioInterimTranscript = '';\n private audioPreflightTranscript = '';\n private finalTranscriptConfidence: number[] = [];\n private lastSpeakingTime: number | undefined;\n private speechStartTime: number | undefined;\n private userTurnCommitted = false;\n private speaking = false;\n private sampleRate?: number;\n\n private userTurnSpan?: Span;\n\n private vadInputStream: ReadableStream<AudioFrame>;\n private sttInputStream: ReadableStream<AudioFrame>;\n private interruptionInputStream: ReadableStream<AudioFrame>;\n private silenceAudioTransform = new IdentityTransform<AudioFrame>();\n private silenceAudioWriter: WritableStreamDefaultWriter<AudioFrame>;\n\n // all cancellable tasks\n private bounceEOUTask?: Task<void>;\n private commitUserTurnTask?: Task<void>;\n private vadTask?: Task<void>;\n private sttTask?: Task<void>;\n private interruptionTask?: Task<void>;\n\n // interruption detection\n private interruptionDetector?: AdaptiveInterruptionDetector;\n private interruptionStream?: InterruptionStreamBase;\n private interruptionEnabled = false;\n private agentSpeaking = false;\n\n constructor(opts: AudioRecognitionOptions) {\n this.hooks = opts.recognitionHooks;\n this.stt = opts.stt;\n this.vad = opts.vad;\n this.interruptionDetector = opts.interruptionDetector;\n this.turnDetector = opts.turnDetector;\n this.turnDetectionMode = opts.turnDetectionMode;\n this.minEndpointingDelay = opts.minEndpointingDelay;\n this.maxEndpointingDelay = opts.maxEndpointingDelay;\n this.lastLanguage = undefined;\n this.rootSpanContext = opts.rootSpanContext;\n\n // Interruption detection is only enabled if both detector and VAD are provided\n this.interruptionEnabled = this.interruptionDetector !== undefined && this.vad !== undefined;\n\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n const [vadInputStream, rest] = this.deferredInputStream.stream.tee();\n const [sttInputStream, interruptionInputStream] = rest.tee();\n this.vadInputStream = vadInputStream;\n this.sttInputStream = mergeReadableStreams(sttInputStream, this.silenceAudioTransform.readable);\n this.interruptionInputStream = interruptionInputStream;\n this.silenceAudioWriter = this.silenceAudioTransform.writable.getWriter();\n }\n\n /**\n * Current transcript of the user's speech, including interim transcript if available.\n */\n get currentTranscript(): string {\n if (this.audioInterimTranscript) {\n return `${this.audioTranscript} ${this.audioInterimTranscript}`.trim();\n }\n return this.audioTranscript;\n }\n\n async start() {\n this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal));\n this.vadTask.result.catch((err) => {\n this.logger.error(`Error running VAD task: ${err}`);\n });\n\n this.sttTask = Task.from(({ signal }) => this.createSttTask(this.stt, signal));\n this.sttTask.result.catch((err) => {\n this.logger.error(`Error running STT task: ${err}`);\n });\n\n if (this.interruptionEnabled && this.interruptionDetector) {\n this.interruptionTask = Task.from(({ signal }) =>\n this.createInterruptionTask(this.interruptionDetector!, signal),\n );\n this.interruptionTask.result.catch((err) => {\n this.logger.error(`Error running interruption task: ${err}`);\n });\n }\n }\n\n private async onSTTEvent(ev: SpeechEvent) {\n if (\n this.turnDetectionMode === 'manual' &&\n this.userTurnCommitted &&\n (this.bounceEOUTask === undefined ||\n this.bounceEOUTask.done ||\n ev.type == SpeechEventType.INTERIM_TRANSCRIPT)\n ) {\n // ignore stt event if user turn already committed and EOU task is done\n // or it's an interim transcript\n this.logger.debug(\n {\n userTurnCommitted: this.userTurnCommitted,\n eouTaskDone: this.bounceEOUTask?.done,\n evType: ev.type,\n turnDetectionMode: this.turnDetectionMode,\n },\n 'ignoring stt event',\n );\n return;\n }\n\n switch (ev.type) {\n case SpeechEventType.FINAL_TRANSCRIPT:\n this.hooks.onFinalTranscript(ev);\n const transcript = ev.alternatives?.[0]?.text;\n const confidence = ev.alternatives?.[0]?.confidence ?? 0;\n this.lastLanguage = ev.alternatives?.[0]?.language;\n\n if (!transcript) {\n // stt final transcript received but no transcript\n return;\n }\n\n this.logger.debug(\n {\n user_transcript: transcript,\n language: this.lastLanguage,\n },\n 'received user transcript',\n );\n\n this.lastFinalTranscriptTime = Date.now();\n this.audioTranscript += ` ${transcript}`;\n this.audioTranscript = this.audioTranscript.trimStart();\n this.finalTranscriptConfidence.push(confidence);\n const transcriptChanged = this.audioTranscript !== this.audioPreflightTranscript;\n this.audioInterimTranscript = '';\n this.audioPreflightTranscript = '';\n\n if (!this.vad || this.lastSpeakingTime === undefined) {\n // vad disabled, use stt timestamp\n // TODO: this would screw up transcription latency metrics\n // but we'll live with it for now.\n // the correct way is to ensure STT fires SpeechEventType.END_OF_SPEECH\n // and using that timestamp for lastSpeakingTime\n this.lastSpeakingTime = Date.now();\n }\n\n if (this.vadBaseTurnDetection || this.userTurnCommitted) {\n if (transcriptChanged) {\n this.logger.debug(\n { transcript: this.audioTranscript },\n 'triggering preemptive generation (FINAL_TRANSCRIPT)',\n );\n this.hooks.onPreemptiveGeneration({\n newTranscript: this.audioTranscript,\n transcriptConfidence:\n this.finalTranscriptConfidence.length > 0\n ? this.finalTranscriptConfidence.reduce((a, b) => a + b, 0) /\n this.finalTranscriptConfidence.length\n : 0,\n });\n }\n\n if (!this.speaking) {\n const chatCtx = this.hooks.retrieveChatCtx();\n this.logger.debug('running EOU detection on stt FINAL_TRANSCRIPT');\n this.runEOUDetection(chatCtx);\n }\n }\n break;\n case SpeechEventType.PREFLIGHT_TRANSCRIPT:\n this.hooks.onInterimTranscript(ev);\n const preflightTranscript = ev.alternatives?.[0]?.text ?? '';\n const preflightConfidence = ev.alternatives?.[0]?.confidence ?? 0;\n const preflightLanguage = ev.alternatives?.[0]?.language;\n\n const MIN_LANGUAGE_DETECTION_LENGTH = 5;\n if (\n !this.lastLanguage ||\n (preflightLanguage && preflightTranscript.length > MIN_LANGUAGE_DETECTION_LENGTH)\n ) {\n this.lastLanguage = preflightLanguage;\n }\n\n if (!preflightTranscript) {\n return;\n }\n\n this.logger.debug(\n {\n user_transcript: preflightTranscript,\n language: this.lastLanguage,\n },\n 'received user preflight transcript',\n );\n\n // still need to increment it as it's used for turn detection,\n this.lastFinalTranscriptTime = Date.now();\n // preflight transcript includes all pre-committed transcripts (including final transcript from the previous STT run)\n this.audioPreflightTranscript =\n `${this.audioTranscript} ${preflightTranscript}`.trimStart();\n this.audioInterimTranscript = preflightTranscript;\n\n if (!this.vad || this.lastSpeakingTime === undefined) {\n // vad disabled, use stt timestamp\n this.lastSpeakingTime = Date.now();\n }\n\n if (this.turnDetectionMode !== 'manual' || this.userTurnCommitted) {\n const confidenceVals = [...this.finalTranscriptConfidence, preflightConfidence];\n this.logger.debug(\n {\n transcript:\n this.audioPreflightTranscript.length > 100\n ? this.audioPreflightTranscript.slice(0, 100) + '...'\n : this.audioPreflightTranscript,\n },\n 'triggering preemptive generation (PREFLIGHT_TRANSCRIPT)',\n );\n this.hooks.onPreemptiveGeneration({\n newTranscript: this.audioPreflightTranscript,\n transcriptConfidence:\n confidenceVals.length > 0\n ? confidenceVals.reduce((a, b) => a + b, 0) / confidenceVals.length\n : 0,\n });\n }\n break;\n case SpeechEventType.INTERIM_TRANSCRIPT:\n this.logger.debug({ transcript: ev.alternatives?.[0]?.text }, 'interim transcript');\n this.hooks.onInterimTranscript(ev);\n this.audioInterimTranscript = ev.alternatives?.[0]?.text ?? '';\n break;\n case SpeechEventType.START_OF_SPEECH:\n if (this.turnDetectionMode !== 'stt') break;\n this.hooks.onStartOfSpeech({\n type: VADEventType.START_OF_SPEECH,\n samplesIndex: 0,\n timestamp: Date.now(),\n speechDuration: 0,\n silenceDuration: 0,\n frames: [],\n probability: 0,\n inferenceDuration: 0,\n speaking: true,\n rawAccumulatedSilence: 0,\n rawAccumulatedSpeech: 0,\n });\n this.speaking = true;\n this.lastSpeakingTime = Date.now();\n\n this.bounceEOUTask?.cancel();\n break;\n case SpeechEventType.END_OF_SPEECH:\n if (this.turnDetectionMode !== 'stt') break;\n this.hooks.onEndOfSpeech({\n type: VADEventType.END_OF_SPEECH,\n samplesIndex: 0,\n timestamp: Date.now(),\n speechDuration: 0,\n silenceDuration: 0,\n frames: [],\n probability: 0,\n inferenceDuration: 0,\n speaking: false,\n rawAccumulatedSilence: 0,\n rawAccumulatedSpeech: 0,\n });\n this.speaking = false;\n this.userTurnCommitted = true;\n this.lastSpeakingTime = Date.now();\n\n if (!this.speaking) {\n const chatCtx = this.hooks.retrieveChatCtx();\n this.logger.debug('running EOU detection on stt END_OF_SPEECH');\n this.runEOUDetection(chatCtx);\n }\n }\n }\n\n private runEOUDetection(chatCtx: ChatContext) {\n this.logger.debug(\n {\n stt: this.stt,\n audioTranscript: this.audioTranscript,\n turnDetectionMode: this.turnDetectionMode,\n },\n 'running EOU detection',\n );\n\n if (this.stt && !this.audioTranscript && this.turnDetectionMode !== 'manual') {\n // stt enabled but no transcript yet\n this.logger.debug('skipping EOU detection');\n return;\n }\n\n chatCtx = chatCtx.copy();\n chatCtx.addMessage({ role: 'user', content: this.audioTranscript });\n\n const turnDetector =\n // disable EOU model if manual turn detection enabled\n this.audioTranscript && this.turnDetectionMode !== 'manual' ? this.turnDetector : undefined;\n\n const bounceEOUTask =\n (\n lastSpeakingTime: number | undefined,\n lastFinalTranscriptTime: number,\n speechStartTime: number | undefined,\n ) =>\n async (controller: AbortController) => {\n let endpointingDelay = this.minEndpointingDelay;\n\n if (turnDetector) {\n await tracer.startActiveSpan(\n async (span) => {\n this.logger.debug('Running turn detector model');\n\n let endOfTurnProbability = 0.0;\n let unlikelyThreshold: number | undefined;\n\n if (!(await turnDetector.supportsLanguage(this.lastLanguage))) {\n this.logger.debug(`Turn detector does not support language ${this.lastLanguage}`);\n } else {\n try {\n endOfTurnProbability = await turnDetector.predictEndOfTurn(chatCtx);\n unlikelyThreshold = await turnDetector.unlikelyThreshold(this.lastLanguage);\n\n this.logger.debug(\n { endOfTurnProbability, unlikelyThreshold, language: this.lastLanguage },\n 'end of turn probability',\n );\n\n if (unlikelyThreshold && endOfTurnProbability < unlikelyThreshold) {\n endpointingDelay = this.maxEndpointingDelay;\n }\n } catch (error) {\n this.logger.error(error, 'Error predicting end of turn');\n }\n }\n\n span.setAttribute(\n traceTypes.ATTR_CHAT_CTX,\n JSON.stringify(chatCtx.toJSON({ excludeTimestamp: false })),\n );\n span.setAttribute(traceTypes.ATTR_EOU_PROBABILITY, endOfTurnProbability);\n span.setAttribute(traceTypes.ATTR_EOU_UNLIKELY_THRESHOLD, unlikelyThreshold ?? 0);\n span.setAttribute(traceTypes.ATTR_EOU_DELAY, endpointingDelay);\n span.setAttribute(traceTypes.ATTR_EOU_LANGUAGE, this.lastLanguage ?? '');\n },\n {\n name: 'eou_detection',\n context: this.rootSpanContext,\n },\n );\n }\n\n let extraSleep = endpointingDelay;\n if (lastSpeakingTime !== undefined) {\n extraSleep += lastSpeakingTime - Date.now();\n }\n\n if (extraSleep > 0) {\n // add delay to see if there's a potential upcoming EOU task that cancels this one\n await delay(Math.max(extraSleep, 0), { signal: controller.signal });\n }\n\n this.logger.debug({ transcript: this.audioTranscript }, 'end of user turn');\n\n const confidenceAvg =\n this.finalTranscriptConfidence.length > 0\n ? this.finalTranscriptConfidence.reduce((a, b) => a + b, 0) /\n this.finalTranscriptConfidence.length\n : 0;\n\n let startedSpeakingAt: number | undefined;\n let stoppedSpeakingAt: number | undefined;\n let transcriptionDelay: number | undefined;\n let endOfUtteranceDelay: number | undefined;\n\n // sometimes, we can't calculate the metrics because VAD was unreliable.\n // in this case, we just ignore the calculation, it's better than providing likely wrong values\n if (\n lastFinalTranscriptTime !== 0 &&\n lastSpeakingTime !== undefined &&\n speechStartTime !== undefined\n ) {\n startedSpeakingAt = speechStartTime;\n stoppedSpeakingAt = lastSpeakingTime;\n transcriptionDelay = Math.max(lastFinalTranscriptTime - lastSpeakingTime, 0);\n endOfUtteranceDelay = Date.now() - lastSpeakingTime;\n }\n\n const committed = await this.hooks.onEndOfTurn({\n newTranscript: this.audioTranscript,\n transcriptConfidence: confidenceAvg,\n transcriptionDelay: transcriptionDelay ?? 0,\n endOfUtteranceDelay: endOfUtteranceDelay ?? 0,\n startedSpeakingAt,\n stoppedSpeakingAt,\n });\n\n if (committed) {\n this._endUserTurnSpan({\n transcript: this.audioTranscript,\n confidence: confidenceAvg,\n transcriptionDelay: transcriptionDelay ?? 0,\n endOfUtteranceDelay: endOfUtteranceDelay ?? 0,\n });\n\n // clear the transcript if the user turn was committed\n this.audioTranscript = '';\n this.finalTranscriptConfidence = [];\n this.lastSpeakingTime = undefined;\n this.lastFinalTranscriptTime = 0;\n this.speechStartTime = undefined;\n }\n\n this.userTurnCommitted = false;\n };\n\n // cancel any existing EOU task\n this.bounceEOUTask?.cancel();\n // copy the values before awaiting (the values can change)\n this.bounceEOUTask = Task.from(\n bounceEOUTask(this.lastSpeakingTime, this.lastFinalTranscriptTime, this.speechStartTime),\n );\n\n this.bounceEOUTask.result\n .then(() => {\n this.logger.debug('EOU detection task completed');\n })\n .catch((err: unknown) => {\n if (err instanceof Error && err.message.includes('This operation was aborted')) {\n // ignore aborted errors\n return;\n }\n this.logger.error(err, 'Error in EOU detection task:');\n });\n }\n\n private async createSttTask(stt: STTNode | undefined, signal: AbortSignal) {\n if (!stt) return;\n\n this.logger.debug('createSttTask: create stt stream from stt node');\n\n const sttStream = await stt(this.sttInputStream, {});\n\n if (signal.aborted || sttStream === null) return;\n\n if (sttStream instanceof ReadableStream) {\n const reader = sttStream.getReader();\n\n signal.addEventListener('abort', async () => {\n try {\n reader.releaseLock();\n await sttStream?.cancel();\n } catch (e) {\n this.logger.debug('createSttTask: error during abort handler:', e);\n }\n });\n\n try {\n while (true) {\n if (signal.aborted) break;\n\n const { done, value: ev } = await reader.read();\n if (done) break;\n\n if (typeof ev === 'string') {\n throw new Error('STT node must yield SpeechEvent');\n } else {\n await this.onSTTEvent(ev);\n }\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) {\n return;\n }\n this.logger.error({ error: e }, 'createSttTask: error reading sttStream');\n } finally {\n reader.releaseLock();\n try {\n await sttStream.cancel();\n } catch (e) {\n this.logger.debug(\n 'createSttTask: error cancelling sttStream (may already be cancelled):',\n e,\n );\n }\n }\n }\n }\n\n private async createVadTask(vad: VAD | undefined, signal: AbortSignal) {\n if (!vad) return;\n\n const vadStream = vad.stream();\n vadStream.updateInputStream(this.vadInputStream);\n\n const abortHandler = () => {\n vadStream.detachInputStream();\n vadStream.close();\n signal.removeEventListener('abort', abortHandler);\n };\n signal.addEventListener('abort', abortHandler);\n\n try {\n for await (const ev of vadStream) {\n if (signal.aborted) break;\n\n switch (ev.type) {\n case VADEventType.START_OF_SPEECH:\n this.logger.debug('VAD task: START_OF_SPEECH');\n this.hooks.onStartOfSpeech(ev);\n this.speaking = true;\n\n if (!this.userTurnSpan) {\n this.userTurnSpan = tracer.startSpan({\n name: 'user_turn',\n context: this.rootSpanContext,\n });\n }\n\n // Capture sample rate from the first VAD event if not already set\n if (ev.frames.length > 0 && ev.frames[0]) {\n this.sampleRate = ev.frames[0].sampleRate;\n }\n\n // If agent is speaking, user speech is overlap - trigger interruption detection\n if (this.agentSpeaking && this.interruptionEnabled) {\n this.onStartOfOverlapSpeech(ev.speechDuration, this.userTurnSpan);\n }\n\n this.bounceEOUTask?.cancel();\n break;\n case VADEventType.INFERENCE_DONE:\n this.hooks.onVADInferenceDone(ev);\n // for metrics, get the \"earliest\" signal of speech as possible\n if (ev.rawAccumulatedSpeech > 0.0) {\n this.lastSpeakingTime = Date.now();\n\n if (this.speechStartTime === undefined) {\n this.speechStartTime = Date.now();\n }\n }\n break;\n case VADEventType.END_OF_SPEECH:\n this.logger.debug('VAD task: END_OF_SPEECH');\n this.hooks.onEndOfSpeech(ev);\n\n // when VAD fires END_OF_SPEECH, it already waited for the silence_duration\n this.speaking = false;\n\n // If we were in overlap speech (agent speaking + user speaking), end it\n if (this.agentSpeaking && this.interruptionEnabled) {\n this.onEndOfOverlapSpeech();\n }\n\n if (\n this.vadBaseTurnDetection ||\n (this.turnDetectionMode === 'stt' && this.userTurnCommitted)\n ) {\n const chatCtx = this.hooks.retrieveChatCtx();\n this.runEOUDetection(chatCtx);\n }\n break;\n }\n }\n } catch (e) {\n this.logger.error(e, 'Error in VAD task');\n } finally {\n this.logger.debug('VAD task closed');\n }\n }\n\n private async createInterruptionTask(\n interruptionDetector: AdaptiveInterruptionDetector,\n signal: AbortSignal,\n ) {\n // Create the interruption stream from the detector\n this.interruptionStream = interruptionDetector.createStream();\n\n // Forward audio frames to the interruption stream\n const reader = this.interruptionInputStream.getReader();\n\n const forwardTask = (async () => {\n try {\n while (!signal.aborted) {\n const { done, value: frame } = await reader.read();\n if (done) break;\n await this.interruptionStream?.pushFrame(frame);\n }\n } catch (e) {\n if (!signal.aborted) {\n this.logger.error(e, 'Error forwarding audio to interruption stream');\n }\n } finally {\n reader.releaseLock();\n }\n })();\n\n // Read interruption events from the stream\n const eventStream = this.interruptionStream.stream;\n const eventReader = eventStream.getReader();\n\n const abortHandler = () => {\n eventReader.releaseLock();\n this.interruptionStream?.close();\n signal.removeEventListener('abort', abortHandler);\n };\n signal.addEventListener('abort', abortHandler);\n\n try {\n while (!signal.aborted) {\n const { done, value: ev } = await eventReader.read();\n if (done) break;\n\n this.logger.debug({ type: ev.type, probability: ev.probability }, 'Interruption event');\n this.hooks.onInterruption(ev);\n }\n } catch (e) {\n if (!signal.aborted) {\n this.logger.error(e, 'Error in interruption task');\n }\n } finally {\n this.logger.debug('Interruption task closed');\n await forwardTask;\n }\n }\n\n /**\n * Called when the agent starts speaking.\n * Enables interruption detection by sending the agent-speech-started sentinel.\n */\n onStartOfAgentSpeech(): void {\n this.agentSpeaking = true;\n\n if (!this.interruptionEnabled || !this.interruptionStream) {\n return;\n }\n\n this.interruptionStream.pushFrame(InterruptionStreamSentinel.speechStarted());\n }\n\n /**\n * Called when the agent stops speaking.\n * Disables interruption detection by sending the agent-speech-ended sentinel.\n */\n onEndOfAgentSpeech(): void {\n if (!this.interruptionEnabled || !this.interruptionStream) {\n this.agentSpeaking = false;\n return;\n }\n\n this.interruptionStream.pushFrame(InterruptionStreamSentinel.speechEnded());\n\n if (this.agentSpeaking) {\n // No interruption was detected, end the overlap inference (idempotent)\n this.onEndOfOverlapSpeech();\n }\n\n this.agentSpeaking = false;\n }\n\n /**\n * Called when user starts speaking while agent is speaking (overlap speech).\n * This triggers the interruption detection inference.\n */\n onStartOfOverlapSpeech(speechDuration: number, userSpeakingSpan?: Span): void {\n if (!this.interruptionEnabled || !this.interruptionStream) {\n return;\n }\n\n if (this.agentSpeaking && userSpeakingSpan) {\n this.interruptionStream.pushFrame(\n InterruptionStreamSentinel.overlapSpeechStarted(speechDuration, userSpeakingSpan),\n );\n }\n }\n\n /**\n * Called when user stops speaking during overlap.\n * This ends the interruption detection inference for this overlap period.\n */\n onEndOfOverlapSpeech(): void {\n if (!this.interruptionEnabled || !this.interruptionStream) {\n return;\n }\n\n this.interruptionStream.pushFrame(InterruptionStreamSentinel.overlapSpeechEnded());\n }\n\n setInputAudioStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputAudioStream() {\n this.deferredInputStream.detachSource();\n }\n\n clearUserTurn() {\n this.audioTranscript = '';\n this.audioInterimTranscript = '';\n this.audioPreflightTranscript = '';\n this.finalTranscriptConfidence = [];\n this.userTurnCommitted = false;\n\n this.sttTask?.cancelAndWait().finally(() => {\n this.sttTask = Task.from(({ signal }) => this.createSttTask(this.stt, signal));\n this.sttTask.result.catch((err) => {\n this.logger.error(`Error running STT task: ${err}`);\n });\n });\n }\n\n commitUserTurn(audioDetached: boolean) {\n const commitUserTurnTask =\n (delayDuration: number = 500) =>\n async (controller: AbortController) => {\n if (Date.now() - this.lastFinalTranscriptTime > delayDuration) {\n // flush the stt by pushing silence\n if (audioDetached && this.sampleRate !== undefined) {\n const numSamples = Math.floor(this.sampleRate * 0.5);\n const silence = new Int16Array(numSamples * 2);\n const silenceFrame = new AudioFrame(silence, this.sampleRate, 1, numSamples);\n this.silenceAudioWriter.write(silenceFrame);\n }\n\n // wait for the final transcript to be available\n await delay(delayDuration, { signal: controller.signal });\n }\n\n if (this.audioInterimTranscript) {\n // append interim transcript in case the final transcript is not ready\n this.audioTranscript = `${this.audioTranscript} ${this.audioInterimTranscript}`.trim();\n }\n this.audioInterimTranscript = '';\n\n const chatCtx = this.hooks.retrieveChatCtx();\n this.logger.debug('running EOU detection on commitUserTurn');\n this.runEOUDetection(chatCtx);\n this.userTurnCommitted = true;\n };\n\n // cancel any existing commit user turn task\n this.commitUserTurnTask?.cancel();\n this.commitUserTurnTask = Task.from(commitUserTurnTask());\n\n this.commitUserTurnTask.result\n .then(() => {\n this.logger.debug('User turn committed');\n })\n .catch((err: unknown) => {\n this.logger.error(err, 'Error in user turn commit task:');\n });\n }\n\n async close() {\n this.detachInputAudioStream();\n this.silenceAudioWriter.releaseLock();\n await this.commitUserTurnTask?.cancelAndWait();\n await this.sttTask?.cancelAndWait();\n await this.vadTask?.cancelAndWait();\n await this.bounceEOUTask?.cancelAndWait();\n await this.interruptionTask?.cancelAndWait();\n await this.interruptionStream?.close();\n }\n\n private _endUserTurnSpan({\n transcript,\n confidence,\n transcriptionDelay,\n endOfUtteranceDelay,\n }: {\n transcript: string;\n confidence: number;\n transcriptionDelay: number;\n endOfUtteranceDelay: number;\n }): void {\n if (this.userTurnSpan) {\n this.userTurnSpan.setAttributes({\n [traceTypes.ATTR_USER_TRANSCRIPT]: transcript,\n [traceTypes.ATTR_TRANSCRIPT_CONFIDENCE]: confidence,\n [traceTypes.ATTR_TRANSCRIPTION_DELAY]: transcriptionDelay,\n [traceTypes.ATTR_END_OF_TURN_DELAY]: endOfUtteranceDelay,\n });\n this.userTurnSpan.end();\n this.userTurnSpan = undefined;\n }\n }\n\n private get vadBaseTurnDetection() {\n return ['vad', undefined].includes(this.turnDetectionMode);\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/voice/background_audio.ts","../../../node_modules/.pnpm/tsup@8.4.0_@microsoft+api-extractor@7.43.7_@types+node@22.15.30__postcss@8.4.38_tsx@4.20.4_typescript@5.4.5/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n AudioFrame,\n AudioMixer,\n AudioSource,\n LocalAudioTrack,\n type LocalTrackPublication,\n type Room,\n TrackPublishOptions,\n} from '@livekit/rtc-node';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { audioFramesFromFile, loopAudioFramesFromFile } from '../audio.js';\nimport { log } from '../log.js';\nimport { Future, Task, cancelAndWait } from '../utils.js';\nimport type { AgentSession } from './agent_session.js';\nimport { AgentSessionEventTypes, type AgentStateChangedEvent } from './events.js';\n\nconst TASK_TIMEOUT_MS = 500;\n\nexport enum BuiltinAudioClip {\n OFFICE_AMBIENCE = 'office-ambience.ogg',\n KEYBOARD_TYPING = 'keyboard-typing.ogg',\n KEYBOARD_TYPING2 = 'keyboard-typing2.ogg',\n}\n\nexport function isBuiltinAudioClip(\n source: AudioSourceType | AudioConfig | AudioConfig[],\n): source is BuiltinAudioClip {\n return (\n typeof source === 'string' &&\n Object.values(BuiltinAudioClip).includes(source as BuiltinAudioClip)\n );\n}\n\nexport function getBuiltinAudioPath(clip: BuiltinAudioClip): string {\n const resourcesPath = join(dirname(fileURLToPath(import.meta.url)), '../../resources');\n return join(resourcesPath, clip);\n}\n\nexport type AudioSourceType = string | BuiltinAudioClip | AsyncIterable<AudioFrame>;\n\nexport interface AudioConfig {\n source: AudioSourceType;\n volume?: number;\n probability?: number;\n}\n\nexport interface BackgroundAudioPlayerOptions {\n /**\n * Ambient sound to play continuously in the background.\n * Can be a file path, BuiltinAudioClip, or AudioConfig.\n * File paths will be looped automatically.\n */\n ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Sound to play when the agent is thinking.\n * Plays when agent state changes to 'thinking' and stops when it changes to other states.\n */\n thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Stream timeout in milliseconds\n * @defaultValue 200\n */\n streamTimeoutMs?: number;\n}\n\nexport interface BackgroundAudioStartOptions {\n room: Room;\n agentSession?: AgentSession;\n trackPublishOptions?: TrackPublishOptions;\n}\n\n// Queue size for AudioSource buffer (400ms)\n// Kept small to avoid abrupt cutoffs when removing sounds\nconst AUDIO_SOURCE_BUFFER_MS = 400;\n\nexport class PlayHandle {\n private doneFuture = new Future<void>();\n private stopFuture = new Future<void>();\n\n done(): boolean {\n return this.doneFuture.done;\n }\n\n stop(): void {\n if (this.done()) return;\n\n if (!this.stopFuture.done) {\n this.stopFuture.resolve();\n }\n\n this._markPlayoutDone();\n }\n\n async waitForPlayout(): Promise<void> {\n return this.doneFuture.await;\n }\n\n _markPlayoutDone(): void {\n if (!this.doneFuture.done) {\n this.doneFuture.resolve();\n }\n }\n}\n\n/**\n * Manages background audio playback for LiveKit agent sessions\n *\n * This class handles playing ambient sounds and manages audio track publishing.\n * It supports:\n * - Continuous ambient sound playback with looping\n * - Thinking sound playback during agent processing\n * - Multiple simultaneous audio streams via AudioMixer\n * - Volume control and probability-based sound selection\n * - Integration with LiveKit rooms and agent sessions\n *\n * @example\n * ```typescript\n * const player = new BackgroundAudioPlayer({\n * ambientSound: { source: BuiltinAudioClip.OFFICE_AMBIENCE, volume: 0.8 },\n * thinkingSound: { source: BuiltinAudioClip.KEYBOARD_TYPING, volume: 0.6 },\n * });\n *\n * await player.start({ room, agentSession });\n * ```\n */\nexport class BackgroundAudioPlayer {\n private ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private streamTimeoutMs: number;\n\n private playTasks: Task<void>[] = [];\n private audioSource = new AudioSource(48000, 1, AUDIO_SOURCE_BUFFER_MS);\n private audioMixer: AudioMixer;\n private mixerTask?: Task<void>;\n\n private room?: Room;\n private agentSession?: AgentSession;\n private publication?: LocalTrackPublication;\n private trackPublishOptions?: TrackPublishOptions;\n private republishTask?: Task<void>;\n\n private ambientHandle?: PlayHandle;\n private thinkingHandle?: PlayHandle;\n\n private closed = true;\n\n // TODO (Brian): add lock\n\n #logger = log();\n\n constructor(options?: BackgroundAudioPlayerOptions) {\n const { ambientSound, thinkingSound, streamTimeoutMs = 200 } = options || {};\n\n this.ambientSound = ambientSound;\n this.thinkingSound = thinkingSound;\n this.streamTimeoutMs = streamTimeoutMs;\n\n this.audioMixer = new AudioMixer(48000, 1, {\n blocksize: 4800, // 100ms at 48kHz\n capacity: 1,\n streamTimeoutMs: this.streamTimeoutMs,\n });\n }\n\n /**\n * Select a sound from a list of background sound based on probability weights\n * Return undefined if no sound is selected (when sum of probabilities < 1.0).\n */\n private selectSoundFromList(sounds: AudioConfig[]): AudioConfig | undefined {\n const totalProbability = sounds.reduce((sum, sound) => sum + (sound.probability ?? 1.0), 0);\n\n if (totalProbability <= 0) {\n return undefined;\n }\n\n if (totalProbability < 1.0 && Math.random() > totalProbability) {\n return undefined;\n }\n\n const normalizeFactor = totalProbability <= 1.0 ? 1.0 : totalProbability;\n const r = Math.random() * Math.min(totalProbability, 1.0);\n let cumulative = 0.0;\n\n for (const sound of sounds) {\n const prob = sound.probability ?? 1.0;\n if (prob <= 0) {\n continue;\n }\n\n const normProb = prob / normalizeFactor;\n cumulative += normProb;\n\n if (r <= cumulative) {\n return sound;\n }\n }\n\n return sounds[sounds.length - 1];\n }\n\n private normalizeSoundSource(\n source?: AudioSourceType | AudioConfig | AudioConfig[],\n ): { source: AudioSourceType; volume: number } | undefined {\n if (source === undefined) {\n return undefined;\n }\n\n if (typeof source === 'string') {\n return {\n source: this.normalizeBuiltinAudio(source),\n volume: 1.0,\n };\n }\n\n if (Array.isArray(source)) {\n const selected = this.selectSoundFromList(source);\n if (selected === undefined) {\n return undefined;\n }\n\n return {\n source: selected.source,\n volume: selected.volume ?? 1.0,\n };\n }\n\n if (typeof source === 'object' && 'source' in source) {\n return {\n source: this.normalizeBuiltinAudio(source.source),\n volume: source.volume ?? 1.0,\n };\n }\n\n return { source, volume: 1.0 };\n }\n\n private normalizeBuiltinAudio(source: AudioSourceType): AudioSourceType {\n if (isBuiltinAudioClip(source)) {\n return getBuiltinAudioPath(source);\n }\n return source;\n }\n\n play(audio: AudioSourceType | AudioConfig | AudioConfig[], loop = false): PlayHandle {\n const normalized = this.normalizeSoundSource(audio);\n if (normalized === undefined) {\n const handle = new PlayHandle();\n handle._markPlayoutDone();\n return handle;\n }\n\n const { source, volume } = normalized;\n const playHandle = new PlayHandle();\n\n const task = Task.from(async ({ signal }) => {\n await this.playTask({ playHandle, sound: source, volume, loop, signal });\n });\n\n task.addDoneCallback(() => {\n playHandle._markPlayoutDone();\n this.playTasks.splice(this.playTasks.indexOf(task), 1);\n });\n\n this.playTasks.push(task);\n return playHandle;\n }\n\n /**\n * Start the background audio system, publishing the audio track\n * and beginning playback of any configured ambient sound.\n *\n * If `ambientSound` is provided (and contains file paths), they will loop\n * automatically. If `ambientSound` contains AsyncIterators, they are assumed\n * to be already infinite or looped.\n *\n * @param options - Options for starting background audio playback\n */\n async start(options: BackgroundAudioStartOptions): Promise<void> {\n const { room, agentSession, trackPublishOptions } = options;\n this.room = room;\n this.agentSession = agentSession;\n this.trackPublishOptions = trackPublishOptions;\n\n this.closed = false;\n\n await this.publishTrack();\n\n // TODO (Brian): check job context is not fake\n\n this.mixerTask = Task.from(async () => {\n try {\n await this.runMixerTask();\n } catch (err) {\n if (this.closed) return; // expected when AudioSource is closed\n throw err;\n }\n });\n\n this.room.on('reconnected', this.onReconnected);\n\n this.agentSession?.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n if (!this.ambientSound) return;\n\n const normalized = this.normalizeSoundSource(this.ambientSound);\n if (!normalized) return;\n\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n this.ambientHandle = this.play(selectedSound, typeof source === 'string');\n }\n\n /**\n * Close and cleanup the background audio system\n */\n async close(): Promise<void> {\n this.closed = true;\n\n await cancelAndWait(this.playTasks, TASK_TIMEOUT_MS);\n\n if (this.republishTask) {\n await this.republishTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n await this.audioMixer.aclose();\n await this.audioSource.close();\n\n if (this.mixerTask) {\n await this.mixerTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n this.agentSession?.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n this.room?.off('reconnected', this.onReconnected);\n\n if (this.publication && this.publication.sid) {\n await this.room?.localParticipant?.unpublishTrack(this.publication.sid);\n }\n }\n\n /**\n * Get the current track publication\n */\n getPublication(): LocalTrackPublication | undefined {\n return this.publication;\n }\n\n private async publishTrack(): Promise<void> {\n if (this.publication !== undefined) {\n return;\n }\n\n const track = LocalAudioTrack.createAudioTrack('background_audio', this.audioSource);\n\n if (this.room?.localParticipant === undefined) {\n throw new Error('Local participant not available');\n }\n\n const publication = await this.room.localParticipant.publishTrack(\n track,\n this.trackPublishOptions ?? new TrackPublishOptions(),\n );\n\n this.publication = publication;\n this.#logger.debug(`Background audio track published: ${this.publication.sid}`);\n }\n\n private onReconnected = (): void => {\n if (this.republishTask) {\n this.republishTask.cancel();\n }\n\n this.publication = undefined;\n this.republishTask = Task.from(async () => {\n await this.republishTrackTask();\n });\n };\n\n private async republishTrackTask(): Promise<void> {\n // TODO (Brian): add lock protection when implementing lock\n await this.publishTrack();\n }\n\n private async runMixerTask(): Promise<void> {\n for await (const frame of this.audioMixer) {\n await this.audioSource.captureFrame(frame);\n }\n }\n\n private onAgentStateChanged = (ev: AgentStateChangedEvent): void => {\n if (!this.thinkingSound) {\n return;\n }\n\n if (ev.newState === 'thinking') {\n if (this.thinkingHandle && !this.thinkingHandle.done()) {\n return;\n }\n\n const normalized = this.normalizeSoundSource(this.thinkingSound);\n if (normalized) {\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n // Loop thinking sound while in thinking state (same as ambient)\n this.thinkingHandle = this.play(selectedSound, typeof source === 'string');\n }\n } else {\n this.thinkingHandle?.stop();\n }\n };\n\n // Note: Python uses numpy, TS uses typed arrays for equivalent logic\n private applyVolumeToFrame(frame: AudioFrame, volume: number): AudioFrame {\n const int16Data = new Int16Array(\n frame.data.buffer,\n frame.data.byteOffset,\n frame.data.byteLength / 2,\n );\n const float32Data = new Float32Array(int16Data.length);\n\n for (let i = 0; i < int16Data.length; i++) {\n float32Data[i] = int16Data[i]!;\n }\n\n const volumeFactor = 10 ** Math.log10(volume);\n for (let i = 0; i < float32Data.length; i++) {\n float32Data[i]! *= volumeFactor;\n }\n\n const outputData = new Int16Array(float32Data.length);\n for (let i = 0; i < float32Data.length; i++) {\n const clipped = Math.max(-32768, Math.min(32767, float32Data[i]!));\n outputData[i] = Math.round(clipped);\n }\n\n return new AudioFrame(outputData, frame.sampleRate, frame.channels, frame.samplesPerChannel);\n }\n\n private async playTask({\n playHandle,\n sound,\n volume,\n loop,\n signal,\n }: {\n playHandle: PlayHandle;\n sound: AudioSourceType;\n volume: number;\n loop: boolean;\n signal: AbortSignal;\n }): Promise<void> {\n if (isBuiltinAudioClip(sound)) {\n sound = getBuiltinAudioPath(sound);\n }\n\n let audioStream: AsyncIterable<AudioFrame>;\n if (typeof sound === 'string') {\n audioStream = loop\n ? loopAudioFramesFromFile(sound, { abortSignal: signal })\n : audioFramesFromFile(sound, { abortSignal: signal });\n } else {\n audioStream = sound;\n }\n\n const applyVolume = this.applyVolumeToFrame.bind(this);\n async function* genWrapper(): AsyncGenerator<AudioFrame> {\n for await (const frame of audioStream) {\n if (signal.aborted || playHandle.done()) break;\n yield volume !== 1.0 ? applyVolume(frame, volume) : frame;\n }\n playHandle._markPlayoutDone();\n }\n\n const gen = genWrapper();\n try {\n this.audioMixer.addStream(gen);\n await playHandle.waitForPlayout();\n } finally {\n this.audioMixer.removeStream(gen);\n playHandle._markPlayoutDone();\n\n if (playHandle.done()) {\n await gen.return(undefined);\n }\n }\n }\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":["BuiltinAudioClip"]}
1
+ {"version":3,"sources":["../../src/voice/background_audio.ts","../../../node_modules/.pnpm/tsup@8.4.0_@microsoft+api-extractor@7.43.7_@types+node@22.15.30__postcss@8.5.6_tsx@4.20.4_typescript@5.4.5/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n AudioFrame,\n AudioMixer,\n AudioSource,\n LocalAudioTrack,\n type LocalTrackPublication,\n type Room,\n TrackPublishOptions,\n} from '@livekit/rtc-node';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { audioFramesFromFile, loopAudioFramesFromFile } from '../audio.js';\nimport { log } from '../log.js';\nimport { Future, Task, cancelAndWait } from '../utils.js';\nimport type { AgentSession } from './agent_session.js';\nimport { AgentSessionEventTypes, type AgentStateChangedEvent } from './events.js';\n\nconst TASK_TIMEOUT_MS = 500;\n\nexport enum BuiltinAudioClip {\n OFFICE_AMBIENCE = 'office-ambience.ogg',\n KEYBOARD_TYPING = 'keyboard-typing.ogg',\n KEYBOARD_TYPING2 = 'keyboard-typing2.ogg',\n}\n\nexport function isBuiltinAudioClip(\n source: AudioSourceType | AudioConfig | AudioConfig[],\n): source is BuiltinAudioClip {\n return (\n typeof source === 'string' &&\n Object.values(BuiltinAudioClip).includes(source as BuiltinAudioClip)\n );\n}\n\nexport function getBuiltinAudioPath(clip: BuiltinAudioClip): string {\n const resourcesPath = join(dirname(fileURLToPath(import.meta.url)), '../../resources');\n return join(resourcesPath, clip);\n}\n\nexport type AudioSourceType = string | BuiltinAudioClip | AsyncIterable<AudioFrame>;\n\nexport interface AudioConfig {\n source: AudioSourceType;\n volume?: number;\n probability?: number;\n}\n\nexport interface BackgroundAudioPlayerOptions {\n /**\n * Ambient sound to play continuously in the background.\n * Can be a file path, BuiltinAudioClip, or AudioConfig.\n * File paths will be looped automatically.\n */\n ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Sound to play when the agent is thinking.\n * Plays when agent state changes to 'thinking' and stops when it changes to other states.\n */\n thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Stream timeout in milliseconds\n * @defaultValue 200\n */\n streamTimeoutMs?: number;\n}\n\nexport interface BackgroundAudioStartOptions {\n room: Room;\n agentSession?: AgentSession;\n trackPublishOptions?: TrackPublishOptions;\n}\n\n// Queue size for AudioSource buffer (400ms)\n// Kept small to avoid abrupt cutoffs when removing sounds\nconst AUDIO_SOURCE_BUFFER_MS = 400;\n\nexport class PlayHandle {\n private doneFuture = new Future<void>();\n private stopFuture = new Future<void>();\n\n done(): boolean {\n return this.doneFuture.done;\n }\n\n stop(): void {\n if (this.done()) return;\n\n if (!this.stopFuture.done) {\n this.stopFuture.resolve();\n }\n\n this._markPlayoutDone();\n }\n\n async waitForPlayout(): Promise<void> {\n return this.doneFuture.await;\n }\n\n _markPlayoutDone(): void {\n if (!this.doneFuture.done) {\n this.doneFuture.resolve();\n }\n }\n}\n\n/**\n * Manages background audio playback for LiveKit agent sessions\n *\n * This class handles playing ambient sounds and manages audio track publishing.\n * It supports:\n * - Continuous ambient sound playback with looping\n * - Thinking sound playback during agent processing\n * - Multiple simultaneous audio streams via AudioMixer\n * - Volume control and probability-based sound selection\n * - Integration with LiveKit rooms and agent sessions\n *\n * @example\n * ```typescript\n * const player = new BackgroundAudioPlayer({\n * ambientSound: { source: BuiltinAudioClip.OFFICE_AMBIENCE, volume: 0.8 },\n * thinkingSound: { source: BuiltinAudioClip.KEYBOARD_TYPING, volume: 0.6 },\n * });\n *\n * await player.start({ room, agentSession });\n * ```\n */\nexport class BackgroundAudioPlayer {\n private ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private streamTimeoutMs: number;\n\n private playTasks: Task<void>[] = [];\n private audioSource = new AudioSource(48000, 1, AUDIO_SOURCE_BUFFER_MS);\n private audioMixer: AudioMixer;\n private mixerTask?: Task<void>;\n\n private room?: Room;\n private agentSession?: AgentSession;\n private publication?: LocalTrackPublication;\n private trackPublishOptions?: TrackPublishOptions;\n private republishTask?: Task<void>;\n\n private ambientHandle?: PlayHandle;\n private thinkingHandle?: PlayHandle;\n\n private closed = true;\n\n // TODO (Brian): add lock\n\n #logger = log();\n\n constructor(options?: BackgroundAudioPlayerOptions) {\n const { ambientSound, thinkingSound, streamTimeoutMs = 200 } = options || {};\n\n this.ambientSound = ambientSound;\n this.thinkingSound = thinkingSound;\n this.streamTimeoutMs = streamTimeoutMs;\n\n this.audioMixer = new AudioMixer(48000, 1, {\n blocksize: 4800, // 100ms at 48kHz\n capacity: 1,\n streamTimeoutMs: this.streamTimeoutMs,\n });\n }\n\n /**\n * Select a sound from a list of background sound based on probability weights\n * Return undefined if no sound is selected (when sum of probabilities < 1.0).\n */\n private selectSoundFromList(sounds: AudioConfig[]): AudioConfig | undefined {\n const totalProbability = sounds.reduce((sum, sound) => sum + (sound.probability ?? 1.0), 0);\n\n if (totalProbability <= 0) {\n return undefined;\n }\n\n if (totalProbability < 1.0 && Math.random() > totalProbability) {\n return undefined;\n }\n\n const normalizeFactor = totalProbability <= 1.0 ? 1.0 : totalProbability;\n const r = Math.random() * Math.min(totalProbability, 1.0);\n let cumulative = 0.0;\n\n for (const sound of sounds) {\n const prob = sound.probability ?? 1.0;\n if (prob <= 0) {\n continue;\n }\n\n const normProb = prob / normalizeFactor;\n cumulative += normProb;\n\n if (r <= cumulative) {\n return sound;\n }\n }\n\n return sounds[sounds.length - 1];\n }\n\n private normalizeSoundSource(\n source?: AudioSourceType | AudioConfig | AudioConfig[],\n ): { source: AudioSourceType; volume: number } | undefined {\n if (source === undefined) {\n return undefined;\n }\n\n if (typeof source === 'string') {\n return {\n source: this.normalizeBuiltinAudio(source),\n volume: 1.0,\n };\n }\n\n if (Array.isArray(source)) {\n const selected = this.selectSoundFromList(source);\n if (selected === undefined) {\n return undefined;\n }\n\n return {\n source: selected.source,\n volume: selected.volume ?? 1.0,\n };\n }\n\n if (typeof source === 'object' && 'source' in source) {\n return {\n source: this.normalizeBuiltinAudio(source.source),\n volume: source.volume ?? 1.0,\n };\n }\n\n return { source, volume: 1.0 };\n }\n\n private normalizeBuiltinAudio(source: AudioSourceType): AudioSourceType {\n if (isBuiltinAudioClip(source)) {\n return getBuiltinAudioPath(source);\n }\n return source;\n }\n\n play(audio: AudioSourceType | AudioConfig | AudioConfig[], loop = false): PlayHandle {\n const normalized = this.normalizeSoundSource(audio);\n if (normalized === undefined) {\n const handle = new PlayHandle();\n handle._markPlayoutDone();\n return handle;\n }\n\n const { source, volume } = normalized;\n const playHandle = new PlayHandle();\n\n const task = Task.from(async ({ signal }) => {\n await this.playTask({ playHandle, sound: source, volume, loop, signal });\n });\n\n task.addDoneCallback(() => {\n playHandle._markPlayoutDone();\n this.playTasks.splice(this.playTasks.indexOf(task), 1);\n });\n\n this.playTasks.push(task);\n return playHandle;\n }\n\n /**\n * Start the background audio system, publishing the audio track\n * and beginning playback of any configured ambient sound.\n *\n * If `ambientSound` is provided (and contains file paths), they will loop\n * automatically. If `ambientSound` contains AsyncIterators, they are assumed\n * to be already infinite or looped.\n *\n * @param options - Options for starting background audio playback\n */\n async start(options: BackgroundAudioStartOptions): Promise<void> {\n const { room, agentSession, trackPublishOptions } = options;\n this.room = room;\n this.agentSession = agentSession;\n this.trackPublishOptions = trackPublishOptions;\n\n this.closed = false;\n\n await this.publishTrack();\n\n // TODO (Brian): check job context is not fake\n\n this.mixerTask = Task.from(async () => {\n try {\n await this.runMixerTask();\n } catch (err) {\n if (this.closed) return; // expected when AudioSource is closed\n throw err;\n }\n });\n\n this.room.on('reconnected', this.onReconnected);\n\n this.agentSession?.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n if (!this.ambientSound) return;\n\n const normalized = this.normalizeSoundSource(this.ambientSound);\n if (!normalized) return;\n\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n this.ambientHandle = this.play(selectedSound, typeof source === 'string');\n }\n\n /**\n * Close and cleanup the background audio system\n */\n async close(): Promise<void> {\n this.closed = true;\n\n await cancelAndWait(this.playTasks, TASK_TIMEOUT_MS);\n\n if (this.republishTask) {\n await this.republishTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n await this.audioMixer.aclose();\n await this.audioSource.close();\n\n if (this.mixerTask) {\n await this.mixerTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n this.agentSession?.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n this.room?.off('reconnected', this.onReconnected);\n\n if (this.publication && this.publication.sid) {\n await this.room?.localParticipant?.unpublishTrack(this.publication.sid);\n }\n }\n\n /**\n * Get the current track publication\n */\n getPublication(): LocalTrackPublication | undefined {\n return this.publication;\n }\n\n private async publishTrack(): Promise<void> {\n if (this.publication !== undefined) {\n return;\n }\n\n const track = LocalAudioTrack.createAudioTrack('background_audio', this.audioSource);\n\n if (this.room?.localParticipant === undefined) {\n throw new Error('Local participant not available');\n }\n\n const publication = await this.room.localParticipant.publishTrack(\n track,\n this.trackPublishOptions ?? new TrackPublishOptions(),\n );\n\n this.publication = publication;\n this.#logger.debug(`Background audio track published: ${this.publication.sid}`);\n }\n\n private onReconnected = (): void => {\n if (this.republishTask) {\n this.republishTask.cancel();\n }\n\n this.publication = undefined;\n this.republishTask = Task.from(async () => {\n await this.republishTrackTask();\n });\n };\n\n private async republishTrackTask(): Promise<void> {\n // TODO (Brian): add lock protection when implementing lock\n await this.publishTrack();\n }\n\n private async runMixerTask(): Promise<void> {\n for await (const frame of this.audioMixer) {\n await this.audioSource.captureFrame(frame);\n }\n }\n\n private onAgentStateChanged = (ev: AgentStateChangedEvent): void => {\n if (!this.thinkingSound) {\n return;\n }\n\n if (ev.newState === 'thinking') {\n if (this.thinkingHandle && !this.thinkingHandle.done()) {\n return;\n }\n\n const normalized = this.normalizeSoundSource(this.thinkingSound);\n if (normalized) {\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n // Loop thinking sound while in thinking state (same as ambient)\n this.thinkingHandle = this.play(selectedSound, typeof source === 'string');\n }\n } else {\n this.thinkingHandle?.stop();\n }\n };\n\n // Note: Python uses numpy, TS uses typed arrays for equivalent logic\n private applyVolumeToFrame(frame: AudioFrame, volume: number): AudioFrame {\n const int16Data = new Int16Array(\n frame.data.buffer,\n frame.data.byteOffset,\n frame.data.byteLength / 2,\n );\n const float32Data = new Float32Array(int16Data.length);\n\n for (let i = 0; i < int16Data.length; i++) {\n float32Data[i] = int16Data[i]!;\n }\n\n const volumeFactor = 10 ** Math.log10(volume);\n for (let i = 0; i < float32Data.length; i++) {\n float32Data[i]! *= volumeFactor;\n }\n\n const outputData = new Int16Array(float32Data.length);\n for (let i = 0; i < float32Data.length; i++) {\n const clipped = Math.max(-32768, Math.min(32767, float32Data[i]!));\n outputData[i] = Math.round(clipped);\n }\n\n return new AudioFrame(outputData, frame.sampleRate, frame.channels, frame.samplesPerChannel);\n }\n\n private async playTask({\n playHandle,\n sound,\n volume,\n loop,\n signal,\n }: {\n playHandle: PlayHandle;\n sound: AudioSourceType;\n volume: number;\n loop: boolean;\n signal: AbortSignal;\n }): Promise<void> {\n if (isBuiltinAudioClip(sound)) {\n sound = getBuiltinAudioPath(sound);\n }\n\n let audioStream: AsyncIterable<AudioFrame>;\n if (typeof sound === 'string') {\n audioStream = loop\n ? loopAudioFramesFromFile(sound, { abortSignal: signal })\n : audioFramesFromFile(sound, { abortSignal: signal });\n } else {\n audioStream = sound;\n }\n\n const applyVolume = this.applyVolumeToFrame.bind(this);\n async function* genWrapper(): AsyncGenerator<AudioFrame> {\n for await (const frame of audioStream) {\n if (signal.aborted || playHandle.done()) break;\n yield volume !== 1.0 ? applyVolume(frame, volume) : frame;\n }\n playHandle._markPlayoutDone();\n }\n\n const gen = genWrapper();\n try {\n this.audioMixer.addStream(gen);\n await playHandle.waitForPlayout();\n } finally {\n this.audioMixer.removeStream(gen);\n playHandle._markPlayoutDone();\n\n if (playHandle.done()) {\n await gen.return(undefined);\n }\n }\n }\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACKA,IAAM,mBAAmB,MACvB,OAAO,aAAa,cAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,OAC7B,SAAS,iBAAiB,SAAS,cAAc,OAClD,IAAI,IAAI,WAAW,SAAS,OAAO,EAAE;AAEpC,IAAM,gBAAgC,iCAAiB;ADR9D,sBAQO;AACP,uBAA8B;AAC9B,sBAA8B;AAC9B,mBAA6D;AAC7D,iBAAoB;AACpB,mBAA4C;AAE5C,oBAAoE;AAEpE,MAAM,kBAAkB;AAEjB,IAAK,mBAAL,kBAAKA,sBAAL;AACL,EAAAA,kBAAA,qBAAkB;AAClB,EAAAA,kBAAA,qBAAkB;AAClB,EAAAA,kBAAA,sBAAmB;AAHT,SAAAA;AAAA,GAAA;AAML,SAAS,mBACd,QAC4B;AAC5B,SACE,OAAO,WAAW,YAClB,OAAO,OAAO,gBAAgB,EAAE,SAAS,MAA0B;AAEvE;AAEO,SAAS,oBAAoB,MAAgC;AAClE,QAAM,oBAAgB,2BAAK,8BAAQ,+BAAc,aAAe,CAAC,GAAG,iBAAiB;AACrF,aAAO,uBAAK,eAAe,IAAI;AACjC;AAuCA,MAAM,yBAAyB;AAExB,MAAM,WAAW;AAAA,EACd,aAAa,IAAI,oBAAa;AAAA,EAC9B,aAAa,IAAI,oBAAa;AAAA,EAEtC,OAAgB;AACd,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,KAAK,EAAG;AAEjB,QAAI,CAAC,KAAK,WAAW,MAAM;AACzB,WAAK,WAAW,QAAQ;AAAA,IAC1B;AAEA,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,MAAM,iBAAgC;AACpC,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA,EAEA,mBAAyB;AACvB,QAAI,CAAC,KAAK,WAAW,MAAM;AACzB,WAAK,WAAW,QAAQ;AAAA,IAC1B;AAAA,EACF;AACF;AAuBO,MAAM,sBAAsB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAA0B,CAAC;AAAA,EAC3B,cAAc,IAAI,4BAAY,MAAO,GAAG,sBAAsB;AAAA,EAC9D;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EAEA,SAAS;AAAA;AAAA,EAIjB,cAAU,gBAAI;AAAA,EAEd,YAAY,SAAwC;AAClD,UAAM,EAAE,cAAc,eAAe,kBAAkB,IAAI,IAAI,WAAW,CAAC;AAE3E,SAAK,eAAe;AACpB,SAAK,gBAAgB;AACrB,SAAK,kBAAkB;AAEvB,SAAK,aAAa,IAAI,2BAAW,MAAO,GAAG;AAAA,MACzC,WAAW;AAAA;AAAA,MACX,UAAU;AAAA,MACV,iBAAiB,KAAK;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,oBAAoB,QAAgD;AAC1E,UAAM,mBAAmB,OAAO,OAAO,CAAC,KAAK,UAAU,OAAO,MAAM,eAAe,IAAM,CAAC;AAE1F,QAAI,oBAAoB,GAAG;AACzB,aAAO;AAAA,IACT;AAEA,QAAI,mBAAmB,KAAO,KAAK,OAAO,IAAI,kBAAkB;AAC9D,aAAO;AAAA,IACT;AAEA,UAAM,kBAAkB,oBAAoB,IAAM,IAAM;AACxD,UAAM,IAAI,KAAK,OAAO,IAAI,KAAK,IAAI,kBAAkB,CAAG;AACxD,QAAI,aAAa;AAEjB,eAAW,SAAS,QAAQ;AAC1B,YAAM,OAAO,MAAM,eAAe;AAClC,UAAI,QAAQ,GAAG;AACb;AAAA,MACF;AAEA,YAAM,WAAW,OAAO;AACxB,oBAAc;AAEd,UAAI,KAAK,YAAY;AACnB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,OAAO,OAAO,SAAS,CAAC;AAAA,EACjC;AAAA,EAEQ,qBACN,QACyD;AACzD,QAAI,WAAW,QAAW;AACxB,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO;AAAA,QACL,QAAQ,KAAK,sBAAsB,MAAM;AAAA,QACzC,QAAQ;AAAA,MACV;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,YAAM,WAAW,KAAK,oBAAoB,MAAM;AAChD,UAAI,aAAa,QAAW;AAC1B,eAAO;AAAA,MACT;AAEA,aAAO;AAAA,QACL,QAAQ,SAAS;AAAA,QACjB,QAAQ,SAAS,UAAU;AAAA,MAC7B;AAAA,IACF;AAEA,QAAI,OAAO,WAAW,YAAY,YAAY,QAAQ;AACpD,aAAO;AAAA,QACL,QAAQ,KAAK,sBAAsB,OAAO,MAAM;AAAA,QAChD,QAAQ,OAAO,UAAU;AAAA,MAC3B;AAAA,IACF;AAEA,WAAO,EAAE,QAAQ,QAAQ,EAAI;AAAA,EAC/B;AAAA,EAEQ,sBAAsB,QAA0C;AACtE,QAAI,mBAAmB,MAAM,GAAG;AAC9B,aAAO,oBAAoB,MAAM;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,OAAsD,OAAO,OAAmB;AACnF,UAAM,aAAa,KAAK,qBAAqB,KAAK;AAClD,QAAI,eAAe,QAAW;AAC5B,YAAM,SAAS,IAAI,WAAW;AAC9B,aAAO,iBAAiB;AACxB,aAAO;AAAA,IACT;AAEA,UAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,UAAM,aAAa,IAAI,WAAW;AAElC,UAAM,OAAO,kBAAK,KAAK,OAAO,EAAE,OAAO,MAAM;AAC3C,YAAM,KAAK,SAAS,EAAE,YAAY,OAAO,QAAQ,QAAQ,MAAM,OAAO,CAAC;AAAA,IACzE,CAAC;AAED,SAAK,gBAAgB,MAAM;AACzB,iBAAW,iBAAiB;AAC5B,WAAK,UAAU,OAAO,KAAK,UAAU,QAAQ,IAAI,GAAG,CAAC;AAAA,IACvD,CAAC;AAED,SAAK,UAAU,KAAK,IAAI;AACxB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,MAAM,SAAqD;AA3RnE;AA4RI,UAAM,EAAE,MAAM,cAAc,oBAAoB,IAAI;AACpD,SAAK,OAAO;AACZ,SAAK,eAAe;AACpB,SAAK,sBAAsB;AAE3B,SAAK,SAAS;AAEd,UAAM,KAAK,aAAa;AAIxB,SAAK,YAAY,kBAAK,KAAK,YAAY;AACrC,UAAI;AACF,cAAM,KAAK,aAAa;AAAA,MAC1B,SAAS,KAAK;AACZ,YAAI,KAAK,OAAQ;AACjB,cAAM;AAAA,MACR;AAAA,IACF,CAAC;AAED,SAAK,KAAK,GAAG,eAAe,KAAK,aAAa;AAE9C,eAAK,iBAAL,mBAAmB,GAAG,qCAAuB,mBAAmB,KAAK;AACrE,QAAI,CAAC,KAAK,aAAc;AAExB,UAAM,aAAa,KAAK,qBAAqB,KAAK,YAAY;AAC9D,QAAI,CAAC,WAAY;AAEjB,UAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,UAAM,gBAA6B,EAAE,QAAQ,QAAQ,aAAa,EAAI;AACtE,SAAK,gBAAgB,KAAK,KAAK,eAAe,OAAO,WAAW,QAAQ;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAhU/B;AAiUI,SAAK,SAAS;AAEd,cAAM,4BAAc,KAAK,WAAW,eAAe;AAEnD,QAAI,KAAK,eAAe;AACtB,YAAM,KAAK,cAAc,cAAc,eAAe;AAAA,IACxD;AAEA,UAAM,KAAK,WAAW,OAAO;AAC7B,UAAM,KAAK,YAAY,MAAM;AAE7B,QAAI,KAAK,WAAW;AAClB,YAAM,KAAK,UAAU,cAAc,eAAe;AAAA,IACpD;AAEA,eAAK,iBAAL,mBAAmB,IAAI,qCAAuB,mBAAmB,KAAK;AACtE,eAAK,SAAL,mBAAW,IAAI,eAAe,KAAK;AAEnC,QAAI,KAAK,eAAe,KAAK,YAAY,KAAK;AAC5C,cAAM,gBAAK,SAAL,mBAAW,qBAAX,mBAA6B,eAAe,KAAK,YAAY;AAAA,IACrE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAoD;AAClD,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,eAA8B;AA/V9C;AAgWI,QAAI,KAAK,gBAAgB,QAAW;AAClC;AAAA,IACF;AAEA,UAAM,QAAQ,gCAAgB,iBAAiB,oBAAoB,KAAK,WAAW;AAEnF,UAAI,UAAK,SAAL,mBAAW,sBAAqB,QAAW;AAC7C,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AAEA,UAAM,cAAc,MAAM,KAAK,KAAK,iBAAiB;AAAA,MACnD;AAAA,MACA,KAAK,uBAAuB,IAAI,oCAAoB;AAAA,IACtD;AAEA,SAAK,cAAc;AACnB,SAAK,QAAQ,MAAM,qCAAqC,KAAK,YAAY,GAAG,EAAE;AAAA,EAChF;AAAA,EAEQ,gBAAgB,MAAY;AAClC,QAAI,KAAK,eAAe;AACtB,WAAK,cAAc,OAAO;AAAA,IAC5B;AAEA,SAAK,cAAc;AACnB,SAAK,gBAAgB,kBAAK,KAAK,YAAY;AACzC,YAAM,KAAK,mBAAmB;AAAA,IAChC,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,qBAAoC;AAEhD,UAAM,KAAK,aAAa;AAAA,EAC1B;AAAA,EAEA,MAAc,eAA8B;AAC1C,qBAAiB,SAAS,KAAK,YAAY;AACzC,YAAM,KAAK,YAAY,aAAa,KAAK;AAAA,IAC3C;AAAA,EACF;AAAA,EAEQ,sBAAsB,CAAC,OAAqC;AAzYtE;AA0YI,QAAI,CAAC,KAAK,eAAe;AACvB;AAAA,IACF;AAEA,QAAI,GAAG,aAAa,YAAY;AAC9B,UAAI,KAAK,kBAAkB,CAAC,KAAK,eAAe,KAAK,GAAG;AACtD;AAAA,MACF;AAEA,YAAM,aAAa,KAAK,qBAAqB,KAAK,aAAa;AAC/D,UAAI,YAAY;AACd,cAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,cAAM,gBAA6B,EAAE,QAAQ,QAAQ,aAAa,EAAI;AAEtE,aAAK,iBAAiB,KAAK,KAAK,eAAe,OAAO,WAAW,QAAQ;AAAA,MAC3E;AAAA,IACF,OAAO;AACL,iBAAK,mBAAL,mBAAqB;AAAA,IACvB;AAAA,EACF;AAAA;AAAA,EAGQ,mBAAmB,OAAmB,QAA4B;AACxE,UAAM,YAAY,IAAI;AAAA,MACpB,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,MAAM,KAAK,aAAa;AAAA,IAC1B;AACA,UAAM,cAAc,IAAI,aAAa,UAAU,MAAM;AAErD,aAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,kBAAY,CAAC,IAAI,UAAU,CAAC;AAAA,IAC9B;AAEA,UAAM,eAAe,MAAM,KAAK,MAAM,MAAM;AAC5C,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,kBAAY,CAAC,KAAM;AAAA,IACrB;AAEA,UAAM,aAAa,IAAI,WAAW,YAAY,MAAM;AACpD,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,YAAM,UAAU,KAAK,IAAI,QAAQ,KAAK,IAAI,OAAO,YAAY,CAAC,CAAE,CAAC;AACjE,iBAAW,CAAC,IAAI,KAAK,MAAM,OAAO;AAAA,IACpC;AAEA,WAAO,IAAI,2BAAW,YAAY,MAAM,YAAY,MAAM,UAAU,MAAM,iBAAiB;AAAA,EAC7F;AAAA,EAEA,MAAc,SAAS;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAMkB;AAChB,QAAI,mBAAmB,KAAK,GAAG;AAC7B,cAAQ,oBAAoB,KAAK;AAAA,IACnC;AAEA,QAAI;AACJ,QAAI,OAAO,UAAU,UAAU;AAC7B,oBAAc,WACV,sCAAwB,OAAO,EAAE,aAAa,OAAO,CAAC,QACtD,kCAAoB,OAAO,EAAE,aAAa,OAAO,CAAC;AAAA,IACxD,OAAO;AACL,oBAAc;AAAA,IAChB;AAEA,UAAM,cAAc,KAAK,mBAAmB,KAAK,IAAI;AACrD,oBAAgB,aAAyC;AACvD,uBAAiB,SAAS,aAAa;AACrC,YAAI,OAAO,WAAW,WAAW,KAAK,EAAG;AACzC,cAAM,WAAW,IAAM,YAAY,OAAO,MAAM,IAAI;AAAA,MACtD;AACA,iBAAW,iBAAiB;AAAA,IAC9B;AAEA,UAAM,MAAM,WAAW;AACvB,QAAI;AACF,WAAK,WAAW,UAAU,GAAG;AAC7B,YAAM,WAAW,eAAe;AAAA,IAClC,UAAE;AACA,WAAK,WAAW,aAAa,GAAG;AAChC,iBAAW,iBAAiB;AAE5B,UAAI,WAAW,KAAK,GAAG;AACrB,cAAM,IAAI,OAAO,MAAS;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;","names":["BuiltinAudioClip"]}
@@ -355,7 +355,8 @@ function performLLMInference(node, chatCtx, toolCtx, modelSettings, controller)
355
355
  name: tool.name,
356
356
  args: tool.args,
357
357
  // Preserve thought signature for Gemini 3+ thinking mode
358
- thoughtSignature: tool.thoughtSignature
358
+ thoughtSignature: tool.thoughtSignature,
359
+ extra: tool.extra || {}
359
360
  });
360
361
  data.generatedToolCalls.push(toolCall);
361
362
  await toolCallWriter.write(toolCall);