@burdenoff/fe-libs 2026.1013.13 → 2026.1014.1

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.
Files changed (48) hide show
  1. package/dist/chrome/AppShellLayout.js +17 -8
  2. package/dist/shared/assistant/avatar/live/LiveAvatarStage.d.ts +15 -0
  3. package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +139 -0
  4. package/dist/shared/assistant/avatar/live/avatarAudioDecode.d.ts +40 -0
  5. package/dist/shared/assistant/avatar/live/avatarAudioDecode.js +72 -0
  6. package/dist/shared/assistant/avatar/live/avatarAudioErrors.d.ts +25 -0
  7. package/dist/shared/assistant/avatar/live/avatarAudioErrors.js +27 -0
  8. package/dist/shared/assistant/avatar/live/avatarAudioMeter.d.ts +37 -0
  9. package/dist/shared/assistant/avatar/live/avatarAudioMeter.js +62 -0
  10. package/dist/shared/assistant/avatar/live/avatarMp3Inspect.d.ts +30 -0
  11. package/dist/shared/assistant/avatar/live/avatarMp3Inspect.js +168 -0
  12. package/dist/shared/assistant/avatar/live/avatarPcm16.d.ts +21 -0
  13. package/dist/shared/assistant/avatar/live/avatarPcm16.js +54 -0
  14. package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.d.ts +181 -0
  15. package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.js +221 -0
  16. package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +66 -0
  17. package/dist/shared/assistant/avatar/live/avatarPorts.js +44 -0
  18. package/dist/shared/assistant/avatar/live/avatarProtocol.d.ts +22 -0
  19. package/dist/shared/assistant/avatar/live/avatarProtocol.js +27 -0
  20. package/dist/shared/assistant/avatar/live/avatarReplyOutputs.d.ts +170 -0
  21. package/dist/shared/assistant/avatar/live/avatarReplyOutputs.js +525 -0
  22. package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +96 -0
  23. package/dist/shared/assistant/avatar/live/avatarSessionClient.js +261 -0
  24. package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +270 -0
  25. package/dist/shared/assistant/avatar/live/avatarTransport.js +333 -0
  26. package/dist/shared/assistant/avatar/live/avatarTransportPort.d.ts +17 -0
  27. package/dist/shared/assistant/avatar/live/avatarTransportPort.js +27 -0
  28. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +44 -0
  29. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +124 -0
  30. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +133 -0
  31. package/dist/shared/assistant/avatar/live/liveAvatarController.js +488 -0
  32. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +4 -0
  33. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +14 -0
  34. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.d.ts +6 -0
  35. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +86 -0
  36. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +2 -0
  37. package/dist/shared/assistant/avatar/useAvatarCopy.js +1 -0
  38. package/dist/shared/assistant/i18nManifest.d.ts +1 -0
  39. package/dist/shared/assistant/i18nManifest.js +1 -0
  40. package/dist/shared/assistant/index.d.ts +17 -0
  41. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +37 -0
  42. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -63
  43. package/dist/shared/assistant/speech/types.d.ts +14 -2
  44. package/dist/shared/assistant/ui/AssistantOverlay.js +98 -95
  45. package/dist/shared/assistant/ui/assistantOverlayState.d.ts +12 -0
  46. package/dist/shared/assistant/ui/assistantOverlayState.js +14 -14
  47. package/dist/shared-assistant.js +250 -233
  48. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ import { AvatarRpcRejectedError as e } from "./avatarPorts.js";
