@voxagent/bridge 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/dist/cli.js +66 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +15 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -1
- package/dist/config.js.map +1 -1
- package/dist/drivers/claude.js +1 -1
- package/dist/drivers/claude.js.map +1 -1
- package/dist/drivers/codex.js +1 -1
- package/dist/drivers/codex.js.map +1 -1
- package/dist/live/frontend.d.ts +134 -0
- package/dist/live/frontend.d.ts.map +1 -0
- package/dist/live/frontend.js +521 -0
- package/dist/live/frontend.js.map +1 -0
- package/dist/live/mixer.d.ts +38 -0
- package/dist/live/mixer.d.ts.map +1 -0
- package/dist/live/mixer.js +113 -0
- package/dist/live/mixer.js.map +1 -0
- package/dist/live/pcm.d.ts +34 -0
- package/dist/live/pcm.d.ts.map +1 -0
- package/dist/live/pcm.js +66 -0
- package/dist/live/pcm.js.map +1 -0
- package/dist/live/prompts.d.ts +54 -0
- package/dist/live/prompts.d.ts.map +1 -0
- package/dist/live/prompts.js +129 -0
- package/dist/live/prompts.js.map +1 -0
- package/dist/live/session.d.ts +132 -0
- package/dist/live/session.d.ts.map +1 -0
- package/dist/live/session.js +300 -0
- package/dist/live/session.js.map +1 -0
- package/dist/live/speakers.d.ts +54 -0
- package/dist/live/speakers.d.ts.map +1 -0
- package/dist/live/speakers.js +138 -0
- package/dist/live/speakers.js.map +1 -0
- package/dist/live/transcript.d.ts +40 -0
- package/dist/live/transcript.d.ts.map +1 -0
- package/dist/live/transcript.js +72 -0
- package/dist/live/transcript.js.map +1 -0
- package/dist/pair.d.ts +6 -0
- package/dist/pair.d.ts.map +1 -1
- package/dist/pair.js +1 -0
- package/dist/pair.js.map +1 -1
- package/dist/protocol.d.ts +4 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/room.d.ts +20 -0
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +173 -3
- package/dist/room.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/live/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file GPT-Live primary WebSocket client (`wss://api.openai.com/v1/live/sessions`).
|
|
3
|
+
*
|
|
4
|
+
* Speaks the raw event protocol rather than pulling in the `openai` SDK —
|
|
5
|
+
* the surface we use is small and the bridge keeps its dependency list
|
|
6
|
+
* short (same approach as the ElevenLabs engine).
|
|
7
|
+
*
|
|
8
|
+
* Behaviour verified against the real API on 2026-09-27:
|
|
9
|
+
* - `session.started` arrives ~1 s after `session.start`; sessions expire
|
|
10
|
+
* 2 h after start (`session.expires_at`).
|
|
11
|
+
* - Output audio is a continuous real-time stream, silence included, from
|
|
12
|
+
* the moment the session starts. GPT-Live never races ahead of the
|
|
13
|
+
* clock, so barge-in needs no local playback flushing.
|
|
14
|
+
* - After `session.closed` the server drops the socket without a close
|
|
15
|
+
* frame (code 1006 + an `error` event). That is a normal end, not a
|
|
16
|
+
* failure.
|
|
17
|
+
*/
|
|
18
|
+
import { WebSocket } from 'undici';
|
|
19
|
+
import { log } from '../logging.js';
|
|
20
|
+
import { base64ToInt16, int16ToBase64 } from './pcm.js';
|
|
21
|
+
export const LIVE_SESSIONS_URL = 'wss://api.openai.com/v1/live/sessions';
|
|
22
|
+
export const DEFAULT_LIVE_MODEL = 'gpt-live-1';
|
|
23
|
+
export const DEFAULT_LIVE_VOICE = 'marin';
|
|
24
|
+
/** Hard cap per context append; callers split long text before this. */
|
|
25
|
+
const APPEND_HARD_LIMIT_CHARS = 1800;
|
|
26
|
+
const SOCKET_OPEN = 1;
|
|
27
|
+
const defaultSocketFactory = (url, headers) => new WebSocket(url, { headers });
|
|
28
|
+
export class LiveSession {
|
|
29
|
+
config;
|
|
30
|
+
onEvent;
|
|
31
|
+
socketFactory;
|
|
32
|
+
socket = null;
|
|
33
|
+
state = 'idle';
|
|
34
|
+
eventSeq = 0;
|
|
35
|
+
closedEmitted = false;
|
|
36
|
+
lastUsageSeconds = null;
|
|
37
|
+
onClosed = null;
|
|
38
|
+
sessionId = null;
|
|
39
|
+
expiresAt = null;
|
|
40
|
+
constructor(config, onEvent, socketFactory = defaultSocketFactory) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.onEvent = onEvent;
|
|
43
|
+
this.socketFactory = socketFactory;
|
|
44
|
+
}
|
|
45
|
+
get started() {
|
|
46
|
+
return this.state === 'started';
|
|
47
|
+
}
|
|
48
|
+
/** Opens the socket and resolves once `session.started` arrives. */
|
|
49
|
+
start(timeoutMs = 15_000) {
|
|
50
|
+
if (this.state !== 'idle')
|
|
51
|
+
return Promise.reject(new Error('LiveSession already started'));
|
|
52
|
+
this.state = 'connecting';
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let settled = false;
|
|
55
|
+
const settle = (error) => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
if (error)
|
|
61
|
+
reject(error);
|
|
62
|
+
else
|
|
63
|
+
resolve();
|
|
64
|
+
};
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
settle(new Error(`GPT-Live did not start within ${timeoutMs} ms`));
|
|
67
|
+
this.abortSocket();
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
const socket = this.socketFactory(this.config.url ?? LIVE_SESSIONS_URL, {
|
|
70
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
71
|
+
});
|
|
72
|
+
this.socket = socket;
|
|
73
|
+
socket.addEventListener('open', () => {
|
|
74
|
+
this.sendRaw({
|
|
75
|
+
type: 'session.start',
|
|
76
|
+
event_id: this.nextEventId('start'),
|
|
77
|
+
session: {
|
|
78
|
+
model: this.config.model,
|
|
79
|
+
instructions: this.config.instructions,
|
|
80
|
+
audio: {
|
|
81
|
+
format: { type: 'audio/pcm', rate: 24_000 },
|
|
82
|
+
output: { voice: this.config.voice },
|
|
83
|
+
},
|
|
84
|
+
delegation: { type: 'client' },
|
|
85
|
+
...(this.config.input && this.config.input.length > 0 ? { input: this.config.input } : {}),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
socket.addEventListener('message', (event) => {
|
|
90
|
+
const message = parseMessage(event.data);
|
|
91
|
+
if (!message)
|
|
92
|
+
return;
|
|
93
|
+
if (message.type === 'session.started') {
|
|
94
|
+
this.state = 'started';
|
|
95
|
+
this.sessionId = message.session?.id ?? null;
|
|
96
|
+
this.expiresAt = message.session?.expires_at ?? null;
|
|
97
|
+
this.onEvent({ type: 'started', sessionId: this.sessionId ?? '', expiresAt: this.expiresAt });
|
|
98
|
+
settle();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (message.type === 'error' && this.state === 'connecting') {
|
|
102
|
+
settle(new Error(`GPT-Live rejected session.start: ${message.error?.message ?? 'unknown error'}`));
|
|
103
|
+
}
|
|
104
|
+
this.handleMessage(message);
|
|
105
|
+
});
|
|
106
|
+
socket.addEventListener('close', (event) => {
|
|
107
|
+
if (!settled) {
|
|
108
|
+
settle(new Error(`GPT-Live socket closed before the session started (code ${event.code ?? '?'}${event.reason ? `: ${event.reason}` : ''})`));
|
|
109
|
+
}
|
|
110
|
+
this.emitClosed('socket_closed');
|
|
111
|
+
});
|
|
112
|
+
socket.addEventListener('error', (event) => {
|
|
113
|
+
// After session.closed the server drops the socket abruptly; that
|
|
114
|
+
// error is expected and the close handler finishes the job.
|
|
115
|
+
if (this.closedEmitted)
|
|
116
|
+
return;
|
|
117
|
+
const message = describeSocketError(event);
|
|
118
|
+
log.warn('live_socket_error', { message });
|
|
119
|
+
if (!settled)
|
|
120
|
+
settle(new Error(`GPT-Live connection failed: ${message}`));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
appendAudio(samples) {
|
|
125
|
+
if (this.state !== 'started' || samples.length === 0)
|
|
126
|
+
return;
|
|
127
|
+
this.sendRaw({ type: 'session.input_audio.append', audio: int16ToBase64(samples) });
|
|
128
|
+
}
|
|
129
|
+
/** Quiet context the model may use later without saying it now. */
|
|
130
|
+
appendThinking(content, delegationId = null) {
|
|
131
|
+
return this.sendAppend('session.thinking.append', 'think', content, delegationId);
|
|
132
|
+
}
|
|
133
|
+
/** Information the model should say aloud, in its own words. */
|
|
134
|
+
appendCommentary(content, delegationId = null) {
|
|
135
|
+
return this.sendAppend('session.commentary.append', 'say', content, delegationId);
|
|
136
|
+
}
|
|
137
|
+
/** Trusted instructions; can interrupt speech in progress. */
|
|
138
|
+
appendInstructions(content, delegationId = null) {
|
|
139
|
+
return this.sendAppend('session.instructions.append', 'instr', content, delegationId);
|
|
140
|
+
}
|
|
141
|
+
setInputMuted(muted) {
|
|
142
|
+
if (this.state !== 'started')
|
|
143
|
+
return;
|
|
144
|
+
this.sendRaw({
|
|
145
|
+
type: muted ? 'session.input_audio.mute' : 'session.input_audio.unmute',
|
|
146
|
+
event_id: this.nextEventId(muted ? 'mute' : 'unmute'),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Graceful close: `session.close`, then wait for `session.closed` (final
|
|
151
|
+
* usage) up to `timeoutMs` before dropping the socket.
|
|
152
|
+
*/
|
|
153
|
+
async close(timeoutMs = 8_000) {
|
|
154
|
+
if (this.state === 'closed' || this.state === 'closing')
|
|
155
|
+
return;
|
|
156
|
+
if (this.state !== 'started') {
|
|
157
|
+
this.abortSocket();
|
|
158
|
+
this.emitClosed('close_requested');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.state = 'closing';
|
|
162
|
+
const closed = new Promise((resolve) => {
|
|
163
|
+
this.onClosed = resolve;
|
|
164
|
+
});
|
|
165
|
+
this.sendRaw({ type: 'session.close', event_id: this.nextEventId('close') });
|
|
166
|
+
let timer;
|
|
167
|
+
await Promise.race([closed, new Promise((resolve) => (timer = setTimeout(resolve, timeoutMs)))]);
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
if (!this.closedEmitted) {
|
|
170
|
+
log.warn('live_close_unconfirmed', { sessionId: this.sessionId, lastUsageSeconds: this.lastUsageSeconds });
|
|
171
|
+
}
|
|
172
|
+
this.abortSocket();
|
|
173
|
+
this.emitClosed('close_requested');
|
|
174
|
+
}
|
|
175
|
+
handleMessage(message) {
|
|
176
|
+
switch (message.type) {
|
|
177
|
+
case 'session.output_audio.delta':
|
|
178
|
+
if (typeof message.delta === 'string' && message.delta) {
|
|
179
|
+
this.onEvent({ type: 'audio', samples: base64ToInt16(message.delta) });
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
case 'session.input_transcript.delta':
|
|
183
|
+
case 'session.output_transcript.delta':
|
|
184
|
+
if (typeof message.delta !== 'string')
|
|
185
|
+
return;
|
|
186
|
+
this.onEvent({
|
|
187
|
+
type: 'transcript',
|
|
188
|
+
role: message.type === 'session.input_transcript.delta' ? 'input' : 'output',
|
|
189
|
+
delta: message.delta,
|
|
190
|
+
startMs: message.start_ms ?? 0,
|
|
191
|
+
endMs: message.end_ms ?? message.start_ms ?? 0,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
case 'session.delegation.created':
|
|
195
|
+
if (message.delegation?.id && message.delegation.target === 'client') {
|
|
196
|
+
this.onEvent({ type: 'delegation', id: message.delegation.id, offsetMs: message.offset_ms ?? 0 });
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
case 'session.usage.updated':
|
|
200
|
+
this.lastUsageSeconds = message.usage?.seconds ?? this.lastUsageSeconds;
|
|
201
|
+
this.onEvent({
|
|
202
|
+
type: 'usage',
|
|
203
|
+
seconds: message.usage?.seconds ?? 0,
|
|
204
|
+
contextRatio: message.context_window?.usage_ratio ?? null,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
case 'session.closed':
|
|
208
|
+
this.lastUsageSeconds = message.usage?.seconds ?? this.lastUsageSeconds;
|
|
209
|
+
this.emitClosed(message.reason ?? 'socket_closed');
|
|
210
|
+
return;
|
|
211
|
+
case 'error':
|
|
212
|
+
this.onEvent({
|
|
213
|
+
type: 'error',
|
|
214
|
+
code: message.error?.code ?? 'unknown',
|
|
215
|
+
message: message.error?.message ?? 'unknown error',
|
|
216
|
+
clientEventId: message.error?.client_event_id ?? message.client_event_id ?? null,
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
default:
|
|
220
|
+
// Acks (*.appended, input_audio.muted, ...) and info events.
|
|
221
|
+
log.debug('live_event', { type: message.type, clientEventId: message.client_event_id });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
sendAppend(type, prefix, content, delegationId) {
|
|
225
|
+
const trimmed = content.trim();
|
|
226
|
+
if (this.state !== 'started' || !trimmed)
|
|
227
|
+
return null;
|
|
228
|
+
const eventId = this.nextEventId(prefix);
|
|
229
|
+
this.sendRaw({
|
|
230
|
+
type,
|
|
231
|
+
event_id: eventId,
|
|
232
|
+
delegation_id: delegationId,
|
|
233
|
+
content: trimmed.slice(0, APPEND_HARD_LIMIT_CHARS),
|
|
234
|
+
});
|
|
235
|
+
return eventId;
|
|
236
|
+
}
|
|
237
|
+
emitClosed(reason) {
|
|
238
|
+
if (this.closedEmitted)
|
|
239
|
+
return;
|
|
240
|
+
this.closedEmitted = true;
|
|
241
|
+
this.state = 'closed';
|
|
242
|
+
this.onEvent({ type: 'closed', reason, usageSeconds: this.lastUsageSeconds });
|
|
243
|
+
this.onClosed?.();
|
|
244
|
+
}
|
|
245
|
+
sendRaw(payload) {
|
|
246
|
+
const socket = this.socket;
|
|
247
|
+
if (!socket || socket.readyState !== SOCKET_OPEN)
|
|
248
|
+
return;
|
|
249
|
+
try {
|
|
250
|
+
socket.send(JSON.stringify(payload));
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
log.warn('live_send_failed', {
|
|
254
|
+
type: payload.type,
|
|
255
|
+
message: error instanceof Error ? error.message : String(error),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
abortSocket() {
|
|
260
|
+
try {
|
|
261
|
+
this.socket?.close();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// ignore
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
nextEventId(prefix) {
|
|
268
|
+
this.eventSeq += 1;
|
|
269
|
+
return `${prefix}_${this.eventSeq}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function parseMessage(data) {
|
|
273
|
+
let text;
|
|
274
|
+
if (typeof data === 'string')
|
|
275
|
+
text = data;
|
|
276
|
+
else if (data instanceof ArrayBuffer)
|
|
277
|
+
text = Buffer.from(data).toString('utf8');
|
|
278
|
+
else if (ArrayBuffer.isView(data))
|
|
279
|
+
text = Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
|
|
280
|
+
else
|
|
281
|
+
return null;
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(text);
|
|
284
|
+
return typeof parsed.type === 'string' ? parsed : null;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function describeSocketError(event) {
|
|
291
|
+
if (event && typeof event === 'object') {
|
|
292
|
+
const withError = event;
|
|
293
|
+
if (withError.error instanceof Error)
|
|
294
|
+
return withError.error.message;
|
|
295
|
+
if (typeof withError.message === 'string' && withError.message)
|
|
296
|
+
return withError.message;
|
|
297
|
+
}
|
|
298
|
+
return 'unknown socket error';
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/live/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Works out who is talking from per-participant mic levels.
|
|
3
|
+
*
|
|
4
|
+
* GPT-Live hears one mixed stream and its transcript has no speaker
|
|
5
|
+
* labels. The bridge still has each participant's audio separately before
|
|
6
|
+
* mixing, so it can tell who holds the floor. That feeds two things:
|
|
7
|
+
*
|
|
8
|
+
* - "Now speaking: David" hints, sent to GPT-Live as quiet context when
|
|
9
|
+
* the floor changes, so it knows who it is talking to.
|
|
10
|
+
* - a timeline of who was talking when, used to label GPT-Live's own
|
|
11
|
+
* transcript fragments (which carry session-timeline timestamps) when
|
|
12
|
+
* building the prompt for a delegated task.
|
|
13
|
+
*
|
|
14
|
+
* Times are milliseconds on the GPT-Live session timeline, i.e. how much
|
|
15
|
+
* mixed audio has been sent since `session.started`.
|
|
16
|
+
*/
|
|
17
|
+
export interface SpeakerTrackerOptions {
|
|
18
|
+
/** RMS level (0..1) at or above which a block counts as speech. */
|
|
19
|
+
speechLevel: number;
|
|
20
|
+
/** Sustained speech (ms) before someone counts as speaking — ignores coughs and clicks. */
|
|
21
|
+
onsetMs: number;
|
|
22
|
+
/** Silence (ms) that ends someone's turn. Bridges pauses between words. */
|
|
23
|
+
releaseMs: number;
|
|
24
|
+
/** Minimum time between two "now speaking" announcements. */
|
|
25
|
+
minAnnounceGapMs: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const DEFAULT_SPEAKER_TRACKER_OPTIONS: SpeakerTrackerOptions;
|
|
28
|
+
export declare class SpeakerTracker {
|
|
29
|
+
private readonly options;
|
|
30
|
+
private states;
|
|
31
|
+
private dominant;
|
|
32
|
+
private lastAnnounced;
|
|
33
|
+
private lastAnnounceAt;
|
|
34
|
+
private segments;
|
|
35
|
+
constructor(options?: Partial<SpeakerTrackerOptions>);
|
|
36
|
+
/** Who currently holds the floor, if anyone. */
|
|
37
|
+
get current(): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Feeds one mixed block's levels (keyed by speaker id).
|
|
40
|
+
*
|
|
41
|
+
* @param nowMs - Session-timeline time at the END of the block.
|
|
42
|
+
* @returns The speaker to announce now, or null.
|
|
43
|
+
*/
|
|
44
|
+
update(levels: ReadonlyMap<string, number>, nowMs: number): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Best guess at who said something spanning [startMs, endMs): the
|
|
47
|
+
* speaker with the most floor time in that window, else whoever last
|
|
48
|
+
* held the floor before it. Null when nobody has spoken yet.
|
|
49
|
+
*/
|
|
50
|
+
speakerAt(startMs: number, endMs: number): string | null;
|
|
51
|
+
/** Forget everything — a new GPT-Live session restarts the timeline at 0. */
|
|
52
|
+
reset(): void;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=speakers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speakers.d.ts","sourceRoot":"","sources":["../../src/live/speakers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,qBAAqB;IACpC,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,+BAA+B,EAAE,qBAK7C,CAAC;AAiBF,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAsB;gBAE1B,OAAO,GAAE,OAAO,CAAC,qBAAqB,CAAM;IAIxD,gDAAgD;IAChD,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAsDzE;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAuBxD,6EAA6E;IAC7E,KAAK,IAAI,IAAI;CAOd"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Works out who is talking from per-participant mic levels.
|
|
3
|
+
*
|
|
4
|
+
* GPT-Live hears one mixed stream and its transcript has no speaker
|
|
5
|
+
* labels. The bridge still has each participant's audio separately before
|
|
6
|
+
* mixing, so it can tell who holds the floor. That feeds two things:
|
|
7
|
+
*
|
|
8
|
+
* - "Now speaking: David" hints, sent to GPT-Live as quiet context when
|
|
9
|
+
* the floor changes, so it knows who it is talking to.
|
|
10
|
+
* - a timeline of who was talking when, used to label GPT-Live's own
|
|
11
|
+
* transcript fragments (which carry session-timeline timestamps) when
|
|
12
|
+
* building the prompt for a delegated task.
|
|
13
|
+
*
|
|
14
|
+
* Times are milliseconds on the GPT-Live session timeline, i.e. how much
|
|
15
|
+
* mixed audio has been sent since `session.started`.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_SPEAKER_TRACKER_OPTIONS = {
|
|
18
|
+
speechLevel: 0.012,
|
|
19
|
+
onsetMs: 240,
|
|
20
|
+
releaseMs: 800,
|
|
21
|
+
minAnnounceGapMs: 1200,
|
|
22
|
+
};
|
|
23
|
+
const MAX_SEGMENTS = 2000;
|
|
24
|
+
export class SpeakerTracker {
|
|
25
|
+
options;
|
|
26
|
+
states = new Map();
|
|
27
|
+
dominant = null;
|
|
28
|
+
lastAnnounced = null;
|
|
29
|
+
lastAnnounceAt = Number.NEGATIVE_INFINITY;
|
|
30
|
+
segments = [];
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.options = { ...DEFAULT_SPEAKER_TRACKER_OPTIONS, ...options };
|
|
33
|
+
}
|
|
34
|
+
/** Who currently holds the floor, if anyone. */
|
|
35
|
+
get current() {
|
|
36
|
+
return this.dominant;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Feeds one mixed block's levels (keyed by speaker id).
|
|
40
|
+
*
|
|
41
|
+
* @param nowMs - Session-timeline time at the END of the block.
|
|
42
|
+
* @returns The speaker to announce now, or null.
|
|
43
|
+
*/
|
|
44
|
+
update(levels, nowMs) {
|
|
45
|
+
const { speechLevel, onsetMs, releaseMs } = this.options;
|
|
46
|
+
for (const [id, level] of levels) {
|
|
47
|
+
const state = this.states.get(id) ?? { activeSince: null, lastSpeechAt: -Infinity, level: 0 };
|
|
48
|
+
state.level = state.level * 0.7 + level * 0.3;
|
|
49
|
+
if (level >= speechLevel) {
|
|
50
|
+
if (state.activeSince === null)
|
|
51
|
+
state.activeSince = nowMs;
|
|
52
|
+
state.lastSpeechAt = nowMs;
|
|
53
|
+
}
|
|
54
|
+
else if (state.activeSince !== null && nowMs - state.lastSpeechAt >= releaseMs) {
|
|
55
|
+
state.activeSince = null;
|
|
56
|
+
}
|
|
57
|
+
this.states.set(id, state);
|
|
58
|
+
}
|
|
59
|
+
for (const id of this.states.keys()) {
|
|
60
|
+
if (!levels.has(id))
|
|
61
|
+
this.states.delete(id);
|
|
62
|
+
}
|
|
63
|
+
let best = null;
|
|
64
|
+
let bestLevel = -1;
|
|
65
|
+
for (const [id, state] of this.states) {
|
|
66
|
+
if (state.activeSince === null)
|
|
67
|
+
continue;
|
|
68
|
+
if (state.lastSpeechAt - state.activeSince < onsetMs)
|
|
69
|
+
continue;
|
|
70
|
+
if (state.level > bestLevel) {
|
|
71
|
+
best = id;
|
|
72
|
+
bestLevel = state.level;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Hold the floor through crosstalk: whoever has it keeps it while still
|
|
76
|
+
// talking, unless someone else is clearly louder.
|
|
77
|
+
if (best !== null && this.dominant !== null && best !== this.dominant) {
|
|
78
|
+
const holder = this.states.get(this.dominant);
|
|
79
|
+
if (holder?.activeSince != null && holder.level * 1.5 >= bestLevel)
|
|
80
|
+
best = this.dominant;
|
|
81
|
+
}
|
|
82
|
+
if (best !== this.dominant) {
|
|
83
|
+
const since = best === null ? nowMs : this.states.get(best)?.activeSince ?? nowMs;
|
|
84
|
+
const last = this.segments[this.segments.length - 1];
|
|
85
|
+
this.segments.push({ startMs: Math.max(since, last?.startMs ?? since), speaker: best });
|
|
86
|
+
if (this.segments.length > MAX_SEGMENTS)
|
|
87
|
+
this.segments.splice(0, this.segments.length - MAX_SEGMENTS);
|
|
88
|
+
this.dominant = best;
|
|
89
|
+
}
|
|
90
|
+
if (this.dominant !== null &&
|
|
91
|
+
this.dominant !== this.lastAnnounced &&
|
|
92
|
+
nowMs - this.lastAnnounceAt >= this.options.minAnnounceGapMs) {
|
|
93
|
+
this.lastAnnounced = this.dominant;
|
|
94
|
+
this.lastAnnounceAt = nowMs;
|
|
95
|
+
return this.dominant;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Best guess at who said something spanning [startMs, endMs): the
|
|
101
|
+
* speaker with the most floor time in that window, else whoever last
|
|
102
|
+
* held the floor before it. Null when nobody has spoken yet.
|
|
103
|
+
*/
|
|
104
|
+
speakerAt(startMs, endMs) {
|
|
105
|
+
const overlap = new Map();
|
|
106
|
+
let before = null;
|
|
107
|
+
for (let i = 0; i < this.segments.length; i++) {
|
|
108
|
+
const seg = this.segments[i];
|
|
109
|
+
const segEnd = this.segments[i + 1]?.startMs ?? Number.POSITIVE_INFINITY;
|
|
110
|
+
if (seg.speaker !== null && seg.startMs <= startMs)
|
|
111
|
+
before = seg.speaker;
|
|
112
|
+
if (seg.speaker === null)
|
|
113
|
+
continue;
|
|
114
|
+
const lo = Math.max(startMs, seg.startMs);
|
|
115
|
+
const hi = Math.min(endMs, segEnd);
|
|
116
|
+
if (hi > lo)
|
|
117
|
+
overlap.set(seg.speaker, (overlap.get(seg.speaker) ?? 0) + (hi - lo));
|
|
118
|
+
}
|
|
119
|
+
let best = null;
|
|
120
|
+
let bestMs = 0;
|
|
121
|
+
for (const [speaker, ms] of overlap) {
|
|
122
|
+
if (ms > bestMs) {
|
|
123
|
+
best = speaker;
|
|
124
|
+
bestMs = ms;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return best ?? before;
|
|
128
|
+
}
|
|
129
|
+
/** Forget everything — a new GPT-Live session restarts the timeline at 0. */
|
|
130
|
+
reset() {
|
|
131
|
+
this.states.clear();
|
|
132
|
+
this.dominant = null;
|
|
133
|
+
this.lastAnnounced = null;
|
|
134
|
+
this.lastAnnounceAt = Number.NEGATIVE_INFINITY;
|
|
135
|
+
this.segments = [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=speakers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speakers.js","sourceRoot":"","sources":["../../src/live/speakers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What GPT-Live heard and said, grouped into speaker-labelled lines.
|
|
3
|
+
*
|
|
4
|
+
* GPT-Live's `session.delegation.created` carries an ID and a timestamp but
|
|
5
|
+
* no task text — the app is expected to work out the request from the
|
|
6
|
+
* transcript. This keeps GPT-Live's own input/output transcript fragments
|
|
7
|
+
* (session-timeline timestamps, no speaker labels), labels input fragments
|
|
8
|
+
* with the speaker tracker's best guess, and renders them as lines for the
|
|
9
|
+
* backend prompt and for reseeding a replacement session.
|
|
10
|
+
*/
|
|
11
|
+
export type LiveLineRole = 'room' | 'assistant';
|
|
12
|
+
export interface LiveTranscriptLine {
|
|
13
|
+
role: LiveLineRole;
|
|
14
|
+
/** Display name for room lines; null when nobody could be identified. */
|
|
15
|
+
speaker: string | null;
|
|
16
|
+
text: string;
|
|
17
|
+
startMs: number;
|
|
18
|
+
endMs: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class LiveTranscript {
|
|
21
|
+
private lines;
|
|
22
|
+
addFragment(role: LiveLineRole, speaker: string | null, delta: string, startMs: number, endMs: number): void;
|
|
23
|
+
/** Lines that ended after `sinceMs`, oldest first. */
|
|
24
|
+
linesSince(sinceMs: number): LiveTranscriptLine[];
|
|
25
|
+
recent(count: number): LiveTranscriptLine[];
|
|
26
|
+
get size(): number;
|
|
27
|
+
clear(): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The slice of the conversation a delegation is about: everything since
|
|
31
|
+
* shortly before the previous hand-off (at least a few lines), and the
|
|
32
|
+
* last identified room speaker as the likely requester.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pickDelegationContext(transcript: LiveTranscript, previousDelegationMs: number, offsetMs: number): {
|
|
35
|
+
heard: LiveTranscriptLine[];
|
|
36
|
+
requester: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** One line as plain text, e.g. `Kay: can you check the build`. */
|
|
39
|
+
export declare function formatLine(line: LiveTranscriptLine, assistantName: string): string;
|
|
40
|
+
//# sourceMappingURL=transcript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/live/transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,WAAW,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,YAAY,CAAC;IACnB,yEAAyE;IACzE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAMD,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAA4B;IAEzC,WAAW,CACT,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,IAAI;IAmBP,sDAAsD;IACtD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAIjD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAI3C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,KAAK,IAAI,IAAI;CAGd;AAMD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,cAAc,EAC1B,oBAAoB,EAAE,MAAM,EAC5B,QAAQ,EAAE,MAAM,GACf;IAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAM3D;AAED,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAGlF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What GPT-Live heard and said, grouped into speaker-labelled lines.
|
|
3
|
+
*
|
|
4
|
+
* GPT-Live's `session.delegation.created` carries an ID and a timestamp but
|
|
5
|
+
* no task text — the app is expected to work out the request from the
|
|
6
|
+
* transcript. This keeps GPT-Live's own input/output transcript fragments
|
|
7
|
+
* (session-timeline timestamps, no speaker labels), labels input fragments
|
|
8
|
+
* with the speaker tracker's best guess, and renders them as lines for the
|
|
9
|
+
* backend prompt and for reseeding a replacement session.
|
|
10
|
+
*/
|
|
11
|
+
/** A pause this long between fragments starts a new line. */
|
|
12
|
+
const LINE_GAP_MS = 1500;
|
|
13
|
+
const MAX_LINES = 400;
|
|
14
|
+
export class LiveTranscript {
|
|
15
|
+
lines = [];
|
|
16
|
+
addFragment(role, speaker, delta, startMs, endMs) {
|
|
17
|
+
if (!delta)
|
|
18
|
+
return;
|
|
19
|
+
// Continue the latest line from the same source unless there was a
|
|
20
|
+
// pause. Fragments from the other role may interleave (full duplex),
|
|
21
|
+
// so look back for this role's newest line rather than only the last.
|
|
22
|
+
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
23
|
+
const line = this.lines[i];
|
|
24
|
+
if (line.role !== role)
|
|
25
|
+
continue;
|
|
26
|
+
if (line.speaker === speaker && startMs - line.endMs < LINE_GAP_MS) {
|
|
27
|
+
line.text += delta;
|
|
28
|
+
line.endMs = Math.max(line.endMs, endMs);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
this.lines.push({ role, speaker, text: delta, startMs, endMs });
|
|
34
|
+
if (this.lines.length > MAX_LINES)
|
|
35
|
+
this.lines.splice(0, this.lines.length - MAX_LINES);
|
|
36
|
+
}
|
|
37
|
+
/** Lines that ended after `sinceMs`, oldest first. */
|
|
38
|
+
linesSince(sinceMs) {
|
|
39
|
+
return this.lines.filter((line) => line.endMs > sinceMs);
|
|
40
|
+
}
|
|
41
|
+
recent(count) {
|
|
42
|
+
return this.lines.slice(-count);
|
|
43
|
+
}
|
|
44
|
+
get size() {
|
|
45
|
+
return this.lines.length;
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
this.lines = [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Overlap with the previous hand-off, so a correction keeps its context. */
|
|
52
|
+
const DELEGATION_OVERLAP_MS = 8_000;
|
|
53
|
+
const MIN_HEARD_LINES = 6;
|
|
54
|
+
/**
|
|
55
|
+
* The slice of the conversation a delegation is about: everything since
|
|
56
|
+
* shortly before the previous hand-off (at least a few lines), and the
|
|
57
|
+
* last identified room speaker as the likely requester.
|
|
58
|
+
*/
|
|
59
|
+
export function pickDelegationContext(transcript, previousDelegationMs, offsetMs) {
|
|
60
|
+
const since = Math.max(0, Math.min(previousDelegationMs, offsetMs) - DELEGATION_OVERLAP_MS);
|
|
61
|
+
let heard = transcript.linesSince(since);
|
|
62
|
+
if (heard.length < MIN_HEARD_LINES)
|
|
63
|
+
heard = transcript.recent(MIN_HEARD_LINES);
|
|
64
|
+
const lastRoomLine = [...heard].reverse().find((line) => line.role === 'room' && line.speaker);
|
|
65
|
+
return { heard, requester: lastRoomLine?.speaker ?? null };
|
|
66
|
+
}
|
|
67
|
+
/** One line as plain text, e.g. `Kay: can you check the build`. */
|
|
68
|
+
export function formatLine(line, assistantName) {
|
|
69
|
+
const who = line.role === 'assistant' ? `${assistantName} (voice assistant)` : line.speaker ?? 'Someone';
|
|
70
|
+
return `${who}: ${line.text.trim()}`;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=transcript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../src/live/transcript.ts"],"names":[],"mappings":"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"}
|