@burdenoff/fe-libs 2026.1013.13 → 2026.1013.14
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/LiveAvatarStage.d.ts +15 -0
- package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +139 -0
- package/dist/shared/assistant/avatar/live/avatarAudioDecode.d.ts +40 -0
- package/dist/shared/assistant/avatar/live/avatarAudioDecode.js +72 -0
- package/dist/shared/assistant/avatar/live/avatarAudioErrors.d.ts +25 -0
- package/dist/shared/assistant/avatar/live/avatarAudioErrors.js +27 -0
- package/dist/shared/assistant/avatar/live/avatarAudioMeter.d.ts +37 -0
- package/dist/shared/assistant/avatar/live/avatarAudioMeter.js +62 -0
- package/dist/shared/assistant/avatar/live/avatarMp3Inspect.d.ts +30 -0
- package/dist/shared/assistant/avatar/live/avatarMp3Inspect.js +168 -0
- package/dist/shared/assistant/avatar/live/avatarPcm16.d.ts +21 -0
- package/dist/shared/assistant/avatar/live/avatarPcm16.js +54 -0
- package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.d.ts +181 -0
- package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.js +221 -0
- package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +66 -0
- package/dist/shared/assistant/avatar/live/avatarPorts.js +44 -0
- package/dist/shared/assistant/avatar/live/avatarProtocol.d.ts +22 -0
- package/dist/shared/assistant/avatar/live/avatarProtocol.js +27 -0
- package/dist/shared/assistant/avatar/live/avatarReplyOutputs.d.ts +170 -0
- package/dist/shared/assistant/avatar/live/avatarReplyOutputs.js +525 -0
- package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +96 -0
- package/dist/shared/assistant/avatar/live/avatarSessionClient.js +261 -0
- package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +270 -0
- package/dist/shared/assistant/avatar/live/avatarTransport.js +333 -0
- package/dist/shared/assistant/avatar/live/avatarTransportPort.d.ts +17 -0
- package/dist/shared/assistant/avatar/live/avatarTransportPort.js +27 -0
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +44 -0
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +124 -0
- package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +133 -0
- package/dist/shared/assistant/avatar/live/liveAvatarController.js +488 -0
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +4 -0
- package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +14 -0
- package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.d.ts +6 -0
- package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +86 -0
- package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +2 -0
- package/dist/shared/assistant/avatar/useAvatarCopy.js +1 -0
- package/dist/shared/assistant/i18nManifest.d.ts +1 -0
- package/dist/shared/assistant/i18nManifest.js +1 -0
- package/dist/shared/assistant/index.d.ts +17 -0
- package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +37 -0
- package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -63
- package/dist/shared/assistant/speech/types.d.ts +14 -2
- package/dist/shared-assistant.js +250 -233
- package/package.json +1 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { AVATAR_AUDIO_STREAM_TOPIC as e, AVATAR_RPC_CLEAR_BUFFER as t, AVATAR_RPC_PLAYBACK_FINISHED as n, AVATAR_RPC_PLAYBACK_STARTED as r, avatarPcmDurationSec as i, avatarSegmentAttributes as a, parseAvatarPlaybackFinished as o } from "./avatarProtocol.js";
|
|
2
|
+
import { AvatarRpcRejectedError as s } from "./avatarPorts.js";
|
|
3
|
+
//#region src/shared/assistant/avatar/live/avatarTransport.ts
|
|
4
|
+
var c = class extends Error {
|
|
5
|
+
missing;
|
|
6
|
+
constructor(e) {
|
|
7
|
+
super(`avatar not ready: ${e}`), this.missing = e, this.name = "AvatarNotReadyError";
|
|
8
|
+
}
|
|
9
|
+
}, l = class extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("a previous segment is still open or not yet reported finished"), this.name = "AvatarSegmentBusyError";
|
|
12
|
+
}
|
|
13
|
+
}, u = class extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("segment ended"), this.name = "AvatarSegmentEndedError";
|
|
16
|
+
}
|
|
17
|
+
}, d = class extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("transport closed"), this.name = "AvatarTransportClosedError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function f(e) {
|
|
23
|
+
let t = typeof e == "object" && e && "code" in e ? e.code : null;
|
|
24
|
+
return t === 1400 ? "unsupported" : t === 1502 ? "timeout" : t === 1503 ? "recipient-disconnected" : "error";
|
|
25
|
+
}
|
|
26
|
+
var p = class {
|
|
27
|
+
id;
|
|
28
|
+
writer;
|
|
29
|
+
owner;
|
|
30
|
+
state = "open";
|
|
31
|
+
pushedBytes = 0;
|
|
32
|
+
constructor(e, t, n) {
|
|
33
|
+
this.id = e, this.writer = t, this.owner = n;
|
|
34
|
+
}
|
|
35
|
+
get pushedSec() {
|
|
36
|
+
return i(this.pushedBytes);
|
|
37
|
+
}
|
|
38
|
+
async write(e) {
|
|
39
|
+
if (this.state !== "open" || e.byteLength !== 0 && (this.pushedBytes += e.byteLength, await this.writer.write(e), this.state !== "open")) throw new u();
|
|
40
|
+
}
|
|
41
|
+
async close() {
|
|
42
|
+
this.state === "open" && (this.state = "closed", await this.writer.close(), this.owner.segmentClosed(this));
|
|
43
|
+
}
|
|
44
|
+
discard() {
|
|
45
|
+
this.end(), this.owner.segmentDiscarded(this);
|
|
46
|
+
}
|
|
47
|
+
end() {
|
|
48
|
+
if (this.state === "ended") return;
|
|
49
|
+
let e = this.state === "open";
|
|
50
|
+
this.state = "ended", e && this.writer.close().catch(() => void 0);
|
|
51
|
+
}
|
|
52
|
+
}, m = class i {
|
|
53
|
+
options;
|
|
54
|
+
static nextInstanceId = 1;
|
|
55
|
+
instanceId = i.nextInstanceId++;
|
|
56
|
+
state = "idle";
|
|
57
|
+
listeners = /* @__PURE__ */ new Set();
|
|
58
|
+
unsubscribes = [];
|
|
59
|
+
nextSegmentId = 1;
|
|
60
|
+
current = null;
|
|
61
|
+
awaiting = [];
|
|
62
|
+
clearInFlight = null;
|
|
63
|
+
clearSettle = null;
|
|
64
|
+
avatarPresent = !1;
|
|
65
|
+
videoPresent = !1;
|
|
66
|
+
degraded = !1;
|
|
67
|
+
audioElement = null;
|
|
68
|
+
disposed = !1;
|
|
69
|
+
rejectReady = null;
|
|
70
|
+
mediaReady = !1;
|
|
71
|
+
resolveMediaReady = null;
|
|
72
|
+
readinessStatus = null;
|
|
73
|
+
wakeProbe = null;
|
|
74
|
+
strayFinishDeadline = null;
|
|
75
|
+
strayAnswers = 0;
|
|
76
|
+
owedFinishes = 0;
|
|
77
|
+
avatarDepartures = 0;
|
|
78
|
+
constructor(e) {
|
|
79
|
+
this.options = e;
|
|
80
|
+
}
|
|
81
|
+
get transportState() {
|
|
82
|
+
return this.state;
|
|
83
|
+
}
|
|
84
|
+
get readiness() {
|
|
85
|
+
return this.readinessStatus;
|
|
86
|
+
}
|
|
87
|
+
get audioBlocked() {
|
|
88
|
+
return this.state === "ready" && !this.options.room.canPlaybackAudio();
|
|
89
|
+
}
|
|
90
|
+
subscribe(e) {
|
|
91
|
+
return this.listeners.add(e), () => {
|
|
92
|
+
this.listeners.delete(e);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
emit(e) {
|
|
96
|
+
for (let t of [...this.listeners]) try {
|
|
97
|
+
t(e);
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
async resumeAudio() {
|
|
101
|
+
await this.options.room.startAudio();
|
|
102
|
+
}
|
|
103
|
+
setOutputMuted(e) {
|
|
104
|
+
this.audioElement && (this.audioElement.muted = e);
|
|
105
|
+
}
|
|
106
|
+
async connect(e) {
|
|
107
|
+
if (this.state !== "idle") throw Error("transport already used");
|
|
108
|
+
this.state = "connecting";
|
|
109
|
+
let { room: t, avatarIdentity: i } = this.options;
|
|
110
|
+
this.audioElement = e.audioElement ?? null, t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
|
|
111
|
+
e === i && (this.avatarPresent = !0, this.checkReady());
|
|
112
|
+
}), t.onParticipantDisconnected((e) => {
|
|
113
|
+
e === i && (this.avatarPresent = !1, this.videoPresent = !1, this.avatarDepartures += 1, this.owedFinishes = 0, this.endAllSegments(), this.emit({ type: "avatarLeft" }));
|
|
114
|
+
}), t.onTrackSubscribed((t) => {
|
|
115
|
+
t.identity === i && (t.kind === "video" ? (e.videoElement && t.attach(e.videoElement), e.onAvatarVideo?.(t), this.videoPresent = !0, this.checkReady()) : (e.audioElement && t.attach(e.audioElement), e.onAvatarAudio?.(t)));
|
|
116
|
+
}), t.onDisconnected(() => {
|
|
117
|
+
this.endAllSegments(), this.state !== "closed" && (this.state = "closed"), this.emit({ type: "disconnected" });
|
|
118
|
+
}), t.onReconnecting(() => {
|
|
119
|
+
this.degraded = !0, this.emit({ type: "reconnecting" });
|
|
120
|
+
}), t.onReconnected(() => {
|
|
121
|
+
this.degraded = !1, this.emit({ type: "reconnected" });
|
|
122
|
+
}), t.onAudioPlaybackChanged((e) => this.emit({
|
|
123
|
+
type: "audioPlayback",
|
|
124
|
+
canPlay: e
|
|
125
|
+
})));
|
|
126
|
+
try {
|
|
127
|
+
await t.connect(e.url, e.token);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
throw await this.teardown(), this.disposed ? new d() : e;
|
|
130
|
+
}
|
|
131
|
+
if (this.disposed) throw await t.disconnect().catch(() => void 0), new d();
|
|
132
|
+
this.avatarPresent = t.hasParticipant(i), this.videoPresent = t.hasVideoTrack(i);
|
|
133
|
+
let a;
|
|
134
|
+
try {
|
|
135
|
+
if (await this.waitMediaReady(), a = await this.probeReadiness(), this.disposed) throw new d();
|
|
136
|
+
if (this.state !== "connecting") throw new c("connection");
|
|
137
|
+
} catch (e) {
|
|
138
|
+
throw await this.teardown(), this.disposed ? new d() : e;
|
|
139
|
+
}
|
|
140
|
+
if (this.state = "ready", this.emit(a), this.disposed || this.state !== "ready") throw await this.teardown(), new d();
|
|
141
|
+
this.emit({ type: "ready" });
|
|
142
|
+
}
|
|
143
|
+
armStrayWindow(e) {
|
|
144
|
+
this.strayAnswers += 1, this.strayFinishDeadline = this.options.scheduler.now() + e;
|
|
145
|
+
}
|
|
146
|
+
expectLateAnswer(e, t, n, r) {
|
|
147
|
+
!e || t === "rejected" || t === "unsupported" || (t === "ok" || n ? this.armStrayWindow(r) : this.owedFinishes += 1);
|
|
148
|
+
}
|
|
149
|
+
checkReady() {
|
|
150
|
+
this.state === "connecting" && !this.mediaReady && this.avatarPresent && this.videoPresent && (this.mediaReady = !0, this.resolveMediaReady?.());
|
|
151
|
+
}
|
|
152
|
+
waitMediaReady() {
|
|
153
|
+
return this.checkReady(), this.mediaReady ? Promise.resolve() : new Promise((e, t) => {
|
|
154
|
+
let n = this.options.scheduler.setTimeout(() => {
|
|
155
|
+
r(), this.resolveMediaReady = null, this.teardown().finally(() => {
|
|
156
|
+
t(new c(this.avatarPresent ? "video" : "participant"));
|
|
157
|
+
});
|
|
158
|
+
}, this.options.readyTimeoutMs ?? 8e3), r = this.subscribe((e) => {
|
|
159
|
+
e.type === "disconnected" && (n(), r(), this.resolveMediaReady = null, t(new c("connection")));
|
|
160
|
+
});
|
|
161
|
+
this.resolveMediaReady = () => {
|
|
162
|
+
n(), r(), this.resolveMediaReady = null, e();
|
|
163
|
+
}, this.rejectReady = (e) => {
|
|
164
|
+
n(), r(), this.resolveMediaReady = null, t(e);
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async probeReadiness() {
|
|
169
|
+
let e = this.options.readinessProbe, { scheduler: n, room: r, avatarIdentity: i } = this.options;
|
|
170
|
+
if (e === !1) return this.readinessStatus = "skipped", {
|
|
171
|
+
type: "readiness",
|
|
172
|
+
status: "skipped",
|
|
173
|
+
attempts: 0,
|
|
174
|
+
ms: 0,
|
|
175
|
+
lastReason: null
|
|
176
|
+
};
|
|
177
|
+
let a = e?.timeoutMs ?? 6e3, o = e?.rpcTimeoutMs ?? 1500, s = e?.maxBackoffMs ?? 1e3, l = this.options.clearTimeoutMs ?? 2e3, u = e?.initialBackoffMs ?? 150, p = n.now(), m = 0, h = null, g = () => {
|
|
178
|
+
if (this.disposed) throw new d();
|
|
179
|
+
if (this.state !== "connecting") throw new c("connection");
|
|
180
|
+
if (!this.avatarPresent) throw new c("participant");
|
|
181
|
+
}, _ = (e) => (this.readinessStatus = e, {
|
|
182
|
+
type: "readiness",
|
|
183
|
+
status: e,
|
|
184
|
+
attempts: m,
|
|
185
|
+
ms: Math.round(n.now() - p),
|
|
186
|
+
lastReason: h
|
|
187
|
+
});
|
|
188
|
+
for (;;) {
|
|
189
|
+
g(), m += 1;
|
|
190
|
+
let e = Math.max(1, a - (n.now() - p)), d = Math.min(o, e), v, y = () => void 0;
|
|
191
|
+
try {
|
|
192
|
+
v = await Promise.race([r.performRpc(i, t, "", d).then((e) => ({ reply: e }), (e) => ({ reason: f(e) })), new Promise((e) => {
|
|
193
|
+
y = n.setTimeout(() => e({ reason: "timeout" }), d);
|
|
194
|
+
})]);
|
|
195
|
+
} finally {
|
|
196
|
+
y();
|
|
197
|
+
}
|
|
198
|
+
if (("reply" in v ? v.reply.trim().toLowerCase() === "reject" : v.reason === "unsupported") || this.armStrayWindow(l), g(), "reply" in v) return v.reply.trim().toLowerCase() === "reject" ? (h = "rejected", _("unconfirmed")) : _("confirmed");
|
|
199
|
+
if (h = v.reason, v.reason === "recipient-disconnected") throw new c("participant");
|
|
200
|
+
if (n.now() - p + u >= a) return _("unconfirmed");
|
|
201
|
+
await new Promise((e) => {
|
|
202
|
+
let t = n.setTimeout(() => {
|
|
203
|
+
this.wakeProbe = null, e();
|
|
204
|
+
}, u);
|
|
205
|
+
this.wakeProbe = () => {
|
|
206
|
+
t(), this.wakeProbe = null, e();
|
|
207
|
+
};
|
|
208
|
+
}), u = Math.min(u * 2, s);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
assertReady() {
|
|
212
|
+
if (this.state !== "ready" || !this.avatarPresent || this.degraded) throw new c(this.state === "ready" && this.avatarPresent ? "connection" : "participant");
|
|
213
|
+
}
|
|
214
|
+
async openSegment() {
|
|
215
|
+
if (this.assertReady(), this.current !== null || this.awaiting.length > 0 || this.clearInFlight !== null) throw new l();
|
|
216
|
+
let t = this.nextSegmentId++, n = await this.options.room.openByteStream({
|
|
217
|
+
topic: e,
|
|
218
|
+
name: `AUDIO_${t}`,
|
|
219
|
+
destinationIdentity: this.options.avatarIdentity,
|
|
220
|
+
attributes: a()
|
|
221
|
+
});
|
|
222
|
+
if (this.state !== "ready") throw await n.close().catch(() => void 0), new d();
|
|
223
|
+
let r = new p(t, n, this);
|
|
224
|
+
return this.current = r, r;
|
|
225
|
+
}
|
|
226
|
+
segmentClosed(e) {
|
|
227
|
+
this.current === e && (this.current = null), e.state === "closed" && this.awaiting.push({
|
|
228
|
+
id: e.id,
|
|
229
|
+
durationSec: e.pushedSec
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
segmentDiscarded(e) {
|
|
233
|
+
this.current === e && (this.current = null);
|
|
234
|
+
let t = this.awaiting.findIndex((t) => t.id === e.id);
|
|
235
|
+
t >= 0 && this.awaiting.splice(t, 1);
|
|
236
|
+
}
|
|
237
|
+
endAllSegments() {
|
|
238
|
+
this.current &&= (this.current.end(), null), this.awaiting.length = 0;
|
|
239
|
+
}
|
|
240
|
+
handleStarted(e) {
|
|
241
|
+
if (e !== this.options.avatarIdentity) throw this.emit({
|
|
242
|
+
type: "error",
|
|
243
|
+
message: "lk.playback_started from an unexpected participant"
|
|
244
|
+
}), new s("unexpected caller");
|
|
245
|
+
return this.current === null && this.awaiting.length === 0 ? (this.emit({ type: "unmatchedStarted" }), "ok") : (this.emit({ type: "playbackStarted" }), "ok");
|
|
246
|
+
}
|
|
247
|
+
handleFinished(e, t) {
|
|
248
|
+
if (e !== this.options.avatarIdentity) throw this.emit({
|
|
249
|
+
type: "error",
|
|
250
|
+
message: "lk.playback_finished from an unexpected participant"
|
|
251
|
+
}), new s("unexpected caller");
|
|
252
|
+
let n = o(t);
|
|
253
|
+
n || this.emit({
|
|
254
|
+
type: "error",
|
|
255
|
+
message: "lk.playback_finished payload was not the documented shape"
|
|
256
|
+
});
|
|
257
|
+
let r = n ? n.positionSec : null, i = n ? n.interrupted : null;
|
|
258
|
+
if (this.owedFinishes > 0) return --this.owedFinishes, this.emit({
|
|
259
|
+
type: "owedFinished",
|
|
260
|
+
positionSec: r,
|
|
261
|
+
interrupted: i
|
|
262
|
+
}), "ok";
|
|
263
|
+
if (this.strayFinishDeadline !== null) {
|
|
264
|
+
if (this.options.scheduler.now() > this.strayFinishDeadline) this.strayFinishDeadline = null, this.strayAnswers = 0;
|
|
265
|
+
else if (i === !0 && this.clearSettle === null && this.strayAnswers > 0) return --this.strayAnswers, this.emit({ type: "unmatchedFinished" }), "ok";
|
|
266
|
+
}
|
|
267
|
+
return this.clearSettle ? (this.clearSettle(r, i, !1), "ok") : this.awaiting.length > 0 ? (this.awaiting.shift(), this.emit({
|
|
268
|
+
type: "playbackFinished",
|
|
269
|
+
positionSec: r,
|
|
270
|
+
interrupted: i,
|
|
271
|
+
synthetic: !1
|
|
272
|
+
}), "ok") : this.current ? (this.current.end(), this.current = null, this.emit({
|
|
273
|
+
type: "playbackFinished",
|
|
274
|
+
positionSec: r,
|
|
275
|
+
interrupted: i,
|
|
276
|
+
synthetic: !1
|
|
277
|
+
}), "ok") : (this.emit({ type: "unmatchedFinished" }), "ok");
|
|
278
|
+
}
|
|
279
|
+
clearBuffer(e = {}) {
|
|
280
|
+
if (this.clearInFlight) return this.clearInFlight;
|
|
281
|
+
if (this.state !== "ready") return Promise.resolve({
|
|
282
|
+
positionSec: null,
|
|
283
|
+
synthetic: !0,
|
|
284
|
+
rpc: "none"
|
|
285
|
+
});
|
|
286
|
+
let n = this.current !== null || this.awaiting.length > 0, r = this.current ? this.current.pushedSec : this.awaiting.reduce((e, t) => e + t.durationSec, 0);
|
|
287
|
+
if (this.endAllSegments(), !n) return Promise.resolve({
|
|
288
|
+
positionSec: null,
|
|
289
|
+
synthetic: !0,
|
|
290
|
+
rpc: "none"
|
|
291
|
+
});
|
|
292
|
+
let { scheduler: i } = this.options, a = this.options.clearTimeoutMs ?? 2e3, o = e.finishOverdue === !0, s = this.avatarPresent ? this.avatarDepartures : null;
|
|
293
|
+
return this.clearInFlight = new Promise((e) => {
|
|
294
|
+
let n = null, c = null, l = !1, u = () => {
|
|
295
|
+
l || n === null || c === null || (l = !0, p(), s === this.avatarDepartures && this.expectLateAnswer(n.synthetic, c, o, a), e({
|
|
296
|
+
...n,
|
|
297
|
+
rpc: c
|
|
298
|
+
}));
|
|
299
|
+
}, d = (e, t, r) => {
|
|
300
|
+
n === null && (n = {
|
|
301
|
+
positionSec: e,
|
|
302
|
+
synthetic: r
|
|
303
|
+
}, this.clearSettle = null, this.emit({
|
|
304
|
+
type: "playbackFinished",
|
|
305
|
+
positionSec: e,
|
|
306
|
+
interrupted: t,
|
|
307
|
+
synthetic: r
|
|
308
|
+
}), u());
|
|
309
|
+
}, p = i.setTimeout(() => {
|
|
310
|
+
d(r, !0, !0), c === null && (c = "timeout"), u();
|
|
311
|
+
}, a);
|
|
312
|
+
this.clearSettle = d, this.options.room.performRpc(this.options.avatarIdentity, t, "", a).then((e) => {
|
|
313
|
+
c === null && (c = e.trim().toLowerCase() === "reject" ? "rejected" : "ok"), c === "rejected" && d(r, !0, !0), u();
|
|
314
|
+
}, (e) => {
|
|
315
|
+
c === null && (c = f(e)), this.emit({
|
|
316
|
+
type: "error",
|
|
317
|
+
message: `lk.clear_buffer failed: ${e instanceof Error ? e.name : "error"}`
|
|
318
|
+
}), d(r, !0, !0), u();
|
|
319
|
+
});
|
|
320
|
+
}).finally(() => {
|
|
321
|
+
this.clearInFlight = null;
|
|
322
|
+
}), this.clearInFlight;
|
|
323
|
+
}
|
|
324
|
+
async dispose() {
|
|
325
|
+
this.disposed || (this.disposed = !0, this.rejectReady?.(new d()), this.wakeProbe?.(), this.endAllSegments(), await this.teardown());
|
|
326
|
+
}
|
|
327
|
+
async teardown() {
|
|
328
|
+
for (let e of this.unsubscribes.splice(0)) e();
|
|
329
|
+
this.options.room.unregisterRpc(n), this.options.room.unregisterRpc(r), this.state !== "closed" && (this.state = "closed", await this.options.room.disconnect().catch(() => void 0));
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
//#endregion
|
|
333
|
+
export { c as AvatarNotReadyError, l as AvatarSegmentBusyError, u as AvatarSegmentEndedError, d as AvatarTransportClosedError, m as LiveKitAvatarTransport };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AvatarOutputPort } from './avatarReplyOutputs';
|
|
2
|
+
import { LiveKitAvatarTransport } from './avatarTransport';
|
|
3
|
+
import { AvatarUnsubscribe } from './avatarPorts';
|
|
4
|
+
export interface AvatarTransportPortDeps {
|
|
5
|
+
readonly transport: LiveKitAvatarTransport;
|
|
6
|
+
/** The user's "Avatar" toggle. The port is active only while this is true AND the transport is ready. */
|
|
7
|
+
readonly isEnabled: () => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Provisional audibility from a meter on the avatar's received audio, changes only; optional. Whoever replaces or stops a meter whose last
|
|
10
|
+
* reading was audible reports false: that track can no longer be heard, and a stale "audible" would hold every drain to its bound and
|
|
11
|
+
* make the next reply's tracker ignore the meter (no meter start, and no no-start, so neither the retry nor the voice fallback).
|
|
12
|
+
*/
|
|
13
|
+
readonly subscribeMeter?: (handler: (audible: boolean) => void) => AvatarUnsubscribe;
|
|
14
|
+
/** Whether that meter can really hear right now (AvatarMeterHandle.isRunning). Without it the meter is treated as unable to prove silence. */
|
|
15
|
+
readonly isMeterHealthy?: () => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function createAvatarTransportPort(deps: AvatarTransportPortDeps): AvatarOutputPort;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/shared/assistant/avatar/live/avatarTransportPort.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
let t = !1;
|
|
4
|
+
return e.subscribeMeter?.((e) => {
|
|
5
|
+
t = e;
|
|
6
|
+
}), {
|
|
7
|
+
isActive: () => e.isEnabled() && e.transport.transportState === "ready",
|
|
8
|
+
isAudioBlocked: () => e.transport.audioBlocked,
|
|
9
|
+
connectionId: () => e.transport.instanceId,
|
|
10
|
+
isMeterHealthy: () => e.subscribeMeter !== void 0 && e.isMeterHealthy !== void 0 ? e.isMeterHealthy() : !1,
|
|
11
|
+
isMeterAudible: () => t,
|
|
12
|
+
openSegment: () => e.transport.openSegment(),
|
|
13
|
+
clearBuffer: (t) => e.transport.clearBuffer(t),
|
|
14
|
+
setOutputMuted: (t) => e.transport.setOutputMuted(t),
|
|
15
|
+
subscribe(t) {
|
|
16
|
+
let n = [e.transport.subscribe(t)];
|
|
17
|
+
return e.subscribeMeter && n.push(e.subscribeMeter((e) => t({
|
|
18
|
+
type: "meter",
|
|
19
|
+
audible: e
|
|
20
|
+
}))), () => {
|
|
21
|
+
for (let e of n) e();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { e as createAvatarTransportPort };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { SpeechEngine } from '../../speech/types';
|
|
3
|
+
import { AssistantLiveAvatarSeam, LiveAvatarEnabledStore, LiveAvatarStageProps } from '../../ui/assistantLiveAvatarSeam';
|
|
4
|
+
import { AvatarRoom, AvatarScheduler } from './avatarPorts';
|
|
5
|
+
import { AvatarSessionClient } from './avatarSessionClient';
|
|
6
|
+
import { LiveAvatarController, LiveAvatarControllerDeps, LiveAvatarLogEvent, LiveAvatarPreferences } from './liveAvatarController';
|
|
7
|
+
export interface LiveAvatarSeamOptions {
|
|
8
|
+
/** Display names for the avatar ids the server offers (it sends ids only); without one the chooser numbers them. */
|
|
9
|
+
readonly avatarNames?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly onLog?: (event: LiveAvatarLogEvent) => void;
|
|
11
|
+
/** Overrides, for tests and for hosts with their own plumbing. Each defaults to the assistant's own. */
|
|
12
|
+
readonly client?: () => AvatarSessionClient | null;
|
|
13
|
+
readonly openRoom?: () => Promise<AvatarRoom>;
|
|
14
|
+
readonly preferences?: LiveAvatarPreferences;
|
|
15
|
+
readonly scheduler?: AvatarScheduler;
|
|
16
|
+
readonly browserSupported?: () => boolean;
|
|
17
|
+
readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
|
|
18
|
+
}
|
|
19
|
+
/** The seam, plus what a host or a test may need of it. */
|
|
20
|
+
export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
|
|
21
|
+
readonly decorateSpeechEngine: (base: SpeechEngine) => SpeechEngine;
|
|
22
|
+
readonly Stage: ComponentType<LiveAvatarStageProps>;
|
|
23
|
+
readonly enabled: LiveAvatarEnabledStore;
|
|
24
|
+
readonly controller: LiveAvatarController;
|
|
25
|
+
/** Ends any session and stops listening. The handle is unusable afterwards. */
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether this browser may show the avatar. Desktop Chrome first (GATE1B §10: no other browser or phone is offered until it is tested,
|
|
30
|
+
* Chromium builds included, other Chromium-based browsers not), with the platform pieces the avatar needs: WebRTC, Web Audio (the level
|
|
31
|
+
* meter and the MP3 decode) and MediaStream.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isLiveAvatarBrowserSupported(scope?: typeof globalThis): boolean;
|
|
34
|
+
/** The `Authorization` the hosted TTS requests carry (the host's `getHeaders`), else the stored access token. Nothing else is read. */
|
|
35
|
+
export declare function liveAvatarAuthorization(): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* The workspace token, as the assistant's own calls mint it (`ensureWorkspaceToken`, the workspace gateway): the stored one while it is
|
|
38
|
+
* valid for the active context (workspace, organisation, project), else a new one. Null when there is no active workspace or no sign-in.
|
|
39
|
+
* A retry after a 401 (`fresh`) asks again the same way: the client can see a token's expiry and context, not a revocation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function liveAvatarWorkspaceAuthorization(_options: {
|
|
42
|
+
readonly fresh: boolean;
|
|
43
|
+
}): Promise<string | null>;
|
|
44
|
+
export declare function createLiveAvatarSeam(options?: LiveAvatarSeamOptions): LiveAvatarSeamHandle;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ensureWorkspaceToken as e, getStoredAuthToken as t, getStoredWorkspaceId as n, resolveGatewayUrl as r } from "../../../graphql/fetch.js";
|
|
2
|
+
import { readSpeechEngineConfig as i } from "../../speech/createSpeechEngine.js";
|
|
3
|
+
import { useAvatarPreferencesStore as a } from "../avatarPreferences.js";
|
|
4
|
+
import { realAvatarScheduler as o } from "./avatarPorts.js";
|
|
5
|
+
import { startAvatarAudioMeter as s } from "./avatarAudioMeter.js";
|
|
6
|
+
import { decodeMp3ToPcm16k as c } from "./avatarAudioDecode.js";
|
|
7
|
+
import { avatarApiBaseFromTtsEndpoint as l, avatarIdempotencyKey as u, createAvatarSessionClient as d } from "./avatarSessionClient.js";
|
|
8
|
+
import { createLiveAvatarController as f } from "./liveAvatarController.js";
|
|
9
|
+
import { openLiveKitAvatarRoom as p } from "./liveKitAvatarRoom.js";
|
|
10
|
+
import { LiveAvatarStageView as m } from "./LiveAvatarStage.js";
|
|
11
|
+
import { jsx as h } from "react/jsx-runtime";
|
|
12
|
+
//#region src/shared/assistant/avatar/live/createLiveAvatarSeam.tsx
|
|
13
|
+
function g(e = globalThis) {
|
|
14
|
+
let t = e;
|
|
15
|
+
if (typeof t.RTCPeerConnection != "function" || typeof t.OfflineAudioContext != "function" || typeof t.AudioContext != "function" || typeof t.MediaStream != "function") return !1;
|
|
16
|
+
let n = t.navigator, r = n?.userAgentData;
|
|
17
|
+
if (r?.brands !== void 0) {
|
|
18
|
+
let e = r.brands.map((e) => e.brand);
|
|
19
|
+
return r.mobile === !1 && e.some((e) => e === "Google Chrome" || e === "Chromium") && !e.some((e) => /Edge|Opera|Brave|Vivaldi|Samsung/i.test(e));
|
|
20
|
+
}
|
|
21
|
+
let i = n?.userAgent ?? "";
|
|
22
|
+
return /\b(?:Chrome|Chromium)\//.test(i) && !/\b(?:Edg|OPR|SamsungBrowser)\/|Mobile|Android|iPhone|iPad/.test(i);
|
|
23
|
+
}
|
|
24
|
+
function _() {
|
|
25
|
+
let e = i().getHeaders?.() ?? {};
|
|
26
|
+
for (let [t, n] of Object.entries(e)) if (t.toLowerCase() === "authorization" && typeof n == "string" && n.trim() !== "") return n.trim();
|
|
27
|
+
let n = t();
|
|
28
|
+
return n ? `Bearer ${n}` : null;
|
|
29
|
+
}
|
|
30
|
+
async function v(t) {
|
|
31
|
+
let i = n(), a = _()?.replace(/^Bearer\s+/i, "") ?? null;
|
|
32
|
+
if (!i || !a) return null;
|
|
33
|
+
let o = await e({
|
|
34
|
+
url: r("workspace"),
|
|
35
|
+
authToken: a,
|
|
36
|
+
workspaceId: i,
|
|
37
|
+
currentQuery: "liveAvatarSession"
|
|
38
|
+
});
|
|
39
|
+
return o ? `Bearer ${o}` : null;
|
|
40
|
+
}
|
|
41
|
+
function y() {
|
|
42
|
+
let e = null;
|
|
43
|
+
return () => {
|
|
44
|
+
let t = typeof location > "u" ? void 0 : location.href, n = l(i().endpoint, t);
|
|
45
|
+
return n === null ? null : (e?.base !== n && (e = {
|
|
46
|
+
base: n,
|
|
47
|
+
client: d({
|
|
48
|
+
baseUrl: n,
|
|
49
|
+
getAuthorization: _,
|
|
50
|
+
getWorkspaceAuthorization: v
|
|
51
|
+
})
|
|
52
|
+
}), e.client);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var b = {
|
|
56
|
+
getState: () => a.getState(),
|
|
57
|
+
setAvatarEnabled: (e) => a.getState().setAvatarEnabled(e),
|
|
58
|
+
setAvatarId: (e) => a.getState().setAvatarId(e),
|
|
59
|
+
subscribe: (e) => a.subscribe(() => e())
|
|
60
|
+
};
|
|
61
|
+
function x() {
|
|
62
|
+
if (typeof document > "u") return null;
|
|
63
|
+
let e = document.createElement("audio");
|
|
64
|
+
return e.autoplay = !0, e.hidden = !0, e.setAttribute("data-testid", "assistant-live-avatar-audio"), document.body.appendChild(e), e;
|
|
65
|
+
}
|
|
66
|
+
function S(e) {
|
|
67
|
+
if (!(typeof window > "u")) return (t) => (window.addEventListener(e, t), () => window.removeEventListener(e, t));
|
|
68
|
+
}
|
|
69
|
+
function C(e = {}) {
|
|
70
|
+
let t = e.preferences ?? b, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? S("burdenoff:workspace-changed"), i = n.subscribePageHide ?? S("pagehide"), a = f({
|
|
71
|
+
client: e.client ?? y(),
|
|
72
|
+
openRoom: e.openRoom ?? p,
|
|
73
|
+
scheduler: e.scheduler ?? o,
|
|
74
|
+
preferences: t,
|
|
75
|
+
decode: n.decode ?? ((e, t) => c(e, { signal: t })),
|
|
76
|
+
createAudioElement: n.createAudioElement ?? x,
|
|
77
|
+
releaseAudioElement: n.releaseAudioElement ?? ((e) => {
|
|
78
|
+
e.srcObject = null, e.remove();
|
|
79
|
+
}),
|
|
80
|
+
startMeter: n.startMeter ?? ((e, t) => typeof MediaStream > "u" ? null : s(new MediaStream([e]), t)),
|
|
81
|
+
browserSupported: e.browserSupported ?? (() => g()),
|
|
82
|
+
now: n.now ?? (() => Date.now()),
|
|
83
|
+
newIdempotencyKey: n.newIdempotencyKey ?? (() => u()),
|
|
84
|
+
...r ? { subscribeWorkspaceChange: r } : {},
|
|
85
|
+
...i ? { subscribePageHide: i } : {},
|
|
86
|
+
...n.idleMs === void 0 ? {} : { idleMs: n.idleMs },
|
|
87
|
+
...n.endBeforeDeadlineMs === void 0 ? {} : { endBeforeDeadlineMs: n.endBeforeDeadlineMs },
|
|
88
|
+
...n.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: n.connectTimeoutMs },
|
|
89
|
+
...n.trackerConfig === void 0 ? {} : { trackerConfig: n.trackerConfig },
|
|
90
|
+
...n.maxDrainMs === void 0 ? {} : { maxDrainMs: n.maxDrainMs },
|
|
91
|
+
...n.stallMs === void 0 ? {} : { stallMs: n.stallMs },
|
|
92
|
+
...n.readinessProbe === void 0 ? {} : { readinessProbe: n.readinessProbe },
|
|
93
|
+
...e.onLog ? { onLog: e.onLog } : {}
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
decorateSpeechEngine: (e) => ({
|
|
97
|
+
id: e.id,
|
|
98
|
+
isSupported: () => e.isSupported(),
|
|
99
|
+
listVoices: () => e.listVoices(),
|
|
100
|
+
subscribeVoices: (t) => e.subscribeVoices(t),
|
|
101
|
+
speak: (t, n, r) => e.speak(t, a.route(n, e) === "avatar" ? {
|
|
102
|
+
...n,
|
|
103
|
+
audioOutput: a.createOutput()
|
|
104
|
+
} : n, r),
|
|
105
|
+
cancel: () => e.cancel()
|
|
106
|
+
}),
|
|
107
|
+
Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ h(m, {
|
|
108
|
+
controller: a,
|
|
109
|
+
expanded: t,
|
|
110
|
+
fallback: n,
|
|
111
|
+
...e.avatarNames ? { avatarNames: e.avatarNames } : {}
|
|
112
|
+
}),
|
|
113
|
+
enabled: {
|
|
114
|
+
subscribe: (e) => t.subscribe(e),
|
|
115
|
+
getSnapshot: () => t.getState().avatarEnabled,
|
|
116
|
+
switchOff: () => a.switchOff(),
|
|
117
|
+
voiceLive: (e) => a.setVoiceLive(e)
|
|
118
|
+
},
|
|
119
|
+
controller: a,
|
|
120
|
+
dispose: () => a.dispose()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { C as createLiveAvatarSeam, g as isLiveAvatarBrowserSupported, _ as liveAvatarAuthorization, v as liveAvatarWorkspaceAuthorization };
|