@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/persona.js
CHANGED
|
@@ -9,13 +9,16 @@ class RuntypePcmPlayerProcessor extends AudioWorkletProcessor {
|
|
|
9
9
|
this.readOffset = 0
|
|
10
10
|
this.buffered = 0
|
|
11
11
|
this.waiting = true
|
|
12
|
-
// INVARIANT:
|
|
12
|
+
// INVARIANT: Pipeline replies need EOS; continuous sessions report queue drain without a provider turn boundary.
|
|
13
13
|
this.eosSeen = false
|
|
14
|
+
this.continuous = false
|
|
14
15
|
this.revision = 0
|
|
15
16
|
this.port.onmessage = (e) => {
|
|
16
17
|
const msg = e.data
|
|
17
18
|
this.revision = msg.revision
|
|
18
|
-
if (msg.type === '
|
|
19
|
+
if (msg.type === 'continuous') {
|
|
20
|
+
this.continuous = msg.enabled
|
|
21
|
+
} else if (msg.type === 'push') {
|
|
19
22
|
this.eosSeen = false
|
|
20
23
|
this.chunks.push(msg.samples)
|
|
21
24
|
this.buffered += msg.samples.length
|
|
@@ -55,7 +58,7 @@ class RuntypePcmPlayerProcessor extends AudioWorkletProcessor {
|
|
|
55
58
|
}
|
|
56
59
|
if (this.buffered === 0) {
|
|
57
60
|
this.waiting = true // mid-reply underrun: re-buffer silently
|
|
58
|
-
if (this.eosSeen) {
|
|
61
|
+
if (this.eosSeen || this.continuous) {
|
|
59
62
|
this.eosSeen = false
|
|
60
63
|
this.port.postMessage({ type: 'drained', revision: this.revision })
|
|
61
64
|
}
|
|
@@ -99,6 +102,9 @@ async function createPcmPlayer(onDrained) {
|
|
|
99
102
|
if (samples.length === 0) return;
|
|
100
103
|
node.port.postMessage({ type: "push", samples, revision }, [samples.buffer]);
|
|
101
104
|
},
|
|
105
|
+
setContinuousMode(enabled) {
|
|
106
|
+
node.port.postMessage({ type: "continuous", enabled, revision });
|
|
107
|
+
},
|
|
102
108
|
endOfStream() {
|
|
103
109
|
node.port.postMessage({ type: "eos", revision });
|
|
104
110
|
},
|
|
@@ -143,9 +149,58 @@ function initialSnapshot() {
|
|
|
143
149
|
error: null,
|
|
144
150
|
errorDetails: void 0,
|
|
145
151
|
interruptionMode: "none",
|
|
146
|
-
canCancel: false
|
|
152
|
+
canCancel: false,
|
|
153
|
+
artifacts: [],
|
|
154
|
+
session: null
|
|
147
155
|
};
|
|
148
156
|
}
|
|
157
|
+
function readString(value) {
|
|
158
|
+
return typeof value === "string" && value ? value : void 0;
|
|
159
|
+
}
|
|
160
|
+
function readArtifactFile(value) {
|
|
161
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
162
|
+
const file = value;
|
|
163
|
+
const path = readString(file.path);
|
|
164
|
+
const mimeType = readString(file.mimeType);
|
|
165
|
+
if (!path || !mimeType) return void 0;
|
|
166
|
+
const language = readString(file.language);
|
|
167
|
+
return { path, mimeType, ...language ? { language } : {} };
|
|
168
|
+
}
|
|
169
|
+
function applyArtifactFrame(existing, frame, identity) {
|
|
170
|
+
if (frame.type === "artifact_start") {
|
|
171
|
+
const title = readString(frame.title);
|
|
172
|
+
const component = readString(frame.component);
|
|
173
|
+
const file = readArtifactFile(frame.file);
|
|
174
|
+
return {
|
|
175
|
+
...identity,
|
|
176
|
+
artifactType: frame.artifactType === "component" ? "component" : "markdown",
|
|
177
|
+
...title ? { title } : {},
|
|
178
|
+
...file ? { file } : {},
|
|
179
|
+
...component ? { component } : {},
|
|
180
|
+
content: "",
|
|
181
|
+
status: "streaming"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (!existing) return void 0;
|
|
185
|
+
if (frame.type === "artifact_delta") {
|
|
186
|
+
const delta = typeof frame.delta === "string" ? frame.delta : "";
|
|
187
|
+
return delta ? { ...existing, content: existing.content + delta } : void 0;
|
|
188
|
+
}
|
|
189
|
+
if (frame.type === "artifact_update") {
|
|
190
|
+
const component = readString(frame.component) ?? existing.component;
|
|
191
|
+
const props = frame.props && typeof frame.props === "object" && !Array.isArray(frame.props) ? frame.props : existing.props;
|
|
192
|
+
return {
|
|
193
|
+
...existing,
|
|
194
|
+
...component ? { component } : {},
|
|
195
|
+
...props ? { props } : {}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (frame.type === "artifact_complete") return { ...existing, status: "complete" };
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function readTurnId(msg) {
|
|
202
|
+
return typeof msg.turnId === "string" && msg.turnId ? msg.turnId : null;
|
|
203
|
+
}
|
|
149
204
|
var VoiceClient = class {
|
|
150
205
|
constructor(options) {
|
|
151
206
|
this.options = options;
|
|
@@ -166,6 +221,17 @@ var VoiceClient = class {
|
|
|
166
221
|
stoppedResponse = false;
|
|
167
222
|
responseEnded = true;
|
|
168
223
|
hasPendingAudio = false;
|
|
224
|
+
/**
|
|
225
|
+
* The assistant entry built from clause captions for the turn being spoken. The
|
|
226
|
+
* authoritative transcript replaces it; a stop commits it, because it captions
|
|
227
|
+
* audio the caller already heard.
|
|
228
|
+
*/
|
|
229
|
+
pendingAssistant = null;
|
|
230
|
+
/** Turn whose clause captions were already committed by a stop, so its authoritative text is redundant. */
|
|
231
|
+
committedAssistantTurnId = null;
|
|
232
|
+
activeTurnId = null;
|
|
233
|
+
suppressedArtifactTurnId = null;
|
|
234
|
+
fullDuplex = false;
|
|
169
235
|
getSnapshot = () => this.snapshot;
|
|
170
236
|
subscribe = (listener) => {
|
|
171
237
|
this.listeners.add(listener);
|
|
@@ -187,7 +253,7 @@ var VoiceClient = class {
|
|
|
187
253
|
if (this.snapshot.status !== "idle" && this.snapshot.status !== "error") return;
|
|
188
254
|
this.cleanup();
|
|
189
255
|
const generation = this.generation;
|
|
190
|
-
this.update({ ...initialSnapshot(), status: "connecting" });
|
|
256
|
+
this.update({ ...initialSnapshot(), session: this.snapshot.session, status: "connecting" });
|
|
191
257
|
try {
|
|
192
258
|
const token = tokenOverride ?? (typeof this.options.clientToken === "function" ? await this.options.clientToken() : this.options.clientToken);
|
|
193
259
|
if (generation !== this.generation) return;
|
|
@@ -202,10 +268,26 @@ var VoiceClient = class {
|
|
|
202
268
|
url.pathname = `${basePath}/ws/agents/${encodeURIComponent(this.options.agentId)}/voice`;
|
|
203
269
|
url.search = "";
|
|
204
270
|
url.searchParams.set("voiceProtocol", "runtype-browser-v1");
|
|
271
|
+
const capabilities = this.options.artifacts ? "partial_transcript,artifacts" : "partial_transcript";
|
|
272
|
+
url.searchParams.set("clientCapabilities", capabilities);
|
|
273
|
+
const sessionId = this.options.sessionId ?? this.snapshot.session?.sessionId;
|
|
274
|
+
if (sessionId) url.searchParams.set("sessionId", sessionId);
|
|
275
|
+
const visitorToken = typeof this.options.visitorToken === "function" ? await this.options.visitorToken() : this.options.visitorToken;
|
|
276
|
+
if (generation !== this.generation) return;
|
|
277
|
+
if (this.options.visitorToken !== void 0 && !visitorToken) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"Voice visitor credential unavailable. Initialize the client session first."
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (visitorToken && !/^cvt_[A-Za-z0-9_-]+$/.test(visitorToken)) {
|
|
283
|
+
throw new Error("Invalid voice visitor credential.");
|
|
284
|
+
}
|
|
285
|
+
if (this.options.fullDuplex !== false)
|
|
286
|
+
url.searchParams.set("voiceCapabilities", "full-duplex-v1");
|
|
205
287
|
url.hash = "";
|
|
206
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
288
|
+
const stream = await (this.options.audioSource?.() ?? navigator.mediaDevices.getUserMedia({
|
|
207
289
|
audio: { sampleRate: CAPTURE_SAMPLE_RATE, channelCount: 1, echoCancellation: true }
|
|
208
|
-
});
|
|
290
|
+
}));
|
|
209
291
|
if (generation !== this.generation) {
|
|
210
292
|
stream.getTracks().forEach((track) => track.stop());
|
|
211
293
|
return;
|
|
@@ -225,7 +307,11 @@ var VoiceClient = class {
|
|
|
225
307
|
return;
|
|
226
308
|
}
|
|
227
309
|
this.player = player;
|
|
228
|
-
const socket = new WebSocket(url.toString(), [
|
|
310
|
+
const socket = new WebSocket(url.toString(), [
|
|
311
|
+
"runtype.bearer",
|
|
312
|
+
token,
|
|
313
|
+
...visitorToken ? [visitorToken] : []
|
|
314
|
+
]);
|
|
229
315
|
socket.binaryType = "arraybuffer";
|
|
230
316
|
this.socket = socket;
|
|
231
317
|
socket.onopen = () => {
|
|
@@ -256,6 +342,11 @@ var VoiceClient = class {
|
|
|
256
342
|
this.cleanup();
|
|
257
343
|
this.update({ status: "idle", interimTranscript: null, audioLevel: 0, isMuted: false });
|
|
258
344
|
};
|
|
345
|
+
/** Forget the remembered conversation so the next startCall opens a new one. No-op during a call. */
|
|
346
|
+
resetSession = () => {
|
|
347
|
+
if (this.snapshot.status !== "idle" && this.snapshot.status !== "error") return;
|
|
348
|
+
this.update({ session: null });
|
|
349
|
+
};
|
|
259
350
|
toggleMute = () => {
|
|
260
351
|
this.update({ isMuted: !this.snapshot.isMuted, audioLevel: 0 });
|
|
261
352
|
};
|
|
@@ -270,9 +361,11 @@ var VoiceClient = class {
|
|
|
270
361
|
this.clearPlayback();
|
|
271
362
|
if (this.snapshot.interruptionMode === "none") {
|
|
272
363
|
this.stoppedResponse = !this.responseEnded;
|
|
364
|
+
this.commitClauseCaptions();
|
|
273
365
|
this.setStatus("listening");
|
|
274
366
|
return;
|
|
275
367
|
}
|
|
368
|
+
this.suppressedArtifactTurnId = this.activeTurnId;
|
|
276
369
|
this.awaitingClear = true;
|
|
277
370
|
this.setStatus("listening");
|
|
278
371
|
this.socket.send(JSON.stringify({ type: "cancel" }));
|
|
@@ -289,6 +382,7 @@ var VoiceClient = class {
|
|
|
289
382
|
});
|
|
290
383
|
}
|
|
291
384
|
cleanup() {
|
|
385
|
+
this.fullDuplex = false;
|
|
292
386
|
this.generation += 1;
|
|
293
387
|
if (this.processor) this.processor.onaudioprocess = null;
|
|
294
388
|
this.processor?.disconnect();
|
|
@@ -314,6 +408,82 @@ var VoiceClient = class {
|
|
|
314
408
|
this.awaitingClear = false;
|
|
315
409
|
this.stoppedResponse = false;
|
|
316
410
|
this.responseEnded = true;
|
|
411
|
+
this.commitClauseCaptions();
|
|
412
|
+
this.committedAssistantTurnId = null;
|
|
413
|
+
this.activeTurnId = null;
|
|
414
|
+
this.suppressedArtifactTurnId = null;
|
|
415
|
+
const settledArtifacts = this.snapshot.artifacts.filter(
|
|
416
|
+
(artifact) => artifact.status === "complete"
|
|
417
|
+
);
|
|
418
|
+
if (settledArtifacts.length !== this.snapshot.artifacts.length)
|
|
419
|
+
this.update({ artifacts: settledArtifacts });
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Folds an `artifact` message into the snapshot. Frames of a turn the caller
|
|
423
|
+
* cancelled are dropped; a playback-only stop leaves the turn running, so its
|
|
424
|
+
* artifacts keep arriving and are kept.
|
|
425
|
+
*/
|
|
426
|
+
applyArtifactMessage(msg) {
|
|
427
|
+
if (this.awaitingClear) return;
|
|
428
|
+
const frame = msg.event;
|
|
429
|
+
if (!frame || typeof frame !== "object" || Array.isArray(frame)) return;
|
|
430
|
+
const event = frame;
|
|
431
|
+
const id = readString(event.id);
|
|
432
|
+
if (!id) return;
|
|
433
|
+
const turnId = readTurnId(msg);
|
|
434
|
+
if (turnId !== null && turnId === this.suppressedArtifactTurnId) return;
|
|
435
|
+
if (turnId !== null) this.activeTurnId = turnId;
|
|
436
|
+
const artifacts = this.snapshot.artifacts;
|
|
437
|
+
const index = artifacts.findIndex((artifact) => artifact.id === id);
|
|
438
|
+
const executionId = readString(msg.executionId);
|
|
439
|
+
const next = applyArtifactFrame(index === -1 ? void 0 : artifacts[index], event, {
|
|
440
|
+
id,
|
|
441
|
+
turnId,
|
|
442
|
+
...executionId ? { executionId } : {}
|
|
443
|
+
});
|
|
444
|
+
if (!next) return;
|
|
445
|
+
this.update({
|
|
446
|
+
artifacts: index === -1 ? [...artifacts, next] : [...artifacts.slice(0, index), next, ...artifacts.slice(index + 1)]
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/** Settles the spoken clauses in place and ends the turn's claim on them. */
|
|
450
|
+
commitClauseCaptions() {
|
|
451
|
+
this.committedAssistantTurnId = this.pendingAssistant?.turnId ?? null;
|
|
452
|
+
const settled = this.pendingAssistant !== null;
|
|
453
|
+
this.pendingAssistant = null;
|
|
454
|
+
if (!settled) return;
|
|
455
|
+
const transcript = this.snapshot.transcript;
|
|
456
|
+
const last = transcript[transcript.length - 1];
|
|
457
|
+
if (last?.role !== "assistant" || last.partial !== true) return;
|
|
458
|
+
const { partial: _partial, ...committed } = last;
|
|
459
|
+
this.update({ transcript: [...transcript.slice(0, -1), committed] });
|
|
460
|
+
}
|
|
461
|
+
/** Extends the turn's spoken-so-far assistant entry, or opens one for a new turn. */
|
|
462
|
+
appendClauseCaption(text, turnId) {
|
|
463
|
+
const transcript = this.snapshot.transcript;
|
|
464
|
+
const last = transcript[transcript.length - 1];
|
|
465
|
+
if (this.pendingAssistant?.turnId === turnId && last?.role === "assistant") {
|
|
466
|
+
return {
|
|
467
|
+
interimTranscript: null,
|
|
468
|
+
transcript: [...transcript.slice(0, -1), { ...last, content: last.content + text }],
|
|
469
|
+
status: "speaking"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
this.pendingAssistant = { turnId };
|
|
473
|
+
return {
|
|
474
|
+
interimTranscript: null,
|
|
475
|
+
transcript: [
|
|
476
|
+
...transcript,
|
|
477
|
+
{
|
|
478
|
+
role: "assistant",
|
|
479
|
+
content: text,
|
|
480
|
+
timestamp: Date.now(),
|
|
481
|
+
partial: true,
|
|
482
|
+
...turnId ? { turnId } : {}
|
|
483
|
+
}
|
|
484
|
+
],
|
|
485
|
+
status: "speaking"
|
|
486
|
+
};
|
|
317
487
|
}
|
|
318
488
|
clearPlayback() {
|
|
319
489
|
this.playbackRevision += 1;
|
|
@@ -348,32 +518,89 @@ var VoiceClient = class {
|
|
|
348
518
|
return;
|
|
349
519
|
}
|
|
350
520
|
switch (msg.type) {
|
|
351
|
-
case "session_config":
|
|
521
|
+
case "session_config": {
|
|
522
|
+
this.fullDuplex = msg.speechMode === "speech_to_speech";
|
|
523
|
+
if (this.fullDuplex) player.setContinuousMode?.(true);
|
|
352
524
|
if (msg.interruptionMode === "none" || msg.interruptionMode === "cancel" || msg.interruptionMode === "barge-in") {
|
|
353
525
|
this.update({ interruptionMode: msg.interruptionMode });
|
|
354
526
|
}
|
|
527
|
+
const sessionId = readString(msg.sessionId);
|
|
528
|
+
const conversationId = readString(msg.conversationId);
|
|
529
|
+
if (sessionId && conversationId) {
|
|
530
|
+
const session = { sessionId, conversationId };
|
|
531
|
+
this.update({ session });
|
|
532
|
+
this.options.onSession?.(session);
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case "artifact":
|
|
537
|
+
this.applyArtifactMessage(msg);
|
|
538
|
+
break;
|
|
539
|
+
case "transcript_update": {
|
|
540
|
+
if (typeof msg.text !== "string" || typeof msg.turnId !== "string" || msg.role !== "user" && msg.role !== "assistant")
|
|
541
|
+
break;
|
|
542
|
+
if (msg.role === "assistant" && this.awaitingClear) break;
|
|
543
|
+
const entries = [...this.snapshot.transcript];
|
|
544
|
+
const index = entries.findIndex(
|
|
545
|
+
(entry2) => entry2.turnId === msg.turnId && entry2.role === msg.role
|
|
546
|
+
);
|
|
547
|
+
const entry = {
|
|
548
|
+
role: msg.role,
|
|
549
|
+
content: msg.text,
|
|
550
|
+
turnId: msg.turnId,
|
|
551
|
+
isFinal: msg.final === true,
|
|
552
|
+
timestamp: index >= 0 ? entries[index].timestamp : Date.now()
|
|
553
|
+
};
|
|
554
|
+
if (index >= 0) entries[index] = entry;
|
|
555
|
+
else entries.push(entry);
|
|
556
|
+
this.update({ transcript: entries, interimTranscript: null });
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "delegation_started":
|
|
560
|
+
if (!this.hasPendingAudio && !this.awaitingClear) this.setStatus("thinking");
|
|
561
|
+
break;
|
|
562
|
+
case "delegation_completed":
|
|
563
|
+
if (!this.hasPendingAudio && !this.awaitingClear) this.setStatus("listening");
|
|
355
564
|
break;
|
|
356
565
|
case "transcript_interim":
|
|
357
566
|
this.update({ interimTranscript: typeof msg.text === "string" ? msg.text || null : null });
|
|
358
567
|
break;
|
|
359
|
-
case "
|
|
568
|
+
case "transcript_partial": {
|
|
569
|
+
if (msg.role !== "assistant" || typeof msg.text !== "string" || !msg.text) break;
|
|
570
|
+
this.activeTurnId = readTurnId(msg) ?? this.activeTurnId;
|
|
571
|
+
if (this.awaitingClear || this.stoppedResponse) break;
|
|
572
|
+
this.responseEnded = false;
|
|
573
|
+
this.update(this.appendClauseCaption(msg.text, readTurnId(msg)));
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case "transcript_final": {
|
|
360
577
|
if (typeof msg.text !== "string" || msg.role !== "user" && msg.role !== "assistant") break;
|
|
361
|
-
|
|
578
|
+
const role = msg.role;
|
|
579
|
+
const text = msg.text;
|
|
580
|
+
if (role === "assistant" && (this.awaitingClear || this.stoppedResponse)) break;
|
|
581
|
+
const turnId = readTurnId(msg);
|
|
582
|
+
if (turnId !== null) this.activeTurnId = turnId;
|
|
583
|
+
if (role === "assistant" && turnId !== null && turnId === this.committedAssistantTurnId)
|
|
584
|
+
break;
|
|
362
585
|
this.responseEnded = false;
|
|
586
|
+
const transcript = this.snapshot.transcript;
|
|
587
|
+
const last = transcript[transcript.length - 1];
|
|
588
|
+
const supersedes = role === "assistant" && this.pendingAssistant !== null && last?.role === "assistant" && (turnId === null || this.pendingAssistant.turnId === null || this.pendingAssistant.turnId === turnId);
|
|
589
|
+
this.pendingAssistant = null;
|
|
590
|
+
const entry = {
|
|
591
|
+
role,
|
|
592
|
+
content: text,
|
|
593
|
+
// INVARIANT: reuse the caption's timestamp so a keyed list does not remount the bubble.
|
|
594
|
+
timestamp: supersedes && last ? last.timestamp : Date.now(),
|
|
595
|
+
...turnId ? { turnId } : {}
|
|
596
|
+
};
|
|
363
597
|
this.update({
|
|
364
598
|
interimTranscript: null,
|
|
365
|
-
transcript: [
|
|
366
|
-
|
|
367
|
-
{
|
|
368
|
-
role: msg.role,
|
|
369
|
-
content: msg.text,
|
|
370
|
-
timestamp: Date.now(),
|
|
371
|
-
...typeof msg.turnId === "string" && msg.turnId ? { turnId: msg.turnId } : {}
|
|
372
|
-
}
|
|
373
|
-
],
|
|
374
|
-
status: this.awaitingClear ? this.snapshot.status : msg.role === "user" ? "thinking" : "speaking"
|
|
599
|
+
transcript: supersedes ? [...transcript.slice(0, -1), entry] : [...transcript, entry],
|
|
600
|
+
status: this.awaitingClear ? this.snapshot.status : role === "user" ? "thinking" : "speaking"
|
|
375
601
|
});
|
|
376
602
|
break;
|
|
603
|
+
}
|
|
377
604
|
case "audio_end": {
|
|
378
605
|
this.responseEnded = true;
|
|
379
606
|
if (this.stoppedResponse) {
|
|
@@ -390,10 +617,12 @@ var VoiceClient = class {
|
|
|
390
617
|
break;
|
|
391
618
|
}
|
|
392
619
|
case "audio_clear":
|
|
620
|
+
this.suppressedArtifactTurnId = this.activeTurnId ?? this.suppressedArtifactTurnId;
|
|
393
621
|
this.clearPlayback();
|
|
394
622
|
this.awaitingClear = false;
|
|
395
623
|
this.stoppedResponse = false;
|
|
396
624
|
this.responseEnded = true;
|
|
625
|
+
this.commitClauseCaptions();
|
|
397
626
|
this.setStatus("listening");
|
|
398
627
|
break;
|
|
399
628
|
case "metrics": {
|
|
@@ -406,8 +635,11 @@ var VoiceClient = class {
|
|
|
406
635
|
// @snake-case-ok: Existing voice wire contract.
|
|
407
636
|
firstAudioMs: number(msg.first_audio_ms),
|
|
408
637
|
// @snake-case-ok: Existing voice wire contract.
|
|
409
|
-
totalMs: number(msg.total_ms)
|
|
638
|
+
totalMs: number(msg.total_ms),
|
|
410
639
|
// @snake-case-ok: Existing voice wire contract.
|
|
640
|
+
firstSynthesisMs: number(msg.first_synthesis_ms),
|
|
641
|
+
// @snake-case-ok: Existing voice wire contract.
|
|
642
|
+
incremental: msg.incremental === true
|
|
411
643
|
}
|
|
412
644
|
});
|
|
413
645
|
break;
|
|
@@ -439,14 +671,14 @@ var VoiceClient = class {
|
|
|
439
671
|
const processor = context.createScriptProcessor(CAPTURE_BUFFER_SIZE, 1, 1);
|
|
440
672
|
this.processor = processor;
|
|
441
673
|
processor.onaudioprocess = (event) => {
|
|
442
|
-
if (generation !== this.generation || this.snapshot.isMuted) return;
|
|
674
|
+
if (generation !== this.generation || this.snapshot.isMuted && !this.fullDuplex) return;
|
|
443
675
|
const input = event.inputBuffer.getChannelData(0);
|
|
444
676
|
let sum = 0;
|
|
445
677
|
for (const sample of input) sum += sample * sample;
|
|
446
|
-
this.update({ audioLevel: Math.sqrt(sum / input.length) });
|
|
678
|
+
this.update({ audioLevel: this.snapshot.isMuted ? 0 : Math.sqrt(sum / input.length) });
|
|
447
679
|
if (socket.readyState !== WebSocket.OPEN) return;
|
|
448
680
|
const pcm = new Int16Array(input.length);
|
|
449
|
-
if (this.snapshot.interruptionMode === "cancel" && (this.awaitingClear || this.snapshot.status === "thinking" || this.hasPendingAudio || this.snapshot.status === "speaking")) {
|
|
681
|
+
if (this.snapshot.isMuted || this.snapshot.interruptionMode === "cancel" && (this.awaitingClear || this.snapshot.status === "thinking" || this.hasPendingAudio || this.snapshot.status === "speaking")) {
|
|
450
682
|
socket.send(pcm.buffer);
|
|
451
683
|
return;
|
|
452
684
|
}
|
|
@@ -463,12 +695,13 @@ var VoiceClient = class {
|
|
|
463
695
|
|
|
464
696
|
// src/persona.ts
|
|
465
697
|
function createPersonaVoiceProvider(options) {
|
|
466
|
-
const client = new VoiceClient(options);
|
|
698
|
+
const client = new VoiceClient({ ...options, fullDuplex: false });
|
|
467
699
|
const statusCallbacks = /* @__PURE__ */ new Set();
|
|
468
700
|
const errorCallbacks = /* @__PURE__ */ new Set();
|
|
469
701
|
const transcriptCallbacks = /* @__PURE__ */ new Set();
|
|
470
702
|
const metricsCallbacks = /* @__PURE__ */ new Set();
|
|
471
703
|
const levelCallbacks = /* @__PURE__ */ new Set();
|
|
704
|
+
const artifactCallbacks = /* @__PURE__ */ new Set();
|
|
472
705
|
let previous = client.getSnapshot();
|
|
473
706
|
let unsubscribe = null;
|
|
474
707
|
const onSnapshot = () => {
|
|
@@ -492,12 +725,18 @@ function createPersonaVoiceProvider(options) {
|
|
|
492
725
|
for (const callback of transcriptCallbacks)
|
|
493
726
|
callback("user", snapshot.interimTranscript, false);
|
|
494
727
|
}
|
|
495
|
-
|
|
728
|
+
snapshot.artifacts.forEach((artifact, index) => {
|
|
729
|
+
if (artifact === before.artifacts[index]) return;
|
|
730
|
+
for (const callback of artifactCallbacks) callback(artifact);
|
|
731
|
+
});
|
|
732
|
+
snapshot.transcript.forEach((entry, index) => {
|
|
733
|
+
if (entry === before.transcript[index]) return;
|
|
734
|
+
const isFinal = entry.partial !== true && entry.isFinal !== false;
|
|
496
735
|
for (const callback of transcriptCallbacks) {
|
|
497
|
-
if (entry.turnId) callback(entry.role, entry.content,
|
|
498
|
-
else callback(entry.role, entry.content,
|
|
736
|
+
if (entry.turnId) callback(entry.role, entry.content, isFinal, { turnId: entry.turnId });
|
|
737
|
+
else callback(entry.role, entry.content, isFinal);
|
|
499
738
|
}
|
|
500
|
-
}
|
|
739
|
+
});
|
|
501
740
|
};
|
|
502
741
|
const subscribe = () => {
|
|
503
742
|
if (unsubscribe) return;
|
|
@@ -518,6 +757,7 @@ function createPersonaVoiceProvider(options) {
|
|
|
518
757
|
transcriptCallbacks.clear();
|
|
519
758
|
metricsCallbacks.clear();
|
|
520
759
|
levelCallbacks.clear();
|
|
760
|
+
artifactCallbacks.clear();
|
|
521
761
|
},
|
|
522
762
|
startListening: async () => {
|
|
523
763
|
subscribe();
|
|
@@ -544,6 +784,9 @@ function createPersonaVoiceProvider(options) {
|
|
|
544
784
|
onLevel: (callback) => {
|
|
545
785
|
levelCallbacks.add(callback);
|
|
546
786
|
},
|
|
787
|
+
onArtifact: (callback) => {
|
|
788
|
+
artifactCallbacks.add(callback);
|
|
789
|
+
},
|
|
547
790
|
getInterruptionMode: () => client.getSnapshot().interruptionMode,
|
|
548
791
|
isBargeInActive: () => !["idle", "error"].includes(client.getSnapshot().status),
|
|
549
792
|
deactivateBargeIn: async () => {
|
|
@@ -552,6 +795,52 @@ function createPersonaVoiceProvider(options) {
|
|
|
552
795
|
stopPlayback: client.stopPlayback
|
|
553
796
|
};
|
|
554
797
|
}
|
|
798
|
+
function toManualUpsert(artifact) {
|
|
799
|
+
const title = artifact.title ?? artifact.file?.path;
|
|
800
|
+
const transcript = artifact.status === "complete";
|
|
801
|
+
if (artifact.artifactType === "component") {
|
|
802
|
+
if (!artifact.component) return null;
|
|
803
|
+
return {
|
|
804
|
+
id: artifact.id,
|
|
805
|
+
artifactType: "component",
|
|
806
|
+
component: artifact.component,
|
|
807
|
+
...title ? { title } : {},
|
|
808
|
+
...artifact.props ? { props: artifact.props } : {},
|
|
809
|
+
transcript
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
id: artifact.id,
|
|
814
|
+
artifactType: "markdown",
|
|
815
|
+
content: artifact.content,
|
|
816
|
+
...title ? { title } : {},
|
|
817
|
+
...artifact.file ? {
|
|
818
|
+
file: {
|
|
819
|
+
path: artifact.file.path,
|
|
820
|
+
mimeType: artifact.file.mimeType,
|
|
821
|
+
...artifact.file.language ? { language: artifact.file.language } : {}
|
|
822
|
+
}
|
|
823
|
+
} : {},
|
|
824
|
+
transcript
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function bindVoiceArtifactsToPersona(provider, widget, options) {
|
|
828
|
+
let released = false;
|
|
829
|
+
let shown = false;
|
|
830
|
+
provider.onArtifact((artifact) => {
|
|
831
|
+
if (released) return;
|
|
832
|
+
const manual = toManualUpsert(artifact);
|
|
833
|
+
if (!manual) return;
|
|
834
|
+
widget.upsertArtifact(manual);
|
|
835
|
+
if (shown || options?.showOnFirst === false) return;
|
|
836
|
+
shown = true;
|
|
837
|
+
widget.showArtifacts();
|
|
838
|
+
});
|
|
839
|
+
return () => {
|
|
840
|
+
released = true;
|
|
841
|
+
};
|
|
842
|
+
}
|
|
555
843
|
export {
|
|
844
|
+
bindVoiceArtifactsToPersona,
|
|
556
845
|
createPersonaVoiceProvider
|
|
557
846
|
};
|