@devicai/ui 0.59.0 → 0.61.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 +10 -0
- package/dist/cjs/api/client.js +41 -6
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/api/types.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +9 -2
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +2 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/LiveVoicePanel.js +106 -0
- package/dist/cjs/components/ChatDrawer/LiveVoicePanel.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/LiveVoicePrompter.js +148 -0
- package/dist/cjs/components/ChatDrawer/LiveVoicePrompter.js.map +1 -0
- package/dist/cjs/hooks/useDevicChat.js +68 -17
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/hooks/useDevicLiveVoice.js +73 -0
- package/dist/cjs/hooks/useDevicLiveVoice.js.map +1 -0
- package/dist/cjs/hooks/usePolling.js +1 -1
- package/dist/cjs/hooks/usePolling.js.map +1 -1
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/cjs/utils/consumeChatStream.js +36 -6
- package/dist/cjs/utils/consumeChatStream.js.map +1 -1
- package/dist/cjs/voice/LiveVoiceController.js +287 -0
- package/dist/cjs/voice/LiveVoiceController.js.map +1 -0
- package/dist/esm/api/client.d.ts +12 -3
- package/dist/esm/api/client.js +41 -6
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/api/liveVoice.types.d.ts +49 -0
- package/dist/esm/api/types.d.ts +3 -0
- package/dist/esm/api/types.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +11 -4
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +6 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.js +2 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/LiveVoicePanel.d.ts +21 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePanel.js +102 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePanel.js.map +1 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePrompter.d.ts +12 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePrompter.js +145 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePrompter.js.map +1 -0
- package/dist/esm/hooks/index.d.ts +2 -0
- package/dist/esm/hooks/useDevicChat.d.ts +6 -0
- package/dist/esm/hooks/useDevicChat.js +68 -17
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/hooks/useDevicLiveVoice.d.ts +20 -0
- package/dist/esm/hooks/useDevicLiveVoice.js +71 -0
- package/dist/esm/hooks/useDevicLiveVoice.js.map +1 -0
- package/dist/esm/hooks/usePolling.d.ts +1 -1
- package/dist/esm/hooks/usePolling.js +1 -1
- package/dist/esm/hooks/usePolling.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/styles.css +1 -1
- package/dist/esm/utils/consumeChatStream.d.ts +8 -4
- package/dist/esm/utils/consumeChatStream.js +36 -6
- package/dist/esm/utils/consumeChatStream.js.map +1 -1
- package/dist/esm/voice/LiveVoiceController.d.ts +38 -0
- package/dist/esm/voice/LiveVoiceController.js +284 -0
- package/dist/esm/voice/LiveVoiceController.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Consume Devic's version-1 SSE
|
|
5
|
-
* boundaries. `
|
|
6
|
-
*
|
|
4
|
+
* Consume Devic's version-1 SSE frames, tolerating arbitrary UTF-8/chunk
|
|
5
|
+
* boundaries. `snapshot` frames carry the whole state. When the stream was
|
|
6
|
+
* opened with `?partial=1`, changes to the reply being written arrive as
|
|
7
|
+
* `partial` frames (the whole `streamingMessage`) or `delta` frames (only the
|
|
8
|
+
* text appended to it); both are merged into the last snapshot, so the caller
|
|
9
|
+
* always receives a full state. `onActivity` fires on every chunk, keep-alive
|
|
10
|
+
* comments included, so the caller can tell a quiet connection from a dead one.
|
|
7
11
|
*/
|
|
8
12
|
async function consumeChatStream(response, onSnapshot, onActivity) {
|
|
9
13
|
if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream') || !response.body) {
|
|
@@ -12,6 +16,7 @@ async function consumeChatStream(response, onSnapshot, onActivity) {
|
|
|
12
16
|
const reader = response.body.getReader();
|
|
13
17
|
const decoder = new TextDecoder();
|
|
14
18
|
let buffer = '';
|
|
19
|
+
let last;
|
|
15
20
|
try {
|
|
16
21
|
while (true) {
|
|
17
22
|
const { value, done } = await reader.read();
|
|
@@ -26,11 +31,36 @@ async function consumeChatStream(response, onSnapshot, onActivity) {
|
|
|
26
31
|
const frame = buffer.slice(0, boundary);
|
|
27
32
|
buffer = buffer.slice(boundary + 2);
|
|
28
33
|
const lines = frame.split('\n');
|
|
29
|
-
|
|
34
|
+
const event = lines.find(line => line.startsWith('event:'))?.slice(6).trim();
|
|
35
|
+
if (event !== 'snapshot' && event !== 'partial' && event !== 'delta')
|
|
30
36
|
continue;
|
|
31
37
|
const data = lines.filter(line => line.startsWith('data:')).map(line => line.slice(5).trimStart()).join('\n');
|
|
32
|
-
if (data)
|
|
33
|
-
|
|
38
|
+
if (!data)
|
|
39
|
+
continue;
|
|
40
|
+
const parsed = JSON.parse(data);
|
|
41
|
+
if (event === 'snapshot') {
|
|
42
|
+
last = parsed;
|
|
43
|
+
}
|
|
44
|
+
else if (!last) {
|
|
45
|
+
// Nothing to merge into yet; the next snapshot carries everything.
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
else if (event === 'partial') {
|
|
49
|
+
last = { ...last, ...parsed };
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const message = last.streamingMessage;
|
|
53
|
+
if (!message || typeof parsed.append !== 'string')
|
|
54
|
+
continue;
|
|
55
|
+
last = {
|
|
56
|
+
...last,
|
|
57
|
+
streamingMessage: {
|
|
58
|
+
...message,
|
|
59
|
+
content: { ...message.content, message: `${message.content?.message ?? ''}${parsed.append}` },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
await onSnapshot(last);
|
|
34
64
|
}
|
|
35
65
|
}
|
|
36
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumeChatStream.js","sources":["../../../../src/utils/consumeChatStream.ts"],"sourcesContent":["/**\n * Consume Devic's version-1 SSE
|
|
1
|
+
{"version":3,"file":"consumeChatStream.js","sources":["../../../../src/utils/consumeChatStream.ts"],"sourcesContent":["/**\n * Consume Devic's version-1 SSE frames, tolerating arbitrary UTF-8/chunk\n * boundaries. `snapshot` frames carry the whole state. When the stream was\n * opened with `?partial=1`, changes to the reply being written arrive as\n * `partial` frames (the whole `streamingMessage`) or `delta` frames (only the\n * text appended to it); both are merged into the last snapshot, so the caller\n * always receives a full state. `onActivity` fires on every chunk, keep-alive\n * comments included, so the caller can tell a quiet connection from a dead one.\n */\nexport async function consumeChatStream<T extends object>(\n response: Response,\n onSnapshot: (snapshot: T) => void | Promise<void>,\n onActivity?: () => void,\n): Promise<void> {\n if (!response.ok || !response.headers.get('content-type')?.includes('text/event-stream') || !response.body) {\n throw new Error('Chat streaming unavailable');\n }\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let last: (T & { streamingMessage?: any }) | undefined;\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n onActivity?.();\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n');\n if (buffer.length > 16 * 1024 * 1024) throw new Error('Chat stream frame too large');\n let boundary: number;\n while ((boundary = buffer.indexOf('\\n\\n')) !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n const lines = frame.split('\\n');\n const event = lines.find(line => line.startsWith('event:'))?.slice(6).trim();\n if (event !== 'snapshot' && event !== 'partial' && event !== 'delta') continue;\n const data = lines.filter(line => line.startsWith('data:')).map(line => line.slice(5).trimStart()).join('\\n');\n if (!data) continue;\n const parsed = JSON.parse(data);\n if (event === 'snapshot') {\n last = parsed;\n } else if (!last) {\n // Nothing to merge into yet; the next snapshot carries everything.\n continue;\n } else if (event === 'partial') {\n last = { ...last, ...parsed };\n } else {\n const message = last.streamingMessage;\n if (!message || typeof parsed.append !== 'string') continue;\n last = {\n ...last,\n streamingMessage: {\n ...message,\n content: { ...message.content, message: `${message.content?.message ?? ''}${parsed.append}` },\n },\n };\n }\n await onSnapshot(last as T);\n }\n }\n } finally {\n await reader.cancel().catch(() => {});\n reader.releaseLock();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const initialVoiceSnapshot = () => ({ state: 'idle', muted: false, seconds: 0, transcript: [] });
|
|
4
|
+
/** No browser globals at import/constructor time; loaded only by start(). */
|
|
5
|
+
class LiveVoiceController {
|
|
6
|
+
constructor(client, assistantId, context, changed, created) {
|
|
7
|
+
this.client = client;
|
|
8
|
+
this.assistantId = assistantId;
|
|
9
|
+
this.context = context;
|
|
10
|
+
this.changed = changed;
|
|
11
|
+
this.created = created;
|
|
12
|
+
this.snapshot = initialVoiceSnapshot();
|
|
13
|
+
this.generation = 0;
|
|
14
|
+
this.attempts = 0;
|
|
15
|
+
this.disposeTransport = () => { };
|
|
16
|
+
this.disposed = false;
|
|
17
|
+
}
|
|
18
|
+
update(patch) {
|
|
19
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
20
|
+
if (!this.disposed)
|
|
21
|
+
this.changed(this.snapshot);
|
|
22
|
+
}
|
|
23
|
+
mute(muted) {
|
|
24
|
+
this.snapshot.input?.getAudioTracks().forEach(track => { track.enabled = !muted && this.snapshot.state === 'connected'; });
|
|
25
|
+
this.update({ muted });
|
|
26
|
+
}
|
|
27
|
+
async play() {
|
|
28
|
+
try {
|
|
29
|
+
await this.audio?.play();
|
|
30
|
+
this.update({ playbackBlocked: false });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
this.update({ playbackBlocked: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async start(chatUid, recovering = false) {
|
|
37
|
+
if (this.disposed || this.closing || (!recovering && !['idle', 'error'].includes(this.snapshot.state)))
|
|
38
|
+
return;
|
|
39
|
+
const generation = ++this.generation;
|
|
40
|
+
const current = () => generation === this.generation && !this.disposed;
|
|
41
|
+
if (!recovering)
|
|
42
|
+
this.attempts = 0;
|
|
43
|
+
this.update({ state: 'connecting', error: undefined, chatUid, sessionId: undefined,
|
|
44
|
+
seconds: 0, ...(recovering ? {} : { transcript: [], muted: false }) });
|
|
45
|
+
let media;
|
|
46
|
+
let peer;
|
|
47
|
+
let channel;
|
|
48
|
+
let audio;
|
|
49
|
+
let health;
|
|
50
|
+
let deadline;
|
|
51
|
+
let connectDeadline;
|
|
52
|
+
let backendReady = false;
|
|
53
|
+
let lastTranscript = 0;
|
|
54
|
+
let sessionId;
|
|
55
|
+
let connectionLostAt = 0;
|
|
56
|
+
let healthFailures = 0;
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
clearInterval(health);
|
|
59
|
+
clearTimeout(deadline);
|
|
60
|
+
clearTimeout(connectDeadline);
|
|
61
|
+
media?.getTracks().forEach(track => track.stop());
|
|
62
|
+
if (channel) {
|
|
63
|
+
channel.onclose = null;
|
|
64
|
+
channel.onmessage = null;
|
|
65
|
+
channel.close();
|
|
66
|
+
}
|
|
67
|
+
if (peer) {
|
|
68
|
+
peer.onconnectionstatechange = null;
|
|
69
|
+
peer.ontrack = null;
|
|
70
|
+
peer.close();
|
|
71
|
+
}
|
|
72
|
+
if (audio) {
|
|
73
|
+
audio.pause();
|
|
74
|
+
audio.srcObject = null;
|
|
75
|
+
}
|
|
76
|
+
this.audio = undefined;
|
|
77
|
+
this.update({ input: undefined, output: undefined });
|
|
78
|
+
};
|
|
79
|
+
this.disposeTransport = cleanup;
|
|
80
|
+
const ready = () => {
|
|
81
|
+
if (!current() || !backendReady || peer?.connectionState !== 'connected')
|
|
82
|
+
return;
|
|
83
|
+
clearTimeout(connectDeadline);
|
|
84
|
+
media?.getAudioTracks().forEach(track => { track.enabled = !this.snapshot.muted; });
|
|
85
|
+
this.update({ state: 'connected' });
|
|
86
|
+
};
|
|
87
|
+
const recover = async () => {
|
|
88
|
+
if (!current())
|
|
89
|
+
return;
|
|
90
|
+
++this.generation;
|
|
91
|
+
const ticket = this.generation;
|
|
92
|
+
cleanup();
|
|
93
|
+
this.update({ state: 'reconnecting' });
|
|
94
|
+
try {
|
|
95
|
+
if (!sessionId || ++this.attempts > 3)
|
|
96
|
+
throw new Error('Voice connection interrupted. Please start again.');
|
|
97
|
+
await this.client.closeLiveSession(this.assistantId, sessionId);
|
|
98
|
+
// Never open another paid transport before the old one is confirmed closed.
|
|
99
|
+
for (let i = 0; i < 35 && ticket === this.generation && !this.disposed; i++) {
|
|
100
|
+
const state = await this.client.getLiveSessionStatus(this.assistantId, sessionId);
|
|
101
|
+
if (state.status === 'closed') {
|
|
102
|
+
if (ticket === this.generation && !this.disposed)
|
|
103
|
+
await this.start(this.snapshot.chatUid, true);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
107
|
+
}
|
|
108
|
+
if (ticket === this.generation)
|
|
109
|
+
throw new Error('Could not confirm the previous voice session closed.');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (ticket === this.generation)
|
|
113
|
+
this.update({ state: 'error', error: error });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia || typeof RTCPeerConnection === 'undefined')
|
|
118
|
+
throw new Error('Voice requires microphone access on HTTPS or localhost.');
|
|
119
|
+
media = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true }, video: false });
|
|
120
|
+
if (!current()) {
|
|
121
|
+
media.getTracks().forEach(track => track.stop());
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
media.getAudioTracks().forEach(track => { track.enabled = false; });
|
|
125
|
+
this.update({ input: media });
|
|
126
|
+
peer = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
|
|
127
|
+
audio = new Audio();
|
|
128
|
+
this.audio = audio;
|
|
129
|
+
audio.autoplay = true;
|
|
130
|
+
peer.ontrack = event => {
|
|
131
|
+
if (!current())
|
|
132
|
+
return;
|
|
133
|
+
const output = new MediaStream([event.track]);
|
|
134
|
+
audio.srcObject = output;
|
|
135
|
+
this.update({ output });
|
|
136
|
+
void this.play();
|
|
137
|
+
};
|
|
138
|
+
peer.onconnectionstatechange = () => {
|
|
139
|
+
ready();
|
|
140
|
+
if (peer?.connectionState === 'disconnected')
|
|
141
|
+
connectionLostAt || (connectionLostAt = Date.now());
|
|
142
|
+
else
|
|
143
|
+
connectionLostAt = 0;
|
|
144
|
+
if (peer?.connectionState === 'failed')
|
|
145
|
+
void recover();
|
|
146
|
+
};
|
|
147
|
+
media.getTracks().forEach(track => peer.addTrack(track, media));
|
|
148
|
+
channel = peer.createDataChannel('oai-events');
|
|
149
|
+
channel.onmessage = message => {
|
|
150
|
+
if (!current())
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
const event = JSON.parse(message.data);
|
|
154
|
+
if (event.type === 'session.started') {
|
|
155
|
+
backendReady = true;
|
|
156
|
+
ready();
|
|
157
|
+
}
|
|
158
|
+
if (['session.input_transcript.delta', 'session.output_transcript.delta'].includes(event.type) && typeof event.delta === 'string') {
|
|
159
|
+
lastTranscript = Date.now();
|
|
160
|
+
const role = event.type === 'session.input_transcript.delta' ? 'user' : 'assistant';
|
|
161
|
+
const turns = this.snapshot.transcript.slice(-3).map(turn => ({ ...turn }));
|
|
162
|
+
if (turns[turns.length - 1]?.role === role)
|
|
163
|
+
turns[turns.length - 1].text = (turns[turns.length - 1].text + event.delta).slice(-2000);
|
|
164
|
+
else
|
|
165
|
+
turns.push({ role, text: event.delta.slice(-2000) });
|
|
166
|
+
this.update({ transcript: turns });
|
|
167
|
+
}
|
|
168
|
+
if (event.type === 'session.closed' && sessionId) {
|
|
169
|
+
void this.client.getLiveSessionStatus(this.assistantId, sessionId).then(state => {
|
|
170
|
+
if (!current())
|
|
171
|
+
return;
|
|
172
|
+
if (state.restartRequested)
|
|
173
|
+
void recover();
|
|
174
|
+
else
|
|
175
|
+
void this.stop();
|
|
176
|
+
}).catch(() => { if (current())
|
|
177
|
+
void this.stop(); });
|
|
178
|
+
}
|
|
179
|
+
if (event.type === 'error') {
|
|
180
|
+
this.update({ error: new Error('Voice session failed.') });
|
|
181
|
+
void this.stop();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* Only lifecycle and spoken transcript events are accepted. */ }
|
|
185
|
+
};
|
|
186
|
+
channel.onclose = () => { if (current())
|
|
187
|
+
void recover(); };
|
|
188
|
+
connectDeadline = setTimeout(() => {
|
|
189
|
+
if (current()) {
|
|
190
|
+
this.update({ error: new Error('Voice connection timed out.') });
|
|
191
|
+
void this.stop();
|
|
192
|
+
}
|
|
193
|
+
}, 30000);
|
|
194
|
+
await peer.setLocalDescription(await peer.createOffer());
|
|
195
|
+
await new Promise((resolve, reject) => {
|
|
196
|
+
if (peer.iceGatheringState === 'complete')
|
|
197
|
+
return resolve();
|
|
198
|
+
const done = () => {
|
|
199
|
+
if (peer.iceGatheringState !== 'complete')
|
|
200
|
+
return;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
peer.removeEventListener('icegatheringstatechange', done);
|
|
203
|
+
resolve();
|
|
204
|
+
};
|
|
205
|
+
const timer = setTimeout(() => { peer.removeEventListener('icegatheringstatechange', done); reject(new Error('Could not prepare the audio connection.')); }, 10000);
|
|
206
|
+
peer.addEventListener('icegatheringstatechange', done);
|
|
207
|
+
});
|
|
208
|
+
if (!current())
|
|
209
|
+
return;
|
|
210
|
+
const result = await this.client.createLiveSession(this.assistantId, { ...this.context, chatUid, sdp: peer.localDescription.sdp });
|
|
211
|
+
sessionId = result.sessionId;
|
|
212
|
+
if (!current()) {
|
|
213
|
+
void this.client.closeLiveSession(this.assistantId, sessionId).catch(() => { });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.update({ sessionId, chatUid: result.chatUid });
|
|
217
|
+
this.created(result.chatUid);
|
|
218
|
+
deadline = setTimeout(() => { void this.stop(); }, Math.max(1, Math.min(6000, result.maxDurationSeconds || 6000)) * 1000);
|
|
219
|
+
await peer.setRemoteDescription({ type: 'answer', sdp: result.sdp });
|
|
220
|
+
let busy = false;
|
|
221
|
+
health = setInterval(async () => {
|
|
222
|
+
if (!current() || busy)
|
|
223
|
+
return;
|
|
224
|
+
busy = true;
|
|
225
|
+
try {
|
|
226
|
+
const state = await this.client.getLiveSessionStatus(this.assistantId, sessionId);
|
|
227
|
+
if (!current())
|
|
228
|
+
return;
|
|
229
|
+
healthFailures = 0;
|
|
230
|
+
this.update({ seconds: state.seconds });
|
|
231
|
+
if (state.interrupted || state.restartRequested || (connectionLostAt && Date.now() - connectionLostAt > 5000)) {
|
|
232
|
+
void recover();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (state.status === 'closed') {
|
|
236
|
+
void this.stop();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
backendReady = state.connected;
|
|
240
|
+
ready();
|
|
241
|
+
if (Date.now() - lastTranscript > 1500 && state.transcript?.length)
|
|
242
|
+
this.update({ transcript: state.transcript.slice(-4).filter(t => t.role === 'user' || t.role === 'assistant').map(t => ({ role: t.role, text: t.text.slice(-2000) })) });
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
if (current() && ++healthFailures >= 5) {
|
|
246
|
+
this.update({ error: new Error('Voice status unavailable. Session stopped.') });
|
|
247
|
+
void this.stop();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
busy = false;
|
|
252
|
+
}
|
|
253
|
+
}, 1000);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (!current())
|
|
257
|
+
return;
|
|
258
|
+
this.update({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
259
|
+
await this.stop();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
stop() {
|
|
263
|
+
if (this.closing)
|
|
264
|
+
return this.closing;
|
|
265
|
+
++this.generation;
|
|
266
|
+
this.disposeTransport();
|
|
267
|
+
const id = this.snapshot.sessionId;
|
|
268
|
+
this.update({ state: id ? 'closing' : (this.snapshot.error ? 'error' : 'idle') });
|
|
269
|
+
this.closing = (async () => {
|
|
270
|
+
if (id) {
|
|
271
|
+
try {
|
|
272
|
+
await this.client.closeLiveSession(this.assistantId, id);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
this.update({ error: error });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.update({ state: this.snapshot.error ? 'error' : 'idle', sessionId: undefined });
|
|
279
|
+
})().finally(() => { this.closing = undefined; });
|
|
280
|
+
return this.closing;
|
|
281
|
+
}
|
|
282
|
+
dispose() { this.disposed = true; void this.stop(); }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
exports.LiveVoiceController = LiveVoiceController;
|
|
286
|
+
exports.initialVoiceSnapshot = initialVoiceSnapshot;
|
|
287
|
+
//# sourceMappingURL=LiveVoiceController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveVoiceController.js","sources":["../../../../src/voice/LiveVoiceController.ts"],"sourcesContent":["import type { DevicApiClient } from '../api/client';\nimport type { LiveVoiceContext, LiveVoiceTurn } from '../api/liveVoice.types';\n\nexport type LiveVoiceState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'closing' | 'error';\nexport interface LiveVoiceSnapshot {\n state: LiveVoiceState;\n chatUid?: string;\n sessionId?: string;\n muted: boolean;\n seconds: number;\n transcript: LiveVoiceTurn[];\n input?: MediaStream;\n output?: MediaStream;\n error?: Error;\n playbackBlocked?: boolean;\n}\nexport const initialVoiceSnapshot = (): LiveVoiceSnapshot => ({ state: 'idle', muted: false, seconds: 0, transcript: [] });\n\n/** No browser globals at import/constructor time; loaded only by start(). */\nexport class LiveVoiceController {\n private snapshot = initialVoiceSnapshot();\n private generation = 0;\n private attempts = 0;\n private disposeTransport: () => void = () => {};\n private audio?: HTMLAudioElement;\n private closing?: Promise<void>;\n private disposed = false;\n constructor(private client: DevicApiClient, private assistantId: string,\n private context: LiveVoiceContext, private changed: (value: LiveVoiceSnapshot) => void,\n private created: (chatUid: string) => void) {}\n\n private update(patch: Partial<LiveVoiceSnapshot>) {\n this.snapshot = { ...this.snapshot, ...patch };\n if (!this.disposed) this.changed(this.snapshot);\n }\n mute(muted: boolean) {\n this.snapshot.input?.getAudioTracks().forEach(track => { track.enabled = !muted && this.snapshot.state === 'connected'; });\n this.update({ muted });\n }\n async play() {\n try { await this.audio?.play(); this.update({ playbackBlocked: false }); }\n catch { this.update({ playbackBlocked: true }); }\n }\n async start(chatUid?: string, recovering = false): Promise<void> {\n if (this.disposed || this.closing || (!recovering && !['idle', 'error'].includes(this.snapshot.state))) return;\n const generation = ++this.generation;\n const current = () => generation === this.generation && !this.disposed;\n if (!recovering) this.attempts = 0;\n this.update({ state: 'connecting', error: undefined, chatUid, sessionId: undefined,\n seconds: 0, ...(recovering ? {} : { transcript: [], muted: false }) });\n let media: MediaStream | undefined;\n let peer: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n let audio: HTMLAudioElement | undefined;\n let health: ReturnType<typeof setInterval> | undefined;\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let connectDeadline: ReturnType<typeof setTimeout> | undefined;\n let backendReady = false;\n let lastTranscript = 0;\n let sessionId: string | undefined;\n let connectionLostAt = 0;\n let healthFailures = 0;\n const cleanup = () => {\n clearInterval(health); clearTimeout(deadline); clearTimeout(connectDeadline);\n media?.getTracks().forEach(track => track.stop());\n if (channel) { channel.onclose = null; channel.onmessage = null; channel.close(); }\n if (peer) { peer.onconnectionstatechange = null; peer.ontrack = null; peer.close(); }\n if (audio) { audio.pause(); audio.srcObject = null; }\n this.audio = undefined;\n this.update({ input: undefined, output: undefined });\n };\n this.disposeTransport = cleanup;\n const ready = () => {\n if (!current() || !backendReady || peer?.connectionState !== 'connected') return;\n clearTimeout(connectDeadline);\n media?.getAudioTracks().forEach(track => { track.enabled = !this.snapshot.muted; });\n this.update({ state: 'connected' });\n };\n const recover = async () => {\n if (!current()) return;\n ++this.generation;\n const ticket = this.generation;\n cleanup();\n this.update({ state: 'reconnecting' });\n try {\n if (!sessionId || ++this.attempts > 3) throw new Error('Voice connection interrupted. Please start again.');\n await this.client.closeLiveSession(this.assistantId, sessionId);\n // Never open another paid transport before the old one is confirmed closed.\n for (let i = 0; i < 35 && ticket === this.generation && !this.disposed; i++) {\n const state = await this.client.getLiveSessionStatus(this.assistantId, sessionId);\n if (state.status === 'closed') {\n if (ticket === this.generation && !this.disposed) await this.start(this.snapshot.chatUid, true);\n return;\n }\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n if (ticket === this.generation) throw new Error('Could not confirm the previous voice session closed.');\n } catch (error) {\n if (ticket === this.generation) this.update({ state: 'error', error: error as Error });\n }\n };\n try {\n if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia || typeof RTCPeerConnection === 'undefined')\n throw new Error('Voice requires microphone access on HTTPS or localhost.');\n media = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true }, video: false });\n if (!current()) { media.getTracks().forEach(track => track.stop()); return; }\n media.getAudioTracks().forEach(track => { track.enabled = false; });\n this.update({ input: media });\n peer = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });\n audio = new Audio(); this.audio = audio; audio.autoplay = true;\n peer.ontrack = event => {\n if (!current()) return;\n const output = new MediaStream([event.track]);\n audio!.srcObject = output;\n this.update({ output }); void this.play();\n };\n peer.onconnectionstatechange = () => {\n ready();\n if (peer?.connectionState === 'disconnected') connectionLostAt ||= Date.now();\n else connectionLostAt = 0;\n if (peer?.connectionState === 'failed') void recover();\n };\n media.getTracks().forEach(track => peer!.addTrack(track, media!));\n channel = peer.createDataChannel('oai-events');\n channel.onmessage = message => {\n if (!current()) return;\n try {\n const event = JSON.parse(message.data);\n if (event.type === 'session.started') { backendReady = true; ready(); }\n if (['session.input_transcript.delta', 'session.output_transcript.delta'].includes(event.type) && typeof event.delta === 'string') {\n lastTranscript = Date.now();\n const role = event.type === 'session.input_transcript.delta' ? 'user' : 'assistant';\n const turns = this.snapshot.transcript.slice(-3).map(turn => ({ ...turn }));\n if (turns[turns.length - 1]?.role === role) turns[turns.length - 1].text = (turns[turns.length - 1].text + event.delta).slice(-2000);\n else turns.push({ role, text: event.delta.slice(-2000) });\n this.update({ transcript: turns });\n }\n if (event.type === 'session.closed' && sessionId) {\n void this.client.getLiveSessionStatus(this.assistantId, sessionId).then(state => {\n if (!current()) return;\n if (state.restartRequested) void recover(); else void this.stop();\n }).catch(() => { if (current()) void this.stop(); });\n }\n if (event.type === 'error') { this.update({ error: new Error('Voice session failed.') }); void this.stop(); }\n } catch { /* Only lifecycle and spoken transcript events are accepted. */ }\n };\n channel.onclose = () => { if (current()) void recover(); };\n connectDeadline = setTimeout(() => {\n if (current()) { this.update({ error: new Error('Voice connection timed out.') }); void this.stop(); }\n }, 30000);\n await peer.setLocalDescription(await peer.createOffer());\n await new Promise<void>((resolve, reject) => {\n if (peer!.iceGatheringState === 'complete') return resolve();\n const done = () => {\n if (peer!.iceGatheringState !== 'complete') return;\n clearTimeout(timer); peer!.removeEventListener('icegatheringstatechange', done); resolve();\n };\n const timer = setTimeout(() => { peer!.removeEventListener('icegatheringstatechange', done); reject(new Error('Could not prepare the audio connection.')); }, 10000);\n peer!.addEventListener('icegatheringstatechange', done);\n });\n if (!current()) return;\n const result = await this.client.createLiveSession(this.assistantId, { ...this.context, chatUid, sdp: peer.localDescription!.sdp });\n sessionId = result.sessionId;\n if (!current()) { void this.client.closeLiveSession(this.assistantId, sessionId).catch(() => {}); return; }\n this.update({ sessionId, chatUid: result.chatUid }); this.created(result.chatUid);\n deadline = setTimeout(() => { void this.stop(); }, Math.max(1, Math.min(6000, result.maxDurationSeconds || 6000)) * 1000);\n await peer.setRemoteDescription({ type: 'answer', sdp: result.sdp });\n let busy = false;\n health = setInterval(async () => {\n if (!current() || busy) return;\n busy = true;\n try {\n const state = await this.client.getLiveSessionStatus(this.assistantId, sessionId!);\n if (!current()) return;\n healthFailures = 0;\n this.update({ seconds: state.seconds });\n if (state.interrupted || state.restartRequested || (connectionLostAt && Date.now() - connectionLostAt > 5000)) { void recover(); return; }\n if (state.status === 'closed') { void this.stop(); return; }\n backendReady = state.connected; ready();\n if (Date.now() - lastTranscript > 1500 && state.transcript?.length)\n this.update({ transcript: state.transcript.slice(-4).filter(t => t.role === 'user' || t.role === 'assistant').map(t => ({ role: t.role, text: t.text.slice(-2000) })) });\n } catch {\n if (current() && ++healthFailures >= 5) { this.update({ error: new Error('Voice status unavailable. Session stopped.')} ); void this.stop(); }\n } finally { busy = false; }\n }, 1000);\n } catch (error) {\n if (!current()) return;\n this.update({ error: error instanceof Error ? error : new Error(String(error)) });\n await this.stop();\n }\n }\n stop(): Promise<void> {\n if (this.closing) return this.closing;\n ++this.generation;\n this.disposeTransport();\n const id = this.snapshot.sessionId;\n this.update({ state: id ? 'closing' : (this.snapshot.error ? 'error' : 'idle') });\n this.closing = (async () => {\n if (id) {\n try { await this.client.closeLiveSession(this.assistantId, id); }\n catch (error) { this.update({ error: error as Error }); }\n }\n this.update({ state: this.snapshot.error ? 'error' : 'idle', sessionId: undefined });\n })().finally(() => { this.closing = undefined; });\n return this.closing;\n }\n dispose() { this.disposed = true; void this.stop(); }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/api/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CreateLiveSessionRequest, LiveVoiceSession, LiveVoiceSessionStatus, LiveVoiceRecording, LiveVoiceUsage } from './liveVoice.types';
|
|
1
2
|
import type { ProcessMessageDto, ChatMessage, AsyncResponse, RealtimeChatHistory, ChatHistory, AssistantSpecialization, ApiError, ToolCallResponse, ListConversationsResponse, FeedbackSubmission, FeedbackEntry, AgentThreadDto, AgentDto, WhisperTranscriptionResponse, TenantUsage, TenantUsageHistoryRow, TenantUsageHistoryQuery, CoreMemoryList, CoreMemoryEntry, Integration, TenantMcpAuthInput, TenantMcpConnectResult, TenantMcpListing, IntegrationAuthScheme, IntegrationSetupRequired, ModelInterfaceToolSchema, StopChatResponse } from "./types";
|
|
2
3
|
/**
|
|
3
4
|
* A tenant session, as the integrator's backend hands it over.
|
|
@@ -82,9 +83,17 @@ export declare class DevicApiClient {
|
|
|
82
83
|
* Make an authenticated request to the API
|
|
83
84
|
*/
|
|
84
85
|
private request;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
private liveRequest;
|
|
87
|
+
createLiveSession(assistantId: string, body: CreateLiveSessionRequest): Promise<LiveVoiceSession>;
|
|
88
|
+
getLiveSessionStatus(assistantId: string, sessionId: string): Promise<LiveVoiceSessionStatus>;
|
|
89
|
+
closeLiveSession(assistantId: string, sessionId: string): Promise<{
|
|
90
|
+
closing: boolean;
|
|
91
|
+
chatUid: string;
|
|
92
|
+
}>;
|
|
93
|
+
getLiveRecordings(assistantId: string, chatUid: string, offset?: number, limit?: number): Promise<LiveVoiceRecording[]>;
|
|
94
|
+
getLiveRecordingAudio(assistantId: string, chatUid: string, sessionId: string): Promise<Blob>;
|
|
95
|
+
getLiveVoiceUsage(assistantId: string, chatUid: string): Promise<LiveVoiceUsage>;
|
|
96
|
+
/** Get all assistant specializations. */
|
|
88
97
|
getAssistants(external?: boolean): Promise<AssistantSpecialization[]>;
|
|
89
98
|
/**
|
|
90
99
|
* Get a specific assistant specialization
|
package/dist/esm/api/client.js
CHANGED
|
@@ -115,7 +115,7 @@ class DevicApiClient {
|
|
|
115
115
|
/**
|
|
116
116
|
* Make an authenticated request to the API
|
|
117
117
|
*/
|
|
118
|
-
async request(endpoint, options = {}, isRetry = false) {
|
|
118
|
+
async request(endpoint, options = {}, isRetry = false, binary = false) {
|
|
119
119
|
const url = `${this.config.baseUrl}${endpoint}`;
|
|
120
120
|
const credential = await this.authorization();
|
|
121
121
|
const headers = {
|
|
@@ -133,7 +133,7 @@ class DevicApiClient {
|
|
|
133
133
|
// turns that into a pause instead of a broken conversation.
|
|
134
134
|
if (response.status === 401 && this.config.getTenantSession && !isRetry) {
|
|
135
135
|
if (await this.recoverSession(credential)) {
|
|
136
|
-
return this.request(endpoint, options, true);
|
|
136
|
+
return this.request(endpoint, options, true, binary);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
if (!response.ok) {
|
|
@@ -156,6 +156,10 @@ class DevicApiClient {
|
|
|
156
156
|
}
|
|
157
157
|
throw new DevicApiError(errorData);
|
|
158
158
|
}
|
|
159
|
+
if (binary)
|
|
160
|
+
return await response.blob();
|
|
161
|
+
if (response.status === 204)
|
|
162
|
+
return undefined;
|
|
159
163
|
// Handle responses that may have a wrapper structure
|
|
160
164
|
const data = await response.json();
|
|
161
165
|
// If the response has a data property, extract it (common wrapper pattern)
|
|
@@ -164,9 +168,38 @@ class DevicApiClient {
|
|
|
164
168
|
}
|
|
165
169
|
return data;
|
|
166
170
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
async liveRequest(path, options = {}) {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const timeout = setTimeout(() => controller.abort(), 25000);
|
|
174
|
+
try {
|
|
175
|
+
return await this.request(path, { ...options, signal: controller.signal });
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
createLiveSession(assistantId, body) {
|
|
182
|
+
// No network-error retry: an ambiguous response could hide a paid session.
|
|
183
|
+
return this.liveRequest(`/api/v1/assistants/${encodeURIComponent(assistantId)}/live/sessions`, {
|
|
184
|
+
method: 'POST', body: JSON.stringify(body),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
getLiveSessionStatus(assistantId, sessionId) {
|
|
188
|
+
return this.liveRequest(`/api/v1/assistants/${encodeURIComponent(assistantId)}/live/sessions/${encodeURIComponent(sessionId)}`);
|
|
189
|
+
}
|
|
190
|
+
closeLiveSession(assistantId, sessionId) {
|
|
191
|
+
return this.liveRequest(`/api/v1/assistants/${encodeURIComponent(assistantId)}/live/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE', keepalive: true });
|
|
192
|
+
}
|
|
193
|
+
getLiveRecordings(assistantId, chatUid, offset = 0, limit = 100) {
|
|
194
|
+
return this.request(`/api/v1/assistants/${encodeURIComponent(assistantId)}/chats/${encodeURIComponent(chatUid)}/recordings?offset=${offset}&limit=${limit}`);
|
|
195
|
+
}
|
|
196
|
+
getLiveRecordingAudio(assistantId, chatUid, sessionId) {
|
|
197
|
+
return this.request(`/api/v1/assistants/${encodeURIComponent(assistantId)}/chats/${encodeURIComponent(chatUid)}/recordings/${encodeURIComponent(sessionId)}/audio`, {}, false, true);
|
|
198
|
+
}
|
|
199
|
+
getLiveVoiceUsage(assistantId, chatUid) {
|
|
200
|
+
return this.request(`/api/v1/assistants/${encodeURIComponent(assistantId)}/chats/${encodeURIComponent(chatUid)}/live-usage`);
|
|
201
|
+
}
|
|
202
|
+
/** Get all assistant specializations. */
|
|
170
203
|
async getAssistants(external = false) {
|
|
171
204
|
const query = external ? "?external=true" : "";
|
|
172
205
|
return this.request(`/api/v1/assistants${query}`);
|
|
@@ -210,7 +243,9 @@ class DevicApiClient {
|
|
|
210
243
|
* on every chunk received, keep-alives included.
|
|
211
244
|
*/
|
|
212
245
|
async streamRealtimeHistory(assistantId, chatUid, onSnapshot, signal, onActivity) {
|
|
213
|
-
|
|
246
|
+
// `partial=1`: while only the reply being written changes, the API sends
|
|
247
|
+
// `partial` frames with just that instead of the whole conversation.
|
|
248
|
+
const url = `${this.config.baseUrl}/api/v1/assistants/${encodeURIComponent(assistantId)}/chats/${encodeURIComponent(chatUid)}/stream?partial=1`;
|
|
214
249
|
let credential = await this.authorization();
|
|
215
250
|
const open = () => fetch(url, { signal, headers: { Authorization: `Bearer ${credential}`, Accept: 'text/event-stream', 'devic-api-source': 'ui' } });
|
|
216
251
|
let response = await open();
|