@craftedxp/voice-js 0.4.1 → 0.4.2
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/README.md +4 -1
- package/dist/browser.d.ts +259 -327
- package/dist/browser.js +717 -720
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +43 -37
- package/dist/browser.mjs.map +1 -1
- package/dist/embed.iife.js +4 -1110
- package/dist/node.d.ts +256 -317
- package/dist/node.js +431 -440
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +5 -0
- package/dist/node.mjs.map +1 -1
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
var __defProp = Object.defineProperty
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
6
|
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
}
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
9
10
|
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
12
|
for (let key of __getOwnPropNames(from))
|
|
12
13
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => from[key],
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
|
|
16
|
-
})
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
15
|
}
|
|
18
|
-
return to
|
|
19
|
-
}
|
|
20
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({},
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
19
|
|
|
22
20
|
// src/browser.ts
|
|
23
|
-
var browser_exports = {}
|
|
21
|
+
var browser_exports = {};
|
|
24
22
|
__export(browser_exports, {
|
|
25
23
|
buildWsUrl: () => buildWsUrl,
|
|
26
24
|
configureVoiceClient: () => configureVoiceClient,
|
|
@@ -28,64 +26,57 @@ __export(browser_exports, {
|
|
|
28
26
|
createAudioPlayback: () => createAudioPlayback,
|
|
29
27
|
createProtocolState: () => createProtocolState,
|
|
30
28
|
createReconnectingWebSocket: () => createReconnectingWebSocket,
|
|
31
|
-
handleServerMessage: () => handleServerMessage
|
|
32
|
-
})
|
|
33
|
-
module.exports = __toCommonJS(browser_exports)
|
|
29
|
+
handleServerMessage: () => handleServerMessage
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(browser_exports);
|
|
34
32
|
|
|
35
33
|
// src/config.ts
|
|
36
34
|
function normalizeConfig(config) {
|
|
37
|
-
if (!config) throw new Error(
|
|
38
|
-
if (
|
|
35
|
+
if (!config) throw new Error("configureVoiceClient: config is required");
|
|
36
|
+
if ("apiKey" in config) {
|
|
39
37
|
throw new Error(
|
|
40
|
-
|
|
41
|
-
)
|
|
38
|
+
"configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead \u2014 see the @craftedxp/voice-js README for the migration recipe."
|
|
39
|
+
);
|
|
42
40
|
}
|
|
43
41
|
if (!config.apiBase) {
|
|
44
|
-
throw new Error(
|
|
42
|
+
throw new Error("configureVoiceClient: apiBase is required");
|
|
45
43
|
}
|
|
46
|
-
if (typeof config.fetchToken !==
|
|
47
|
-
throw new Error(
|
|
44
|
+
if (typeof config.fetchToken !== "function") {
|
|
45
|
+
throw new Error("configureVoiceClient: fetchToken must be a function");
|
|
48
46
|
}
|
|
49
47
|
return {
|
|
50
48
|
...config,
|
|
51
|
-
apiBase: config.apiBase.replace(/\/+$/,
|
|
52
|
-
}
|
|
49
|
+
apiBase: config.apiBase.replace(/\/+$/, "")
|
|
50
|
+
};
|
|
53
51
|
}
|
|
54
52
|
function mergeStartCallContext(factory, call) {
|
|
55
|
-
const context =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
: void 0
|
|
59
|
-
const metadata =
|
|
60
|
-
factory.defaultMetadata || call.metadata
|
|
61
|
-
? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }
|
|
62
|
-
: void 0
|
|
63
|
-
return { context, metadata }
|
|
53
|
+
const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
|
|
54
|
+
const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
|
|
55
|
+
return { context, metadata };
|
|
64
56
|
}
|
|
65
57
|
|
|
66
58
|
// src/worklets/mic-downsampler.worklet.js
|
|
67
|
-
var mic_downsampler_worklet_default =
|
|
68
|
-
"// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.\n//\n// Input: Float32 samples at the AudioContext's native sampleRate (typically\n// 48000 Hz on desktop, 44100 Hz on some iOS devices).\n// Output: 16 kHz mono Int16 PCM, shipped to the main thread via\n// `port.postMessage(ArrayBuffer, [ArrayBuffer])` (transferred, not copied).\n//\n// Why AudioWorklet instead of ScriptProcessorNode: ScriptProcessorNode is\n// deprecated + main-thread-bound, so any JS jank produces audible audio\n// glitches. AudioWorklet's `process()` runs on the audio rendering thread\n// at the graph's block cadence (128 frames by default) and backpressures\n// via returning `true` / `false`.\n//\n// This file is loaded as text (see tsup.config.ts loader) and registered\n// at runtime via `audioWorklet.addModule(blobUrl)`.\n\nclass MicDownsampler extends AudioWorkletProcessor {\n constructor() {\n super()\n // Target sample rate for STT. Matches Deepgram Nova-3 + the platform's\n // server-side SAMPLE_RATE constant in AgentCallHandler.\n this.targetRate = 16000\n // Accumulator for the downsample. We collect incoming samples and emit\n // an Int16 chunk when we've accumulated ~1024 target-rate samples\n // (~64 ms at 16 kHz) \u2014 matches the mobile SDK's chunk size so both\n // platforms have the same server-side framing.\n this.outputFrames = 1024\n this.acc = []\n // Running index used for fractional resampling.\n this.readCursor = 0\n }\n\n // `inputs[0][0]` = first channel of first input. 128 Float32 samples per\n // call at the context's sampleRate. Return true = keep processing.\n process(inputs) {\n const input = inputs[0]\n if (!input || input.length === 0) return true\n const channel = input[0]\n if (!channel || channel.length === 0) return true\n\n const ctxRate = sampleRate // global inside AudioWorkletProcessor\n const ratio = ctxRate / this.targetRate\n\n // Simple linear-interp downsample. For 48000 \u2192 16000 that's 3:1, which\n // linear handles fine for voice. Anti-alias filtering would be\n // theoretically better but inaudible for speech.\n for (let i = 0; i < channel.length; i++) {\n this.acc.push(channel[i])\n }\n\n while (this.acc.length - this.readCursor >= ratio * this.outputFrames) {\n const out = new Int16Array(this.outputFrames)\n let readIdx = this.readCursor\n for (let i = 0; i < this.outputFrames; i++) {\n // Linear interp between floor(readIdx) and ceil(readIdx)\n const low = Math.floor(readIdx)\n const high = Math.min(low + 1, this.acc.length - 1)\n const frac = readIdx - low\n const sample = this.acc[low] * (1 - frac) + this.acc[high] * frac\n // Clip + convert to int16\n const clipped = Math.max(-1, Math.min(1, sample))\n out[i] = clipped < 0 ? clipped * 0x8000 : clipped * 0x7fff\n readIdx += ratio\n }\n // Transfer the ArrayBuffer (zero-copy) to the main thread.\n this.port.postMessage(out.buffer, [out.buffer])\n this.readCursor = readIdx\n }\n\n // Garbage-collect the consumed portion of `acc` every so often so it\n // doesn't grow without bound. Leave ~one chunk of headroom.\n if (this.readCursor > ratio * this.outputFrames) {\n this.acc = this.acc.slice(Math.floor(this.readCursor))\n this.readCursor -= Math.floor(this.readCursor)\n }\n\n return true\n }\n}\n\nregisterProcessor('mic-downsampler', MicDownsampler)\n"
|
|
59
|
+
var mic_downsampler_worklet_default = "// AudioWorklet \u2014 runs off the main thread in the audio rendering graph.\n//\n// Input: Float32 samples at the AudioContext's native sampleRate (typically\n// 48000 Hz on desktop, 44100 Hz on some iOS devices).\n// Output: 16 kHz mono Int16 PCM, shipped to the main thread via\n// `port.postMessage(ArrayBuffer, [ArrayBuffer])` (transferred, not copied).\n//\n// Why AudioWorklet instead of ScriptProcessorNode: ScriptProcessorNode is\n// deprecated + main-thread-bound, so any JS jank produces audible audio\n// glitches. AudioWorklet's `process()` runs on the audio rendering thread\n// at the graph's block cadence (128 frames by default) and backpressures\n// via returning `true` / `false`.\n//\n// This file is loaded as text (see tsup.config.ts loader) and registered\n// at runtime via `audioWorklet.addModule(blobUrl)`.\n\nclass MicDownsampler extends AudioWorkletProcessor {\n constructor() {\n super()\n // Target sample rate for STT. Matches Deepgram Nova-3 + the platform's\n // server-side SAMPLE_RATE constant in AgentCallHandler.\n this.targetRate = 16000\n // Accumulator for the downsample. We collect incoming samples and emit\n // an Int16 chunk when we've accumulated ~1024 target-rate samples\n // (~64 ms at 16 kHz) \u2014 matches the mobile SDK's chunk size so both\n // platforms have the same server-side framing.\n this.outputFrames = 1024\n this.acc = []\n // Running index used for fractional resampling.\n this.readCursor = 0\n }\n\n // `inputs[0][0]` = first channel of first input. 128 Float32 samples per\n // call at the context's sampleRate. Return true = keep processing.\n process(inputs) {\n const input = inputs[0]\n if (!input || input.length === 0) return true\n const channel = input[0]\n if (!channel || channel.length === 0) return true\n\n const ctxRate = sampleRate // global inside AudioWorkletProcessor\n const ratio = ctxRate / this.targetRate\n\n // Simple linear-interp downsample. For 48000 \u2192 16000 that's 3:1, which\n // linear handles fine for voice. Anti-alias filtering would be\n // theoretically better but inaudible for speech.\n for (let i = 0; i < channel.length; i++) {\n this.acc.push(channel[i])\n }\n\n while (this.acc.length - this.readCursor >= ratio * this.outputFrames) {\n const out = new Int16Array(this.outputFrames)\n let readIdx = this.readCursor\n for (let i = 0; i < this.outputFrames; i++) {\n // Linear interp between floor(readIdx) and ceil(readIdx)\n const low = Math.floor(readIdx)\n const high = Math.min(low + 1, this.acc.length - 1)\n const frac = readIdx - low\n const sample = this.acc[low] * (1 - frac) + this.acc[high] * frac\n // Clip + convert to int16\n const clipped = Math.max(-1, Math.min(1, sample))\n out[i] = clipped < 0 ? clipped * 0x8000 : clipped * 0x7fff\n readIdx += ratio\n }\n // Transfer the ArrayBuffer (zero-copy) to the main thread.\n this.port.postMessage(out.buffer, [out.buffer])\n this.readCursor = readIdx\n }\n\n // Garbage-collect the consumed portion of `acc` every so often so it\n // doesn't grow without bound. Leave ~one chunk of headroom.\n if (this.readCursor > ratio * this.outputFrames) {\n this.acc = this.acc.slice(Math.floor(this.readCursor))\n this.readCursor -= Math.floor(this.readCursor)\n }\n\n return true\n }\n}\n\nregisterProcessor('mic-downsampler', MicDownsampler)\n";
|
|
69
60
|
|
|
70
61
|
// src/AudioCapture.ts
|
|
71
|
-
var VOLUME_INTERVAL_MS = 100
|
|
62
|
+
var VOLUME_INTERVAL_MS = 100;
|
|
72
63
|
var createAudioCapture = (options) => {
|
|
73
|
-
let audioContext = null
|
|
74
|
-
let mediaStream = null
|
|
75
|
-
let sourceNode = null
|
|
76
|
-
let workletNode = null
|
|
77
|
-
let analyser = null
|
|
78
|
-
let volumeTimer = null
|
|
79
|
-
let muted = false
|
|
80
|
-
let capturing = false
|
|
64
|
+
let audioContext = null;
|
|
65
|
+
let mediaStream = null;
|
|
66
|
+
let sourceNode = null;
|
|
67
|
+
let workletNode = null;
|
|
68
|
+
let analyser = null;
|
|
69
|
+
let volumeTimer = null;
|
|
70
|
+
let muted = false;
|
|
71
|
+
let capturing = false;
|
|
81
72
|
const computeRms = (buf) => {
|
|
82
|
-
let sum = 0
|
|
83
|
-
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
|
|
84
|
-
const rms = Math.sqrt(sum / buf.length)
|
|
85
|
-
return Math.min(1, rms * 1.8)
|
|
86
|
-
}
|
|
73
|
+
let sum = 0;
|
|
74
|
+
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
|
|
75
|
+
const rms = Math.sqrt(sum / buf.length);
|
|
76
|
+
return Math.min(1, rms * 1.8);
|
|
77
|
+
};
|
|
87
78
|
const start = async () => {
|
|
88
|
-
if (capturing) return
|
|
79
|
+
if (capturing) return;
|
|
89
80
|
try {
|
|
90
81
|
mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
91
82
|
audio: {
|
|
@@ -96,748 +87,751 @@ var createAudioCapture = (options) => {
|
|
|
96
87
|
echoCancellation: true,
|
|
97
88
|
noiseSuppression: true,
|
|
98
89
|
autoGainControl: true,
|
|
99
|
-
channelCount: 1
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
audioContext = new AudioContext()
|
|
103
|
-
if (audioContext.state ===
|
|
104
|
-
const blob = new Blob([mic_downsampler_worklet_default], { type:
|
|
105
|
-
const url = URL.createObjectURL(blob)
|
|
90
|
+
channelCount: 1
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
audioContext = new AudioContext();
|
|
94
|
+
if (audioContext.state === "suspended") await audioContext.resume();
|
|
95
|
+
const blob = new Blob([mic_downsampler_worklet_default], { type: "application/javascript" });
|
|
96
|
+
const url = URL.createObjectURL(blob);
|
|
106
97
|
try {
|
|
107
|
-
await audioContext.audioWorklet.addModule(url)
|
|
98
|
+
await audioContext.audioWorklet.addModule(url);
|
|
108
99
|
} finally {
|
|
109
|
-
URL.revokeObjectURL(url)
|
|
100
|
+
URL.revokeObjectURL(url);
|
|
110
101
|
}
|
|
111
|
-
sourceNode = audioContext.createMediaStreamSource(mediaStream)
|
|
112
|
-
workletNode = new AudioWorkletNode(audioContext,
|
|
102
|
+
sourceNode = audioContext.createMediaStreamSource(mediaStream);
|
|
103
|
+
workletNode = new AudioWorkletNode(audioContext, "mic-downsampler");
|
|
113
104
|
workletNode.port.onmessage = (event) => {
|
|
114
|
-
if (muted) return
|
|
115
|
-
options.onChunk(event.data)
|
|
116
|
-
}
|
|
105
|
+
if (muted) return;
|
|
106
|
+
options.onChunk(event.data);
|
|
107
|
+
};
|
|
117
108
|
if (options.onVolume) {
|
|
118
|
-
analyser = audioContext.createAnalyser()
|
|
119
|
-
analyser.fftSize = 256
|
|
120
|
-
sourceNode.connect(analyser)
|
|
121
|
-
const buf = new Float32Array(analyser.fftSize)
|
|
109
|
+
analyser = audioContext.createAnalyser();
|
|
110
|
+
analyser.fftSize = 256;
|
|
111
|
+
sourceNode.connect(analyser);
|
|
112
|
+
const buf = new Float32Array(analyser.fftSize);
|
|
122
113
|
volumeTimer = setInterval(() => {
|
|
123
|
-
if (!analyser) return
|
|
124
|
-
analyser.getFloatTimeDomainData(buf)
|
|
125
|
-
options.onVolume?.(computeRms(buf))
|
|
126
|
-
}, VOLUME_INTERVAL_MS)
|
|
114
|
+
if (!analyser) return;
|
|
115
|
+
analyser.getFloatTimeDomainData(buf);
|
|
116
|
+
options.onVolume?.(computeRms(buf));
|
|
117
|
+
}, VOLUME_INTERVAL_MS);
|
|
127
118
|
}
|
|
128
|
-
sourceNode.connect(workletNode)
|
|
129
|
-
const sink = audioContext.createGain()
|
|
130
|
-
sink.gain.value = 0
|
|
131
|
-
workletNode.connect(sink).connect(audioContext.destination)
|
|
132
|
-
capturing = true
|
|
119
|
+
sourceNode.connect(workletNode);
|
|
120
|
+
const sink = audioContext.createGain();
|
|
121
|
+
sink.gain.value = 0;
|
|
122
|
+
workletNode.connect(sink).connect(audioContext.destination);
|
|
123
|
+
capturing = true;
|
|
133
124
|
} catch (err) {
|
|
134
|
-
const wrapped =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
throw wrapped
|
|
125
|
+
const wrapped = err instanceof Error ? err : new Error(typeof err === "string" ? err : "capture failed");
|
|
126
|
+
options.onError?.(wrapped);
|
|
127
|
+
throw wrapped;
|
|
138
128
|
}
|
|
139
|
-
}
|
|
129
|
+
};
|
|
140
130
|
const stop = () => {
|
|
141
|
-
if (!capturing) return
|
|
142
|
-
capturing = false
|
|
131
|
+
if (!capturing) return;
|
|
132
|
+
capturing = false;
|
|
143
133
|
if (volumeTimer) {
|
|
144
|
-
clearInterval(volumeTimer)
|
|
145
|
-
volumeTimer = null
|
|
134
|
+
clearInterval(volumeTimer);
|
|
135
|
+
volumeTimer = null;
|
|
146
136
|
}
|
|
147
137
|
try {
|
|
148
|
-
workletNode?.disconnect()
|
|
149
|
-
analyser?.disconnect()
|
|
150
|
-
sourceNode?.disconnect()
|
|
151
|
-
} catch {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
138
|
+
workletNode?.disconnect();
|
|
139
|
+
analyser?.disconnect();
|
|
140
|
+
sourceNode?.disconnect();
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
workletNode = null;
|
|
144
|
+
analyser = null;
|
|
145
|
+
sourceNode = null;
|
|
155
146
|
if (mediaStream) {
|
|
156
|
-
for (const track of mediaStream.getTracks()) track.stop()
|
|
157
|
-
mediaStream = null
|
|
147
|
+
for (const track of mediaStream.getTracks()) track.stop();
|
|
148
|
+
mediaStream = null;
|
|
158
149
|
}
|
|
159
|
-
if (audioContext && audioContext.state !==
|
|
160
|
-
void audioContext.close().catch(() => void 0)
|
|
150
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
151
|
+
void audioContext.close().catch(() => void 0);
|
|
161
152
|
}
|
|
162
|
-
audioContext = null
|
|
163
|
-
}
|
|
153
|
+
audioContext = null;
|
|
154
|
+
};
|
|
164
155
|
return {
|
|
165
156
|
start,
|
|
166
157
|
stop,
|
|
167
158
|
mute: (v) => {
|
|
168
|
-
muted = v
|
|
159
|
+
muted = v;
|
|
169
160
|
},
|
|
170
|
-
isCapturing: () => capturing
|
|
171
|
-
}
|
|
172
|
-
}
|
|
161
|
+
isCapturing: () => capturing
|
|
162
|
+
};
|
|
163
|
+
};
|
|
173
164
|
|
|
174
165
|
// src/AudioPlayback.ts
|
|
175
|
-
var DEFAULT_SAMPLE_RATE = 16e3
|
|
176
|
-
var VOLUME_INTERVAL_MS2 = 100
|
|
166
|
+
var DEFAULT_SAMPLE_RATE = 16e3;
|
|
167
|
+
var VOLUME_INTERVAL_MS2 = 100;
|
|
177
168
|
var createAudioPlayback = (options = {}) => {
|
|
178
|
-
const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE
|
|
179
|
-
let audioContext = null
|
|
180
|
-
let gainNode = null
|
|
181
|
-
let analyser = null
|
|
182
|
-
let volumeTimer = null
|
|
183
|
-
let nextStartTime = 0
|
|
184
|
-
let scheduledNodes = []
|
|
185
|
-
let speaking = false
|
|
169
|
+
const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
170
|
+
let audioContext = null;
|
|
171
|
+
let gainNode = null;
|
|
172
|
+
let analyser = null;
|
|
173
|
+
let volumeTimer = null;
|
|
174
|
+
let nextStartTime = 0;
|
|
175
|
+
let scheduledNodes = [];
|
|
176
|
+
let speaking = false;
|
|
186
177
|
const ensureContext = async () => {
|
|
187
178
|
if (audioContext) {
|
|
188
|
-
if (audioContext.state ===
|
|
189
|
-
return
|
|
179
|
+
if (audioContext.state === "suspended") await audioContext.resume();
|
|
180
|
+
return;
|
|
190
181
|
}
|
|
191
|
-
audioContext = new AudioContext({ sampleRate })
|
|
192
|
-
gainNode = audioContext.createGain()
|
|
182
|
+
audioContext = new AudioContext({ sampleRate });
|
|
183
|
+
gainNode = audioContext.createGain();
|
|
193
184
|
if (options.onVolume) {
|
|
194
|
-
analyser = audioContext.createAnalyser()
|
|
195
|
-
analyser.fftSize = 256
|
|
196
|
-
gainNode.connect(analyser)
|
|
197
|
-
const buf = new Float32Array(analyser.fftSize)
|
|
185
|
+
analyser = audioContext.createAnalyser();
|
|
186
|
+
analyser.fftSize = 256;
|
|
187
|
+
gainNode.connect(analyser);
|
|
188
|
+
const buf = new Float32Array(analyser.fftSize);
|
|
198
189
|
volumeTimer = setInterval(() => {
|
|
199
|
-
if (!analyser) return
|
|
200
|
-
analyser.getFloatTimeDomainData(buf)
|
|
201
|
-
let sum = 0
|
|
202
|
-
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
|
|
203
|
-
const rms = Math.sqrt(sum / buf.length)
|
|
204
|
-
options.onVolume?.(Math.min(1, rms * 1.8))
|
|
205
|
-
}, VOLUME_INTERVAL_MS2)
|
|
190
|
+
if (!analyser) return;
|
|
191
|
+
analyser.getFloatTimeDomainData(buf);
|
|
192
|
+
let sum = 0;
|
|
193
|
+
for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
|
|
194
|
+
const rms = Math.sqrt(sum / buf.length);
|
|
195
|
+
options.onVolume?.(Math.min(1, rms * 1.8));
|
|
196
|
+
}, VOLUME_INTERVAL_MS2);
|
|
206
197
|
}
|
|
207
|
-
gainNode.connect(audioContext.destination)
|
|
208
|
-
nextStartTime = audioContext.currentTime
|
|
209
|
-
}
|
|
198
|
+
gainNode.connect(audioContext.destination);
|
|
199
|
+
nextStartTime = audioContext.currentTime;
|
|
200
|
+
};
|
|
210
201
|
const setSpeaking = (v) => {
|
|
211
|
-
if (v === speaking) return
|
|
212
|
-
speaking = v
|
|
213
|
-
options.onSpeakingChange?.(v)
|
|
214
|
-
}
|
|
202
|
+
if (v === speaking) return;
|
|
203
|
+
speaking = v;
|
|
204
|
+
options.onSpeakingChange?.(v);
|
|
205
|
+
};
|
|
215
206
|
const pruneFinished = () => {
|
|
216
|
-
const now = audioContext?.currentTime ?? 0
|
|
207
|
+
const now = audioContext?.currentTime ?? 0;
|
|
217
208
|
scheduledNodes = scheduledNodes.filter((n) => {
|
|
218
|
-
const node = n
|
|
219
|
-
return (node._endsAt ?? 0) > now
|
|
220
|
-
})
|
|
221
|
-
if (scheduledNodes.length === 0) setSpeaking(false)
|
|
222
|
-
}
|
|
209
|
+
const node = n;
|
|
210
|
+
return (node._endsAt ?? 0) > now;
|
|
211
|
+
});
|
|
212
|
+
if (scheduledNodes.length === 0) setSpeaking(false);
|
|
213
|
+
};
|
|
223
214
|
const enqueue = (pcm) => {
|
|
224
215
|
if (!audioContext) {
|
|
225
|
-
void ensureContext().then(() => enqueue(pcm))
|
|
226
|
-
return
|
|
216
|
+
void ensureContext().then(() => enqueue(pcm));
|
|
217
|
+
return;
|
|
227
218
|
}
|
|
228
|
-
if (!audioContext || !gainNode) return
|
|
229
|
-
const int16 = new Int16Array(pcm)
|
|
230
|
-
if (int16.length === 0) return
|
|
231
|
-
const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate)
|
|
232
|
-
const float32 = audioBuffer.getChannelData(0)
|
|
219
|
+
if (!audioContext || !gainNode) return;
|
|
220
|
+
const int16 = new Int16Array(pcm);
|
|
221
|
+
if (int16.length === 0) return;
|
|
222
|
+
const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate);
|
|
223
|
+
const float32 = audioBuffer.getChannelData(0);
|
|
233
224
|
for (let i = 0; i < int16.length; i++) {
|
|
234
|
-
float32[i] = int16[i] / 32768
|
|
225
|
+
float32[i] = int16[i] / 32768;
|
|
235
226
|
}
|
|
236
|
-
const node = audioContext.createBufferSource()
|
|
237
|
-
node.buffer = audioBuffer
|
|
238
|
-
node.connect(gainNode)
|
|
239
|
-
const now = audioContext.currentTime
|
|
240
|
-
const startAt = Math.max(now, nextStartTime)
|
|
241
|
-
node.start(startAt)
|
|
242
|
-
const duration = int16.length / sampleRate
|
|
243
|
-
node._endsAt = startAt + duration
|
|
244
|
-
nextStartTime = startAt + duration
|
|
245
|
-
scheduledNodes.push(node)
|
|
246
|
-
setSpeaking(true)
|
|
247
|
-
node.onended = () => pruneFinished()
|
|
248
|
-
}
|
|
227
|
+
const node = audioContext.createBufferSource();
|
|
228
|
+
node.buffer = audioBuffer;
|
|
229
|
+
node.connect(gainNode);
|
|
230
|
+
const now = audioContext.currentTime;
|
|
231
|
+
const startAt = Math.max(now, nextStartTime);
|
|
232
|
+
node.start(startAt);
|
|
233
|
+
const duration = int16.length / sampleRate;
|
|
234
|
+
node._endsAt = startAt + duration;
|
|
235
|
+
nextStartTime = startAt + duration;
|
|
236
|
+
scheduledNodes.push(node);
|
|
237
|
+
setSpeaking(true);
|
|
238
|
+
node.onended = () => pruneFinished();
|
|
239
|
+
};
|
|
249
240
|
const flush = () => {
|
|
250
|
-
if (!audioContext || !gainNode) return
|
|
241
|
+
if (!audioContext || !gainNode) return;
|
|
251
242
|
for (const node of scheduledNodes) {
|
|
252
243
|
try {
|
|
253
|
-
node.stop()
|
|
254
|
-
} catch {
|
|
244
|
+
node.stop();
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
255
247
|
}
|
|
256
|
-
scheduledNodes = []
|
|
257
|
-
gainNode.disconnect()
|
|
258
|
-
gainNode = audioContext.createGain()
|
|
248
|
+
scheduledNodes = [];
|
|
249
|
+
gainNode.disconnect();
|
|
250
|
+
gainNode = audioContext.createGain();
|
|
259
251
|
if (analyser) {
|
|
260
|
-
analyser.disconnect()
|
|
261
|
-
gainNode.connect(analyser)
|
|
252
|
+
analyser.disconnect();
|
|
253
|
+
gainNode.connect(analyser);
|
|
262
254
|
}
|
|
263
|
-
gainNode.connect(audioContext.destination)
|
|
264
|
-
nextStartTime = audioContext.currentTime
|
|
265
|
-
setSpeaking(false)
|
|
266
|
-
}
|
|
255
|
+
gainNode.connect(audioContext.destination);
|
|
256
|
+
nextStartTime = audioContext.currentTime;
|
|
257
|
+
setSpeaking(false);
|
|
258
|
+
};
|
|
267
259
|
const close = () => {
|
|
268
|
-
flush()
|
|
260
|
+
flush();
|
|
269
261
|
if (volumeTimer) {
|
|
270
|
-
clearInterval(volumeTimer)
|
|
271
|
-
volumeTimer = null
|
|
262
|
+
clearInterval(volumeTimer);
|
|
263
|
+
volumeTimer = null;
|
|
272
264
|
}
|
|
273
|
-
if (audioContext && audioContext.state !==
|
|
274
|
-
void audioContext.close().catch(() => void 0)
|
|
265
|
+
if (audioContext && audioContext.state !== "closed") {
|
|
266
|
+
void audioContext.close().catch(() => void 0);
|
|
275
267
|
}
|
|
276
|
-
audioContext = null
|
|
277
|
-
gainNode = null
|
|
278
|
-
analyser = null
|
|
279
|
-
}
|
|
268
|
+
audioContext = null;
|
|
269
|
+
gainNode = null;
|
|
270
|
+
analyser = null;
|
|
271
|
+
};
|
|
280
272
|
const resume = async () => {
|
|
281
|
-
await ensureContext()
|
|
282
|
-
}
|
|
283
|
-
return { enqueue, flush, close, resume }
|
|
284
|
-
}
|
|
273
|
+
await ensureContext();
|
|
274
|
+
};
|
|
275
|
+
return { enqueue, flush, close, resume };
|
|
276
|
+
};
|
|
285
277
|
|
|
286
278
|
// src/ReconnectingWebSocket.ts
|
|
287
|
-
var READYSTATE_OPEN = 1
|
|
288
|
-
var READYSTATE_CLOSED = 3
|
|
279
|
+
var READYSTATE_OPEN = 1;
|
|
280
|
+
var READYSTATE_CLOSED = 3;
|
|
289
281
|
var createReconnectingWebSocket = (options, onEvent) => {
|
|
290
|
-
const maxRetries = options.maxRetries ?? 3
|
|
291
|
-
const initialBackoff = options.initialBackoffMs ?? 500
|
|
292
|
-
const maxBackoff = options.maxBackoffMs ?? 8e3
|
|
293
|
-
let ws = null
|
|
294
|
-
let intentionalClose = false
|
|
295
|
-
let retries = 0
|
|
296
|
-
let backoff = initialBackoff
|
|
297
|
-
let reconnectTimer = null
|
|
282
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
283
|
+
const initialBackoff = options.initialBackoffMs ?? 500;
|
|
284
|
+
const maxBackoff = options.maxBackoffMs ?? 8e3;
|
|
285
|
+
let ws = null;
|
|
286
|
+
let intentionalClose = false;
|
|
287
|
+
let retries = 0;
|
|
288
|
+
let backoff = initialBackoff;
|
|
289
|
+
let reconnectTimer = null;
|
|
298
290
|
const openOnce = () => {
|
|
299
|
-
ws = options.wsFactory(options.url)
|
|
300
|
-
ws.binaryType =
|
|
291
|
+
ws = options.wsFactory(options.url);
|
|
292
|
+
ws.binaryType = "arraybuffer";
|
|
301
293
|
ws.onopen = () => {
|
|
302
|
-
if (retries === 0) onEvent({ type:
|
|
303
|
-
else onEvent({ type:
|
|
304
|
-
retries = 0
|
|
305
|
-
backoff = initialBackoff
|
|
306
|
-
}
|
|
294
|
+
if (retries === 0) onEvent({ type: "open" });
|
|
295
|
+
else onEvent({ type: "reconnected" });
|
|
296
|
+
retries = 0;
|
|
297
|
+
backoff = initialBackoff;
|
|
298
|
+
};
|
|
307
299
|
ws.onmessage = (ev) => {
|
|
308
|
-
onEvent({ type:
|
|
309
|
-
}
|
|
300
|
+
onEvent({ type: "message", data: ev.data });
|
|
301
|
+
};
|
|
310
302
|
ws.onerror = () => {
|
|
311
|
-
onEvent({ type:
|
|
312
|
-
}
|
|
303
|
+
onEvent({ type: "error", error: new Error("WebSocket error") });
|
|
304
|
+
};
|
|
313
305
|
ws.onclose = (ev) => {
|
|
314
|
-
ws = null
|
|
315
|
-
const shouldRetry = !intentionalClose && retries < maxRetries
|
|
306
|
+
ws = null;
|
|
307
|
+
const shouldRetry = !intentionalClose && retries < maxRetries;
|
|
316
308
|
if (!shouldRetry) {
|
|
317
309
|
onEvent({
|
|
318
|
-
type:
|
|
310
|
+
type: "close",
|
|
319
311
|
code: ev.code,
|
|
320
312
|
reason: ev.reason,
|
|
321
|
-
permanent: true
|
|
322
|
-
})
|
|
323
|
-
return
|
|
313
|
+
permanent: true
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
324
316
|
}
|
|
325
317
|
onEvent({
|
|
326
|
-
type:
|
|
318
|
+
type: "close",
|
|
327
319
|
code: ev.code,
|
|
328
320
|
reason: ev.reason,
|
|
329
|
-
permanent: false
|
|
330
|
-
})
|
|
331
|
-
retries
|
|
332
|
-
const delay = Math.min(backoff, maxBackoff)
|
|
333
|
-
backoff = Math.min(backoff * 2, maxBackoff)
|
|
334
|
-
reconnectTimer = setTimeout(openOnce, delay)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
openOnce()
|
|
321
|
+
permanent: false
|
|
322
|
+
});
|
|
323
|
+
retries++;
|
|
324
|
+
const delay = Math.min(backoff, maxBackoff);
|
|
325
|
+
backoff = Math.min(backoff * 2, maxBackoff);
|
|
326
|
+
reconnectTimer = setTimeout(openOnce, delay);
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
openOnce();
|
|
338
330
|
return {
|
|
339
331
|
send: (data) => {
|
|
340
|
-
if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)
|
|
332
|
+
if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
|
|
341
333
|
},
|
|
342
|
-
close: (code = 1e3, reason =
|
|
343
|
-
intentionalClose = true
|
|
334
|
+
close: (code = 1e3, reason = "client-requested") => {
|
|
335
|
+
intentionalClose = true;
|
|
344
336
|
if (reconnectTimer) {
|
|
345
|
-
clearTimeout(reconnectTimer)
|
|
346
|
-
reconnectTimer = null
|
|
337
|
+
clearTimeout(reconnectTimer);
|
|
338
|
+
reconnectTimer = null;
|
|
347
339
|
}
|
|
348
340
|
try {
|
|
349
|
-
ws?.close(code, reason)
|
|
350
|
-
} catch {
|
|
341
|
+
ws?.close(code, reason);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
351
344
|
},
|
|
352
|
-
readyState: () => ws?.readyState ?? READYSTATE_CLOSED
|
|
353
|
-
}
|
|
354
|
-
}
|
|
345
|
+
readyState: () => ws?.readyState ?? READYSTATE_CLOSED
|
|
346
|
+
};
|
|
347
|
+
};
|
|
355
348
|
|
|
356
349
|
// src/protocol.ts
|
|
357
350
|
var createProtocolState = () => ({
|
|
358
|
-
state:
|
|
351
|
+
state: "idle",
|
|
359
352
|
transcript: [],
|
|
360
353
|
agentBubbleId: null,
|
|
361
354
|
idCounter: 0,
|
|
362
|
-
endReason: null
|
|
363
|
-
})
|
|
355
|
+
endReason: null
|
|
356
|
+
});
|
|
364
357
|
var mapEndReason = (raw) => {
|
|
365
|
-
if (raw ===
|
|
366
|
-
if (raw ===
|
|
367
|
-
if (raw ===
|
|
368
|
-
return
|
|
369
|
-
}
|
|
358
|
+
if (raw === "agent_ended") return "agent_ended";
|
|
359
|
+
if (raw === "caller_hung_up") return "user_hangup";
|
|
360
|
+
if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
|
|
361
|
+
return "error";
|
|
362
|
+
};
|
|
370
363
|
function handleServerMessage(raw, state, cb) {
|
|
371
|
-
let msg
|
|
364
|
+
let msg;
|
|
372
365
|
try {
|
|
373
|
-
msg = JSON.parse(raw)
|
|
366
|
+
msg = JSON.parse(raw);
|
|
374
367
|
} catch {
|
|
375
|
-
return
|
|
368
|
+
return;
|
|
376
369
|
}
|
|
377
370
|
switch (msg.type) {
|
|
378
|
-
case
|
|
379
|
-
cb.onConnected()
|
|
380
|
-
setState(state,
|
|
381
|
-
return
|
|
382
|
-
case
|
|
383
|
-
const text = msg.text ??
|
|
384
|
-
if (!text) return
|
|
385
|
-
const isFinal = !!msg.isFinal
|
|
386
|
-
if (!isFinal) setState(state,
|
|
387
|
-
upsertUserPartial(state, text, isFinal)
|
|
388
|
-
cb.onTranscript(state.transcript)
|
|
389
|
-
return
|
|
371
|
+
case "connected":
|
|
372
|
+
cb.onConnected();
|
|
373
|
+
setState(state, "listening", cb);
|
|
374
|
+
return;
|
|
375
|
+
case "transcript": {
|
|
376
|
+
const text = msg.text ?? "";
|
|
377
|
+
if (!text) return;
|
|
378
|
+
const isFinal = !!msg.isFinal;
|
|
379
|
+
if (!isFinal) setState(state, "user_speaking", cb);
|
|
380
|
+
upsertUserPartial(state, text, isFinal);
|
|
381
|
+
cb.onTranscript(state.transcript);
|
|
382
|
+
return;
|
|
390
383
|
}
|
|
391
|
-
case
|
|
392
|
-
const id = `m${state.idCounter++}
|
|
393
|
-
state.agentBubbleId = id
|
|
394
|
-
state.transcript = [...state.transcript, { id, role:
|
|
395
|
-
cb.onTranscript(state.transcript)
|
|
396
|
-
const seq = typeof msg.seq ===
|
|
397
|
-
cb.onAgentTurnStart(seq)
|
|
398
|
-
setState(state,
|
|
399
|
-
return
|
|
384
|
+
case "agent_turn_start": {
|
|
385
|
+
const id = `m${state.idCounter++}`;
|
|
386
|
+
state.agentBubbleId = id;
|
|
387
|
+
state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
|
|
388
|
+
cb.onTranscript(state.transcript);
|
|
389
|
+
const seq = typeof msg.seq === "number" ? msg.seq : void 0;
|
|
390
|
+
cb.onAgentTurnStart(seq);
|
|
391
|
+
setState(state, "agent_speaking", cb);
|
|
392
|
+
return;
|
|
400
393
|
}
|
|
401
|
-
case
|
|
402
|
-
const delta = msg.text ??
|
|
403
|
-
if (!delta || !state.agentBubbleId) return
|
|
404
|
-
const id = state.agentBubbleId
|
|
405
|
-
state.transcript = state.transcript.map(
|
|
406
|
-
e.id === id && e.role ===
|
|
407
|
-
)
|
|
408
|
-
cb.onTranscript(state.transcript)
|
|
409
|
-
return
|
|
394
|
+
case "agent_text": {
|
|
395
|
+
const delta = msg.text ?? "";
|
|
396
|
+
if (!delta || !state.agentBubbleId) return;
|
|
397
|
+
const id = state.agentBubbleId;
|
|
398
|
+
state.transcript = state.transcript.map(
|
|
399
|
+
(e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
|
|
400
|
+
);
|
|
401
|
+
cb.onTranscript(state.transcript);
|
|
402
|
+
return;
|
|
410
403
|
}
|
|
411
|
-
case
|
|
412
|
-
state.agentBubbleId = null
|
|
413
|
-
const seq = typeof msg.seq ===
|
|
414
|
-
cb.onAgentTurnEnd(seq)
|
|
415
|
-
setState(state,
|
|
416
|
-
return
|
|
404
|
+
case "agent_turn_end": {
|
|
405
|
+
state.agentBubbleId = null;
|
|
406
|
+
const seq = typeof msg.seq === "number" ? msg.seq : void 0;
|
|
407
|
+
cb.onAgentTurnEnd(seq);
|
|
408
|
+
setState(state, "listening", cb);
|
|
409
|
+
return;
|
|
417
410
|
}
|
|
418
|
-
case
|
|
419
|
-
cb.onInterrupt()
|
|
420
|
-
return
|
|
421
|
-
case
|
|
422
|
-
const committed = (msg.committedText ??
|
|
411
|
+
case "interrupt":
|
|
412
|
+
cb.onInterrupt();
|
|
413
|
+
return;
|
|
414
|
+
case "agent_turn_abort": {
|
|
415
|
+
const committed = (msg.committedText ?? "").trim();
|
|
423
416
|
if (state.agentBubbleId) {
|
|
424
|
-
const id = state.agentBubbleId
|
|
417
|
+
const id = state.agentBubbleId;
|
|
425
418
|
if (committed) {
|
|
426
|
-
state.transcript = state.transcript.map(
|
|
427
|
-
e.id === id && e.role ===
|
|
428
|
-
)
|
|
419
|
+
state.transcript = state.transcript.map(
|
|
420
|
+
(e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
|
|
421
|
+
);
|
|
429
422
|
} else {
|
|
430
|
-
state.transcript = state.transcript.filter((e) => e.id !== id)
|
|
423
|
+
state.transcript = state.transcript.filter((e) => e.id !== id);
|
|
431
424
|
}
|
|
432
|
-
cb.onTranscript(state.transcript)
|
|
425
|
+
cb.onTranscript(state.transcript);
|
|
433
426
|
}
|
|
434
|
-
state.agentBubbleId = null
|
|
435
|
-
return
|
|
427
|
+
state.agentBubbleId = null;
|
|
428
|
+
return;
|
|
436
429
|
}
|
|
437
|
-
case
|
|
430
|
+
case "tool_call":
|
|
438
431
|
state.transcript = [
|
|
439
432
|
...state.transcript,
|
|
440
433
|
{
|
|
441
434
|
id: `m${state.idCounter++}`,
|
|
442
|
-
role:
|
|
443
|
-
text: `\u2192 ${String(msg.tool ??
|
|
444
|
-
}
|
|
445
|
-
]
|
|
446
|
-
cb.onTranscript(state.transcript)
|
|
447
|
-
return
|
|
448
|
-
case
|
|
435
|
+
role: "tool",
|
|
436
|
+
text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
|
|
437
|
+
}
|
|
438
|
+
];
|
|
439
|
+
cb.onTranscript(state.transcript);
|
|
440
|
+
return;
|
|
441
|
+
case "tool_result":
|
|
449
442
|
state.transcript = [
|
|
450
443
|
...state.transcript,
|
|
451
444
|
{
|
|
452
445
|
id: `m${state.idCounter++}`,
|
|
453
|
-
role:
|
|
454
|
-
text: `${msg.ok ?
|
|
455
|
-
}
|
|
456
|
-
]
|
|
457
|
-
cb.onTranscript(state.transcript)
|
|
458
|
-
return
|
|
459
|
-
case
|
|
460
|
-
const toolCallId = String(msg.toolCallId ??
|
|
461
|
-
const name = String(msg.name ??
|
|
462
|
-
const args = msg.args ?? {}
|
|
463
|
-
if (!toolCallId || !name) return
|
|
464
|
-
cb.onClientToolCall({ toolCallId, name, args })
|
|
465
|
-
return
|
|
446
|
+
role: "tool",
|
|
447
|
+
text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
|
|
448
|
+
}
|
|
449
|
+
];
|
|
450
|
+
cb.onTranscript(state.transcript);
|
|
451
|
+
return;
|
|
452
|
+
case "client_tool_call": {
|
|
453
|
+
const toolCallId = String(msg.toolCallId ?? "");
|
|
454
|
+
const name = String(msg.name ?? "");
|
|
455
|
+
const args = msg.args ?? {};
|
|
456
|
+
if (!toolCallId || !name) return;
|
|
457
|
+
cb.onClientToolCall({ toolCallId, name, args });
|
|
458
|
+
return;
|
|
466
459
|
}
|
|
467
|
-
case
|
|
468
|
-
const reasonRaw = String(msg.reason ??
|
|
469
|
-
const reason = mapEndReason(reasonRaw)
|
|
470
|
-
state.endReason = reason
|
|
460
|
+
case "call_end": {
|
|
461
|
+
const reasonRaw = String(msg.reason ?? "");
|
|
462
|
+
const reason = mapEndReason(reasonRaw);
|
|
463
|
+
state.endReason = reason;
|
|
471
464
|
state.transcript = [
|
|
472
465
|
...state.transcript,
|
|
473
466
|
{
|
|
474
467
|
id: `m${state.idCounter++}`,
|
|
475
|
-
role:
|
|
476
|
-
text: `call ended${reasonRaw ? ` (${reasonRaw})` :
|
|
477
|
-
}
|
|
478
|
-
]
|
|
479
|
-
cb.onTranscript(state.transcript)
|
|
480
|
-
cb.onCallEnd(reason)
|
|
481
|
-
return
|
|
468
|
+
role: "system",
|
|
469
|
+
text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
|
|
470
|
+
}
|
|
471
|
+
];
|
|
472
|
+
cb.onTranscript(state.transcript);
|
|
473
|
+
cb.onCallEnd(reason);
|
|
474
|
+
return;
|
|
482
475
|
}
|
|
483
|
-
case
|
|
484
|
-
const code = msg.code ??
|
|
485
|
-
const message = msg.message ??
|
|
486
|
-
cb.onError({ code, message })
|
|
487
|
-
return
|
|
476
|
+
case "error": {
|
|
477
|
+
const code = msg.code ?? "server_error";
|
|
478
|
+
const message = msg.message ?? "server error";
|
|
479
|
+
cb.onError({ code, message });
|
|
480
|
+
return;
|
|
488
481
|
}
|
|
489
482
|
}
|
|
490
483
|
}
|
|
491
484
|
var setState = (state, next, cb) => {
|
|
492
|
-
if (state.state === next) return
|
|
493
|
-
cb.onState(next)
|
|
494
|
-
}
|
|
485
|
+
if (state.state === next) return;
|
|
486
|
+
cb.onState(next);
|
|
487
|
+
};
|
|
495
488
|
var upsertUserPartial = (state, text, isFinal) => {
|
|
496
|
-
let idx = -1
|
|
489
|
+
let idx = -1;
|
|
497
490
|
for (let i = state.transcript.length - 1; i >= 0; i--) {
|
|
498
|
-
const e = state.transcript[i]
|
|
499
|
-
if (e.role ===
|
|
500
|
-
idx = i
|
|
501
|
-
break
|
|
491
|
+
const e = state.transcript[i];
|
|
492
|
+
if (e.role === "user" && e.committed === false) {
|
|
493
|
+
idx = i;
|
|
494
|
+
break;
|
|
502
495
|
}
|
|
503
496
|
}
|
|
504
497
|
if (idx === -1) {
|
|
505
498
|
state.transcript = [
|
|
506
499
|
...state.transcript,
|
|
507
|
-
{ id: `m${state.idCounter++}`, role:
|
|
508
|
-
]
|
|
509
|
-
return
|
|
500
|
+
{ id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
|
|
501
|
+
];
|
|
502
|
+
return;
|
|
510
503
|
}
|
|
511
|
-
const target = state.transcript[idx]
|
|
512
|
-
const next = [...state.transcript]
|
|
513
|
-
next[idx] = { ...target, text, committed: isFinal }
|
|
514
|
-
state.transcript = next
|
|
515
|
-
}
|
|
504
|
+
const target = state.transcript[idx];
|
|
505
|
+
const next = [...state.transcript];
|
|
506
|
+
next[idx] = { ...target, text, committed: isFinal };
|
|
507
|
+
state.transcript = next;
|
|
508
|
+
};
|
|
516
509
|
function buildWsUrl(args) {
|
|
517
|
-
const base = new URL(args.apiBase)
|
|
518
|
-
const proto = base.protocol ===
|
|
519
|
-
const bargeQS = args.bargeIn === false ?
|
|
520
|
-
return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}
|
|
510
|
+
const base = new URL(args.apiBase);
|
|
511
|
+
const proto = base.protocol === "https:" ? "wss:" : "ws:";
|
|
512
|
+
const bargeQS = args.bargeIn === false ? "&barge=off" : "";
|
|
513
|
+
return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
|
|
521
514
|
}
|
|
522
515
|
|
|
523
516
|
// src/clientTools.ts
|
|
524
|
-
var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]
|
|
525
|
-
var MAX_TOOLS = 64
|
|
526
|
-
var MAX_USAGE = 500
|
|
527
|
-
var MAX_TIMEOUT_MS = 3e4
|
|
517
|
+
var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
518
|
+
var MAX_TOOLS = 64;
|
|
519
|
+
var MAX_USAGE = 500;
|
|
520
|
+
var MAX_TIMEOUT_MS = 3e4;
|
|
528
521
|
var validateClientToolMap = (tools) => {
|
|
529
|
-
if (tools === void 0) return
|
|
530
|
-
if (typeof tools !==
|
|
531
|
-
throw new Error(
|
|
522
|
+
if (tools === void 0) return;
|
|
523
|
+
if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
|
|
524
|
+
throw new Error("clientTools must be an object keyed by tool name");
|
|
532
525
|
}
|
|
533
|
-
const entries = Object.entries(tools)
|
|
526
|
+
const entries = Object.entries(tools);
|
|
534
527
|
if (entries.length > MAX_TOOLS) {
|
|
535
|
-
throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)
|
|
528
|
+
throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`);
|
|
536
529
|
}
|
|
537
530
|
for (const [name, def] of entries) {
|
|
538
531
|
if (!NAME_RE.test(name)) {
|
|
539
532
|
throw new Error(
|
|
540
|
-
`clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)
|
|
541
|
-
)
|
|
533
|
+
`clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
|
|
534
|
+
);
|
|
542
535
|
}
|
|
543
|
-
if (!def || typeof def !==
|
|
544
|
-
throw new Error(`clientTools["${name}"]: must be an object`)
|
|
536
|
+
if (!def || typeof def !== "object") {
|
|
537
|
+
throw new Error(`clientTools["${name}"]: must be an object`);
|
|
545
538
|
}
|
|
546
|
-
if (typeof def.description !==
|
|
547
|
-
throw new Error(`clientTools["${name}"]: must have a description`)
|
|
539
|
+
if (typeof def.description !== "string" || def.description.length === 0) {
|
|
540
|
+
throw new Error(`clientTools["${name}"]: must have a description`);
|
|
548
541
|
}
|
|
549
|
-
if (typeof def.handler !==
|
|
550
|
-
throw new Error(`clientTools["${name}"]: must have a handler function`)
|
|
542
|
+
if (typeof def.handler !== "function") {
|
|
543
|
+
throw new Error(`clientTools["${name}"]: must have a handler function`);
|
|
551
544
|
}
|
|
552
545
|
if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
|
|
553
|
-
throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
|
|
546
|
+
throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
|
|
554
547
|
}
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
(!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)
|
|
558
|
-
) {
|
|
559
|
-
throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`)
|
|
548
|
+
if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
|
|
549
|
+
throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`);
|
|
560
550
|
}
|
|
561
551
|
}
|
|
562
|
-
}
|
|
552
|
+
};
|
|
563
553
|
var buildRegisterFrame = (tools) => ({
|
|
564
|
-
type:
|
|
554
|
+
type: "client_tools_register",
|
|
565
555
|
tools: Object.entries(tools).map(([name, def]) => ({
|
|
566
556
|
name,
|
|
567
557
|
description: def.description,
|
|
568
558
|
parameters: def.parameters,
|
|
569
|
-
...
|
|
570
|
-
...
|
|
571
|
-
}))
|
|
572
|
-
})
|
|
559
|
+
...def.usage !== void 0 ? { usage: def.usage } : {},
|
|
560
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
561
|
+
}))
|
|
562
|
+
});
|
|
573
563
|
var dispatchClientToolCall = (send, tools, frame) => {
|
|
574
564
|
const safeSend = (payload) => {
|
|
575
565
|
try {
|
|
576
|
-
send(payload)
|
|
577
|
-
} catch {
|
|
578
|
-
|
|
579
|
-
|
|
566
|
+
send(payload);
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
const tool = tools[frame.name];
|
|
580
571
|
if (!tool) {
|
|
581
572
|
safeSend({
|
|
582
|
-
type:
|
|
573
|
+
type: "client_tool_result",
|
|
583
574
|
toolCallId: frame.toolCallId,
|
|
584
|
-
error: `No handler for ${frame.name}
|
|
585
|
-
})
|
|
586
|
-
return
|
|
575
|
+
error: `No handler for ${frame.name}`
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
587
578
|
}
|
|
588
579
|
void (async () => {
|
|
589
580
|
try {
|
|
590
|
-
const out = await tool.handler(frame.args)
|
|
581
|
+
const out = await tool.handler(frame.args);
|
|
591
582
|
safeSend({
|
|
592
|
-
type:
|
|
583
|
+
type: "client_tool_result",
|
|
593
584
|
toolCallId: frame.toolCallId,
|
|
594
|
-
result: typeof out ===
|
|
595
|
-
})
|
|
585
|
+
result: typeof out === "string" ? out : JSON.stringify(out)
|
|
586
|
+
});
|
|
596
587
|
} catch (err) {
|
|
597
588
|
safeSend({
|
|
598
|
-
type:
|
|
589
|
+
type: "client_tool_result",
|
|
599
590
|
toolCallId: frame.toolCallId,
|
|
600
|
-
error: err instanceof Error ? err.message : String(err)
|
|
601
|
-
})
|
|
591
|
+
error: err instanceof Error ? err.message : String(err)
|
|
592
|
+
});
|
|
602
593
|
}
|
|
603
|
-
})()
|
|
604
|
-
}
|
|
594
|
+
})();
|
|
595
|
+
};
|
|
605
596
|
|
|
606
597
|
// src/ClientMarksBuffer.ts
|
|
607
598
|
var createClientMarksBuffer = (args) => {
|
|
608
|
-
const now = args.now ?? (() => performance.now())
|
|
609
|
-
let pendingFirstOutboundAt = null
|
|
610
|
-
const inFlight = /* @__PURE__ */ new Map()
|
|
599
|
+
const now = args.now ?? (() => performance.now());
|
|
600
|
+
let pendingFirstOutboundAt = null;
|
|
601
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
611
602
|
const tryEmit = (seq) => {
|
|
612
|
-
const slot = inFlight.get(seq)
|
|
613
|
-
if (!slot) return
|
|
614
|
-
if (!slot.ended) return
|
|
615
|
-
const marks = {}
|
|
603
|
+
const slot = inFlight.get(seq);
|
|
604
|
+
if (!slot) return;
|
|
605
|
+
if (!slot.ended) return;
|
|
606
|
+
const marks = {};
|
|
616
607
|
if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {
|
|
617
|
-
marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt
|
|
608
|
+
marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt;
|
|
618
609
|
}
|
|
619
610
|
args.send({
|
|
620
|
-
type:
|
|
611
|
+
type: "client_marks",
|
|
621
612
|
seq,
|
|
622
613
|
marks,
|
|
623
|
-
clientNow: Date.now()
|
|
624
|
-
})
|
|
625
|
-
inFlight.delete(seq)
|
|
626
|
-
}
|
|
614
|
+
clientNow: Date.now()
|
|
615
|
+
});
|
|
616
|
+
inFlight.delete(seq);
|
|
617
|
+
};
|
|
627
618
|
const markFirstOutboundAudio = () => {
|
|
628
|
-
if (pendingFirstOutboundAt !== null) return
|
|
629
|
-
pendingFirstOutboundAt = now()
|
|
630
|
-
}
|
|
619
|
+
if (pendingFirstOutboundAt !== null) return;
|
|
620
|
+
pendingFirstOutboundAt = now();
|
|
621
|
+
};
|
|
631
622
|
const markFirstAudibleOutput = () => {
|
|
632
|
-
let target
|
|
623
|
+
let target;
|
|
633
624
|
for (const slot of inFlight.values()) {
|
|
634
625
|
if (!slot.ended) {
|
|
635
|
-
target = slot
|
|
626
|
+
target = slot;
|
|
636
627
|
}
|
|
637
628
|
}
|
|
638
|
-
if (!target) return
|
|
639
|
-
if (target.firstAudibleAt !== null) return
|
|
640
|
-
target.firstAudibleAt = now()
|
|
641
|
-
}
|
|
629
|
+
if (!target) return;
|
|
630
|
+
if (target.firstAudibleAt !== null) return;
|
|
631
|
+
target.firstAudibleAt = now();
|
|
632
|
+
};
|
|
642
633
|
const onAgentTurnStart = (seq) => {
|
|
643
634
|
inFlight.set(seq, {
|
|
644
635
|
firstOutboundAt: pendingFirstOutboundAt,
|
|
645
636
|
firstAudibleAt: null,
|
|
646
|
-
ended: false
|
|
647
|
-
})
|
|
648
|
-
pendingFirstOutboundAt = null
|
|
649
|
-
}
|
|
637
|
+
ended: false
|
|
638
|
+
});
|
|
639
|
+
pendingFirstOutboundAt = null;
|
|
640
|
+
};
|
|
650
641
|
const onAgentTurnEnd = (seq) => {
|
|
651
|
-
const slot = inFlight.get(seq)
|
|
642
|
+
const slot = inFlight.get(seq);
|
|
652
643
|
if (!slot) {
|
|
653
|
-
args.send({ type:
|
|
654
|
-
return
|
|
644
|
+
args.send({ type: "client_marks", seq, marks: {}, clientNow: Date.now() });
|
|
645
|
+
return;
|
|
655
646
|
}
|
|
656
|
-
slot.ended = true
|
|
657
|
-
tryEmit(seq)
|
|
658
|
-
}
|
|
647
|
+
slot.ended = true;
|
|
648
|
+
tryEmit(seq);
|
|
649
|
+
};
|
|
659
650
|
const flush = () => {
|
|
660
651
|
for (const seq of [...inFlight.keys()]) {
|
|
661
|
-
const slot = inFlight.get(seq)
|
|
662
|
-
slot.ended = true
|
|
663
|
-
tryEmit(seq)
|
|
652
|
+
const slot = inFlight.get(seq);
|
|
653
|
+
slot.ended = true;
|
|
654
|
+
tryEmit(seq);
|
|
664
655
|
}
|
|
665
|
-
pendingFirstOutboundAt = null
|
|
666
|
-
}
|
|
656
|
+
pendingFirstOutboundAt = null;
|
|
657
|
+
};
|
|
667
658
|
return {
|
|
668
659
|
markFirstOutboundAudio,
|
|
669
660
|
markFirstAudibleOutput,
|
|
670
661
|
onAgentTurnStart,
|
|
671
662
|
onAgentTurnEnd,
|
|
672
|
-
flush
|
|
673
|
-
}
|
|
674
|
-
}
|
|
663
|
+
flush
|
|
664
|
+
};
|
|
665
|
+
};
|
|
675
666
|
|
|
676
667
|
// src/VoiceClient.ts
|
|
677
668
|
var BrowserVoiceClient = class {
|
|
678
669
|
constructor(args) {
|
|
679
|
-
this.rws = null
|
|
680
|
-
this.capture = null
|
|
681
|
-
this.playback = null
|
|
682
|
-
this.muted = false
|
|
683
|
-
this.inputVolume = 0
|
|
684
|
-
this.outputVolume = 0
|
|
685
|
-
this.startedAt = null
|
|
686
|
-
this.endedFired = false
|
|
687
|
-
this.lastError = null
|
|
670
|
+
this.rws = null;
|
|
671
|
+
this.capture = null;
|
|
672
|
+
this.playback = null;
|
|
673
|
+
this.muted = false;
|
|
674
|
+
this.inputVolume = 0;
|
|
675
|
+
this.outputVolume = 0;
|
|
676
|
+
this.startedAt = null;
|
|
677
|
+
this.endedFired = false;
|
|
678
|
+
this.lastError = null;
|
|
688
679
|
this.end = () => {
|
|
689
|
-
this.teardown(
|
|
690
|
-
}
|
|
680
|
+
this.teardown("user_hangup");
|
|
681
|
+
};
|
|
691
682
|
this.mute = () => {
|
|
692
|
-
if (this.muted) return
|
|
693
|
-
this.muted = true
|
|
694
|
-
this.capture?.mute(true)
|
|
695
|
-
}
|
|
683
|
+
if (this.muted) return;
|
|
684
|
+
this.muted = true;
|
|
685
|
+
this.capture?.mute(true);
|
|
686
|
+
};
|
|
696
687
|
this.unmute = () => {
|
|
697
|
-
if (!this.muted) return
|
|
698
|
-
this.muted = false
|
|
699
|
-
this.capture?.mute(false)
|
|
700
|
-
}
|
|
688
|
+
if (!this.muted) return;
|
|
689
|
+
this.muted = false;
|
|
690
|
+
this.capture?.mute(false);
|
|
691
|
+
};
|
|
701
692
|
// ---------------------------------------------------------------
|
|
702
693
|
// Internal
|
|
703
694
|
// ---------------------------------------------------------------
|
|
704
695
|
this.sendClientToolsRegister = () => {
|
|
705
|
-
const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
|
|
706
|
-
this.rws?.send(JSON.stringify(frame))
|
|
707
|
-
}
|
|
696
|
+
const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
|
|
697
|
+
this.rws?.send(JSON.stringify(frame));
|
|
698
|
+
};
|
|
708
699
|
this.setState = (next) => {
|
|
709
|
-
if (this.proto.state === next) return
|
|
710
|
-
this.proto.state = next
|
|
711
|
-
this.args.options.onStateChange?.(next)
|
|
712
|
-
}
|
|
700
|
+
if (this.proto.state === next) return;
|
|
701
|
+
this.proto.state = next;
|
|
702
|
+
this.args.options.onStateChange?.(next);
|
|
703
|
+
};
|
|
713
704
|
this.emitError = (err) => {
|
|
714
|
-
this.lastError = err
|
|
715
|
-
this.args.options.onError?.(err)
|
|
716
|
-
}
|
|
705
|
+
this.lastError = err;
|
|
706
|
+
this.args.options.onError?.(err);
|
|
707
|
+
};
|
|
717
708
|
this.handleSocketEvent = (ev) => {
|
|
718
709
|
switch (ev.type) {
|
|
719
|
-
case
|
|
720
|
-
void this.startCapture()
|
|
721
|
-
break
|
|
722
|
-
case
|
|
723
|
-
this.proto.transcript = []
|
|
724
|
-
this.proto.agentBubbleId = null
|
|
725
|
-
this.args.options.onTranscript?.(this.proto.transcript)
|
|
726
|
-
void this.startCapture()
|
|
727
|
-
this.setState(
|
|
728
|
-
break
|
|
729
|
-
case
|
|
730
|
-
if (typeof ev.data ===
|
|
710
|
+
case "open":
|
|
711
|
+
void this.startCapture();
|
|
712
|
+
break;
|
|
713
|
+
case "reconnected":
|
|
714
|
+
this.proto.transcript = [];
|
|
715
|
+
this.proto.agentBubbleId = null;
|
|
716
|
+
this.args.options.onTranscript?.(this.proto.transcript);
|
|
717
|
+
void this.startCapture();
|
|
718
|
+
this.setState("listening");
|
|
719
|
+
break;
|
|
720
|
+
case "message":
|
|
721
|
+
if (typeof ev.data === "string") {
|
|
731
722
|
handleServerMessage(ev.data, this.proto, {
|
|
732
723
|
onState: this.setState,
|
|
733
724
|
onTranscript: (entries) => this.args.options.onTranscript?.(entries),
|
|
734
725
|
onError: this.emitError,
|
|
735
726
|
onInterrupt: () => {
|
|
736
|
-
this.playback?.flush()
|
|
737
|
-
this.args.options.onInterrupt?.()
|
|
727
|
+
this.playback?.flush();
|
|
728
|
+
this.args.options.onInterrupt?.();
|
|
738
729
|
},
|
|
739
730
|
onAgentTurnStart: (seq) => {
|
|
740
|
-
if (typeof seq ===
|
|
741
|
-
this.args.options.onAgentTurnStart?.()
|
|
731
|
+
if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
|
|
732
|
+
this.args.options.onAgentTurnStart?.();
|
|
742
733
|
},
|
|
743
734
|
onAgentTurnEnd: (seq) => {
|
|
744
|
-
if (typeof seq ===
|
|
735
|
+
if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
|
|
745
736
|
},
|
|
746
737
|
onCallEnd: (reason) => this.teardown(reason),
|
|
747
738
|
onConnected: () => this.sendClientToolsRegister(),
|
|
748
|
-
onClientToolCall: (frame) =>
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
})
|
|
739
|
+
onClientToolCall: (frame) => dispatchClientToolCall(
|
|
740
|
+
(f) => this.rws?.send(JSON.stringify(f)),
|
|
741
|
+
this.args.options.clientTools ?? {},
|
|
742
|
+
frame
|
|
743
|
+
)
|
|
744
|
+
});
|
|
755
745
|
} else {
|
|
756
|
-
this.marks.markFirstAudibleOutput()
|
|
757
|
-
this.playback?.enqueue(ev.data)
|
|
746
|
+
this.marks.markFirstAudibleOutput();
|
|
747
|
+
this.playback?.enqueue(ev.data);
|
|
758
748
|
}
|
|
759
|
-
break
|
|
760
|
-
case
|
|
749
|
+
break;
|
|
750
|
+
case "close":
|
|
761
751
|
if (ev.permanent) {
|
|
762
|
-
const reason = this.proto.endReason ?? (this.lastError ?
|
|
763
|
-
this.teardown(reason)
|
|
752
|
+
const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
|
|
753
|
+
this.teardown(reason);
|
|
764
754
|
}
|
|
765
|
-
break
|
|
766
|
-
case
|
|
767
|
-
this.emitError({ code:
|
|
768
|
-
break
|
|
755
|
+
break;
|
|
756
|
+
case "error":
|
|
757
|
+
this.emitError({ code: "socket_error", message: ev.error.message });
|
|
758
|
+
break;
|
|
769
759
|
}
|
|
770
|
-
}
|
|
760
|
+
};
|
|
771
761
|
this.startCapture = async () => {
|
|
772
|
-
if (this.capture?.isCapturing()) return
|
|
762
|
+
if (this.capture?.isCapturing()) return;
|
|
773
763
|
this.capture = createAudioCapture({
|
|
774
764
|
onChunk: (pcm) => {
|
|
775
|
-
this.marks.markFirstOutboundAudio()
|
|
776
|
-
this.rws?.send(pcm)
|
|
765
|
+
this.marks.markFirstOutboundAudio();
|
|
766
|
+
this.rws?.send(pcm);
|
|
777
767
|
},
|
|
778
768
|
onVolume: (v) => {
|
|
779
|
-
this.inputVolume = v
|
|
780
|
-
this.args.options.onVolume?.({ input: v, output: this.outputVolume })
|
|
769
|
+
this.inputVolume = v;
|
|
770
|
+
this.args.options.onVolume?.({ input: v, output: this.outputVolume });
|
|
781
771
|
},
|
|
782
772
|
onError: (err) => {
|
|
783
773
|
this.emitError({
|
|
784
|
-
code: err.name ===
|
|
785
|
-
message: err.message
|
|
786
|
-
})
|
|
787
|
-
}
|
|
788
|
-
})
|
|
789
|
-
if (this.muted) this.capture.mute(true)
|
|
774
|
+
code: err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed",
|
|
775
|
+
message: err.message
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
if (this.muted) this.capture.mute(true);
|
|
790
780
|
try {
|
|
791
|
-
await this.capture.start()
|
|
792
|
-
} catch {
|
|
793
|
-
|
|
781
|
+
await this.capture.start();
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
};
|
|
794
785
|
this.teardown = (reason) => {
|
|
795
786
|
try {
|
|
796
|
-
this.marks.flush()
|
|
797
|
-
} catch {
|
|
798
|
-
|
|
799
|
-
this.capture
|
|
800
|
-
this.
|
|
801
|
-
this.playback
|
|
787
|
+
this.marks.flush();
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
this.capture?.stop();
|
|
791
|
+
this.capture = null;
|
|
792
|
+
this.playback?.close();
|
|
793
|
+
this.playback = null;
|
|
802
794
|
try {
|
|
803
|
-
this.rws?.close(1e3, reason)
|
|
804
|
-
} catch {
|
|
805
|
-
|
|
806
|
-
this.
|
|
807
|
-
this.
|
|
808
|
-
|
|
795
|
+
this.rws?.close(1e3, reason);
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
this.rws = null;
|
|
799
|
+
this.setState("ended");
|
|
800
|
+
this.fireEndOnce(reason);
|
|
801
|
+
};
|
|
809
802
|
this.fireEndOnce = (reason) => {
|
|
810
|
-
if (this.endedFired) return
|
|
811
|
-
this.endedFired = true
|
|
812
|
-
const startedAt = this.startedAt ?? Date.now()
|
|
803
|
+
if (this.endedFired) return;
|
|
804
|
+
this.endedFired = true;
|
|
805
|
+
const startedAt = this.startedAt ?? Date.now();
|
|
813
806
|
this.args.options.onEnd?.({
|
|
814
807
|
reason,
|
|
815
|
-
errorCode: reason ===
|
|
816
|
-
durationMs: Date.now() - startedAt
|
|
817
|
-
})
|
|
818
|
-
}
|
|
819
|
-
this.args = args
|
|
820
|
-
this.proto = createProtocolState()
|
|
821
|
-
validateClientToolMap(args.options.clientTools)
|
|
808
|
+
errorCode: reason === "error" ? this.lastError?.code : void 0,
|
|
809
|
+
durationMs: Date.now() - startedAt
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
this.args = args;
|
|
813
|
+
this.proto = createProtocolState();
|
|
814
|
+
validateClientToolMap(args.options.clientTools);
|
|
822
815
|
this.marks = createClientMarksBuffer({
|
|
823
816
|
send: (frame) => {
|
|
824
817
|
try {
|
|
825
|
-
this.rws?.send(JSON.stringify(frame))
|
|
826
|
-
} catch {
|
|
827
|
-
|
|
828
|
-
|
|
818
|
+
this.rws?.send(JSON.stringify(frame));
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
});
|
|
829
823
|
}
|
|
830
824
|
// ---------------------------------------------------------------
|
|
831
825
|
// Call interface
|
|
832
826
|
// ---------------------------------------------------------------
|
|
833
827
|
get state() {
|
|
834
|
-
return this.proto.state
|
|
828
|
+
return this.proto.state;
|
|
835
829
|
}
|
|
836
830
|
get transcript() {
|
|
837
|
-
return this.proto.transcript.slice()
|
|
831
|
+
return this.proto.transcript.slice();
|
|
838
832
|
}
|
|
839
833
|
get isMuted() {
|
|
840
|
-
return this.muted
|
|
834
|
+
return this.muted;
|
|
841
835
|
}
|
|
842
836
|
// ---------------------------------------------------------------
|
|
843
837
|
// Lifecycle — called by the factory immediately after construction.
|
|
@@ -845,52 +839,54 @@ var BrowserVoiceClient = class {
|
|
|
845
839
|
// failures arrive via `onError`.
|
|
846
840
|
// ---------------------------------------------------------------
|
|
847
841
|
async start() {
|
|
848
|
-
this.setState(
|
|
849
|
-
this.startedAt = Date.now()
|
|
842
|
+
this.setState("connecting");
|
|
843
|
+
this.startedAt = Date.now();
|
|
850
844
|
const url = buildWsUrl({
|
|
851
845
|
apiBase: this.args.config.apiBase,
|
|
852
846
|
agentId: this.args.options.agentId,
|
|
853
847
|
token: this.args.token,
|
|
854
|
-
bargeIn: this.args.options.bargeIn
|
|
855
|
-
})
|
|
848
|
+
bargeIn: this.args.options.bargeIn
|
|
849
|
+
});
|
|
856
850
|
this.playback = createAudioPlayback({
|
|
857
851
|
onVolume: (v) => {
|
|
858
|
-
this.outputVolume = v
|
|
859
|
-
this.args.options.onVolume?.({ input: this.inputVolume, output: v })
|
|
860
|
-
}
|
|
861
|
-
})
|
|
852
|
+
this.outputVolume = v;
|
|
853
|
+
this.args.options.onVolume?.({ input: this.inputVolume, output: v });
|
|
854
|
+
}
|
|
855
|
+
});
|
|
862
856
|
try {
|
|
863
|
-
await this.playback.resume()
|
|
864
|
-
} catch {
|
|
857
|
+
await this.playback.resume();
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
865
860
|
this.rws = createReconnectingWebSocket(
|
|
866
861
|
{
|
|
867
862
|
url,
|
|
868
863
|
wsFactory: this.args.wsFactory,
|
|
869
|
-
maxRetries: 3
|
|
864
|
+
maxRetries: 3
|
|
870
865
|
},
|
|
871
|
-
(ev) => this.handleSocketEvent(ev)
|
|
872
|
-
)
|
|
866
|
+
(ev) => this.handleSocketEvent(ev)
|
|
867
|
+
);
|
|
873
868
|
}
|
|
874
|
-
}
|
|
869
|
+
};
|
|
875
870
|
|
|
876
871
|
// src/webrtc/createWebRtcCall.ts
|
|
877
872
|
async function createWebRtcCall(opts) {
|
|
878
|
-
validateClientToolMap(opts.clientTools)
|
|
879
|
-
const proto = createProtocolState()
|
|
880
|
-
let muted = false
|
|
881
|
-
let ended = false
|
|
882
|
-
const tools = opts.clientTools ?? {}
|
|
873
|
+
validateClientToolMap(opts.clientTools);
|
|
874
|
+
const proto = createProtocolState();
|
|
875
|
+
let muted = false;
|
|
876
|
+
let ended = false;
|
|
877
|
+
const tools = opts.clientTools ?? {};
|
|
883
878
|
const sendControl = (frame) => {
|
|
884
|
-
if (dc?.readyState !==
|
|
879
|
+
if (dc?.readyState !== "open") return;
|
|
885
880
|
try {
|
|
886
|
-
dc.send(JSON.stringify(frame))
|
|
887
|
-
} catch {
|
|
888
|
-
|
|
881
|
+
dc.send(JSON.stringify(frame));
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
};
|
|
889
885
|
const fireState = (next) => {
|
|
890
|
-
if (proto.state === next) return
|
|
891
|
-
proto.state = next
|
|
892
|
-
opts.onStateChange?.(next)
|
|
893
|
-
}
|
|
886
|
+
if (proto.state === next) return;
|
|
887
|
+
proto.state = next;
|
|
888
|
+
opts.onStateChange?.(next);
|
|
889
|
+
};
|
|
894
890
|
const dispatch = (raw) => {
|
|
895
891
|
handleServerMessage(raw, proto, {
|
|
896
892
|
onState: fireState,
|
|
@@ -898,184 +894,188 @@ async function createWebRtcCall(opts) {
|
|
|
898
894
|
onError: (err) => opts.onError?.(err),
|
|
899
895
|
onInterrupt: () => opts.onInterrupt?.(),
|
|
900
896
|
onAgentTurnStart: () => opts.onAgentTurnStart?.(),
|
|
901
|
-
onAgentTurnEnd: () => {
|
|
897
|
+
onAgentTurnEnd: () => {
|
|
898
|
+
},
|
|
902
899
|
onCallEnd: () => teardown(),
|
|
903
900
|
onConnected: () => {
|
|
904
901
|
if (Object.keys(tools).length > 0) {
|
|
905
|
-
sendControl(buildRegisterFrame(tools))
|
|
902
|
+
sendControl(buildRegisterFrame(tools));
|
|
906
903
|
}
|
|
907
904
|
},
|
|
908
905
|
onClientToolCall: (frame) => {
|
|
909
|
-
dispatchClientToolCall(sendControl, tools, frame)
|
|
910
|
-
}
|
|
911
|
-
})
|
|
912
|
-
}
|
|
913
|
-
fireState(
|
|
906
|
+
dispatchClientToolCall(sendControl, tools, frame);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
};
|
|
910
|
+
fireState("connecting");
|
|
914
911
|
const pc = new RTCPeerConnection({
|
|
915
|
-
iceServers: [{ urls:
|
|
916
|
-
})
|
|
917
|
-
const audioEl = document.createElement(
|
|
918
|
-
audioEl.autoplay = true
|
|
919
|
-
audioEl.style.display =
|
|
920
|
-
document.body.appendChild(audioEl)
|
|
912
|
+
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
913
|
+
});
|
|
914
|
+
const audioEl = document.createElement("audio");
|
|
915
|
+
audioEl.autoplay = true;
|
|
916
|
+
audioEl.style.display = "none";
|
|
917
|
+
document.body.appendChild(audioEl);
|
|
921
918
|
pc.ontrack = (event) => {
|
|
922
|
-
audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track])
|
|
923
|
-
}
|
|
924
|
-
let mic
|
|
919
|
+
audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track]);
|
|
920
|
+
};
|
|
921
|
+
let mic;
|
|
925
922
|
try {
|
|
926
|
-
mic = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
923
|
+
mic = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
927
924
|
} catch (err) {
|
|
928
|
-
const code =
|
|
929
|
-
err instanceof DOMException && err.name === 'NotAllowedError'
|
|
930
|
-
? 'mic_denied'
|
|
931
|
-
: 'mic_start_failed'
|
|
925
|
+
const code = err instanceof DOMException && err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed";
|
|
932
926
|
opts.onError?.({
|
|
933
927
|
code,
|
|
934
|
-
message: err instanceof Error ? err.message :
|
|
935
|
-
})
|
|
936
|
-
fireState(
|
|
937
|
-
pc.close()
|
|
938
|
-
audioEl.remove()
|
|
939
|
-
throw err
|
|
928
|
+
message: err instanceof Error ? err.message : "getUserMedia failed"
|
|
929
|
+
});
|
|
930
|
+
fireState("error");
|
|
931
|
+
pc.close();
|
|
932
|
+
audioEl.remove();
|
|
933
|
+
throw err;
|
|
940
934
|
}
|
|
941
|
-
for (const track of mic.getAudioTracks()) pc.addTrack(track, mic)
|
|
942
|
-
const dc = pc.createDataChannel(
|
|
935
|
+
for (const track of mic.getAudioTracks()) pc.addTrack(track, mic);
|
|
936
|
+
const dc = pc.createDataChannel("control", { ordered: true });
|
|
943
937
|
dc.onmessage = (e) => {
|
|
944
|
-
if (typeof e.data ===
|
|
945
|
-
}
|
|
938
|
+
if (typeof e.data === "string") dispatch(e.data);
|
|
939
|
+
};
|
|
946
940
|
dc.onerror = () => {
|
|
947
|
-
opts.onError?.({ code:
|
|
948
|
-
}
|
|
941
|
+
opts.onError?.({ code: "socket_error", message: "control channel error" });
|
|
942
|
+
};
|
|
949
943
|
dc.onopen = () => {
|
|
950
944
|
if (Object.keys(tools).length > 0) {
|
|
951
|
-
sendControl(buildRegisterFrame(tools))
|
|
945
|
+
sendControl(buildRegisterFrame(tools));
|
|
952
946
|
}
|
|
953
|
-
}
|
|
954
|
-
const gateway = opts.webrtcGatewayBase ||
|
|
955
|
-
const offerUrl = gateway
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
947
|
+
};
|
|
948
|
+
const gateway = opts.webrtcGatewayBase || "";
|
|
949
|
+
const offerUrl = gateway ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`;
|
|
950
|
+
const iceUrl = gateway ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`;
|
|
951
|
+
const teardown = () => {
|
|
952
|
+
if (ended) return;
|
|
953
|
+
ended = true;
|
|
954
|
+
try {
|
|
955
|
+
mic.getTracks().forEach((t) => t.stop());
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
pc.close();
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
audioEl.remove();
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
fireState("ended");
|
|
967
|
+
opts.onEnd?.();
|
|
968
|
+
};
|
|
969
|
+
let callId = null;
|
|
970
|
+
const pendingCandidates = [];
|
|
971
|
+
const postCandidate = (candidate) => {
|
|
972
|
+
void fetch(iceUrl, {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers: { "content-type": "application/json" },
|
|
975
|
+
body: JSON.stringify({ callId, candidate })
|
|
976
|
+
}).catch(() => {
|
|
977
|
+
});
|
|
978
|
+
};
|
|
979
|
+
pc.onicecandidate = (e) => {
|
|
980
|
+
if (!e.candidate) return;
|
|
981
|
+
if (callId) postCandidate(e.candidate);
|
|
982
|
+
else pendingCandidates.push(e.candidate);
|
|
983
|
+
};
|
|
984
|
+
pc.onconnectionstatechange = () => {
|
|
985
|
+
const s = pc.connectionState;
|
|
986
|
+
if (s === "connected") fireState("listening");
|
|
987
|
+
if (s === "failed" || s === "disconnected") {
|
|
988
|
+
opts.onError?.({ code: "socket_error", message: `webrtc connection ${s}` });
|
|
989
|
+
teardown();
|
|
990
|
+
}
|
|
991
|
+
if (s === "closed" && !ended) teardown();
|
|
992
|
+
};
|
|
993
|
+
await pc.setLocalDescription(await pc.createOffer());
|
|
963
994
|
try {
|
|
964
995
|
const offerRes = await fetch(offerUrl, {
|
|
965
|
-
method:
|
|
966
|
-
headers: {
|
|
967
|
-
body: JSON.stringify({ sdp: pc.localDescription.sdp, type:
|
|
968
|
-
})
|
|
996
|
+
method: "POST",
|
|
997
|
+
headers: { "content-type": "application/json" },
|
|
998
|
+
body: JSON.stringify({ sdp: pc.localDescription.sdp, type: "offer", agentId: opts.agentId })
|
|
999
|
+
});
|
|
969
1000
|
if (!offerRes.ok) {
|
|
970
|
-
const code = offerRes.status === 401 ?
|
|
971
|
-
opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` })
|
|
972
|
-
fireState(
|
|
973
|
-
mic.getTracks().forEach((t) => t.stop())
|
|
974
|
-
pc.close()
|
|
975
|
-
audioEl.remove()
|
|
976
|
-
throw new Error(`webrtc offer failed: ${offerRes.status}`)
|
|
1001
|
+
const code = offerRes.status === 401 ? "unauthorized" : "server_error";
|
|
1002
|
+
opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` });
|
|
1003
|
+
fireState("error");
|
|
1004
|
+
mic.getTracks().forEach((t) => t.stop());
|
|
1005
|
+
pc.close();
|
|
1006
|
+
audioEl.remove();
|
|
1007
|
+
throw new Error(`webrtc offer failed: ${offerRes.status}`);
|
|
977
1008
|
}
|
|
978
|
-
const body = await offerRes.json()
|
|
979
|
-
callId = body.callId
|
|
980
|
-
await pc.setRemoteDescription({ type:
|
|
1009
|
+
const body = await offerRes.json();
|
|
1010
|
+
callId = body.callId;
|
|
1011
|
+
await pc.setRemoteDescription({ type: "answer", sdp: body.sdp });
|
|
1012
|
+
while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift());
|
|
981
1013
|
} catch (err) {
|
|
982
1014
|
if (!ended) {
|
|
983
1015
|
opts.onError?.({
|
|
984
|
-
code:
|
|
985
|
-
message: err instanceof Error ? err.message :
|
|
986
|
-
})
|
|
987
|
-
fireState(
|
|
988
|
-
mic.getTracks().forEach((t) => t.stop())
|
|
989
|
-
pc.close()
|
|
990
|
-
audioEl.remove()
|
|
1016
|
+
code: "network_unreachable",
|
|
1017
|
+
message: err instanceof Error ? err.message : "signaling failed"
|
|
1018
|
+
});
|
|
1019
|
+
fireState("error");
|
|
1020
|
+
mic.getTracks().forEach((t) => t.stop());
|
|
1021
|
+
pc.close();
|
|
1022
|
+
audioEl.remove();
|
|
991
1023
|
}
|
|
992
|
-
throw err
|
|
993
|
-
}
|
|
994
|
-
pc.onicecandidate = (e) => {
|
|
995
|
-
if (!e.candidate) return
|
|
996
|
-
void fetch(iceUrl, {
|
|
997
|
-
method: 'POST',
|
|
998
|
-
headers: { 'content-type': 'application/json' },
|
|
999
|
-
body: JSON.stringify({ callId, candidate: e.candidate }),
|
|
1000
|
-
}).catch(() => {})
|
|
1001
|
-
}
|
|
1002
|
-
pc.onconnectionstatechange = () => {
|
|
1003
|
-
const s = pc.connectionState
|
|
1004
|
-
if (s === 'connected') fireState('listening')
|
|
1005
|
-
if (s === 'failed' || s === 'disconnected') {
|
|
1006
|
-
opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })
|
|
1007
|
-
teardown()
|
|
1008
|
-
}
|
|
1009
|
-
if (s === 'closed' && !ended) teardown()
|
|
1010
|
-
}
|
|
1011
|
-
const teardown = () => {
|
|
1012
|
-
if (ended) return
|
|
1013
|
-
ended = true
|
|
1014
|
-
try {
|
|
1015
|
-
mic.getTracks().forEach((t) => t.stop())
|
|
1016
|
-
} catch {}
|
|
1017
|
-
try {
|
|
1018
|
-
pc.close()
|
|
1019
|
-
} catch {}
|
|
1020
|
-
try {
|
|
1021
|
-
audioEl.remove()
|
|
1022
|
-
} catch {}
|
|
1023
|
-
fireState('ended')
|
|
1024
|
-
opts.onEnd?.()
|
|
1024
|
+
throw err;
|
|
1025
1025
|
}
|
|
1026
1026
|
return {
|
|
1027
1027
|
get state() {
|
|
1028
|
-
return proto.state
|
|
1028
|
+
return proto.state;
|
|
1029
1029
|
},
|
|
1030
1030
|
get transcript() {
|
|
1031
|
-
return proto.transcript.slice()
|
|
1031
|
+
return proto.transcript.slice();
|
|
1032
1032
|
},
|
|
1033
1033
|
get isMuted() {
|
|
1034
|
-
return muted
|
|
1034
|
+
return muted;
|
|
1035
1035
|
},
|
|
1036
1036
|
end: () => teardown(),
|
|
1037
1037
|
mute: () => {
|
|
1038
|
-
if (muted) return
|
|
1039
|
-
muted = true
|
|
1040
|
-
mic.getAudioTracks().forEach((t) =>
|
|
1038
|
+
if (muted) return;
|
|
1039
|
+
muted = true;
|
|
1040
|
+
mic.getAudioTracks().forEach((t) => t.enabled = false);
|
|
1041
1041
|
},
|
|
1042
1042
|
unmute: () => {
|
|
1043
|
-
if (!muted) return
|
|
1044
|
-
muted = false
|
|
1045
|
-
mic.getAudioTracks().forEach((t) =>
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1043
|
+
if (!muted) return;
|
|
1044
|
+
muted = false;
|
|
1045
|
+
mic.getAudioTracks().forEach((t) => t.enabled = true);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
1048
|
}
|
|
1049
1049
|
|
|
1050
1050
|
// src/browser.ts
|
|
1051
|
-
var browserWsFactory = (url) => new globalThis.WebSocket(url)
|
|
1051
|
+
var browserWsFactory = (url) => new globalThis.WebSocket(url);
|
|
1052
1052
|
var BrowserVoiceFactory = class {
|
|
1053
1053
|
constructor(config) {
|
|
1054
1054
|
this.startCall = async (options) => {
|
|
1055
1055
|
if (!options.agentId) {
|
|
1056
|
-
throw new Error(
|
|
1056
|
+
throw new Error("startCall: agentId is required");
|
|
1057
1057
|
}
|
|
1058
|
-
const { context, metadata } = mergeStartCallContext(this.config, options)
|
|
1058
|
+
const { context, metadata } = mergeStartCallContext(this.config, options);
|
|
1059
1059
|
const fetchArgs = {
|
|
1060
1060
|
agentId: options.agentId,
|
|
1061
1061
|
userId: options.userId,
|
|
1062
1062
|
context,
|
|
1063
|
-
metadata
|
|
1064
|
-
}
|
|
1065
|
-
let resolved
|
|
1063
|
+
metadata
|
|
1064
|
+
};
|
|
1065
|
+
let resolved;
|
|
1066
1066
|
if (options.token) {
|
|
1067
|
-
resolved = { token: options.token, transport:
|
|
1067
|
+
resolved = { token: options.token, transport: "ws" };
|
|
1068
1068
|
} else {
|
|
1069
|
-
const r = await this.config.fetchToken(fetchArgs)
|
|
1069
|
+
const r = await this.config.fetchToken(fetchArgs);
|
|
1070
1070
|
if (!r) {
|
|
1071
|
-
throw new Error(
|
|
1071
|
+
throw new Error("configureVoiceClient.fetchToken returned empty token");
|
|
1072
1072
|
}
|
|
1073
|
-
resolved = typeof r ===
|
|
1073
|
+
resolved = typeof r === "string" ? { token: r, transport: "ws" } : r;
|
|
1074
1074
|
if (!resolved.token) {
|
|
1075
|
-
throw new Error(
|
|
1075
|
+
throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
|
|
1076
1076
|
}
|
|
1077
1077
|
}
|
|
1078
|
-
if (resolved.transport ===
|
|
1078
|
+
if (resolved.transport === "webrtc") {
|
|
1079
1079
|
return createWebRtcCall({
|
|
1080
1080
|
agentId: options.agentId,
|
|
1081
1081
|
apiBase: this.config.apiBase,
|
|
@@ -1087,13 +1087,11 @@ var BrowserVoiceFactory = class {
|
|
|
1087
1087
|
// Synthesise a minimal CallEndEvent. WebRTC doesn't carry an end reason
|
|
1088
1088
|
// from the server yet — use 'agent_ended' as placeholder. durationMs is
|
|
1089
1089
|
// tracked at 0 until the followup lands (see spec Followups section).
|
|
1090
|
-
onEnd: options.onEnd
|
|
1091
|
-
? () => options.onEnd({ reason: 'agent_ended', durationMs: 0 })
|
|
1092
|
-
: void 0,
|
|
1090
|
+
onEnd: options.onEnd ? () => options.onEnd({ reason: "agent_ended", durationMs: 0 }) : void 0,
|
|
1093
1091
|
onInterrupt: options.onInterrupt,
|
|
1094
1092
|
onAgentTurnStart: options.onAgentTurnStart,
|
|
1095
|
-
clientTools: options.clientTools
|
|
1096
|
-
})
|
|
1093
|
+
clientTools: options.clientTools
|
|
1094
|
+
});
|
|
1097
1095
|
}
|
|
1098
1096
|
const client = new BrowserVoiceClient({
|
|
1099
1097
|
config: this.config,
|
|
@@ -1101,26 +1099,25 @@ var BrowserVoiceFactory = class {
|
|
|
1101
1099
|
// see what the SDK saw.
|
|
1102
1100
|
options: { ...options, context, metadata },
|
|
1103
1101
|
token: resolved.token,
|
|
1104
|
-
wsFactory: browserWsFactory
|
|
1105
|
-
})
|
|
1106
|
-
await client.start()
|
|
1107
|
-
return client
|
|
1108
|
-
}
|
|
1109
|
-
this.config = config
|
|
1102
|
+
wsFactory: browserWsFactory
|
|
1103
|
+
});
|
|
1104
|
+
await client.start();
|
|
1105
|
+
return client;
|
|
1106
|
+
};
|
|
1107
|
+
this.config = config;
|
|
1110
1108
|
}
|
|
1111
|
-
}
|
|
1109
|
+
};
|
|
1112
1110
|
function configureVoiceClient(config) {
|
|
1113
|
-
return new BrowserVoiceFactory(normalizeConfig(config))
|
|
1111
|
+
return new BrowserVoiceFactory(normalizeConfig(config));
|
|
1114
1112
|
}
|
|
1115
1113
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1116
|
-
0 &&
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
//# sourceMappingURL=browser.js.map
|
|
1114
|
+
0 && (module.exports = {
|
|
1115
|
+
buildWsUrl,
|
|
1116
|
+
configureVoiceClient,
|
|
1117
|
+
createAudioCapture,
|
|
1118
|
+
createAudioPlayback,
|
|
1119
|
+
createProtocolState,
|
|
1120
|
+
createReconnectingWebSocket,
|
|
1121
|
+
handleServerMessage
|
|
1122
|
+
});
|
|
1123
|
+
//# sourceMappingURL=browser.js.map
|