@burdenoff/fe-libs 2026.1016.2 → 2026.1017.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 (49) hide show
  1. package/dist/shared/assistant/avatar/live/AvatarFace.d.ts +13 -0
  2. package/dist/shared/assistant/avatar/live/AvatarFace.js +46 -0
  3. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +2 -0
  4. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +172 -145
  5. package/dist/shared/assistant/avatar/live/avatarFaces.d.ts +31 -0
  6. package/dist/shared/assistant/avatar/live/avatarFaces.js +33 -0
  7. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +12 -0
  8. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +142 -75
  9. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +5 -0
  10. package/dist/shared/assistant/avatar/live/liveAvatarController.js +3 -1
  11. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +14 -0
  12. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -2
  13. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +42 -0
  14. package/dist/shared/assistant/avatar/useAvatarCopy.js +113 -75
  15. package/dist/shared/assistant/i18nManifest.d.ts +29 -0
  16. package/dist/shared/assistant/i18nManifest.js +29 -0
  17. package/dist/shared/assistant/index.d.ts +8 -0
  18. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +6 -0
  19. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -101
  20. package/dist/shared/assistant/speech/notifyCancellationSpeechEngine.js +3 -0
  21. package/dist/shared/assistant/speech/replySpeechRequests.d.ts +84 -0
  22. package/dist/shared/assistant/speech/replySpeechRequests.js +70 -0
  23. package/dist/shared/assistant/speech/routingSpeechEngine.js +13 -9
  24. package/dist/shared/assistant/speech/types.d.ts +39 -1
  25. package/dist/shared/assistant/ui/AIAssistantPanel.js +128 -119
  26. package/dist/shared/assistant/ui/AssistantChatArea.js +1572 -1430
  27. package/dist/shared/assistant/ui/AssistantMessageItem.js +110 -100
  28. package/dist/shared/assistant/ui/AssistantSpeechProvider.d.ts +10 -0
  29. package/dist/shared/assistant/ui/AssistantSpeechProvider.js +286 -225
  30. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +40 -0
  31. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +6 -0
  32. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +99 -52
  33. package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.d.ts +8 -0
  34. package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.js +87 -0
  35. package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.d.ts +13 -0
  36. package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.js +97 -0
  37. package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +104 -0
  38. package/dist/shared/assistant/ui/avatar/avatarAskGate.js +39 -0
  39. package/dist/shared/assistant/ui/avatar/avatarTabDraft.d.ts +17 -0
  40. package/dist/shared/assistant/ui/avatar/avatarTabDraft.js +22 -0
  41. package/dist/shared/assistant/ui/avatar/avatarTabExchange.d.ts +47 -0
  42. package/dist/shared/assistant/ui/avatar/avatarTabExchange.js +50 -0
  43. package/dist/shared/assistant/ui/panelOverlays.d.ts +8 -1
  44. package/dist/shared/assistant/ui/panelOverlays.js +1 -1
  45. package/dist/shared/assistant/ui/useVoiceSession.js +222 -227
  46. package/dist/shared/assistant/voice/handsfree/playbackReference.d.ts +15 -4
  47. package/dist/shared/assistant/voice/handsfree/playbackReference.js +9 -6
  48. package/dist/shared-assistant.js +270 -262
  49. package/package.json +1 -1
@@ -1,39 +1,39 @@
1
- import { tapPlaybackElement as e } from "../voice/handsfree/playbackReference.js";
2
- import { HOSTED_TTS_TIER as t, HOSTED_VOICES as n, hostedLocaleFor as r } from "./hostedVoiceCatalogue.js";
3
- import { buildRequest as i, classifyHttpFailure as a, decodeAudioResponse as o, disableDurationMs as s, shouldDisableEngine as c, speakableChunks as l } from "./hostedTtsProtocol.js";
4
- import { createPieceSink as u, isOutputRefusal as d } from "./pieceSink.js";
1
+ import { isPlaybackReferenceAttached as e, tapPlaybackElement as t } from "../voice/handsfree/playbackReference.js";
2
+ import { HOSTED_TTS_TIER as n, HOSTED_VOICES as r, hostedLocaleFor as i } from "./hostedVoiceCatalogue.js";
3
+ import { buildRequest as a, classifyHttpFailure as o, decodeAudioResponse as s, disableDurationMs as c, shouldDisableEngine as l, speakableChunks as u } from "./hostedTtsProtocol.js";
4
+ import { createPieceSink as d, isOutputRefusal as f } from "./pieceSink.js";
5
5
  //#region src/shared/assistant/speech/hostedTtsSpeechEngine.ts
