@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.
- package/dist/agent/index.cjs +119 -55
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +29 -7
- package/dist/agent/index.d.ts +29 -7
- package/dist/agent/index.js +112 -48
- package/dist/agent/index.js.map +1 -1
- package/dist/{chunk-N5EKKQVO.js → chunk-B3SD2KKR.js} +3 -3
- package/dist/{chunk-N5EKKQVO.js.map → chunk-B3SD2KKR.js.map} +1 -1
- package/dist/{chunk-D25IRY5V.cjs → chunk-GTXJFMHN.cjs} +3 -3
- package/dist/{chunk-D25IRY5V.cjs.map → chunk-GTXJFMHN.cjs.map} +1 -1
- package/dist/index.cjs +35 -35
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/dist/agent/index.d.cts
CHANGED
|
@@ -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
|
|
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-
|
|
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;
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -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
|
|
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-
|
|
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;
|
package/dist/agent/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
}
|
|
2112
|
+
mediaWs.onAudio((ulawAudio, timestamp) => {
|
|
2113
|
+
latestMediaTs = timestamp;
|
|
2114
|
+
const gained = applyUlawGain(ulawAudio, this._rxGain);
|
|
2043
2115
|
if (recorder) {
|
|
2044
|
-
recorder.writeInbound(ulawToPcm16(
|
|
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-
|
|
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 =
|
|
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
|
|
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,
|
|
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]);
|