@teamlearners/clawops 0.16.0 → 0.16.4

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.
@@ -320,7 +320,7 @@ interface Session {
320
320
  /** Start the realtime session with the call session and tools. */
321
321
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
322
322
  /** Feed raw audio into the session. */
323
- feedAudio(audio: Buffer): void;
323
+ feedAudio(audio: Buffer, timestamp?: number): void;
324
324
  /** Feed DTMF digits into the LLM context and trigger a response. */
325
325
  feedDtmf?(digits: string): Promise<void>;
326
326
  /** Stop the session. */
@@ -430,6 +430,16 @@ interface ClawOpsAgentOptions {
430
430
  logger?: Logger;
431
431
  /** Tool 실행 관련 설정. */
432
432
  toolConfig?: ToolConfig;
433
+ /**
434
+ * Gain applied to inbound audio (caller → AI). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
435
+ * AI/STT receive the gained audio, and recording captures it post-gain.
436
+ */
437
+ rxGain?: number;
438
+ /**
439
+ * Gain applied to outbound audio (AI → caller). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
440
+ * The caller hears the gained audio, and recording captures it post-gain.
441
+ */
442
+ txGain?: number;
433
443
  }
434
444
  declare class ClawOpsAgent {
435
445
  private _apiKey;
@@ -454,7 +464,10 @@ declare class ClawOpsAgent {
454
464
  private _pipelineLog;
455
465
  private _isPipelineSession;
456
466
  private _holdAudioChunks;
467
+ private _rxGain;
468
+ private _txGain;
457
469
  constructor(options: ClawOpsAgentOptions);
470
+ private static _validateGain;
458
471
  /**
459
472
  * Register a function tool.
460
473
  *
@@ -493,6 +506,14 @@ declare class ClawOpsAgent {
493
506
  private _handleRinging;
494
507
  private _handleFailed;
495
508
  private _onDtmfEvent;
509
+ /**
510
+ * _startCallSession 의 예외를 잡아 control WS 로 call.session_failed 전송한다.
511
+ *
512
+ * OpenAI/Gemini API 키 누락 등 session.start() 단계 실패는 media WS connect 에
513
+ * 도달하지 못해 call-engine 이 30 초간 무음 통화를 유지하게 만든다. 서버에 즉시
514
+ * 알려서 fail-fast 시키고 _activeSessions 에서 정리한다.
515
+ */
516
+ private _safeStartCallSession;
496
517
  private _startCallSession;
497
518
  }
498
519
 
@@ -584,17 +605,18 @@ declare class AudioRecorder {
584
605
  private _inWritten;
585
606
  private _outWritten;
586
607
  private _mixWritten;
587
- private _startTime;
588
608
  private _started;
609
+ private _baseTs;
610
+ private _outCursor;
589
611
  private _log;
590
612
  setLogger(logger: Logger): void;
591
613
  constructor(recordingPath: string, callId: string);
592
614
  start(): void;
593
- private _expectedBytes;
615
+ private _timestampToBytes;
594
616
  private _padSilence;
595
617
  private _writeToMix;
596
- writeInbound(pcm16_8k: Buffer): void;
597
- writeOutbound(pcm16_8k: Buffer): void;
618
+ writeInbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
619
+ writeOutbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
598
620
  stop(): void;
599
621
  }
600
622
 
@@ -679,7 +701,7 @@ interface OpenAIRealtimeOptions {
679
701
  apiKey?: string;
680
702
  /** System prompt / instructions for the AI. */
681
703
  systemPrompt?: string;
682
- /** Model to use. Default: 'gpt-realtime-1.5' */
704
+ /** Model to use. Default: 'gpt-realtime-2' */
683
705
  model?: string;
684
706
  /** Voice ID. Default: 'marin' */
685
707
  voice?: string;
@@ -728,7 +750,7 @@ declare class OpenAIRealtime implements Session {
728
750
  setHoldAudio(chunks: Buffer[]): void;
729
751
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
730
752
  feedDtmf(digits: string): Promise<void>;
731
- feedAudio(audio: Buffer): void;
753
+ feedAudio(audio: Buffer, timestamp?: number): void;
732
754
  stop(): Promise<void>;
733
755
  private _sendSessionUpdate;
734
756
  private _handleMessage;
@@ -320,7 +320,7 @@ interface Session {
320
320
  /** Start the realtime session with the call session and tools. */
321
321
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
322
322
  /** Feed raw audio into the session. */
323
- feedAudio(audio: Buffer): void;
323
+ feedAudio(audio: Buffer, timestamp?: number): void;
324
324
  /** Feed DTMF digits into the LLM context and trigger a response. */
325
325
  feedDtmf?(digits: string): Promise<void>;
326
326
  /** Stop the session. */
@@ -430,6 +430,16 @@ interface ClawOpsAgentOptions {
430
430
  logger?: Logger;
431
431
  /** Tool 실행 관련 설정. */
432
432
  toolConfig?: ToolConfig;
433
+ /**
434
+ * Gain applied to inbound audio (caller → AI). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
435
+ * AI/STT receive the gained audio, and recording captures it post-gain.
436
+ */
437
+ rxGain?: number;
438
+ /**
439
+ * Gain applied to outbound audio (AI → caller). 1.0 = pass-through (default), 0 = mute, 2.0 = 2x amplify.
440
+ * The caller hears the gained audio, and recording captures it post-gain.
441
+ */
442
+ txGain?: number;
433
443
  }
434
444
  declare class ClawOpsAgent {
435
445
  private _apiKey;
@@ -454,7 +464,10 @@ declare class ClawOpsAgent {
454
464
  private _pipelineLog;
455
465
  private _isPipelineSession;
456
466
  private _holdAudioChunks;
467
+ private _rxGain;
468
+ private _txGain;
457
469
  constructor(options: ClawOpsAgentOptions);
470
+ private static _validateGain;
458
471
  /**
459
472
  * Register a function tool.
460
473
  *
@@ -493,6 +506,14 @@ declare class ClawOpsAgent {
493
506
  private _handleRinging;
494
507
  private _handleFailed;
495
508
  private _onDtmfEvent;
509
+ /**
510
+ * _startCallSession 의 예외를 잡아 control WS 로 call.session_failed 전송한다.
511
+ *
512
+ * OpenAI/Gemini API 키 누락 등 session.start() 단계 실패는 media WS connect 에
513
+ * 도달하지 못해 call-engine 이 30 초간 무음 통화를 유지하게 만든다. 서버에 즉시
514
+ * 알려서 fail-fast 시키고 _activeSessions 에서 정리한다.
515
+ */
516
+ private _safeStartCallSession;
496
517
  private _startCallSession;
497
518
  }
498
519
 
@@ -584,17 +605,18 @@ declare class AudioRecorder {
584
605
  private _inWritten;
585
606
  private _outWritten;
586
607
  private _mixWritten;
587
- private _startTime;
588
608
  private _started;
609
+ private _baseTs;
610
+ private _outCursor;
589
611
  private _log;
590
612
  setLogger(logger: Logger): void;
591
613
  constructor(recordingPath: string, callId: string);
592
614
  start(): void;
593
- private _expectedBytes;
615
+ private _timestampToBytes;
594
616
  private _padSilence;
595
617
  private _writeToMix;
596
- writeInbound(pcm16_8k: Buffer): void;
597
- writeOutbound(pcm16_8k: Buffer): void;
618
+ writeInbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
619
+ writeOutbound(pcm16_8k: Buffer, mediaTsMs?: number): void;
598
620
  stop(): void;
599
621
  }
600
622
 
@@ -679,7 +701,7 @@ interface OpenAIRealtimeOptions {
679
701
  apiKey?: string;
680
702
  /** System prompt / instructions for the AI. */
681
703
  systemPrompt?: string;
682
- /** Model to use. Default: 'gpt-realtime-1.5' */
704
+ /** Model to use. Default: 'gpt-realtime-2' */
683
705
  model?: string;
684
706
  /** Voice ID. Default: 'marin' */
685
707
  voice?: string;
@@ -728,7 +750,7 @@ declare class OpenAIRealtime implements Session {
728
750
  setHoldAudio(chunks: Buffer[]): void;
729
751
  start(callSession: CallSession, tools?: ToolRegistry): Promise<void>;
730
752
  feedDtmf(digits: string): Promise<void>;
731
- feedAudio(audio: Buffer): void;
753
+ feedAudio(audio: Buffer, timestamp?: number): void;
732
754
  stop(): Promise<void>;
733
755
  private _sendSessionUpdate;
734
756
  private _handleMessage;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-N5EKKQVO.js';
1
+ import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-B3SD2KKR.js';
2
2
  import pino from 'pino';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
@@ -304,6 +304,27 @@ function ulawToPcm16(ulaw) {
304
304
  }
305
305
  return out;
306
306
  }
307
+ function applyPcm16Gain(pcm, gain) {
308
+ if (!pcm.length) return Buffer.alloc(0);
309
+ if (gain === 1) return pcm;
310
+ if (!Number.isFinite(gain) || gain < 0) {
311
+ throw new Error("gain must be a finite number >= 0");
312
+ }
313
+ const nSamples = pcm.length >> 1;
314
+ const out = Buffer.alloc(nSamples * 2);
315
+ for (let i = 0; i < nSamples; i++) {
316
+ let value = Math.round(pcm.readInt16LE(i * 2) * gain);
317
+ if (value > 32767) value = 32767;
318
+ else if (value < -32768) value = -32768;
319
+ out.writeInt16LE(value, i * 2);
320
+ }
321
+ return out;
322
+ }
323
+ function applyUlawGain(ulaw, gain) {
324
+ if (!ulaw.length) return Buffer.alloc(0);
325
+ if (gain === 1) return ulaw;
326
+ return pcm16ToUlaw(applyPcm16Gain(ulawToPcm16(ulaw), gain));
327
+ }
307
328
  function resamplePcm16(pcm, fromRate, toRate) {
308
329
  if (fromRate === toRate || !pcm.length) return pcm;
309
330
  const nSamples = pcm.length >> 1;
@@ -892,8 +913,9 @@ var AudioRecorder = class {
892
913
  _inWritten = 0;
893
914
  _outWritten = 0;
894
915
  _mixWritten = 0;
895
- _startTime = 0;
896
916
  _started = false;
917
+ _baseTs = null;
918
+ _outCursor = 0;
897
919
  _log = NOOP_LOGGER;
898
920
  setLogger(logger) {
899
921
  this._log = logger;
@@ -910,17 +932,18 @@ var AudioRecorder = class {
910
932
  fs.writeSync(this._fdIn, header);
911
933
  fs.writeSync(this._fdOut, header);
912
934
  fs.writeSync(this._fdMix, header);
913
- this._startTime = performance.now();
914
935
  this._started = true;
915
936
  this._log.info("Recording started: %s", this._dir);
916
937
  }
917
- _expectedBytes() {
918
- const elapsed = (performance.now() - this._startTime) / 1e3;
919
- return Math.floor(elapsed * BYTES_PER_SECOND);
938
+ _timestampToBytes(mediaTsMs) {
939
+ if (this._baseTs === null) {
940
+ this._baseTs = mediaTsMs;
941
+ }
942
+ const target = Math.floor((mediaTsMs - this._baseTs) * BYTES_PER_SECOND / 1e3);
943
+ return Math.max(0, target - target % 2);
920
944
  }
921
- _padSilence(fd, written) {
922
- const expected = this._expectedBytes();
923
- let gap = expected - written;
945
+ _padSilence(fd, written, target) {
946
+ let gap = target - written;
924
947
  if (gap <= 0) return 0;
925
948
  gap = gap - gap % 2;
926
949
  if (gap > 0) {
@@ -930,9 +953,12 @@ var AudioRecorder = class {
930
953
  }
931
954
  _writeToMix(data, trackPos) {
932
955
  if (this._fdMix === null) return;
956
+ data = data.subarray(0, data.length - data.length % 2);
957
+ if (data.length === 0) return;
933
958
  const filePos = 44 + trackPos;
934
959
  if (trackPos < this._mixWritten) {
935
- const overlap = Math.min(data.length, this._mixWritten - trackPos);
960
+ let overlap = Math.min(data.length, this._mixWritten - trackPos);
961
+ overlap = overlap - overlap % 2;
936
962
  const existing = Buffer.alloc(overlap);
937
963
  fs.readSync(this._fdMix, existing, 0, overlap, filePos);
938
964
  const mixed = mixSamples(existing, data.subarray(0, overlap));
@@ -955,10 +981,13 @@ var AudioRecorder = class {
955
981
  this._mixWritten += data.length;
956
982
  }
957
983
  }
958
- writeInbound(pcm16_8k) {
984
+ writeInbound(pcm16_8k, mediaTsMs = 0) {
959
985
  if (!this._started || this._fdIn === null) return;
960
986
  try {
961
- const gap = this._padSilence(this._fdIn, this._inWritten);
987
+ pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
988
+ if (pcm16_8k.length === 0) return;
989
+ const target = this._timestampToBytes(mediaTsMs);
990
+ const gap = this._padSilence(this._fdIn, this._inWritten, target);
962
991
  this._inWritten += gap;
963
992
  const posBefore = this._inWritten;
964
993
  fs.writeSync(this._fdIn, pcm16_8k);
@@ -968,14 +997,21 @@ var AudioRecorder = class {
968
997
  this._log.error({ err }, "Recording write error (inbound)");
969
998
  }
970
999
  }
971
- writeOutbound(pcm16_8k) {
1000
+ writeOutbound(pcm16_8k, mediaTsMs) {
972
1001
  if (!this._started || this._fdOut === null) return;
973
1002
  try {
974
- const gap = this._padSilence(this._fdOut, this._outWritten);
1003
+ pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
1004
+ if (pcm16_8k.length === 0) return;
1005
+ let target = this._outCursor;
1006
+ if (mediaTsMs !== void 0) {
1007
+ target = Math.max(target, this._timestampToBytes(mediaTsMs));
1008
+ }
1009
+ const gap = this._padSilence(this._fdOut, this._outWritten, target);
975
1010
  this._outWritten += gap;
976
1011
  const posBefore = this._outWritten;
977
1012
  fs.writeSync(this._fdOut, pcm16_8k);
978
1013
  this._outWritten += pcm16_8k.length;
1014
+ this._outCursor = this._outWritten;
979
1015
  this._writeToMix(pcm16_8k, posBefore);
980
1016
  } catch (err) {
981
1017
  this._log.error({ err }, "Recording write error (outbound)");
@@ -1671,7 +1707,7 @@ var ATTR_CALL_DIRECTION = "clawops.call.direction";
1671
1707
  var ATTR_AGENT_ID = "clawops.agent.id";
1672
1708
 
1673
1709
  // src/agent/agent.ts
1674
- var ClawOpsAgent = class {
1710
+ var ClawOpsAgent = class _ClawOpsAgent {
1675
1711
  _apiKey;
1676
1712
  _accountId;
1677
1713
  _baseUrl;
@@ -1694,6 +1730,8 @@ var ClawOpsAgent = class {
1694
1730
  _pipelineLog;
1695
1731
  _isPipelineSession = false;
1696
1732
  _holdAudioChunks = null;
1733
+ _rxGain;
1734
+ _txGain;
1697
1735
  constructor(options) {
1698
1736
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1699
1737
  this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
@@ -1705,6 +1743,8 @@ var ClawOpsAgent = class {
1705
1743
  this._mcpServers = options.mcpServers ?? [];
1706
1744
  this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
1707
1745
  this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
1746
+ this._rxGain = _ClawOpsAgent._validateGain("rxGain", options.rxGain ?? 1);
1747
+ this._txGain = _ClawOpsAgent._validateGain("txGain", options.txGain ?? 1);
1708
1748
  if (options.tracing) {
1709
1749
  setTracingConfig(options.tracing);
1710
1750
  }
@@ -1715,6 +1755,12 @@ var ClawOpsAgent = class {
1715
1755
  this._holdAudioChunks = loadHoldAudio(options.toolConfig.holdAudio);
1716
1756
  }
1717
1757
  }
1758
+ static _validateGain(name, gain) {
1759
+ if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) {
1760
+ throw new AgentError(`${name}=${gain} must be a finite number >= 0`);
1761
+ }
1762
+ return gain;
1763
+ }
1718
1764
  /**
1719
1765
  * Register a function tool.
1720
1766
  *
@@ -1882,9 +1928,7 @@ var ClawOpsAgent = class {
1882
1928
  this._controlWs.send({ event: "call.accept", callId });
1883
1929
  }
1884
1930
  if (mediaUrl) {
1885
- this._startCallSession(session, mediaUrl).catch((err) => {
1886
- this._log.error({ err }, "Call session error: %s", callId);
1887
- });
1931
+ this._safeStartCallSession(session, mediaUrl, callId);
1888
1932
  }
1889
1933
  }
1890
1934
  _handleEnded(event) {
@@ -1918,9 +1962,7 @@ var ClawOpsAgent = class {
1918
1962
  }
1919
1963
  if (mediaUrl) {
1920
1964
  this._log.info("Outbound call answered: %s -> %s (%s)", this._fromNumber, session.toNumber, callId);
1921
- this._startCallSession(session, mediaUrl).catch((err) => {
1922
- this._log.error({ err }, "Call session error: %s", callId);
1923
- });
1965
+ this._safeStartCallSession(session, mediaUrl, callId);
1924
1966
  }
1925
1967
  }
1926
1968
  _handleRinging(event) {
@@ -1964,6 +2006,32 @@ var ClawOpsAgent = class {
1964
2006
  }
1965
2007
  }, this._passiveDtmfDebounceMs);
1966
2008
  }
2009
+ /**
2010
+ * _startCallSession 의 예외를 잡아 control WS 로 call.session_failed 전송한다.
2011
+ *
2012
+ * OpenAI/Gemini API 키 누락 등 session.start() 단계 실패는 media WS connect 에
2013
+ * 도달하지 못해 call-engine 이 30 초간 무음 통화를 유지하게 만든다. 서버에 즉시
2014
+ * 알려서 fail-fast 시키고 _activeSessions 에서 정리한다.
2015
+ */
2016
+ _safeStartCallSession(session, mediaWsUrl, callId) {
2017
+ this._startCallSession(session, mediaWsUrl).catch((err) => {
2018
+ const error = err;
2019
+ this._log.error({ err }, "Session start failed for %s", callId);
2020
+ if (this._controlWs) {
2021
+ try {
2022
+ this._controlWs.send({
2023
+ event: "call.session_failed",
2024
+ callId,
2025
+ reason: error?.name ?? "Error",
2026
+ message: error?.message ?? String(err)
2027
+ });
2028
+ } catch {
2029
+ }
2030
+ }
2031
+ this._activeSessions.delete(callId);
2032
+ this._callSessions.delete(callId);
2033
+ });
2034
+ }
1967
2035
  async _startCallSession(session, mediaWsUrl) {
1968
2036
  await withSpan(
1969
2037
  "clawops.call_session",
@@ -1997,9 +2065,14 @@ var ClawOpsAgent = class {
1997
2065
  }
1998
2066
  const mediaWs = new MediaWebSocket();
1999
2067
  mediaWs.setLogger(this._log);
2068
+ let latestMediaTs = 0;
2000
2069
  session._bindTransport(
2001
2070
  (audio) => {
2002
- mediaWs.sendAudio(audio.toString("base64"));
2071
+ const gained = applyUlawGain(audio, this._txGain);
2072
+ if (recorder) {
2073
+ recorder.writeOutbound(ulawToPcm16(gained), latestMediaTs);
2074
+ }
2075
+ mediaWs.sendAudio(gained.toString("base64"));
2003
2076
  session.recordFirstResponse();
2004
2077
  },
2005
2078
  () => {
@@ -2036,12 +2109,14 @@ var ClawOpsAgent = class {
2036
2109
  sessionHandler.setHoldAudio(this._holdAudioChunks);
2037
2110
  }
2038
2111
  this._callSessions.set(session.callId, sessionHandler);
2039
- mediaWs.onAudio((ulawAudio, _timestamp) => {
2040
- if (sessionHandler) {
2041
- sessionHandler.feedAudio(ulawAudio);
2042
- }
2112
+ mediaWs.onAudio((ulawAudio, timestamp) => {
2113
+ latestMediaTs = timestamp;
2114
+ const gained = applyUlawGain(ulawAudio, this._rxGain);
2043
2115
  if (recorder) {
2044
- recorder.writeInbound(ulawToPcm16(ulawAudio));
2116
+ recorder.writeInbound(ulawToPcm16(gained), timestamp);
2117
+ }
2118
+ if (sessionHandler) {
2119
+ sessionHandler.feedAudio(gained, timestamp);
2045
2120
  }
2046
2121
  });
2047
2122
  mediaWs.onDtmf((digit) => {
@@ -2512,10 +2587,6 @@ var PipelineSession = class {
2512
2587
  sampleRate: this._sampleRate
2513
2588
  })) {
2514
2589
  if (!this._running || !this._speaking) break;
2515
- if (this._recorder) {
2516
- const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2517
- this._recorder.writeOutbound(pcm8k2);
2518
- }
2519
2590
  const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2520
2591
  const ulaw = pcm16ToUlaw(pcm8k);
2521
2592
  for (let off = 0; off < ulaw.length; off += 160) {
@@ -2584,8 +2655,13 @@ var OpenAIRealtime = class {
2584
2655
  _holdAudioChunks = null;
2585
2656
  constructor(options = {}) {
2586
2657
  this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2658
+ if (!this._apiKey) {
2659
+ throw new Error(
2660
+ "OpenAI API key is required. Set OPENAI_API_KEY env var or pass apiKey option."
2661
+ );
2662
+ }
2587
2663
  this._systemPrompt = options.systemPrompt ?? "";
2588
- this._model = options.model ?? "gpt-realtime-1.5";
2664
+ this._model = options.model ?? "gpt-realtime-2";
2589
2665
  this._voice = options.voice ?? "marin";
2590
2666
  this._language = options.language ?? "ko";
2591
2667
  this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
@@ -2609,9 +2685,6 @@ var OpenAIRealtime = class {
2609
2685
  this._closed = false;
2610
2686
  this._playback = null;
2611
2687
  this._latestMediaTs = 0;
2612
- if (!this._apiKey) {
2613
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
2614
- }
2615
2688
  const { WebSocket } = await import('ws');
2616
2689
  const url = `${OPENAI_REALTIME_URL}${this._model}`;
2617
2690
  this._ws = new WebSocket(url, {
@@ -2660,8 +2733,8 @@ var OpenAIRealtime = class {
2660
2733
  });
2661
2734
  this._send({ type: "response.create" });
2662
2735
  }
2663
- feedAudio(audio) {
2664
- this._latestMediaTs = Date.now();
2736
+ feedAudio(audio, timestamp) {
2737
+ this._latestMediaTs = timestamp ?? this._latestMediaTs;
2665
2738
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
2666
2739
  this._send({
2667
2740
  type: "input_audio_buffer.append",
@@ -2772,7 +2845,7 @@ var OpenAIRealtime = class {
2772
2845
  if (this._playback === null) {
2773
2846
  this._playback = {
2774
2847
  itemId: msg["item_id"] || "",
2775
- startTs: this._latestMediaTs || Date.now(),
2848
+ startTs: this._latestMediaTs,
2776
2849
  sentChunks: 0,
2777
2850
  generating: true,
2778
2851
  audioRemainder: Buffer.alloc(0)
@@ -2782,9 +2855,6 @@ var OpenAIRealtime = class {
2782
2855
  }
2783
2856
  const pb = this._playback;
2784
2857
  const ulaw = Buffer.from(msg["delta"], "base64");
2785
- if (this._recorder) {
2786
- this._recorder.writeOutbound(ulawToPcm16(ulaw));
2787
- }
2788
2858
  const combined = Buffer.concat([pb.audioRemainder, ulaw]);
2789
2859
  const chunkSize = 160;
2790
2860
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
@@ -2808,7 +2878,7 @@ var OpenAIRealtime = class {
2808
2878
  if (pb === null) {
2809
2879
  return;
2810
2880
  }
2811
- const playedMs = Math.max(0, (this._latestMediaTs || Date.now()) - pb.startTs);
2881
+ const playedMs = Math.max(0, this._latestMediaTs - pb.startTs);
2812
2882
  this._log.info(
2813
2883
  "[Interrupt] item=%s played=%dms total=%dms",
2814
2884
  pb.itemId,
@@ -3100,9 +3170,6 @@ var GeminiRealtime = class _GeminiRealtime {
3100
3170
  feedAudio(audio) {
3101
3171
  if (this._session && !this._closed) {
3102
3172
  const pcm8k = ulawToPcm16(audio);
3103
- if (this._recorder) {
3104
- this._recorder.writeInbound(pcm8k);
3105
- }
3106
3173
  const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
3107
3174
  this._session.sendRealtimeInput({
3108
3175
  audio: {
@@ -3200,9 +3267,6 @@ var GeminiRealtime = class _GeminiRealtime {
3200
3267
  _handleAudioData(b64Data) {
3201
3268
  if (!this._call) return;
3202
3269
  const pcm24k = Buffer.from(b64Data, "base64");
3203
- if (this._recorder) {
3204
- this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
3205
- }
3206
3270
  const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
3207
3271
  const ulaw = pcm16ToUlaw(pcm8k);
3208
3272
  const combined = Buffer.concat([this._audioRemainder, ulaw]);