6
- function f(e) {
6
+ function p(e) {
7
7
  let t = Object.entries(e).map(([e, t]) => `${e.toLowerCase()}:${t}`).sort().join("\n"), n = 5381;
8
8
  for (let e = 0; e < t.length; e += 1) n = (n << 5) + n + t.charCodeAt(e) | 0;
9
9
  return `${t.length}:${n >>> 0}`;
10
10
  }
11
- var p = new Set([
11
+ var m = new Set([
12
12
  429,
13
13
  502,
14
14
  503,
15
15
  504
16
- ]), m = 3, h = 400, g = 2e3;
17
- function _(e, t) {
16
+ ]), h = 3, g = 400, _ = 2e3;
17
+ function v(e, t) {
18
18
  let n = e === null ? NaN : Number(e);
19
- return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : h * 2 ** (t - 1), g);
19
+ return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : g * 2 ** (t - 1), _);
20
20
  }
21
- var v = 6e4, y = 2, b = 1e3;
22
- function x(e) {
23
- return v + e / b * 2 * 1e3;
21
+ var y = 6e4, b = 2, x = 1e3;
22
+ function S(e) {
23
+ return y + e / x * 2 * 1e3;
24
24
  }
25
- var S = class extends Error {
25
+ var C = class extends Error {
26
26
  reason = "unknown";
27
27
  constructor() {
28
28
  super("audio output made no progress"), this.name = "OutputStalledError";
29
29
  }
30
30
  };
31
- function C(e) {
31
+ function w(e) {
32
32
  let t = 0, n = !1, r = null, i = () => {
33
33
  let n = r;
34
34
  n !== null && (n.cancel(), n.cancel = e(() => {
35
- r === n && (r = null, n.fail(new S()));
36
- }, x(t)));
35
+ r === n && (r = null, n.fail(new C()));
36
+ }, S(t)));
37
37
  };
38
38
  return {
39
39
  handed(e) {
@@ -64,7 +64,14 @@ function C(e) {
64
64
  }
65
65
  };
66
66
  }
67
- function w(e, t) {
67
+ var T = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=", E = [], D = 4;
68
+ function O() {
69
+ for (let e of [...E]) e();
70
+ }
71
+ function k() {
72
+ E.length = 0;
73
+ }
74
+ function A(e, t) {
68
75
  return new Promise((n) => {
69
76
  if (t.aborted) {
70
77
  n();
@@ -79,183 +86,193 @@ function w(e, t) {
79
86
  t.addEventListener("abort", i, { once: !0 });
80
87
  });
81
88
  }
82
- function T(h) {
83
- let g = h.fetchImpl ?? ((...e) => fetch(...e)), v = h.now ?? (() => Date.now()), y = h.setTimer ?? ((e, t) => {
89
+ function j(g) {
90
+ let _ = g.fetchImpl ?? ((...e) => fetch(...e)), y = g.now ?? (() => Date.now()), b = g.setTimer ?? ((e, t) => {
84
91
  let n = setTimeout(e, t);
85
92
  return () => clearTimeout(n);
86
- }), b = null, x = null, T = 0, E = null, D = null, O = null, k = 0, A = null, j = null, M = () => {
93
+ }), x = null, S = null, O = null, k = !1, j = () => {
94
+ if (!(k || typeof Audio != "function")) try {
95
+ let e = O ?? new Audio();
96
+ if (O = e, x === e) return;
97
+ e.muted = !0, e.src = T, Promise.resolve(e.play()).then(() => {
98
+ O === e && x !== e && e.pause();
99
+ }, () => {});
100
+ } catch {}
101
+ };
102
+ E.push(j), E.length > D && E.shift();
103
+ let M = 0, N = null, P = null, F = null, I = 0, L = null, R = null, z = () => {
87
104
  try {
88
- return h.getHeaders?.() ?? {};
105
+ return g.getHeaders?.() ?? {};
89
106
  } catch {
90
107
  return {};
91
108
  }
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();
109
+ }, B = () => y() >= I ? !1 : R === null ? !0 : p(z()) === R, V = /* @__PURE__ */ new Set(), H = () => {
110
+ for (let e of [...V]) e();
111
+ }, U = () => {
112
+ L !== null && clearTimeout(L), L = null;
113
+ }, W = (e) => {
114
+ U(), L = setTimeout(() => {
115
+ L = null;
116
+ let e = I - y();
100
117
  if (e > 0) {
101
- L(e);
118
+ W(e);
102
119
  return;
103
120
  }
104
- F();
121
+ H();
105
122
  }, e);
106
- }, R = (e, t) => {
107
- k = v() + e, j = t, L(e), F();
108
- }, z = () => {
109
- x &&= (URL.revokeObjectURL(x), null);
110
- }, B = () => {
111
- if (b) {
112
- b.onended = null, b.onerror = null;
123
+ }, G = (e, t) => {
124
+ I = y() + e, R = t, W(e), H();
125
+ }, K = () => {
126
+ S &&= (URL.revokeObjectURL(S), null);
127
+ }, q = () => {
128
+ if (x) {
129
+ x.onended = null, x.onerror = null;
113
130
  try {
114
- b.pause();
131
+ x.pause();
115
132
  } catch {}
116
- b = null;
133
+ x = null;
117
134
  }
118
- z();
135
+ K();
119
136
  };
120
- async function V(e, n, s) {
121
- let c = i(e, r(n.lang) ?? n.lang, t, n.voice?.name ?? null, n.rate), l = h.getHeaders?.() ?? {}, u;
122
- for (let e = 1; u = await g(h.endpoint, {
137
+ async function J(e, t, r) {
138
+ let c = a(e, i(t.lang) ?? t.lang, n, t.voice?.name ?? null, t.rate), l = g.getHeaders?.() ?? {}, u;
139
+ for (let e = 1; u = await _(g.endpoint, {
123
140
  method: "POST",
124
- signal: s,
141
+ signal: r,
125
142
  headers: {
126
143
  "content-type": "application/json",
127
144
  ...l
128
145
  },
129
146
  body: JSON.stringify(c)
130
147
  }), !u.ok; e += 1) {
131
- if (!(p.has(u.status) && e < m && !s.aborted)) {
148
+ if (!(m.has(u.status) && e < h && !r.aborted)) {
132
149
  let e = /* @__PURE__ */ Error(`hosted tts ${u.status}`);
133
- throw e.reason = a(u.status), e.headers = f(l), e;
150
+ throw e.reason = o(u.status), e.headers = p(l), e;
134
151
  }
135
- await w(_(u.headers.get("retry-after"), e), s);
152
+ await A(v(u.headers.get("retry-after"), e), r);
136
153
  }
137
- let d = u.headers.get("content-type"), v = o(d, d?.includes("json") ? await u.json() : await u.arrayBuffer());
138
- if (!v) {
154
+ let d = u.headers.get("content-type"), f = s(d, d?.includes("json") ? await u.json() : await u.arrayBuffer());
155
+ if (!f) {
139
156
  let e = /* @__PURE__ */ Error("hosted tts returned no audio");
140
157
  throw e.reason = "unknown", e;
141
158
  }
142
- return v;
159
+ return f;
143
160
  }
144
- function H(t, n, r) {
145
- return new Promise((i, a) => {
146
- if (n !== T) {
147
- i();
161
+ function Y(n, r, i) {
162
+ return new Promise((a, o) => {
163
+ if (r !== M) {
164
+ a();
148
165
  return;
149
166
  }
150
- let o = new Blob([t.slice().buffer], { type: "audio/mpeg" });
151
- z(), x = URL.createObjectURL(o);
152
- let s = new Audio(x);
153
- e(s), b = s, s.onended = () => {
154
- n === T && i();
155
- }, s.onerror = () => {
156
- if (n !== T) {
157
- i();
167
+ let s = new Blob([n.slice().buffer], { type: "audio/mpeg" });
168
+ K(), S = URL.createObjectURL(s);
169
+ let c;
170
+ e() ? (c = new Audio(S), t(c)) : (c = O ?? new Audio(), O = c, c.muted = !1, c.src = S), x = c, c.onended = () => {
171
+ r === M && a();
172
+ }, c.onerror = () => {
173
+ if (r !== M) {
174
+ a();
158
175
  return;
159
176
  }
160
177
  let e = /* @__PURE__ */ Error("audio playback failed");
161
- e.reason = "unknown", a(e);
162
- }, s.play().then(() => {
163
- n === T && r?.();
178
+ e.reason = "unknown", o(e);
179
+ }, c.play().then(() => {
180
+ c === O && !c.muted && (k = !0), r === M && i?.();
164
181
  }, (e) => {
165
- if (n !== T) {
166
- i();
182
+ if (r !== M) {
183
+ a();
167
184
  return;
168
185
  }
169
- let t = /* @__PURE__ */ Error("audio play rejected"), r = e?.name === "NotAllowedError";
170
- t.reason = r ? "not-allowed" : "unknown", t.transient = r, a(t);
186
+ let t = /* @__PURE__ */ Error("audio play rejected"), n = e?.name === "NotAllowedError";
187
+ t.reason = n ? "not-allowed" : "unknown", t.transient = n, o(t);
171
188
  });
172
189
  });
173
190
  }
174
191
  return {
175
192
  id: "hosted-tts",
176
193
  isSupported() {
177
- return N() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
194
+ return B() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
178
195
  },
179
196
  listVoices() {
180
- return N() ? [] : [...n];
197
+ return B() ? [] : [...r];
181
198
  },
182
199
  subscribeVoices(e) {
183
- return P.add(e), () => {
184
- P.delete(e);
200
+ return V.add(e), () => {
201
+ V.delete(e);
185
202
  };
186
203
  },
187
204
  speak(e, t, n = {}) {
188
- let r = l(e);
205
+ let r = u(e);
189
206
  if (r.length === 0) {
190
207
  n.onEnd?.();
191
208
  return;
192
209
  }
193
- T += 1;
194
- let i = T;
195
- B(), D?.cancel(), O?.stop();
210
+ M += 1;
211
+ let i = M;
212
+ q(), P?.cancel(), F?.stop(), j();
196
213
  let a = new AbortController();
197
- E = a;
198
- let o = !1, f = () => {
214
+ N = a;
215
+ let o = !1, s = () => {
199
216
  o || (o = !0, n.onStart?.());
200
217
  }, p = (e) => {
201
- let n = V(e, t, a.signal);
218
+ let n = J(e, t, a.signal);
202
219
  return n.catch(() => {}), n;
203
- }, m = t.audioOutput ? C(y) : null, h = t.audioOutput ? u(t.audioOutput, {
204
- play: (e) => H(e, i, f),
205
- isCurrent: () => i === T,
220
+ }, m = t.audioOutput ? w(b) : null, h = t.audioOutput ? d(t.audioOutput, {
221
+ play: (e) => Y(e, i, s),
222
+ isCurrent: () => i === M,
206
223
  onChunkStart: (e) => {
207
224
  m?.progress(), n.onChunkStart?.(e);
208
225
  },
209
- onStart: f
226
+ onStart: s
210
227
  }) : null;
211
- D = h, O = m;
228
+ P = h, F = m;
212
229
  let g = (e) => m ? m.guard(e) : e, _ = () => {
213
- h && (m?.stop(), O === m && (O = null), D === h && (D = null));
230
+ h && (m?.stop(), F === m && (F = null), P === h && (P = null));
214
231
  };
215
232
  (async () => {
216
233
  try {
217
234
  let e = p(r[0]);
218
235
  for (let t = 0; t < r.length; t += 1) {
219
- if (i !== T) return;
236
+ if (i !== M) return;
220
237
  let a = await e;
221
- if (i !== T) return;
238
+ if (i !== M) return;
222
239
  let o = r[t + 1];
223
240
  if (o !== void 0 && (e = p(o)), h) {
224
241
  m?.handed(a.byteLength), await g(h.write(a));
225
242
  continue;
226
243
  }
227
- n.onChunkStart?.(t), await H(a, i, f);
244
+ n.onChunkStart?.(t), await Y(a, i, s);
228
245
  }
229
246
  if (h) {
230
247
  let e = await g(h.drain());
231
- if (i !== T) return;
248
+ if (i !== M) return;
232
249
  if (!e) throw Object.assign(/* @__PURE__ */ Error("audio output did not finish"), { reason: "unknown" });
233
250
  }
234
- i === T && (_(), n.onEnd?.());
251
+ i === M && (_(), n.onEnd?.());
235
252
  } catch (e) {
236
- if (a.abort(), i !== T) return;
253
+ if (a.abort(), i !== M) return;
237
254
  let t = null;
238
255
  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);
256
+ let n = e instanceof C ? null : await g(h.drain()).then(() => null, (e) => e);
257
+ if (i !== M) return;
258
+ h.cancel(), f(e) ? t = e : f(n) && (t = n);
242
259
  }
243
- if (T += 1, B(), _(), t) {
260
+ if (M += 1, q(), _(), t) {
244
261
  n.onError?.("output-not-cleared");
245
262
  return;
246
263
  }
247
264
  let r = e?.reason;
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");
265
+ l(r, e?.transient === !0) && G(c(r), r === "not-allowed" ? e.headers ?? null : null), n.onError?.(r === "not-allowed" || r === "unsupported" || r === "cancelled" ? r : "unknown");
249
266
  }
250
267
  })();
251
268
  },
252
269
  headersChanged() {
253
- v() >= k || j === null || N() || (k = 0, j = null, I(), F());
270
+ y() >= I || R === null || B() || (I = 0, R = null, U(), H());
254
271
  },
255
272
  cancel() {
256
- T += 1, E?.abort(), E = null, B(), D?.cancel(), D = null, O?.stop(), O = null;
273
+ M += 1, N?.abort(), N = null, q(), P?.cancel(), P = null, F?.stop(), F = null;
257
274
  }
258
275
  };
259
276
  }
260
277
  //#endregion
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 };
278
+ export { x as HOSTED_OUTPUT_MIN_AUDIO_BYTES_PER_SECOND, b as HOSTED_OUTPUT_STALL_AUDIO_RATIO, y as HOSTED_OUTPUT_STALL_BASE_MS, T as SILENT_WAV_DATA_URI, j as createHostedTtsEngine, S as hostedOutputStallMs, O as primeHostedPlayback, k as resetHostedPlaybackForTest, v as retryDelayMs };
@@ -25,6 +25,9 @@ function e(e) {
25
25
  onChunkStart: (e) => {
26
26
  t === o && a.onChunkStart?.(e);
27
27
  },
28
+ onDeviceFallback: () => {
29
+ t === o && a.onDeviceFallback?.();
30
+ },
28
31
  onEnd: () => s(),
29
32
  onError: (e) => s(e)
30
33
  });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The replies the Avatar tab asked to hear on the avatar (BOFF-9360): typed
3
+ * questions whose answers the avatar speaks, whatever Chat's auto-read is.
4
+ *
5
+ * ★★ Only the tab writes here. A reply is registered where the chat area
6
+ * creates it for a question sent from the tab, with the conversation and the
7
+ * question it belongs to (critic M2): a reply this module has never seen is
8
+ * spoken by today's rules, so a host without the live avatar, and every reply
9
+ * that was not asked from the tab, behaves exactly as before.
10
+ *
11
+ * ★★ Fail-safe by construction. A reply missing from here (evicted, never
12
+ * registered, released) stays text: the worst a gap can do is leave an answer
13
+ * silent, never speak one that should not be heard.
14
+ *
15
+ * ★ Each entry carries what happened to its speech (`outcome`), so the tab can
16
+ * say it without claiming anything was heard: it records what the audio path
17
+ * did, never what the person heard.
18
+ *
19
+ * ★ Module scope on purpose, like `speechFeed` and `supersededTurns`: the chat
20
+ * area (which registers), the speech provider (which speaks and settles) and
21
+ * the tab (which reads) are components that do not know each other.
22
+ *
23
+ * ★ Bounded: the tab asks one question at a time, so 16 entries is far more
24
+ * than one opening of the tab needs; the oldest is dropped first.
25
+ */
26
+ /** What became of one registered reply's speech. */
27
+ export type ReplySpeechOutcome =
28
+ /** Registered; the reply has not been spoken or refused yet. */
29
+ 'pending'
30
+ /** Being spoken. */
31
+ | 'speaking'
32
+ /** Spoken to its end. */
33
+ | 'spoken'
34
+ /** The avatar would not take it (off, covered, ending, sound blocked, no hosted voice), so it was not read aloud. */
35
+ | 'declined'
36
+ /** It left the avatar part way (an end, a failure), and the rest was stopped, never moved to the speakers (critic M1). */
37
+ | 'cut'
38
+ /** The avatar's audio output refused it (D7 b): what it played before could not be shown to have stopped. */
39
+ | 'not-cleared'
40
+ /** A Stop, a newer reply, a conversation switch or a hidden panel stopped it. */
41
+ | 'stopped'
42
+ /** The voice failed. */
43
+ | 'failed'
44
+ /** Released unspoken: its conversation is no longer the open one, or the tab closed. */
45
+ | 'dropped';
46
+ /** One reply the tab asked to hear on the avatar. */
47
+ export interface ReplySpeechRequest {
48
+ /** The conversation the question landed in (it may have been created by the send). */
49
+ readonly conversationId: string;
50
+ readonly questionId: string;
51
+ readonly replyId: string;
52
+ readonly outcome: ReplySpeechOutcome;
53
+ }
54
+ /** Record that the reply `replyId`, the answer to `questionId` in `conversationId`, should be spoken on the avatar. */
55
+ export declare function requestReplyOnAvatar(request: {
56
+ readonly conversationId: string;
57
+ readonly questionId: string;
58
+ readonly replyId: string;
59
+ }): void;
60
+ /** Is `replyId` registered and not yet spoken or refused? Null and undefined never are. */
61
+ export declare function replyWantsAvatar(replyId: string | null | undefined): boolean;
62
+ /** The registered request for `replyId`, or null. */
63
+ export declare function replySpeechRequest(replyId: string | null | undefined): ReplySpeechRequest | null;
64
+ /**
65
+ * What became of `replyId`'s speech. Only a reply still pending or speaking moves, and `speaking` only from
66
+ * `pending`: the first outcome that ends it is the one kept. Unknown ids are ignored.
67
+ */
68
+ export declare function settleReplyRequest(replyId: string | null | undefined, outcome: Exclude<ReplySpeechOutcome, 'pending'>): void;
69
+ /**
70
+ * ★★ Critic M2: release every pending request whose conversation is not `activeConversationId`, read from the live
71
+ * store by the caller. Never on a timer, and never the open conversation's: a send that creates its conversation
72
+ * registers the reply in it before any switch handler runs, so its own reply survives the switch it caused.
73
+ */
74
+ export declare function releaseReplyRequestsOutside(activeConversationId: string | null): void;
75
+ /** Forget every request: the Avatar tab closed. A reply arriving later follows Chat's own auto-read. */
76
+ export declare function clearReplyRequests(): void;
77
+ /** Is any request still waiting for its reply? */
78
+ export declare function hasPendingReplyRequests(): boolean;
79
+ /** Subscribe to every change; returns an unsubscribe function. For `useSyncExternalStore`. */
80
+ export declare function subscribeReplyRequests(listener: () => void): () => void;
81
+ /** A number that changes on every change: a `useSyncExternalStore` snapshot. */
82
+ export declare function replyRequestsVersion(): number;
83
+ /** Tests only. */
84
+ export declare function resetReplyRequestsForTest(): void;
@@ -0,0 +1,70 @@
1
+ //#region src/shared/assistant/speech/replySpeechRequests.ts
2
+ var e = 16, t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Set(), r = 0;
3
+ function i() {
4
+ r += 1;
5
+ for (let e of [...n]) e();
6
+ }
7
+ var a = new Set([
8
+ "spoken",
9
+ "declined",
10
+ "cut",
11
+ "not-cleared",
12
+ "stopped",
13
+ "failed",
14
+ "dropped"
15
+ ]);
16
+ function o(n) {
17
+ for (t.delete(n.replyId), t.set(n.replyId, {
18
+ conversationId: n.conversationId,
19
+ questionId: n.questionId,
20
+ replyId: n.replyId,
21
+ outcome: "pending"
22
+ }); t.size > e;) {
23
+ let e = t.keys().next();
24
+ if (e.done) break;
25
+ t.delete(e.value);
26
+ }
27
+ i();
28
+ }
29
+ function s(e) {
30
+ return typeof e == "string" && t.get(e)?.outcome === "pending";
31
+ }
32
+ function c(e) {
33
+ return typeof e == "string" ? t.get(e) ?? null : null;
34
+ }
35
+ function l(e, n) {
36
+ if (typeof e != "string") return;
37
+ let r = t.get(e);
38
+ !r || a.has(r.outcome) || n === "speaking" && r.outcome !== "pending" || (t.set(e, {
39
+ ...r,
40
+ outcome: n
41
+ }), i());
42
+ }
43
+ function u(e) {
44
+ let n = !1;
45
+ for (let [r, i] of t) i.outcome !== "pending" || i.conversationId === e || (t.set(r, {
46
+ ...i,
47
+ outcome: "dropped"
48
+ }), n = !0);
49
+ n && i();
50
+ }
51
+ function d() {
52
+ t.size !== 0 && (t.clear(), i());
53
+ }
54
+ function f() {
55
+ for (let e of t.values()) if (e.outcome === "pending") return !0;
56
+ return !1;
57
+ }
58
+ function p(e) {
59
+ return n.add(e), () => {
60
+ n.delete(e);
61
+ };
62
+ }
63
+ function m() {
64
+ return r;
65
+ }
66
+ function h() {
67
+ t.clear(), n.clear(), r = 0;
68
+ }
69
+ //#endregion
70
+ export { d as clearReplyRequests, f as hasPendingReplyRequests, u as releaseReplyRequestsOutside, m as replyRequestsVersion, c as replySpeechRequest, s as replyWantsAvatar, o as requestReplyOnAvatar, h as resetReplyRequestsForTest, l as settleReplyRequest, p as subscribeReplyRequests };
@@ -5,7 +5,8 @@ import { selectVoiceForLocale as n } from "../voice/speechVoices.js";
5
5
  function r(e) {
6
6
  switch (e) {
7
7
  case "cancelled":
8
- case "output-not-cleared": return !1;
8
+ case "output-not-cleared":
9
+ case "output-left": return !1;
9
10
  case "not-allowed":
10
11
  case "unsupported":
11
12
  case "unknown": return !0;
@@ -14,23 +15,26 @@ function r(e) {
14
15
  }
15
16
  function i({ device: i, hosted: a }) {
16
17
  if (!a) return i;
17
- let o = new Set(a.listVoices().map((e) => e.name)), s = (e) => e.source === "hosted" || o.has(e.name), c = (e, t, r) => {
18
+ let o = new Set(a.listVoices().map((e) => e.name)), s = (e) => e.source === "hosted" || o.has(e.name), c = (e, t, r, a = !1) => {
19
+ let o = () => {
20
+ a && r?.onDeviceFallback?.();
21
+ };
18
22
  if (!t.voice || !s(t.voice)) {
19
- i.speak(e, t, r);
23
+ o(), i.speak(e, t, r);
20
24
  return;
21
25
  }
22
26
  if (t.fallback === !1) {
23
27
  r?.onError?.("unsupported");
24
28
  return;
25
29
  }
26
- let a = n(i.listVoices(), t.lang);
27
- if (!a) {
30
+ let c = n(i.listVoices(), t.lang);
31
+ if (!c) {
28
32
  r?.onError?.("unsupported");
29
33
  return;
30
34
  }
31
- i.speak(e, {
35
+ o(), i.speak(e, {
32
36
  ...t,
33
- voice: a
37
+ voice: c
34
38
  }, r);
35
39
  }, l = (t) => a.isSupported() ? t.voice ? o.has(t.voice.name) ? a : i : e(t.lang) ? a : i : i;
36
40
  return {
@@ -67,7 +71,7 @@ function i({ device: i, hosted: a }) {
67
71
  return;
68
72
  }
69
73
  if (!s) {
70
- c(e, n, o);
74
+ c(e, n, o, !0);
71
75
  return;
72
76
  }
73
77
  let a = u >= 0 ? t(e).slice(u + 1) : [];
@@ -80,7 +84,7 @@ function i({ device: i, hosted: a }) {
80
84
  ...o,
81
85
  onStart: void 0,
82
86
  onChunkStart: (e) => o?.onChunkStart?.(l + e)
83
- });
87
+ }, !0);
84
88
  }
85
89
  });
86
90
  },
@@ -27,6 +27,19 @@ export interface SpeechSpeakOptions {
27
27
  * stop, whether the reply is one `speak()` or a queue that grows by appends.
28
28
  */
29
29
  audioOutput?: SpeechAudioOutput;
30
+ /**
31
+ * ★★ BOFF-9360 (critic M1): heard on the avatar, or nowhere. Set by the
32
+ * speech provider for a reply asked from the Avatar tab while Chat's
33
+ * auto-read is off, and read only by the live avatar's engine decorator
34
+ * (`wouldSpeakOnAvatar`); every other engine ignores it.
35
+ *
36
+ * Such a reply is never moved to the speakers: when its avatar output
37
+ * leaves it (the avatar is switched off, the tab closes, the session ends)
38
+ * or fails, part way or before its first piece, the rest is STOPPED and the
39
+ * reply ends with `onError('output-left')`. A device-voice fallback is off
40
+ * for it too. A refusal (`output-not-cleared`) is reported as it always is.
41
+ */
42
+ avatarOnly?: boolean;
30
43
  }
31
44
  /**
32
45
  * Somewhere other than the speakers to play a reply's hosted audio
@@ -138,8 +151,13 @@ export interface SpeechOutputRefusal {
138
151
  * played before could not be shown to have stopped. Nothing re-speaks it, the
139
152
  * device voice included, and its text stays where it is. Only a reply with an
140
153
  * `audioOutput` can end this way.
154
+ *
155
+ * `output-left` ends an `avatarOnly` reply (BOFF-9360) that stopped going to
156
+ * the avatar, or was never given to it: the rest of it was stopped, never
157
+ * played on the speakers. Only the live avatar's engine decorator reports it,
158
+ * and nothing re-speaks it.
141
159
  */
142
- export type SpeechErrorReason = 'cancelled' | 'not-allowed' | 'unsupported' | 'unknown' | 'output-not-cleared';
160
+ export type SpeechErrorReason = 'cancelled' | 'not-allowed' | 'unsupported' | 'unknown' | 'output-not-cleared' | 'output-left';
143
161
  export interface SpeechSpeakHandlers {
144
162
  /**
145
163
  * Audio has actually begun playing — not merely been requested.
@@ -173,6 +191,17 @@ export interface SpeechSpeakHandlers {
173
191
  /** The whole queue finished on its own. */
174
192
  onEnd?: () => void;
175
193
  onError?: (reason: SpeechErrorReason) => void;
194
+ /**
195
+ * ★ BOFF-9408: the hosted voice could not play this reply, so the rest of it
196
+ * (or all of it) is about to be spoken on the DEVICE voice.
197
+ *
198
+ * The routing engine calls it once, just before the device voice starts, so
199
+ * the caller can say so on screen instead of the user hearing a different
200
+ * voice with no explanation. The fallback changes nothing the user chose:
201
+ * their saved voice is untouched and the next reply tries the hosted voice
202
+ * again. Optional; an engine with no fallback never calls it.
203
+ */
204
+ onDeviceFallback?: () => void;
176
205
  }
177
206
  export interface SpeechEngine {
178
207
  /** Stable id, for logging and for telling engines apart in a future swap. */
@@ -187,4 +216,13 @@ export interface SpeechEngine {
187
216
  speak(chunks: readonly string[], options: SpeechSpeakOptions, handlers?: SpeechSpeakHandlers): void;
188
217
  /** Stop immediately and drop anything queued. Safe to call when idle. */
189
218
  cancel(): void;
219
+ /**
220
+ * ★ BOFF-9360: would `speak` with these options play the reply on the live
221
+ * avatar right now? The decorated live-avatar engine only (it asks the same
222
+ * per-reply route its `speak` asks, with the same avatar voice, so the two
223
+ * cannot disagree); every other engine omits it, which callers read as
224
+ * "no". Asked by the speech provider before an `avatarOnly` reply starts,
225
+ * and only then.
226
+ */
227
+ wouldSpeakOnAvatar?(options: SpeechSpeakOptions): boolean;
190
228
  }