@voxagent/bridge 0.4.1 → 0.6.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 +32 -0
- package/dist/cli.js +44 -7
- 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.d.ts +8 -0
- package/dist/drivers/claude.d.ts.map +1 -1
- package/dist/drivers/claude.js +105 -32
- 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/drivers/inbox.d.ts +26 -0
- package/dist/drivers/inbox.d.ts.map +1 -0
- package/dist/drivers/inbox.js +53 -0
- package/dist/drivers/inbox.js.map +1 -0
- package/dist/drivers/types.d.ts +9 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/history.d.ts +31 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +170 -0
- package/dist/history.js.map +1 -0
- package/dist/live/frontend.d.ts +136 -0
- package/dist/live/frontend.d.ts.map +1 -0
- package/dist/live/frontend.js +529 -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 +65 -0
- package/dist/live/prompts.d.ts.map +1 -0
- package/dist/live/prompts.js +148 -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/protocol.d.ts +63 -4
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/room.d.ts +25 -0
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +218 -5
- 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 +5 -2
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
export declare const LIVE_SESSIONS_URL = "wss://api.openai.com/v1/live/sessions";
|
|
19
|
+
export declare const DEFAULT_LIVE_MODEL = "gpt-live-1";
|
|
20
|
+
export declare const DEFAULT_LIVE_VOICE = "marin";
|
|
21
|
+
export type LiveCloseReason = 'close_requested' | 'expired' | 'content' | 'remote_hangup' | 'connection_lost'
|
|
22
|
+
/** The socket closed before a `session.closed` event arrived. */
|
|
23
|
+
| 'socket_closed';
|
|
24
|
+
/** A text message used to seed a new session's history. */
|
|
25
|
+
export type LiveInitialItem = {
|
|
26
|
+
type: 'message';
|
|
27
|
+
role: 'developer' | 'user';
|
|
28
|
+
content: [{
|
|
29
|
+
type: 'input_text';
|
|
30
|
+
text: string;
|
|
31
|
+
}];
|
|
32
|
+
} | {
|
|
33
|
+
type: 'message';
|
|
34
|
+
role: 'assistant';
|
|
35
|
+
content: [{
|
|
36
|
+
type: 'output_text';
|
|
37
|
+
text: string;
|
|
38
|
+
}];
|
|
39
|
+
};
|
|
40
|
+
export interface LiveSessionConfig {
|
|
41
|
+
apiKey: string;
|
|
42
|
+
model: string;
|
|
43
|
+
voice: string;
|
|
44
|
+
instructions: string;
|
|
45
|
+
/** Prior text history (≤ 128 messages, ≤ 8,192 tokens). */
|
|
46
|
+
input?: LiveInitialItem[];
|
|
47
|
+
url?: string;
|
|
48
|
+
}
|
|
49
|
+
export type LiveSessionEvent = {
|
|
50
|
+
type: 'started';
|
|
51
|
+
sessionId: string;
|
|
52
|
+
expiresAt: number | null;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'audio';
|
|
55
|
+
samples: Int16Array;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'transcript';
|
|
58
|
+
role: 'input' | 'output';
|
|
59
|
+
delta: string;
|
|
60
|
+
startMs: number;
|
|
61
|
+
endMs: number;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'delegation';
|
|
64
|
+
id: string;
|
|
65
|
+
offsetMs: number;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'usage';
|
|
68
|
+
seconds: number;
|
|
69
|
+
contextRatio: number | null;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'error';
|
|
72
|
+
code: string;
|
|
73
|
+
message: string;
|
|
74
|
+
clientEventId: string | null;
|
|
75
|
+
} | {
|
|
76
|
+
type: 'closed';
|
|
77
|
+
reason: LiveCloseReason;
|
|
78
|
+
usageSeconds: number | null;
|
|
79
|
+
};
|
|
80
|
+
/** The slice of the WebSocket API the session uses — lets tests inject a fake. */
|
|
81
|
+
export interface LiveSocket {
|
|
82
|
+
readonly readyState: number;
|
|
83
|
+
send(data: string): void;
|
|
84
|
+
close(): void;
|
|
85
|
+
addEventListener(type: 'open', listener: () => void): void;
|
|
86
|
+
addEventListener(type: 'message', listener: (event: {
|
|
87
|
+
data: unknown;
|
|
88
|
+
}) => void): void;
|
|
89
|
+
addEventListener(type: 'close', listener: (event: {
|
|
90
|
+
code?: number;
|
|
91
|
+
reason?: string;
|
|
92
|
+
}) => void): void;
|
|
93
|
+
addEventListener(type: 'error', listener: (event: unknown) => void): void;
|
|
94
|
+
}
|
|
95
|
+
export type LiveSocketFactory = (url: string, headers: Record<string, string>) => LiveSocket;
|
|
96
|
+
export declare class LiveSession {
|
|
97
|
+
private readonly config;
|
|
98
|
+
private readonly onEvent;
|
|
99
|
+
private readonly socketFactory;
|
|
100
|
+
private socket;
|
|
101
|
+
private state;
|
|
102
|
+
private eventSeq;
|
|
103
|
+
private closedEmitted;
|
|
104
|
+
private lastUsageSeconds;
|
|
105
|
+
private onClosed;
|
|
106
|
+
sessionId: string | null;
|
|
107
|
+
expiresAt: number | null;
|
|
108
|
+
constructor(config: LiveSessionConfig, onEvent: (event: LiveSessionEvent) => void, socketFactory?: LiveSocketFactory);
|
|
109
|
+
get started(): boolean;
|
|
110
|
+
/** Opens the socket and resolves once `session.started` arrives. */
|
|
111
|
+
start(timeoutMs?: number): Promise<void>;
|
|
112
|
+
appendAudio(samples: Int16Array): void;
|
|
113
|
+
/** Quiet context the model may use later without saying it now. */
|
|
114
|
+
appendThinking(content: string, delegationId?: string | null): string | null;
|
|
115
|
+
/** Information the model should say aloud, in its own words. */
|
|
116
|
+
appendCommentary(content: string, delegationId?: string | null): string | null;
|
|
117
|
+
/** Trusted instructions; can interrupt speech in progress. */
|
|
118
|
+
appendInstructions(content: string, delegationId?: string | null): string | null;
|
|
119
|
+
setInputMuted(muted: boolean): void;
|
|
120
|
+
/**
|
|
121
|
+
* Graceful close: `session.close`, then wait for `session.closed` (final
|
|
122
|
+
* usage) up to `timeoutMs` before dropping the socket.
|
|
123
|
+
*/
|
|
124
|
+
close(timeoutMs?: number): Promise<void>;
|
|
125
|
+
private handleMessage;
|
|
126
|
+
private sendAppend;
|
|
127
|
+
private emitClosed;
|
|
128
|
+
private sendRaw;
|
|
129
|
+
private abortSocket;
|
|
130
|
+
private nextEventId;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;GAeG;AAaH,MAAM,CAAC,MAAM,+BAA+B,GAA0B;IACpE,WAAW,EAAE,KAAK;IAClB,OAAO,EAAE,GAAG;IACZ,SAAS,EAAE,GAAG;IACd,gBAAgB,EAAE,IAAI;CACvB,CAAC;AAeF,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,MAAM,OAAO,cAAc;IACR,OAAO,CAAwB;IACxC,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IACzC,QAAQ,GAAkB,IAAI,CAAC;IAC/B,aAAa,GAAkB,IAAI,CAAC;IACpC,cAAc,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC1C,QAAQ,GAAmB,EAAE,CAAC;IAEtC,YAAY,UAA0C,EAAE;QACtD,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,+BAA+B,EAAE,GAAG,OAAO,EAAE,CAAC;IACpE,CAAC;IAED,gDAAgD;IAChD,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAmC,EAAE,KAAa;QACvD,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACzD,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;YAC9F,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,CAAC;YAC9C,IAAI,KAAK,IAAI,WAAW,EAAE,CAAC;gBACzB,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI;oBAAE,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC;gBAC1D,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YAC7B,CAAC;iBAAM,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,KAAK,GAAG,KAAK,CAAC,YAAY,IAAI,SAAS,EAAE,CAAC;gBACjF,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;YAC3B,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC7B,CAAC;QACD,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACpC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,IAAI,GAAkB,IAAI,CAAC;QAC/B,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;QACnB,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI;gBAAE,SAAS;YACzC,IAAI,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,WAAW,GAAG,OAAO;gBAAE,SAAS;YAC/D,IAAI,KAAK,CAAC,KAAK,GAAG,SAAS,EAAE,CAAC;gBAC5B,IAAI,GAAG,EAAE,CAAC;gBACV,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,CAAC;QACH,CAAC;QACD,wEAAwE;QACxE,kDAAkD;QAClD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;YACtE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC9C,IAAI,MAAM,EAAE,WAAW,IAAI,IAAI,IAAI,MAAM,CAAC,KAAK,GAAG,GAAG,IAAI,SAAS;gBAAE,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC3F,CAAC;QAED,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,IAAI,KAAK,CAAC;YAClF,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACrD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACxF,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,YAAY;gBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC;YACtG,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;QAED,IACE,IAAI,CAAC,QAAQ,KAAK,IAAI;YACtB,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,aAAa;YACpC,KAAK,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAC5D,CAAC;YACD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;YAC5B,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,OAAe,EAAE,KAAa;QACtC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC1C,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,iBAAiB,CAAC;YACzE,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI,IAAI,GAAG,CAAC,OAAO,IAAI,OAAO;gBAAE,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC;YACzE,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI;gBAAE,SAAS;YACnC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAC1C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACnC,IAAI,EAAE,GAAG,EAAE;gBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACrF,CAAC;QACD,IAAI,IAAI,GAAkB,IAAI,CAAC;QAC/B,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC;YACpC,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC;gBAChB,IAAI,GAAG,OAAO,CAAC;gBACf,MAAM,GAAG,EAAE,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,IAAI,IAAI,MAAM,CAAC;IACxB,CAAC;IAED,6EAA6E;IAC7E,KAAK;QACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,iBAAiB,CAAC;QAC/C,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;IACrB,CAAC;CACF"}
|