@runtypelabs/voice 0.2.4 → 0.4.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 +149 -4
- package/dist/index.cjs +257 -25
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +257 -25
- package/dist/persona.cjs +319 -30
- package/dist/persona.d.cts +23 -4
- package/dist/persona.d.ts +23 -4
- package/dist/persona.js +319 -30
- package/dist/react.cjs +320 -32
- package/dist/react.d.cts +7 -4
- package/dist/react.d.ts +7 -4
- package/dist/react.js +321 -33
- package/dist/types-Dkh4LxEA.d.cts +100 -0
- package/dist/types-Dkh4LxEA.d.ts +100 -0
- package/package.json +1 -1
- package/dist/types-9SfA7oHc.d.cts +0 -40
- package/dist/types-9SfA7oHc.d.ts +0 -40
package/dist/react.cjs
CHANGED
|
@@ -37,13 +37,16 @@ class RuntypePcmPlayerProcessor extends AudioWorkletProcessor {
|
|
|
37
37
|
this.readOffset = 0
|
|
38
38
|
this.buffered = 0
|
|
39
39
|
this.waiting = true
|
|
40
|
-
// INVARIANT:
|
|
40
|
+
// INVARIANT: Pipeline replies need EOS; continuous sessions report queue drain without a provider turn boundary.
|
|
41
41
|
this.eosSeen = false
|
|
42
|
+
this.continuous = false
|
|
42
43
|
this.revision = 0
|
|
43
44
|
this.port.onmessage = (e) => {
|
|
44
45
|
const msg = e.data
|
|
45
46
|
this.revision = msg.revision
|
|
46
|
-
if (msg.type === '
|
|
47
|
+
if (msg.type === 'continuous') {
|
|
48
|
+
this.continuous = msg.enabled
|
|
49
|
+
} else if (msg.type === 'push') {
|
|
47
50
|
this.eosSeen = false
|
|
48
51
|
this.chunks.push(msg.samples)
|
|
49
52
|
this.buffered += msg.samples.length
|
|
@@ -83,7 +86,7 @@ class RuntypePcmPlayerProcessor extends AudioWorkletProcessor {
|
|
|
83
86
|
}
|
|
84
87
|
if (this.buffered === 0) {
|
|
85
88
|
this.waiting = true // mid-reply underrun: re-buffer silently
|
|
86
|
-
if (this.eosSeen) {
|
|
89
|
+
if (this.eosSeen || this.continuous) {
|
|
87
90
|
this.eosSeen = false
|
|
88
91
|
this.port.postMessage({ type: 'drained', revision: this.revision })
|
|
89
92
|
}
|
|
@@ -127,6 +130,9 @@ async function createPcmPlayer(onDrained) {
|
|
|
127
130
|
if (samples.length === 0) return;
|
|
128
131
|
node.port.postMessage({ type: "push", samples, revision }, [samples.buffer]);
|
|
129
132
|
},
|
|
133
|
+
setContinuousMode(enabled) {
|
|
134
|
+
node.port.postMessage({ type: "continuous", enabled, revision });
|
|
135
|
+
},
|
|
130
136
|
endOfStream() {
|
|
131
137
|
node.port.postMessage({ type: "eos", revision });
|
|
132
138
|
},
|
|
@@ -171,9 +177,58 @@ function initialSnapshot() {
|
|
|
171
177
|
error: null,
|
|
172
178
|
errorDetails: void 0,
|
|
173
179
|
interruptionMode: "none",
|
|
174
|
-
canCancel: false
|
|
180
|
+
canCancel: false,
|
|
181
|
+
artifacts: [],
|
|
182
|
+
session: null
|
|
175
183
|
};
|
|
176
184
|
}
|
|
185
|
+
function readString(value) {
|
|
186
|
+
return typeof value === "string" && value ? value : void 0;
|
|
187
|
+
}
|
|
188
|
+
function readArtifactFile(value) {
|
|
189
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
190
|
+
const file = value;
|
|
191
|
+
const path = readString(file.path);
|
|
192
|
+
const mimeType = readString(file.mimeType);
|
|
193
|
+
if (!path || !mimeType) return void 0;
|
|
194
|
+
const language = readString(file.language);
|
|
195
|
+
return { path, mimeType, ...language ? { language } : {} };
|
|
196
|
+
}
|
|
197
|
+
function applyArtifactFrame(existing, frame, identity) {
|
|
198
|
+
if (frame.type === "artifact_start") {
|
|
199
|
+
const title = readString(frame.title);
|
|
200
|
+
const component = readString(frame.component);
|
|
201
|
+
const file = readArtifactFile(frame.file);
|
|
202
|
+
return {
|
|
203
|
+
...identity,
|
|
204
|
+
artifactType: frame.artifactType === "component" ? "component" : "markdown",
|
|
205
|
+
...title ? { title } : {},
|
|
206
|
+
...file ? { file } : {},
|
|
207
|
+
...component ? { component } : {},
|
|
208
|
+
content: "",
|
|
209
|
+
status: "streaming"
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (!existing) return void 0;
|
|
213
|
+
if (frame.type === "artifact_delta") {
|
|
214
|
+
const delta = typeof frame.delta === "string" ? frame.delta : "";
|
|
215
|
+
return delta ? { ...existing, content: existing.content + delta } : void 0;
|
|
216
|
+
}
|
|
217
|
+
if (frame.type === "artifact_update") {
|
|
218
|
+
const component = readString(frame.component) ?? existing.component;
|
|
219
|
+
const props = frame.props && typeof frame.props === "object" && !Array.isArray(frame.props) ? frame.props : existing.props;
|
|
220
|
+
return {
|
|
221
|
+
...existing,
|
|
222
|
+
...component ? { component } : {},
|
|
223
|
+
...props ? { props } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (frame.type === "artifact_complete") return { ...existing, status: "complete" };
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
function readTurnId(msg) {
|
|
230
|
+
return typeof msg.turnId === "string" && msg.turnId ? msg.turnId : null;
|
|
231
|
+
}
|
|
177
232
|
var VoiceClient = class {
|
|
178
233
|
constructor(options) {
|
|
179
234
|
this.options = options;
|
|
@@ -194,6 +249,17 @@ var VoiceClient = class {
|
|
|
194
249
|
stoppedResponse = false;
|
|
195
250
|
responseEnded = true;
|
|
196
251
|
hasPendingAudio = false;
|
|
252
|
+
/**
|
|
253
|
+
* The assistant entry built from clause captions for the turn being spoken. The
|
|
254
|
+
* authoritative transcript replaces it; a stop commits it, because it captions
|
|
255
|
+
* audio the caller already heard.
|
|
256
|
+
*/
|
|
257
|
+
pendingAssistant = null;
|
|
258
|
+
/** Turn whose clause captions were already committed by a stop, so its authoritative text is redundant. */
|
|
259
|
+
committedAssistantTurnId = null;
|
|
260
|
+
activeTurnId = null;
|
|
261
|
+
suppressedArtifactTurnId = null;
|
|
262
|
+
fullDuplex = false;
|
|
197
263
|
getSnapshot = () => this.snapshot;
|
|
198
264
|
subscribe = (listener) => {
|
|
199
265
|
this.listeners.add(listener);
|
|
@@ -215,7 +281,7 @@ var VoiceClient = class {
|
|
|
215
281
|
if (this.snapshot.status !== "idle" && this.snapshot.status !== "error") return;
|
|
216
282
|
this.cleanup();
|
|
217
283
|
const generation = this.generation;
|
|
218
|
-
this.update({ ...initialSnapshot(), status: "connecting" });
|
|
284
|
+
this.update({ ...initialSnapshot(), session: this.snapshot.session, status: "connecting" });
|
|
219
285
|
try {
|
|
220
286
|
const token = tokenOverride ?? (typeof this.options.clientToken === "function" ? await this.options.clientToken() : this.options.clientToken);
|
|
221
287
|
if (generation !== this.generation) return;
|
|
@@ -230,10 +296,26 @@ var VoiceClient = class {
|
|
|
230
296
|
url.pathname = `${basePath}/ws/agents/${encodeURIComponent(this.options.agentId)}/voice`;
|
|
231
297
|
url.search = "";
|
|
232
298
|
url.searchParams.set("voiceProtocol", "runtype-browser-v1");
|
|
299
|
+
const capabilities = this.options.artifacts ? "partial_transcript,artifacts" : "partial_transcript";
|
|
300
|
+
url.searchParams.set("clientCapabilities", capabilities);
|
|
301
|
+
const sessionId = this.options.sessionId ?? this.snapshot.session?.sessionId;
|
|
302
|
+
if (sessionId) url.searchParams.set("sessionId", sessionId);
|
|
303
|
+
const visitorToken = typeof this.options.visitorToken === "function" ? await this.options.visitorToken() : this.options.visitorToken;
|
|
304
|
+
if (generation !== this.generation) return;
|
|
305
|
+
if (this.options.visitorToken !== void 0 && !visitorToken) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"Voice visitor credential unavailable. Initialize the client session first."
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (visitorToken && !/^cvt_[A-Za-z0-9_-]+$/.test(visitorToken)) {
|
|
311
|
+
throw new Error("Invalid voice visitor credential.");
|
|
312
|
+
}
|
|
313
|
+
if (this.options.fullDuplex !== false)
|
|
314
|
+
url.searchParams.set("voiceCapabilities", "full-duplex-v1");
|
|
233
315
|
url.hash = "";
|
|
234
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
316
|
+
const stream = await (this.options.audioSource?.() ?? navigator.mediaDevices.getUserMedia({
|
|
235
317
|
audio: { sampleRate: CAPTURE_SAMPLE_RATE, channelCount: 1, echoCancellation: true }
|
|
236
|
-
});
|
|
318
|
+
}));
|
|
237
319
|
if (generation !== this.generation) {
|
|
238
320
|
stream.getTracks().forEach((track) => track.stop());
|
|
239
321
|
return;
|
|
@@ -253,7 +335,11 @@ var VoiceClient = class {
|
|
|
253
335
|
return;
|
|
254
336
|
}
|
|
255
337
|
this.player = player;
|
|
256
|
-
const socket = new WebSocket(url.toString(), [
|
|
338
|
+
const socket = new WebSocket(url.toString(), [
|
|
339
|
+
"runtype.bearer",
|
|
340
|
+
token,
|
|
341
|
+
...visitorToken ? [visitorToken] : []
|
|
342
|
+
]);
|
|
257
343
|
socket.binaryType = "arraybuffer";
|
|
258
344
|
this.socket = socket;
|
|
259
345
|
socket.onopen = () => {
|
|
@@ -284,6 +370,11 @@ var VoiceClient = class {
|
|
|
284
370
|
this.cleanup();
|
|
285
371
|
this.update({ status: "idle", interimTranscript: null, audioLevel: 0, isMuted: false });
|
|
286
372
|
};
|
|
373
|
+
/** Forget the remembered conversation so the next startCall opens a new one. No-op during a call. */
|
|
374
|
+
resetSession = () => {
|
|
375
|
+
if (this.snapshot.status !== "idle" && this.snapshot.status !== "error") return;
|
|
376
|
+
this.update({ session: null });
|
|
377
|
+
};
|
|
287
378
|
toggleMute = () => {
|
|
288
379
|
this.update({ isMuted: !this.snapshot.isMuted, audioLevel: 0 });
|
|
289
380
|
};
|
|
@@ -298,9 +389,11 @@ var VoiceClient = class {
|
|
|
298
389
|
this.clearPlayback();
|
|
299
390
|
if (this.snapshot.interruptionMode === "none") {
|
|
300
391
|
this.stoppedResponse = !this.responseEnded;
|
|
392
|
+
this.commitClauseCaptions();
|
|
301
393
|
this.setStatus("listening");
|
|
302
394
|
return;
|
|
303
395
|
}
|
|
396
|
+
this.suppressedArtifactTurnId = this.activeTurnId;
|
|
304
397
|
this.awaitingClear = true;
|
|
305
398
|
this.setStatus("listening");
|
|
306
399
|
this.socket.send(JSON.stringify({ type: "cancel" }));
|
|
@@ -317,6 +410,7 @@ var VoiceClient = class {
|
|
|
317
410
|
});
|
|
318
411
|
}
|
|
319
412
|
cleanup() {
|
|
413
|
+
this.fullDuplex = false;
|
|
320
414
|
this.generation += 1;
|
|
321
415
|
if (this.processor) this.processor.onaudioprocess = null;
|
|
322
416
|
this.processor?.disconnect();
|
|
@@ -342,6 +436,82 @@ var VoiceClient = class {
|
|
|
342
436
|
this.awaitingClear = false;
|
|
343
437
|
this.stoppedResponse = false;
|
|
344
438
|
this.responseEnded = true;
|
|
439
|
+
this.commitClauseCaptions();
|
|
440
|
+
this.committedAssistantTurnId = null;
|
|
441
|
+
this.activeTurnId = null;
|
|
442
|
+
this.suppressedArtifactTurnId = null;
|
|
443
|
+
const settledArtifacts = this.snapshot.artifacts.filter(
|
|
444
|
+
(artifact) => artifact.status === "complete"
|
|
445
|
+
);
|
|
446
|
+
if (settledArtifacts.length !== this.snapshot.artifacts.length)
|
|
447
|
+
this.update({ artifacts: settledArtifacts });
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Folds an `artifact` message into the snapshot. Frames of a turn the caller
|
|
451
|
+
* cancelled are dropped; a playback-only stop leaves the turn running, so its
|
|
452
|
+
* artifacts keep arriving and are kept.
|
|
453
|
+
*/
|
|
454
|
+
applyArtifactMessage(msg) {
|
|
455
|
+
if (this.awaitingClear) return;
|
|
456
|
+
const frame = msg.event;
|
|
457
|
+
if (!frame || typeof frame !== "object" || Array.isArray(frame)) return;
|
|
458
|
+
const event = frame;
|
|
459
|
+
const id = readString(event.id);
|
|
460
|
+
if (!id) return;
|
|
461
|
+
const turnId = readTurnId(msg);
|
|
462
|
+
if (turnId !== null && turnId === this.suppressedArtifactTurnId) return;
|
|
463
|
+
if (turnId !== null) this.activeTurnId = turnId;
|
|
464
|
+
const artifacts = this.snapshot.artifacts;
|
|
465
|
+
const index = artifacts.findIndex((artifact) => artifact.id === id);
|
|
466
|
+
const executionId = readString(msg.executionId);
|
|
467
|
+
const next = applyArtifactFrame(index === -1 ? void 0 : artifacts[index], event, {
|
|
468
|
+
id,
|
|
469
|
+
turnId,
|
|
470
|
+
...executionId ? { executionId } : {}
|
|
471
|
+
});
|
|
472
|
+
if (!next) return;
|
|
473
|
+
this.update({
|
|
474
|
+
artifacts: index === -1 ? [...artifacts, next] : [...artifacts.slice(0, index), next, ...artifacts.slice(index + 1)]
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/** Settles the spoken clauses in place and ends the turn's claim on them. */
|
|
478
|
+
commitClauseCaptions() {
|
|
479
|
+
this.committedAssistantTurnId = this.pendingAssistant?.turnId ?? null;
|
|
480
|
+
const settled = this.pendingAssistant !== null;
|
|
481
|
+
this.pendingAssistant = null;
|
|
482
|
+
if (!settled) return;
|
|
483
|
+
const transcript = this.snapshot.transcript;
|
|
484
|
+
const last = transcript[transcript.length - 1];
|
|
485
|
+
if (last?.role !== "assistant" || last.partial !== true) return;
|
|
486
|
+
const { partial: _partial, ...committed } = last;
|
|
487
|
+
this.update({ transcript: [...transcript.slice(0, -1), committed] });
|
|
488
|
+
}
|
|
489
|
+
/** Extends the turn's spoken-so-far assistant entry, or opens one for a new turn. */
|
|
490
|
+
appendClauseCaption(text, turnId) {
|
|
491
|
+
const transcript = this.snapshot.transcript;
|
|
492
|
+
const last = transcript[transcript.length - 1];
|
|
493
|
+
if (this.pendingAssistant?.turnId === turnId && last?.role === "assistant") {
|
|
494
|
+
return {
|
|
495
|
+
interimTranscript: null,
|
|
496
|
+
transcript: [...transcript.slice(0, -1), { ...last, content: last.content + text }],
|
|
497
|
+
status: "speaking"
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
this.pendingAssistant = { turnId };
|
|
501
|
+
return {
|
|
502
|
+
interimTranscript: null,
|
|
503
|
+
transcript: [
|
|
504
|
+
...transcript,
|
|
505
|
+
{
|
|
506
|
+
role: "assistant",
|
|
507
|
+
content: text,
|
|
508
|
+
timestamp: Date.now(),
|
|
509
|
+
partial: true,
|
|
510
|
+
...turnId ? { turnId } : {}
|
|
511
|
+
}
|
|
512
|
+
],
|
|
513
|
+
status: "speaking"
|
|
514
|
+
};
|
|
345
515
|
}
|
|
346
516
|
clearPlayback() {
|
|
347
517
|
this.playbackRevision += 1;
|
|
@@ -376,32 +546,89 @@ var VoiceClient = class {
|
|
|
376
546
|
return;
|
|
377
547
|
}
|
|
378
548
|
switch (msg.type) {
|
|
379
|
-
case "session_config":
|
|
549
|
+
case "session_config": {
|
|
550
|
+
this.fullDuplex = msg.speechMode === "speech_to_speech";
|
|
551
|
+
if (this.fullDuplex) player.setContinuousMode?.(true);
|
|
380
552
|
if (msg.interruptionMode === "none" || msg.interruptionMode === "cancel" || msg.interruptionMode === "barge-in") {
|
|
381
553
|
this.update({ interruptionMode: msg.interruptionMode });
|
|
382
554
|
}
|
|
555
|
+
const sessionId = readString(msg.sessionId);
|
|
556
|
+
const conversationId = readString(msg.conversationId);
|
|
557
|
+
if (sessionId && conversationId) {
|
|
558
|
+
const session = { sessionId, conversationId };
|
|
559
|
+
this.update({ session });
|
|
560
|
+
this.options.onSession?.(session);
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case "artifact":
|
|
565
|
+
this.applyArtifactMessage(msg);
|
|
566
|
+
break;
|
|
567
|
+
case "transcript_update": {
|
|
568
|
+
if (typeof msg.text !== "string" || typeof msg.turnId !== "string" || msg.role !== "user" && msg.role !== "assistant")
|
|
569
|
+
break;
|
|
570
|
+
if (msg.role === "assistant" && this.awaitingClear) break;
|
|
571
|
+
const entries = [...this.snapshot.transcript];
|
|
572
|
+
const index = entries.findIndex(
|
|
573
|
+
(entry2) => entry2.turnId === msg.turnId && entry2.role === msg.role
|
|
574
|
+
);
|
|
575
|
+
const entry = {
|
|
576
|
+
role: msg.role,
|
|
577
|
+
content: msg.text,
|
|
578
|
+
turnId: msg.turnId,
|
|
579
|
+
isFinal: msg.final === true,
|
|
580
|
+
timestamp: index >= 0 ? entries[index].timestamp : Date.now()
|
|
581
|
+
};
|
|
582
|
+
if (index >= 0) entries[index] = entry;
|
|
583
|
+
else entries.push(entry);
|
|
584
|
+
this.update({ transcript: entries, interimTranscript: null });
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "delegation_started":
|
|
588
|
+
if (!this.hasPendingAudio && !this.awaitingClear) this.setStatus("thinking");
|
|
589
|
+
break;
|
|
590
|
+
case "delegation_completed":
|
|
591
|
+
if (!this.hasPendingAudio && !this.awaitingClear) this.setStatus("listening");
|
|
383
592
|
break;
|
|
384
593
|
case "transcript_interim":
|
|
385
594
|
this.update({ interimTranscript: typeof msg.text === "string" ? msg.text || null : null });
|
|
386
595
|
break;
|
|
387
|
-
case "
|
|
596
|
+
case "transcript_partial": {
|
|
597
|
+
if (msg.role !== "assistant" || typeof msg.text !== "string" || !msg.text) break;
|
|
598
|
+
this.activeTurnId = readTurnId(msg) ?? this.activeTurnId;
|
|
599
|
+
if (this.awaitingClear || this.stoppedResponse) break;
|
|
600
|
+
this.responseEnded = false;
|
|
601
|
+
this.update(this.appendClauseCaption(msg.text, readTurnId(msg)));
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case "transcript_final": {
|
|
388
605
|
if (typeof msg.text !== "string" || msg.role !== "user" && msg.role !== "assistant") break;
|
|
389
|
-
|
|
606
|
+
const role = msg.role;
|
|
607
|
+
const text = msg.text;
|
|
608
|
+
if (role === "assistant" && (this.awaitingClear || this.stoppedResponse)) break;
|
|
609
|
+
const turnId = readTurnId(msg);
|
|
610
|
+
if (turnId !== null) this.activeTurnId = turnId;
|
|
611
|
+
if (role === "assistant" && turnId !== null && turnId === this.committedAssistantTurnId)
|
|
612
|
+
break;
|
|
390
613
|
this.responseEnded = false;
|
|
614
|
+
const transcript = this.snapshot.transcript;
|
|
615
|
+
const last = transcript[transcript.length - 1];
|
|
616
|
+
const supersedes = role === "assistant" && this.pendingAssistant !== null && last?.role === "assistant" && (turnId === null || this.pendingAssistant.turnId === null || this.pendingAssistant.turnId === turnId);
|
|
617
|
+
this.pendingAssistant = null;
|
|
618
|
+
const entry = {
|
|
619
|
+
role,
|
|
620
|
+
content: text,
|
|
621
|
+
// INVARIANT: reuse the caption's timestamp so a keyed list does not remount the bubble.
|
|
622
|
+
timestamp: supersedes && last ? last.timestamp : Date.now(),
|
|
623
|
+
...turnId ? { turnId } : {}
|
|
624
|
+
};
|
|
391
625
|
this.update({
|
|
392
626
|
interimTranscript: null,
|
|
393
|
-
transcript: [
|
|
394
|
-
|
|
395
|
-
{
|
|
396
|
-
role: msg.role,
|
|
397
|
-
content: msg.text,
|
|
398
|
-
timestamp: Date.now(),
|
|
399
|
-
...typeof msg.turnId === "string" && msg.turnId ? { turnId: msg.turnId } : {}
|
|
400
|
-
}
|
|
401
|
-
],
|
|
402
|
-
status: this.awaitingClear ? this.snapshot.status : msg.role === "user" ? "thinking" : "speaking"
|
|
627
|
+
transcript: supersedes ? [...transcript.slice(0, -1), entry] : [...transcript, entry],
|
|
628
|
+
status: this.awaitingClear ? this.snapshot.status : role === "user" ? "thinking" : "speaking"
|
|
403
629
|
});
|
|
404
630
|
break;
|
|
631
|
+
}
|
|
405
632
|
case "audio_end": {
|
|
406
633
|
this.responseEnded = true;
|
|
407
634
|
if (this.stoppedResponse) {
|
|
@@ -418,10 +645,12 @@ var VoiceClient = class {
|
|
|
418
645
|
break;
|
|
419
646
|
}
|
|
420
647
|
case "audio_clear":
|
|
648
|
+
this.suppressedArtifactTurnId = this.activeTurnId ?? this.suppressedArtifactTurnId;
|
|
421
649
|
this.clearPlayback();
|
|
422
650
|
this.awaitingClear = false;
|
|
423
651
|
this.stoppedResponse = false;
|
|
424
652
|
this.responseEnded = true;
|
|
653
|
+
this.commitClauseCaptions();
|
|
425
654
|
this.setStatus("listening");
|
|
426
655
|
break;
|
|
427
656
|
case "metrics": {
|
|
@@ -434,8 +663,11 @@ var VoiceClient = class {
|
|
|
434
663
|
// @snake-case-ok: Existing voice wire contract.
|
|
435
664
|
firstAudioMs: number(msg.first_audio_ms),
|
|
436
665
|
// @snake-case-ok: Existing voice wire contract.
|
|
437
|
-
totalMs: number(msg.total_ms)
|
|
666
|
+
totalMs: number(msg.total_ms),
|
|
667
|
+
// @snake-case-ok: Existing voice wire contract.
|
|
668
|
+
firstSynthesisMs: number(msg.first_synthesis_ms),
|
|
438
669
|
// @snake-case-ok: Existing voice wire contract.
|
|
670
|
+
incremental: msg.incremental === true
|
|
439
671
|
}
|
|
440
672
|
});
|
|
441
673
|
break;
|
|
@@ -467,14 +699,14 @@ var VoiceClient = class {
|
|
|
467
699
|
const processor = context.createScriptProcessor(CAPTURE_BUFFER_SIZE, 1, 1);
|
|
468
700
|
this.processor = processor;
|
|
469
701
|
processor.onaudioprocess = (event) => {
|
|
470
|
-
if (generation !== this.generation || this.snapshot.isMuted) return;
|
|
702
|
+
if (generation !== this.generation || this.snapshot.isMuted && !this.fullDuplex) return;
|
|
471
703
|
const input = event.inputBuffer.getChannelData(0);
|
|
472
704
|
let sum = 0;
|
|
473
705
|
for (const sample of input) sum += sample * sample;
|
|
474
|
-
this.update({ audioLevel: Math.sqrt(sum / input.length) });
|
|
706
|
+
this.update({ audioLevel: this.snapshot.isMuted ? 0 : Math.sqrt(sum / input.length) });
|
|
475
707
|
if (socket.readyState !== WebSocket.OPEN) return;
|
|
476
708
|
const pcm = new Int16Array(input.length);
|
|
477
|
-
if (this.snapshot.interruptionMode === "cancel" && (this.awaitingClear || this.snapshot.status === "thinking" || this.hasPendingAudio || this.snapshot.status === "speaking")) {
|
|
709
|
+
if (this.snapshot.isMuted || this.snapshot.interruptionMode === "cancel" && (this.awaitingClear || this.snapshot.status === "thinking" || this.hasPendingAudio || this.snapshot.status === "speaking")) {
|
|
478
710
|
socket.send(pcm.buffer);
|
|
479
711
|
return;
|
|
480
712
|
}
|
|
@@ -490,25 +722,81 @@ var VoiceClient = class {
|
|
|
490
722
|
};
|
|
491
723
|
|
|
492
724
|
// src/react.ts
|
|
493
|
-
function useVoiceClient({
|
|
725
|
+
function useVoiceClient({
|
|
726
|
+
agentId,
|
|
727
|
+
apiUrl,
|
|
728
|
+
clientToken,
|
|
729
|
+
artifacts,
|
|
730
|
+
sessionId,
|
|
731
|
+
visitorToken,
|
|
732
|
+
onSession,
|
|
733
|
+
fullDuplex,
|
|
734
|
+
audioSource
|
|
735
|
+
}) {
|
|
736
|
+
const sessionRef = (0, import_react.useRef)(sessionId);
|
|
737
|
+
(0, import_react.useEffect)(() => {
|
|
738
|
+
sessionRef.current = sessionId;
|
|
739
|
+
}, [sessionId]);
|
|
494
740
|
const tokenRef = (0, import_react.useRef)(clientToken);
|
|
741
|
+
const visitorRef = (0, import_react.useRef)(visitorToken);
|
|
742
|
+
const sessionCallbackRef = (0, import_react.useRef)(onSession);
|
|
743
|
+
const audioSourceRef = (0, import_react.useRef)(audioSource);
|
|
744
|
+
const hasAudioSource = audioSource !== void 0;
|
|
745
|
+
(0, import_react.useEffect)(() => {
|
|
746
|
+
visitorRef.current = visitorToken;
|
|
747
|
+
sessionCallbackRef.current = onSession;
|
|
748
|
+
}, [visitorToken, onSession]);
|
|
749
|
+
(0, import_react.useEffect)(() => {
|
|
750
|
+
audioSourceRef.current = audioSource;
|
|
751
|
+
}, [audioSource]);
|
|
495
752
|
(0, import_react.useEffect)(() => {
|
|
496
753
|
tokenRef.current = clientToken;
|
|
497
754
|
}, [clientToken]);
|
|
498
|
-
const
|
|
499
|
-
|
|
755
|
+
const createClient = () => new VoiceClient({
|
|
756
|
+
agentId,
|
|
757
|
+
apiUrl,
|
|
758
|
+
artifacts,
|
|
759
|
+
fullDuplex,
|
|
760
|
+
...hasAudioSource ? { audioSource: () => audioSourceRef.current() } : {},
|
|
761
|
+
get sessionId() {
|
|
762
|
+
return sessionRef.current;
|
|
763
|
+
},
|
|
764
|
+
get visitorToken() {
|
|
765
|
+
return visitorRef.current;
|
|
766
|
+
},
|
|
767
|
+
onSession: (session) => sessionCallbackRef.current?.(session),
|
|
768
|
+
clientToken: () => typeof tokenRef.current === "function" ? tokenRef.current() : tokenRef.current
|
|
769
|
+
});
|
|
770
|
+
const [binding, setBinding] = (0, import_react.useState)(() => ({
|
|
771
|
+
agentId,
|
|
772
|
+
apiUrl,
|
|
773
|
+
artifacts,
|
|
774
|
+
fullDuplex,
|
|
775
|
+
hasAudioSource,
|
|
776
|
+
sessionId,
|
|
777
|
+
client: createClient()
|
|
778
|
+
}));
|
|
779
|
+
const configurationChanged = binding.agentId !== agentId || binding.apiUrl !== apiUrl || binding.artifacts !== artifacts || binding.fullDuplex !== fullDuplex || binding.hasAudioSource !== hasAudioSource;
|
|
780
|
+
if (configurationChanged || binding.sessionId !== sessionId) {
|
|
781
|
+
const acknowledgesSession = sessionId !== void 0 && sessionId === binding.client.getSnapshot().session?.sessionId;
|
|
782
|
+
setBinding({
|
|
500
783
|
agentId,
|
|
501
784
|
apiUrl,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
785
|
+
artifacts,
|
|
786
|
+
fullDuplex,
|
|
787
|
+
hasAudioSource,
|
|
788
|
+
sessionId,
|
|
789
|
+
client: !configurationChanged && acknowledgesSession ? binding.client : createClient()
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
const client = binding.client;
|
|
506
793
|
const snapshot = (0, import_react.useSyncExternalStore)(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
507
794
|
(0, import_react.useEffect)(() => () => client.endCall(), [client]);
|
|
508
795
|
return {
|
|
509
796
|
...snapshot,
|
|
510
797
|
startCall: client.startCall,
|
|
511
798
|
endCall: client.endCall,
|
|
799
|
+
resetSession: client.resetSession,
|
|
512
800
|
toggleMute: client.toggleMute,
|
|
513
801
|
cancelResponse: client.cancelResponse
|
|
514
802
|
};
|
package/dist/react.d.cts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { V as VoiceClientOptions,
|
|
2
|
-
export { a as VoiceSnapshot } from './types-
|
|
1
|
+
import { V as VoiceClientOptions, f as VoiceStatus, T as TranscriptEntry, d as VoiceMetrics, I as InterruptionMode, b as VoiceArtifact, e as VoiceSession } from './types-Dkh4LxEA.cjs';
|
|
2
|
+
export { a as VoiceSnapshot } from './types-Dkh4LxEA.cjs';
|
|
3
3
|
|
|
4
4
|
/** Subscribe to a voice client and end its call when the component unmounts or changes agent. */
|
|
5
|
-
declare function useVoiceClient({ agentId, apiUrl, clientToken }: VoiceClientOptions): {
|
|
5
|
+
declare function useVoiceClient({ agentId, apiUrl, clientToken, artifacts, sessionId, visitorToken, onSession, fullDuplex, audioSource, }: VoiceClientOptions): {
|
|
6
6
|
startCall: (tokenOverride?: string) => Promise<void>;
|
|
7
7
|
endCall: () => void;
|
|
8
|
+
resetSession: () => void;
|
|
8
9
|
toggleMute: () => void;
|
|
9
10
|
cancelResponse: () => void;
|
|
10
11
|
status: VoiceStatus;
|
|
@@ -22,6 +23,8 @@ declare function useVoiceClient({ agentId, apiUrl, clientToken }: VoiceClientOpt
|
|
|
22
23
|
};
|
|
23
24
|
interruptionMode: InterruptionMode;
|
|
24
25
|
canCancel: boolean;
|
|
26
|
+
artifacts: readonly VoiceArtifact[];
|
|
27
|
+
session: VoiceSession | null;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
export { VoiceClientOptions, VoiceStatus, useVoiceClient };
|
|
30
|
+
export { VoiceArtifact, VoiceClientOptions, VoiceSession, VoiceStatus, useVoiceClient };
|
package/dist/react.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { V as VoiceClientOptions,
|
|
2
|
-
export { a as VoiceSnapshot } from './types-
|
|
1
|
+
import { V as VoiceClientOptions, f as VoiceStatus, T as TranscriptEntry, d as VoiceMetrics, I as InterruptionMode, b as VoiceArtifact, e as VoiceSession } from './types-Dkh4LxEA.js';
|
|
2
|
+
export { a as VoiceSnapshot } from './types-Dkh4LxEA.js';
|
|
3
3
|
|
|
4
4
|
/** Subscribe to a voice client and end its call when the component unmounts or changes agent. */
|
|
5
|
-
declare function useVoiceClient({ agentId, apiUrl, clientToken }: VoiceClientOptions): {
|
|
5
|
+
declare function useVoiceClient({ agentId, apiUrl, clientToken, artifacts, sessionId, visitorToken, onSession, fullDuplex, audioSource, }: VoiceClientOptions): {
|
|
6
6
|
startCall: (tokenOverride?: string) => Promise<void>;
|
|
7
7
|
endCall: () => void;
|
|
8
|
+
resetSession: () => void;
|
|
8
9
|
toggleMute: () => void;
|
|
9
10
|
cancelResponse: () => void;
|
|
10
11
|
status: VoiceStatus;
|
|
@@ -22,6 +23,8 @@ declare function useVoiceClient({ agentId, apiUrl, clientToken }: VoiceClientOpt
|
|
|
22
23
|
};
|
|
23
24
|
interruptionMode: InterruptionMode;
|
|
24
25
|
canCancel: boolean;
|
|
26
|
+
artifacts: readonly VoiceArtifact[];
|
|
27
|
+
session: VoiceSession | null;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
export { VoiceClientOptions, VoiceStatus, useVoiceClient };
|
|
30
|
+
export { VoiceArtifact, VoiceClientOptions, VoiceSession, VoiceStatus, useVoiceClient };
|