2
+ //#region src/shared/assistant/avatar/live/liveKitAvatarRoom.ts
3
+ async function t() {
4
+ let t = await import("livekit-client"), n = new t.Room({
5
+ adaptiveStream: !0,
6
+ dynacast: !1,
7
+ disconnectOnPageLeave: !0
8
+ }), r = (e, t) => (n.on(e, t), () => {
9
+ n.off(e, t);
10
+ }), i = (e, n) => ({
11
+ identity: n,
12
+ kind: e.kind === t.Track.Kind.Video ? "video" : "audio",
13
+ attach: (t) => {
14
+ e.attach(t);
15
+ },
16
+ detach: () => {
17
+ e.detach();
18
+ },
19
+ mediaStreamTrack: e.mediaStreamTrack ?? null
20
+ });
21
+ return {
22
+ async connect(e, t) {
23
+ await n.connect(e, t);
24
+ },
25
+ async disconnect() {
26
+ await n.disconnect();
27
+ },
28
+ isConnected: () => n.state === t.ConnectionState.Connected,
29
+ hasParticipant: (e) => n.remoteParticipants.has(e),
30
+ hasVideoTrack: (e) => {
31
+ let t = n.remoteParticipants.get(e);
32
+ if (!t) return !1;
33
+ for (let e of t.videoTrackPublications.values()) if (e.isSubscribed) return !0;
34
+ return !1;
35
+ },
36
+ onParticipantConnected: (e) => r(t.RoomEvent.ParticipantConnected, (t) => e(t.identity)),
37
+ onParticipantDisconnected: (e) => r(t.RoomEvent.ParticipantDisconnected, (t) => e(t.identity)),
38
+ onTrackSubscribed: (e) => r(t.RoomEvent.TrackSubscribed, (t, n, r) => e(i(t, r.identity))),
39
+ onDisconnected: (e) => r(t.RoomEvent.Disconnected, () => e()),
40
+ onReconnecting: (e) => r(t.RoomEvent.Reconnecting, () => e()),
41
+ onReconnected: (e) => r(t.RoomEvent.Reconnected, () => e()),
42
+ onAudioPlaybackChanged: (e) => r(t.RoomEvent.AudioPlaybackStatusChanged, (t) => e(t)),
43
+ registerRpc(r, i) {
44
+ try {
45
+ n.unregisterRpcMethod(r);
46
+ } catch {}
47
+ n.registerRpcMethod(r, async (n) => {
48
+ try {
49
+ return i(n.callerIdentity, n.payload);
50
+ } catch (n) {
51
+ throw n instanceof e ? new t.RpcError(t.RpcError.ErrorCode.APPLICATION_ERROR, "rejected") : n;
52
+ }
53
+ });
54
+ },
55
+ unregisterRpc(e) {
56
+ try {
57
+ n.unregisterRpcMethod(e);
58
+ } catch {}
59
+ },
60
+ performRpc: (e, t, r, i) => n.localParticipant.performRpc({
61
+ destinationIdentity: e,
62
+ method: t,
63
+ payload: r,
64
+ responseTimeout: i
65
+ }),
66
+ async openByteStream({ topic: e, name: t, destinationIdentity: r, attributes: i }) {
67
+ let a = await n.localParticipant.streamBytes({
68
+ name: t,
69
+ topic: e,
70
+ attributes: i,
71
+ destinationIdentities: [r],
72
+ mimeType: "audio/pcm"
73
+ });
74
+ return {
75
+ write: (e) => a.write(e),
76
+ close: () => a.close()
77
+ };
78
+ },
79
+ async startAudio() {
80
+ await n.startAudio();
81
+ },
82
+ canPlaybackAudio: () => n.canPlaybackAudio
83
+ };
84
+ }
85
+ //#endregion
86
+ export { t as openLiveKitAvatarRoom };
@@ -32,6 +32,8 @@ export interface AvatarCopy {
32
32
  stateEnding: string;
33
33
  stateInterrupted: string;
34
34
  stateVoiceOnly: string;
35
+ /** The session reached its time limit and ended between replies (BOFF-8822 P8); the voice goes on. */
36
+ stateTimeLimit: string;
35
37
  stageLabel: string;
36
38
  stageWaiting: string;
37
39
  chooserTitle: string;
@@ -17,6 +17,7 @@ function n() {
17
17
  stateEnding: n("avatar.stateEnding", "Stopping the avatar…"),
18
18
  stateInterrupted: n("avatar.stateInterrupted", "The avatar was interrupted. You can keep talking."),
19
19
  stateVoiceOnly: n("avatar.stateVoiceOnly", "The avatar is unavailable right now. Continuing with voice only."),
20
+ stateTimeLimit: n("avatar.stateTimeLimit", "The avatar reached its time limit. Continuing with voice only."),
20
21
  stageLabel: n("avatar.stageLabel", "Live avatar"),
21
22
  stageWaiting: n("avatar.stageWaiting", "Waiting for the avatar…"),
22
23
  chooserTitle: n("avatar.chooserTitle", "Choose an avatar"),
@@ -246,6 +246,7 @@ export declare const ASSISTANT_I18N_LITERAL_COPY: {
246
246
  readonly "avatar.stateOn": "Avatar on";
247
247
  readonly "avatar.stateReady": "Avatar ready";
248
248
  readonly "avatar.stateStarting": "Starting the avatar…";
249
+ readonly "avatar.stateTimeLimit": "The avatar reached its time limit. Continuing with voice only.";
249
250
  readonly "avatar.stateVoiceOnly": "The avatar is unavailable right now. Continuing with voice only.";
250
251
  readonly "avatar.stop": "Turn off the avatar";
251
252
  readonly "avatar.toggle": "Live avatar";
@@ -140,6 +140,7 @@ var m = {
140
140
  "avatar.stateOn": "Avatar on",
141
141
  "avatar.stateReady": "Avatar ready",
142
142
  "avatar.stateStarting": "Starting the avatar…",
143
+ "avatar.stateTimeLimit": "The avatar reached its time limit. Continuing with voice only.",
143
144
  "avatar.stateVoiceOnly": "The avatar is unavailable right now. Continuing with voice only.",
144
145
  "avatar.stop": "Turn off the avatar",
145
146
  "avatar.toggle": "Live avatar",
@@ -313,3 +313,20 @@ export * from './workflowApi';
313
313
  export * from './ui/WorkflowAssistantPanel';
314
314
  export * from './speech/notifyCancellationSpeechEngine';
315
315
  export * from './avatar/avatarPreferences';
316
+ export * from './avatar/live/avatarProtocol';
317
+ export * from './avatar/live/avatarPorts';
318
+ export * from './avatar/live/avatarTransport';
319
+ export * from './avatar/live/avatarPlaybackTracker';
320
+ export * from './avatar/live/avatarAudioMeter';
321
+ export * from './avatar/live/avatarAudioErrors';
322
+ export * from './avatar/live/avatarMp3Inspect';
323
+ export * from './avatar/live/avatarPcm16';
324
+ export * from './avatar/live/avatarAudioDecode';
325
+ export * from './avatar/live/avatarTransportPort';
326
+ export * from './avatar/live/avatarReplyOutputs';
327
+ export * from './avatar/live/avatarSessionClient';
328
+ export * from './avatar/live/liveAvatarController';
329
+ export * from './avatar/live/liveKitAvatarRoom';
330
+ export * from './avatar/live/LiveAvatarStage';
331
+ export * from './avatar/live/liveAvatarStatus';
332
+ export * from './avatar/live/createLiveAvatarSeam';
@@ -7,6 +7,11 @@ export interface HostedTtsOptions {
7
7
  fetchImpl?: typeof fetch;
8
8
  /** Test seam: the clock the disable window is measured against. */
9
9
  now?: () => number;
10
+ /**
11
+ * Test seam: the timer behind the audio output's no-progress watchdog
12
+ * (`hostedOutputStallMs`, BOFF-8822). Returns a function that cancels it.
13
+ */
14
+ setTimer?: (callback: () => void, ms: number) => () => void;
10
15
  }
11
16
  /**
12
17
  * The hosted engine, plus the one hook its owner needs beyond `SpeechEngine`.
@@ -23,4 +28,36 @@ export interface HostedSpeechEngine extends SpeechEngine {
23
28
  }
24
29
  /** `Retry-After` in seconds when the server sent a usable one, else backoff. */
25
30
  export declare function retryDelayMs(retryAfter: string | null, attempt: number): number;
31
+ /**
32
+ * ★★ The backstop for an audio output that stops making progress (BOFF-8822,
33
+ * the voice owners' P1b note a, 2026-10-13).
34
+ *
35
+ * `speak()` waits for its output's `write()` and `drain()` before it fetches
36
+ * more or ends the reply, and the output contract says both settle in bounded
37
+ * time (`SpeechAudioOutput`). One that never did (a contract bug in an output)
38
+ * would leave the reply "speaking", and the voice conversation with it, until
39
+ * the user pressed Stop. So while the engine waits on its output, a watchdog
40
+ * runs, and every sign of progress starts it again: a piece beginning, on the
41
+ * output or replayed on the speakers, and every write or drain settling.
42
+ *
43
+ * ★ Not a fixed timeout. A write may resolve only when its piece BEGINS, so it
44
+ * can wait for every earlier piece to play out, and a drain waits for all the
45
+ * audio held: a fixed cap would end long, healthy replies. The window grows with
46
+ * the audio handed over, bounded from above by its bytes at the lowest MP3
47
+ * bitrate (8 kbit/s; the proxy sends 32), so it is never shorter than twice that
48
+ * audio plus a minute. The output's own bounds (the live avatar's start, stall
49
+ * and finish deadlines: its start allowance, 1.25 times the reply's audio and a
50
+ * few seconds of slack) run out well inside it, so the output always decides
51
+ * first, and this only catches an output that never does.
52
+ *
53
+ * On expiry the output and the speakers are silenced BEFORE the reply ends with
54
+ * `onError('unknown')`, which latches nothing; the output is not drained again.
55
+ */
56
+ export declare const HOSTED_OUTPUT_STALL_BASE_MS = 60000;
57
+ /** How many times the audio handed over the window adds on top of the base. */
58
+ export declare const HOSTED_OUTPUT_STALL_AUDIO_RATIO = 2;
59
+ /** 8 kbit/s, the lowest MP3 bitrate: bytes over this are at least the audio's seconds. */
60
+ export declare const HOSTED_OUTPUT_MIN_AUDIO_BYTES_PER_SECOND = 1000;
61
+ /** The watchdog's window once `bytesHanded` bytes of audio went to the output. */
62
+ export declare function hostedOutputStallMs(bytesHanded: number): number;
26
63
  export declare function createHostedTtsEngine(options: HostedTtsOptions): HostedSpeechEngine;
@@ -18,7 +18,53 @@ function _(e, t) {
18
18
  let n = e === null ? NaN : Number(e);
19
19
  return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : h * 2 ** (t - 1), g);
20
20
  }
21
- function v(e, t) {
21
+ var v = 6e4, y = 2, b = 1e3;
22
+ function x(e) {
23
+ return v + e / b * 2 * 1e3;
24
+ }
25
+ var S = class extends Error {
26
+ reason = "unknown";
27
+ constructor() {
28
+ super("audio output made no progress"), this.name = "OutputStalledError";
29
+ }
30
+ };
31
+ function C(e) {
32
+ let t = 0, n = !1, r = null, i = () => {
33
+ let n = r;
34
+ n !== null && (n.cancel(), n.cancel = e(() => {
35
+ r === n && (r = null, n.fail(new S()));
36
+ }, x(t)));
37
+ };
38
+ return {
39
+ handed(e) {
40
+ t += e;
41
+ },
42
+ progress: i,
43
+ guard(e) {
44
+ return n ? e : new Promise((t, n) => {
45
+ let a = {
46
+ fail: n,
47
+ cancel: () => void 0
48
+ };
49
+ r = a, i();
50
+ let o = () => {
51
+ r === a && (r = null, a.cancel());
52
+ };
53
+ e.then((e) => {
54
+ o(), t(e);
55
+ }, (e) => {
56
+ o(), n(e);
57
+ });
58
+ });
59
+ },
60
+ stop() {
61
+ n = !0;
62
+ let e = r;
63
+ r = null, e?.cancel();
64
+ }
65
+ };
66
+ }
67
+ function w(e, t) {
22
68
  return new Promise((n) => {
23
69
  if (t.aborted) {
24
70
  n();
@@ -33,32 +79,35 @@ function v(e, t) {
33
79
  t.addEventListener("abort", i, { once: !0 });
34
80
  });
35
81
  }
36
- function y(h) {
37
- let g = h.fetchImpl ?? ((...e) => fetch(...e)), y = h.now ?? (() => Date.now()), b = null, x = null, S = 0, C = null, w = null, T = 0, E = null, D = null, O = () => {
82
+ function T(h) {
83
+ let g = h.fetchImpl ?? ((...e) => fetch(...e)), v = h.now ?? (() => Date.now()), y = h.setTimer ?? ((e, t) => {
84
+ let n = setTimeout(e, t);
85
+ return () => clearTimeout(n);
86
+ }), b = null, x = null, T = 0, E = null, D = null, O = null, k = 0, A = null, j = null, M = () => {
38
87
  try {
39
88
  return h.getHeaders?.() ?? {};
40
89
  } catch {
41
90
  return {};
42
91
  }
43
- }, k = () => y() >= T ? !1 : D === null ? !0 : f(O()) === D, A = /* @__PURE__ */ new Set(), j = () => {
44
- for (let e of [...A]) e();
45
- }, M = () => {
46
- E !== null && clearTimeout(E), E = null;
47
- }, N = (e) => {
48
- M(), E = setTimeout(() => {
49
- E = null;
50
- let e = T - y();
92
+ }, N = () => v() >= k ? !1 : j === null ? !0 : f(M()) === j, P = /* @__PURE__ */ new Set(), F = () => {
93
+ for (let e of [...P]) e();
94
+ }, I = () => {
95
+ A !== null && clearTimeout(A), A = null;
96
+ }, L = (e) => {
97
+ I(), A = setTimeout(() => {
98
+ A = null;
99
+ let e = k - v();
51
100
  if (e > 0) {
52
- N(e);
101
+ L(e);
53
102
  return;
54
103
  }
55
- j();
104
+ F();
56
105
  }, e);
57
- }, P = (e, t) => {
58
- T = y() + e, D = t, N(e), j();
59
- }, F = () => {
106
+ }, R = (e, t) => {
107
+ k = v() + e, j = t, L(e), F();
108
+ }, z = () => {
60
109
  x &&= (URL.revokeObjectURL(x), null);
61
- }, I = () => {
110
+ }, B = () => {
62
111
  if (b) {
63
112
  b.onended = null, b.onerror = null;
64
113
  try {
@@ -66,9 +115,9 @@ function y(h) {
66
115
  } catch {}
67
116
  b = null;
68
117
  }
69
- F();
118
+ z();
70
119
  };
71
- async function L(e, n, s) {
120
+ async function V(e, n, s) {
72
121
  let c = i(e, r(n.lang) ?? n.lang, t, n.voice?.name ?? null, n.rate), l = h.getHeaders?.() ?? {}, u;
73
122
  for (let e = 1; u = await g(h.endpoint, {
74
123
  method: "POST",
@@ -83,37 +132,37 @@ function y(h) {
83
132
  let e = /* @__PURE__ */ Error(`hosted tts ${u.status}`);
84
133
  throw e.reason = a(u.status), e.headers = f(l), e;
85
134
  }
86
- await v(_(u.headers.get("retry-after"), e), s);
135
+ await w(_(u.headers.get("retry-after"), e), s);
87
136
  }
88
- let d = u.headers.get("content-type"), y = o(d, d?.includes("json") ? await u.json() : await u.arrayBuffer());
89
- if (!y) {
137
+ let d = u.headers.get("content-type"), v = o(d, d?.includes("json") ? await u.json() : await u.arrayBuffer());
138
+ if (!v) {
90
139
  let e = /* @__PURE__ */ Error("hosted tts returned no audio");
91
140
  throw e.reason = "unknown", e;
92
141
  }
93
- return y;
142
+ return v;
94
143
  }
95
- function R(t, n, r) {
144
+ function H(t, n, r) {
96
145
  return new Promise((i, a) => {
97
- if (n !== S) {
146
+ if (n !== T) {
98
147
  i();
99
148
  return;
100
149
  }
101
150
  let o = new Blob([t.slice().buffer], { type: "audio/mpeg" });
102
- F(), x = URL.createObjectURL(o);
151
+ z(), x = URL.createObjectURL(o);
103
152
  let s = new Audio(x);
104
153
  e(s), b = s, s.onended = () => {
105
- n === S && i();
154
+ n === T && i();
106
155
  }, s.onerror = () => {
107
- if (n !== S) {
156
+ if (n !== T) {
108
157
  i();
109
158
  return;
110
159
  }
111
160
  let e = /* @__PURE__ */ Error("audio playback failed");
112
161
  e.reason = "unknown", a(e);
113
162
  }, s.play().then(() => {
114
- n === S && r?.();
163
+ n === T && r?.();
115
164
  }, (e) => {
116
- if (n !== S) {
165
+ if (n !== T) {
117
166
  i();
118
167
  return;
119
168
  }
@@ -125,14 +174,14 @@ function y(h) {
125
174
  return {
126
175
  id: "hosted-tts",
127
176
  isSupported() {
128
- return k() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
177
+ return N() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
129
178
  },
130
179
  listVoices() {
131
- return k() ? [] : [...n];
180
+ return N() ? [] : [...n];
132
181
  },
133
182
  subscribeVoices(e) {
134
- return A.add(e), () => {
135
- A.delete(e);
183
+ return P.add(e), () => {
184
+ P.delete(e);
136
185
  };
137
186
  },
138
187
  speak(e, t, n = {}) {
@@ -141,66 +190,72 @@ function y(h) {
141
190
  n.onEnd?.();
142
191
  return;
143
192
  }
144
- S += 1;
145
- let i = S;
146
- I(), w?.cancel();
193
+ T += 1;
194
+ let i = T;
195
+ B(), D?.cancel(), O?.stop();
147
196
  let a = new AbortController();
148
- C = a;
197
+ E = a;
149
198
  let o = !1, f = () => {
150
199
  o || (o = !0, n.onStart?.());
151
200
  }, p = (e) => {
152
- let n = L(e, t, a.signal);
201
+ let n = V(e, t, a.signal);
153
202
  return n.catch(() => {}), n;
154
- }, m = t.audioOutput ? u(t.audioOutput, {
155
- play: (e) => R(e, i, f),
156
- isCurrent: () => i === S,
157
- onChunkStart: (e) => n.onChunkStart?.(e),
203
+ }, m = t.audioOutput ? C(y) : null, h = t.audioOutput ? u(t.audioOutput, {
204
+ play: (e) => H(e, i, f),
205
+ isCurrent: () => i === T,
206
+ onChunkStart: (e) => {
207
+ m?.progress(), n.onChunkStart?.(e);
208
+ },
158
209
  onStart: f
159
210
  }) : null;
160
- w = m, (async () => {
211
+ D = h, O = m;
212
+ let g = (e) => m ? m.guard(e) : e, _ = () => {
213
+ h && (m?.stop(), O === m && (O = null), D === h && (D = null));
214
+ };
215
+ (async () => {
161
216
  try {
162
217
  let e = p(r[0]);
163
218
  for (let t = 0; t < r.length; t += 1) {
164
- if (i !== S) return;
219
+ if (i !== T) return;
165
220
  let a = await e;
166
- if (i !== S) return;
221
+ if (i !== T) return;
167
222
  let o = r[t + 1];
168
- if (o !== void 0 && (e = p(o)), m) {
169
- await m.write(a);
223
+ if (o !== void 0 && (e = p(o)), h) {
224
+ m?.handed(a.byteLength), await g(h.write(a));
170
225
  continue;
171
226
  }
172
- n.onChunkStart?.(t), await R(a, i, f);
227
+ n.onChunkStart?.(t), await H(a, i, f);
173
228
  }
174
- if (m) {
175
- let e = await m.drain();
176
- if (i !== S) return;
229
+ if (h) {
230
+ let e = await g(h.drain());
231
+ if (i !== T) return;
177
232
  if (!e) throw Object.assign(/* @__PURE__ */ Error("audio output did not finish"), { reason: "unknown" });
178
233
  }
179
- i === S && n.onEnd?.();
234
+ i === T && (_(), n.onEnd?.());
180
235
  } catch (e) {
181
- if (a.abort(), i !== S) return;
236
+ if (a.abort(), i !== T) return;
182
237
  let t = null;
183
- if (m) {
184
- let n = await m.drain().then(() => null, (e) => e);
185
- if (i !== S) return;
186
- m.cancel(), d(e) ? t = e : d(n) && (t = n);
238
+ if (h) {
239
+ let n = e instanceof S ? null : await g(h.drain()).then(() => null, (e) => e);
240
+ if (i !== T) return;
241
+ h.cancel(), d(e) ? t = e : d(n) && (t = n);
187
242
  }
188
- if (S += 1, I(), t) {
243
+ if (T += 1, B(), _(), t) {
189
244
  n.onError?.("output-not-cleared");
190
245
  return;
191
246
  }
192
247
  let r = e?.reason;
193
- c(r, e?.transient === !0) && P(s(r), r === "not-allowed" ? e.headers ?? null : null), n.onError?.(r === "not-allowed" || r === "unsupported" || r === "cancelled" ? r : "unknown");
248
+ c(r, e?.transient === !0) && R(s(r), r === "not-allowed" ? e.headers ?? null : null), n.onError?.(r === "not-allowed" || r === "unsupported" || r === "cancelled" ? r : "unknown");
194
249
  }
195
250
  })();
196
251
  },
197
252
  headersChanged() {
198
- y() >= T || D === null || k() || (T = 0, D = null, M(), j());
253
+ v() >= k || j === null || N() || (k = 0, j = null, I(), F());
199
254
  },
200
255
  cancel() {
201
- S += 1, C?.abort(), C = null, I(), w?.cancel(), w = null;
256
+ T += 1, E?.abort(), E = null, B(), D?.cancel(), D = null, O?.stop(), O = null;
202
257
  }
203
258
  };
204
259
  }
205
260
  //#endregion
206
- export { y as createHostedTtsEngine, _ as retryDelayMs };
261
+ export { b as HOSTED_OUTPUT_MIN_AUDIO_BYTES_PER_SECOND, y as HOSTED_OUTPUT_STALL_AUDIO_RATIO, v as HOSTED_OUTPUT_STALL_BASE_MS, T as createHostedTtsEngine, x as hostedOutputStallMs, _ as retryDelayMs };
@@ -61,6 +61,16 @@ export interface SpeechSpeakOptions {
61
61
  *
62
62
  * One object serves exactly one reply: attach a fresh one to each, so neither
63
63
  * a leave, a refusal nor the hard stop carries over to the next reply.
64
+ *
65
+ * ★★ `write()` and `drain()` MUST settle in bounded time. The engine waits for
66
+ * each one before it fetches more, falls back, or ends the reply, so a call that
67
+ * never settles would leave the reply "speaking" until the user pressed Stop.
68
+ * Each output bounds its own waits (the live avatar: its start, stall and finish
69
+ * deadlines); an output that cannot tell what happened settles as uncertain, by
70
+ * the rules above, rather than wait. The engine keeps only a backstop: when its
71
+ * output makes no progress for far longer than any such bound
72
+ * (`hostedOutputStallMs`), it silences the output and ends the reply with
73
+ * `onError('unknown')`, which latches nothing.
64
74
  */
65
75
  export interface SpeechAudioOutput {
66
76
  /**
@@ -74,7 +84,8 @@ export interface SpeechAudioOutput {
74
84
  * before rejecting, for any piece that MAY have been heard: the speakers
75
85
  * resume after the last piece that began, so nothing is heard twice. A piece
76
86
  * retracted before it began resolves its `write` without `began()`. Writing
77
- * may go on after a `drain()`, until `cancel()` or the reply's end.
87
+ * may go on after a `drain()`, until `cancel()` or the reply's end. MUST
88
+ * settle in bounded time, whatever the output hears or does not hear.
78
89
  */
79
90
  write(bytes: Uint8Array, index: number, began: () => void): Promise<void>;
80
91
  /**
@@ -82,7 +93,8 @@ export interface SpeechAudioOutput {
82
93
  * `write` does. Callable more than once: a queue may drain when its speech
83
94
  * pauses and write again afterwards, even before that drain settles (a piece
84
95
  * written meanwhile is not part of it). The reply's last drain follows its
85
- * end and its last write.
96
+ * end and its last write. MUST settle in bounded time: when the end cannot be
97
+ * confirmed, reject, rather than wait for a signal that may never come.
86
98
  */
87
99
  drain(): Promise<void>;
88
100
  /**