@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.
- package/dist/shared/assistant/avatar/live/AvatarFace.d.ts +13 -0
- package/dist/shared/assistant/avatar/live/AvatarFace.js +46 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +2 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +172 -145
- package/dist/shared/assistant/avatar/live/avatarFaces.d.ts +31 -0
- package/dist/shared/assistant/avatar/live/avatarFaces.js +33 -0
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +12 -0
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +142 -75
- package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +5 -0
- package/dist/shared/assistant/avatar/live/liveAvatarController.js +3 -1
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +14 -0
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -2
- package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +42 -0
- package/dist/shared/assistant/avatar/useAvatarCopy.js +113 -75
- package/dist/shared/assistant/i18nManifest.d.ts +29 -0
- package/dist/shared/assistant/i18nManifest.js +29 -0
- package/dist/shared/assistant/index.d.ts +8 -0
- package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +6 -0
- package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -101
- package/dist/shared/assistant/speech/notifyCancellationSpeechEngine.js +3 -0
- package/dist/shared/assistant/speech/replySpeechRequests.d.ts +84 -0
- package/dist/shared/assistant/speech/replySpeechRequests.js +70 -0
- package/dist/shared/assistant/speech/routingSpeechEngine.js +13 -9
- package/dist/shared/assistant/speech/types.d.ts +39 -1
- package/dist/shared/assistant/ui/AIAssistantPanel.js +128 -119
- package/dist/shared/assistant/ui/AssistantChatArea.js +1572 -1430
- package/dist/shared/assistant/ui/AssistantMessageItem.js +110 -100
- package/dist/shared/assistant/ui/AssistantSpeechProvider.d.ts +10 -0
- package/dist/shared/assistant/ui/AssistantSpeechProvider.js +286 -225
- package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +40 -0
- package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.d.ts +6 -0
- package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +99 -52
- package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.d.ts +8 -0
- package/dist/shared/assistant/ui/avatar/AvatarTabAskBox.js +87 -0
- package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.d.ts +13 -0
- package/dist/shared/assistant/ui/avatar/AvatarTabLastExchange.js +97 -0
- package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +104 -0
- package/dist/shared/assistant/ui/avatar/avatarAskGate.js +39 -0
- package/dist/shared/assistant/ui/avatar/avatarTabDraft.d.ts +17 -0
- package/dist/shared/assistant/ui/avatar/avatarTabDraft.js +22 -0
- package/dist/shared/assistant/ui/avatar/avatarTabExchange.d.ts +47 -0
- package/dist/shared/assistant/ui/avatar/avatarTabExchange.js +50 -0
- package/dist/shared/assistant/ui/panelOverlays.d.ts +8 -1
- package/dist/shared/assistant/ui/panelOverlays.js +1 -1
- package/dist/shared/assistant/ui/useVoiceSession.js +222 -227
- package/dist/shared/assistant/voice/handsfree/playbackReference.d.ts +15 -4
- package/dist/shared/assistant/voice/handsfree/playbackReference.js +9 -6
- package/dist/shared-assistant.js +270 -262
- package/package.json +1 -1
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { HOSTED_TTS_TIER as
|
|
3
|
-
import { buildRequest as
|
|
4
|
-
import { createPieceSink as
|
|
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
|
|
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
|
|
11
|
+
var m = new Set([
|
|
12
12
|
429,
|
|
13
13
|
502,
|
|
14
14
|
503,
|
|
15
15
|
504
|
|
16
|
-
]),
|
|
17
|
-
function
|
|
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 :
|
|
19
|
+
return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : g * 2 ** (t - 1), _);
|
|
20
20
|
}
|
|
21
|
-
var
|
|
22
|
-
function
|
|
23
|
-
return
|
|
21
|
+
var y = 6e4, b = 2, x = 1e3;
|
|
22
|
+
function S(e) {
|
|
23
|
+
return y + e / x * 2 * 1e3;
|
|
24
24
|
}
|
|
25
|
-
var
|
|
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
|
|
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
|
|
36
|
-
},
|
|
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
|
-
|
|
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
|
|
83
|
-
let
|
|
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
|
-
}),
|
|
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
|
|
105
|
+
return g.getHeaders?.() ?? {};
|
|
89
106
|
} catch {
|
|
90
107
|
return {};
|
|
91
108
|
}
|
|
92
|
-
},
|
|
93
|
-
for (let e of [...
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let e =
|
|
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
|
-
|
|
118
|
+
W(e);
|
|
102
119
|
return;
|
|
103
120
|
}
|
|
104
|
-
|
|
121
|
+
H();
|
|
105
122
|
}, e);
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
if (
|
|
112
|
-
|
|
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
|
-
|
|
131
|
+
x.pause();
|
|
115
132
|
} catch {}
|
|
116
|
-
|
|
133
|
+
x = null;
|
|
117
134
|
}
|
|
118
|
-
|
|
135
|
+
K();
|
|
119
136
|
};
|
|
120
|
-
async function
|
|
121
|
-
let c =
|
|
122
|
-
for (let e = 1; u = await g
|
|
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:
|
|
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 (!(
|
|
148
|
+
if (!(m.has(u.status) && e < h && !r.aborted)) {
|
|
132
149
|
let e = /* @__PURE__ */ Error(`hosted tts ${u.status}`);
|
|
133
|
-
throw e.reason =
|
|
150
|
+
throw e.reason = o(u.status), e.headers = p(l), e;
|
|
134
151
|
}
|
|
135
|
-
await
|
|
152
|
+
await A(v(u.headers.get("retry-after"), e), r);
|
|
136
153
|
}
|
|
137
|
-
let d = u.headers.get("content-type"),
|
|
138
|
-
if (!
|
|
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
|
|
159
|
+
return f;
|
|
143
160
|
}
|
|
144
|
-
function
|
|
145
|
-
return new Promise((
|
|
146
|
-
if (
|
|
147
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
let
|
|
153
|
-
e(
|
|
154
|
-
|
|
155
|
-
},
|
|
156
|
-
if (
|
|
157
|
-
|
|
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",
|
|
162
|
-
},
|
|
163
|
-
|
|
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 (
|
|
166
|
-
|
|
182
|
+
if (r !== M) {
|
|
183
|
+
a();
|
|
167
184
|
return;
|
|
168
185
|
}
|
|
169
|
-
let t = /* @__PURE__ */ Error("audio play rejected"),
|
|
170
|
-
t.reason =
|
|
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
|
|
194
|
+
return B() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
|
|
178
195
|
},
|
|
179
196
|
listVoices() {
|
|
180
|
-
return
|
|
197
|
+
return B() ? [] : [...r];
|
|
181
198
|
},
|
|
182
199
|
subscribeVoices(e) {
|
|
183
|
-
return
|
|
184
|
-
|
|
200
|
+
return V.add(e), () => {
|
|
201
|
+
V.delete(e);
|
|
185
202
|
};
|
|
186
203
|
},
|
|
187
204
|
speak(e, t, n = {}) {
|
|
188
|
-
let r =
|
|
205
|
+
let r = u(e);
|
|
189
206
|
if (r.length === 0) {
|
|
190
207
|
n.onEnd?.();
|
|
191
208
|
return;
|
|
192
209
|
}
|
|
193
|
-
|
|
194
|
-
let i =
|
|
195
|
-
|
|
210
|
+
M += 1;
|
|
211
|
+
let i = M;
|
|
212
|
+
q(), P?.cancel(), F?.stop(), j();
|
|
196
213
|
let a = new AbortController();
|
|
197
|
-
|
|
198
|
-
let o = !1,
|
|
214
|
+
N = a;
|
|
215
|
+
let o = !1, s = () => {
|
|
199
216
|
o || (o = !0, n.onStart?.());
|
|
200
217
|
}, p = (e) => {
|
|
201
|
-
let n =
|
|
218
|
+
let n = J(e, t, a.signal);
|
|
202
219
|
return n.catch(() => {}), n;
|
|
203
|
-
}, m = t.audioOutput ?
|
|
204
|
-
play: (e) =>
|
|
205
|
-
isCurrent: () => i ===
|
|
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:
|
|
226
|
+
onStart: s
|
|
210
227
|
}) : null;
|
|
211
|
-
|
|
228
|
+
P = h, F = m;
|
|
212
229
|
let g = (e) => m ? m.guard(e) : e, _ = () => {
|
|
213
|
-
h && (m?.stop(),
|
|
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 !==
|
|
236
|
+
if (i !== M) return;
|
|
220
237
|
let a = await e;
|
|
221
|
-
if (i !==
|
|
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
|
|
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 !==
|
|
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 ===
|
|
251
|
+
i === M && (_(), n.onEnd?.());
|
|
235
252
|
} catch (e) {
|
|
236
|
-
if (a.abort(), i !==
|
|
253
|
+
if (a.abort(), i !== M) return;
|
|
237
254
|
let t = null;
|
|
238
255
|
if (h) {
|
|
239
|
-
let n = e instanceof
|
|
240
|
-
if (i !==
|
|
241
|
-
h.cancel(),
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
y() >= I || R === null || B() || (I = 0, R = null, U(), H());
|
|
254
271
|
},
|
|
255
272
|
cancel() {
|
|
256
|
-
|
|
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 {
|
|
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 };
|
|
@@ -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":
|
|
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
|
|
27
|
-
if (!
|
|
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:
|
|
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
|
}
|