@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/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: Only report drained after end-of-stream, never during a jitter gap.
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 === 'push') {
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(), ["runtype.bearer", token]);
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 "transcript_final":
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
- if (msg.role === "assistant" && (this.awaitingClear || this.stoppedResponse)) break;
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
- ...this.snapshot.transcript,
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
- for (const entry of snapshot.transcript.slice(before.transcript.length)) {
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, true, { turnId: entry.turnId });
498
- else callback(entry.role, entry.content, true);
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
  };