@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,133 @@
|
|
|
1
|
+
import { SpeechEngine, SpeechSpeakOptions } from '../../speech/types';
|
|
2
|
+
import { AvatarProblem } from '../useAvatarCopy';
|
|
3
|
+
import { AvatarMeterHandle } from './avatarAudioMeter';
|
|
4
|
+
import { AvatarRoom, AvatarScheduler } from './avatarPorts';
|
|
5
|
+
import { AvatarTrackerConfig } from './avatarPlaybackTracker';
|
|
6
|
+
import { AvatarDecodedPiece, AvatarNotice, AvatarReplyOutputs } from './avatarReplyOutputs';
|
|
7
|
+
import { AvatarClientEndReason, AvatarSessionClient } from './avatarSessionClient';
|
|
8
|
+
import { AvatarReadinessProbeOptions } from './avatarTransport';
|
|
9
|
+
/** End after this long without a reply on the avatar while it is on (OD8, BACKEND-DESIGN §8.7). */
|
|
10
|
+
export declare const LIVE_AVATAR_IDLE_MS = 120000;
|
|
11
|
+
/** End gracefully, between replies, this long before the server's hard end (`endsAt`, §3.6). */
|
|
12
|
+
export declare const LIVE_AVATAR_END_BEFORE_DEADLINE_MS = 10000;
|
|
13
|
+
/** The longest the availability and status questions may take: a hung request never leaves the toggle disabled. */
|
|
14
|
+
export declare const LIVE_AVATAR_REQUEST_TIMEOUT_MS = 8000;
|
|
15
|
+
/** The longest a connect may take (the avatar's own wait for its video, 8 s, plus the readiness probe, 6 s, plus slack). */
|
|
16
|
+
export declare const LIVE_AVATAR_CONNECT_TIMEOUT_MS = 20000;
|
|
17
|
+
export type LiveAvatarPhase =
|
|
18
|
+
/** Nothing running. */
|
|
19
|
+
'off'
|
|
20
|
+
/** Asking the server whether the avatar is offered here. */
|
|
21
|
+
| 'checking'
|
|
22
|
+
/** Offered: the person chooses an avatar and confirms the disclosure. */
|
|
23
|
+
| 'choosing'
|
|
24
|
+
/** The server is starting a session. */
|
|
25
|
+
| 'starting'
|
|
26
|
+
/** Joining the session's room and waiting for the avatar. */
|
|
27
|
+
| 'connecting'
|
|
28
|
+
/** Replies may play on the avatar. */
|
|
29
|
+
| 'ready'
|
|
30
|
+
/** Ending the session. */
|
|
31
|
+
| 'ending';
|
|
32
|
+
export type LiveAvatarNotice = 'interrupted' | 'voice-only' | 'time-limit';
|
|
33
|
+
export interface LiveAvatarSnapshot {
|
|
34
|
+
/** A voice conversation is live: the only time the avatar is offered (P2). */
|
|
35
|
+
readonly voiceLive: boolean;
|
|
36
|
+
/** The toggle may be shown: the browser can show the avatar, it was not refused on this page, and a voice conversation is live. */
|
|
37
|
+
readonly offered: boolean;
|
|
38
|
+
readonly phase: LiveAvatarPhase;
|
|
39
|
+
/** The avatars the server offers, from the last availability answer. */
|
|
40
|
+
readonly avatars: readonly string[];
|
|
41
|
+
/** The avatar the next session starts with. */
|
|
42
|
+
readonly selectedAvatarId: string | null;
|
|
43
|
+
/** What the person must read before the avatar starts; null until the server said which version. */
|
|
44
|
+
readonly consentVersion: string | null;
|
|
45
|
+
readonly problem: AvatarProblem | null;
|
|
46
|
+
readonly notice: LiveAvatarNotice | null;
|
|
47
|
+
/** The browser blocks the avatar's sound; a tap on the stage (`resumeAudio`) lifts it. */
|
|
48
|
+
readonly audioBlocked: boolean;
|
|
49
|
+
/** The avatar's video has arrived. */
|
|
50
|
+
readonly videoReady: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** The person's stored choices (P4), read and written through this narrow port so tests need no storage. */
|
|
53
|
+
export interface LiveAvatarPreferences {
|
|
54
|
+
getState(): {
|
|
55
|
+
readonly avatarEnabled: boolean;
|
|
56
|
+
readonly avatarId: string | null;
|
|
57
|
+
};
|
|
58
|
+
setAvatarEnabled(enabled: boolean): void;
|
|
59
|
+
setAvatarId(avatarId: string | null): void;
|
|
60
|
+
subscribe(listener: () => void): () => void;
|
|
61
|
+
}
|
|
62
|
+
/** A diagnostic event for the host's logs: never a token, an id of the person, or text of a reply. */
|
|
63
|
+
export type LiveAvatarLogEvent = {
|
|
64
|
+
readonly event: 'notice';
|
|
65
|
+
readonly notice: AvatarNotice;
|
|
66
|
+
} | {
|
|
67
|
+
readonly event: 'refused';
|
|
68
|
+
readonly status: number;
|
|
69
|
+
readonly code: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly event: 'session-ended';
|
|
72
|
+
readonly reason: AvatarClientEndReason | 'server';
|
|
73
|
+
} | {
|
|
74
|
+
readonly event: 'connect-failed';
|
|
75
|
+
readonly error: string;
|
|
76
|
+
};
|
|
77
|
+
export interface LiveAvatarControllerDeps {
|
|
78
|
+
/** The session API, or null when the host configured no hosted TTS (the avatar then has nothing to animate). */
|
|
79
|
+
readonly client: () => AvatarSessionClient | null;
|
|
80
|
+
/** Loads the media SDK (lazily) and opens a room: called only after a session started. */
|
|
81
|
+
readonly openRoom: () => Promise<AvatarRoom>;
|
|
82
|
+
readonly scheduler: AvatarScheduler;
|
|
83
|
+
readonly preferences: LiveAvatarPreferences;
|
|
84
|
+
readonly decode: (bytes: Uint8Array, signal: AbortSignal) => Promise<AvatarDecodedPiece>;
|
|
85
|
+
/** The element the avatar's audio plays in, kept outside the stage so a collapse never cuts a reply; null without a DOM. */
|
|
86
|
+
readonly createAudioElement: () => HTMLAudioElement | null;
|
|
87
|
+
readonly releaseAudioElement: (element: HTMLAudioElement) => void;
|
|
88
|
+
/** A level meter on the avatar's received audio; null when none can be built (the output then cannot prove silence). */
|
|
89
|
+
readonly startMeter: (track: MediaStreamTrack, onChange: (audible: boolean) => void) => AvatarMeterHandle | null;
|
|
90
|
+
/** The browser can show the avatar at all (desktop Chromium first: GATE1B §10). */
|
|
91
|
+
readonly browserSupported: () => boolean;
|
|
92
|
+
/** Wall-clock milliseconds: only the fallback for `endsAt` when the server announced no session length (the server's clock may differ). */
|
|
93
|
+
readonly now: () => number;
|
|
94
|
+
readonly newIdempotencyKey: () => string;
|
|
95
|
+
readonly subscribeWorkspaceChange?: (listener: () => void) => () => void;
|
|
96
|
+
readonly subscribePageHide?: (listener: () => void) => () => void;
|
|
97
|
+
readonly idleMs?: number;
|
|
98
|
+
readonly endBeforeDeadlineMs?: number;
|
|
99
|
+
readonly connectTimeoutMs?: number;
|
|
100
|
+
readonly requestTimeoutMs?: number;
|
|
101
|
+
readonly trackerConfig?: AvatarTrackerConfig;
|
|
102
|
+
readonly maxDrainMs?: number;
|
|
103
|
+
readonly stallMs?: number;
|
|
104
|
+
readonly readinessProbe?: AvatarReadinessProbeOptions | false;
|
|
105
|
+
readonly onLog?: (event: LiveAvatarLogEvent) => void;
|
|
106
|
+
}
|
|
107
|
+
export interface LiveAvatarController {
|
|
108
|
+
subscribe(listener: () => void): () => void;
|
|
109
|
+
getSnapshot(): LiveAvatarSnapshot;
|
|
110
|
+
/** From the seam (P2): a voice conversation is live, or over. */
|
|
111
|
+
setVoiceLive(live: boolean): void;
|
|
112
|
+
/** The person asked for the avatar: ask the server whether it is offered, then show the chooser and the disclosure. */
|
|
113
|
+
requestOn(): void;
|
|
114
|
+
/** The person confirmed the disclosure: start a session with the chosen avatar. */
|
|
115
|
+
confirm(): void;
|
|
116
|
+
/** The person declined the disclosure. */
|
|
117
|
+
cancelChoice(): void;
|
|
118
|
+
selectAvatar(avatarId: string): void;
|
|
119
|
+
/** Avatar OFF: the toggle, the compact view's stop, the end of the voice conversation (P2). Idempotent. */
|
|
120
|
+
switchOff(): void;
|
|
121
|
+
/** A user gesture: let the avatar's sound play (autoplay) and wake the level meter. */
|
|
122
|
+
resumeAudio(): void;
|
|
123
|
+
/** The stage that shows the avatar mounted (or unmounted): replies go to the avatar only while it is shown (D4). */
|
|
124
|
+
setStageMounted(mounted: boolean): void;
|
|
125
|
+
/** The stage's video element, or null when it goes. */
|
|
126
|
+
attachVideo(element: HTMLVideoElement | null): void;
|
|
127
|
+
/** Where one reply goes: the avatar, or the speakers (untouched). */
|
|
128
|
+
route(options: SpeechSpeakOptions, base: SpeechEngine): 'avatar' | 'speakers';
|
|
129
|
+
/** A fresh audio output for one reply routed to the avatar. */
|
|
130
|
+
createOutput(): ReturnType<AvatarReplyOutputs['createOutput']>;
|
|
131
|
+
dispose(): void;
|
|
132
|
+
}
|
|
133
|
+
export declare function createLiveAvatarController(deps: LiveAvatarControllerDeps): LiveAvatarController;
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { AvatarNotReadyError as e, LiveKitAvatarTransport as t } from "./avatarTransport.js";
|
|
2
|
+
import { createAvatarTransportPort as n } from "./avatarTransportPort.js";
|
|
3
|
+
import { createAvatarReplyOutputs as r } from "./avatarReplyOutputs.js";
|
|
4
|
+
import { AvatarApiError as i, avatarRefusalFor as a } from "./avatarSessionClient.js";
|
|
5
|
+
//#region src/shared/assistant/avatar/live/liveAvatarController.ts
|
|
6
|
+
var o = 12e4, s = 1e4, c = 8e3, l = 5e3, u = 2e4, d = {
|
|
7
|
+
voiceLive: !1,
|
|
8
|
+
offered: !1,
|
|
9
|
+
phase: "off",
|
|
10
|
+
avatars: [],
|
|
11
|
+
selectedAvatarId: null,
|
|
12
|
+
consentVersion: null,
|
|
13
|
+
problem: null,
|
|
14
|
+
notice: null,
|
|
15
|
+
audioBlocked: !1,
|
|
16
|
+
videoReady: !1
|
|
17
|
+
};
|
|
18
|
+
function f(e) {
|
|
19
|
+
switch (e) {
|
|
20
|
+
case "start_superseded": return {
|
|
21
|
+
problem: "already-running",
|
|
22
|
+
notice: null
|
|
23
|
+
};
|
|
24
|
+
case "deadline": return {
|
|
25
|
+
problem: null,
|
|
26
|
+
notice: "time-limit"
|
|
27
|
+
};
|
|
28
|
+
case "kill_switch":
|
|
29
|
+
case "disabled":
|
|
30
|
+
case "shutdown": return {
|
|
31
|
+
problem: "paused",
|
|
32
|
+
notice: null
|
|
33
|
+
};
|
|
34
|
+
case "provider_ended":
|
|
35
|
+
case "provider_failed":
|
|
36
|
+
case "worker_absent":
|
|
37
|
+
case "no_show":
|
|
38
|
+
case "terms_mismatch":
|
|
39
|
+
case "start_timeout": return {
|
|
40
|
+
problem: "provider",
|
|
41
|
+
notice: null
|
|
42
|
+
};
|
|
43
|
+
default: return {
|
|
44
|
+
problem: "connection",
|
|
45
|
+
notice: null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var p = new Set(["playback-blocked", "needs-hosted-voice"]);
|
|
50
|
+
function m(o) {
|
|
51
|
+
let { scheduler: s, preferences: c } = o, u = o.idleMs ?? 12e4, m = o.endBeforeDeadlineMs ?? 1e4, h = o.connectTimeoutMs ?? 2e4, g = o.requestTimeoutMs ?? 8e3, _ = d, v = /* @__PURE__ */ new Set(), y = !1, b = null, x = 0, S = !1, C = !1, w = 0, T = null, E = null, D = null, O = 0, k = !1, A = null, j = null, M = !1;
|
|
52
|
+
function N(e) {
|
|
53
|
+
try {
|
|
54
|
+
o.onLog?.(e);
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
function P(e) {
|
|
58
|
+
return e && !y && o.browserSupported();
|
|
59
|
+
}
|
|
60
|
+
function F(e) {
|
|
61
|
+
let t = {
|
|
62
|
+
..._,
|
|
63
|
+
...e
|
|
64
|
+
}, n = P(t.voiceLive), r = {
|
|
65
|
+
...t,
|
|
66
|
+
offered: n
|
|
67
|
+
};
|
|
68
|
+
if (!Object.keys(r).every((e) => r[e] === _[e])) {
|
|
69
|
+
_ = r;
|
|
70
|
+
for (let e of [...v]) try {
|
|
71
|
+
e();
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function I() {
|
|
76
|
+
c.getState().avatarEnabled !== S && c.setAvatarEnabled(S);
|
|
77
|
+
}
|
|
78
|
+
let L = {
|
|
79
|
+
positionSec: null,
|
|
80
|
+
synthetic: !0,
|
|
81
|
+
rpc: "none"
|
|
82
|
+
}, R = r({
|
|
83
|
+
port: {
|
|
84
|
+
isActive: () => E !== null && E.port.isActive(),
|
|
85
|
+
isAudioBlocked: () => E?.port.isAudioBlocked() ?? !1,
|
|
86
|
+
connectionId: () => E?.port.connectionId() ?? 0,
|
|
87
|
+
isMeterHealthy: () => E?.port.isMeterHealthy() ?? !1,
|
|
88
|
+
isMeterAudible: () => E?.port.isMeterAudible() ?? !1,
|
|
89
|
+
openSegment: () => E ? E.port.openSegment() : Promise.reject(new e("connection")),
|
|
90
|
+
clearBuffer: (e) => E ? E.port.clearBuffer(e) : Promise.resolve(L),
|
|
91
|
+
setOutputMuted: (e) => E?.port.setOutputMuted(e),
|
|
92
|
+
subscribe: (e) => E ? E.port.subscribe(e) : () => void 0
|
|
93
|
+
},
|
|
94
|
+
scheduler: s,
|
|
95
|
+
decode: o.decode,
|
|
96
|
+
...o.trackerConfig ? { trackerConfig: o.trackerConfig } : {},
|
|
97
|
+
...o.maxDrainMs === void 0 ? {} : { maxDrainMs: o.maxDrainMs },
|
|
98
|
+
...o.stallMs === void 0 ? {} : { stallMs: o.stallMs },
|
|
99
|
+
onNotice: (e) => {
|
|
100
|
+
switch (N({
|
|
101
|
+
event: "notice",
|
|
102
|
+
notice: e
|
|
103
|
+
}), e.kind) {
|
|
104
|
+
case "interrupted":
|
|
105
|
+
F({ notice: "interrupted" });
|
|
106
|
+
return;
|
|
107
|
+
case "voice-fallback":
|
|
108
|
+
S && F({ notice: "voice-only" });
|
|
109
|
+
return;
|
|
110
|
+
case "audio-blocked":
|
|
111
|
+
F({
|
|
112
|
+
audioBlocked: !0,
|
|
113
|
+
problem: "playback-blocked"
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
default: return;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
onActivity: (e) => {
|
|
120
|
+
if (e) {
|
|
121
|
+
A?.(), A = null;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (C) {
|
|
125
|
+
H("user", { notice: "time-limit" });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
z();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
function z() {
|
|
132
|
+
A?.(), A = null, _.phase === "ready" && (A = s.setTimeout(() => {
|
|
133
|
+
A = null, _.phase === "ready" && !R.isBusy() && H("idle", {});
|
|
134
|
+
}, u));
|
|
135
|
+
}
|
|
136
|
+
function B() {
|
|
137
|
+
A?.(), A = null, j?.(), j = null;
|
|
138
|
+
}
|
|
139
|
+
function V() {
|
|
140
|
+
R.leave();
|
|
141
|
+
let e = E;
|
|
142
|
+
if (E = null, e !== null) {
|
|
143
|
+
if (e.unsubscribe?.(), e.meter !== null) {
|
|
144
|
+
if (e.meterAudible) {
|
|
145
|
+
e.meterAudible = !1;
|
|
146
|
+
for (let t of [...e.meterListeners]) t(!1);
|
|
147
|
+
}
|
|
148
|
+
e.meter.stop();
|
|
149
|
+
}
|
|
150
|
+
e.videoTrack?.detach(), e.transport.dispose(), e.audioElement !== null && o.releaseAudioElement(e.audioElement);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function H(e, t) {
|
|
154
|
+
w += 1, S = !1, C = !1, V(), B();
|
|
155
|
+
let n = T;
|
|
156
|
+
T = null, I(), n !== null && (N({
|
|
157
|
+
event: "session-ended",
|
|
158
|
+
reason: e
|
|
159
|
+
}), o.client()?.end(n, {
|
|
160
|
+
reason: e,
|
|
161
|
+
keepalive: t.keepalive === !0
|
|
162
|
+
}).catch(() => void 0)), F({
|
|
163
|
+
phase: "off",
|
|
164
|
+
audioBlocked: !1,
|
|
165
|
+
videoReady: !1,
|
|
166
|
+
...t.problem === void 0 ? _.problem !== null && p.has(_.problem) ? { problem: null } : {} : { problem: t.problem },
|
|
167
|
+
...t.notice === void 0 ? {} : { notice: t.notice }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function U(e) {
|
|
171
|
+
let t = a(e);
|
|
172
|
+
t.hide && (y = !0), e instanceof i && N({
|
|
173
|
+
event: "refused",
|
|
174
|
+
status: e.status,
|
|
175
|
+
code: e.code
|
|
176
|
+
}), S = !1, I(), F({
|
|
177
|
+
phase: "off",
|
|
178
|
+
problem: t.problem
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function W() {
|
|
182
|
+
y = !0, S = !1, I(), F({
|
|
183
|
+
phase: "off",
|
|
184
|
+
problem: "unavailable",
|
|
185
|
+
avatars: []
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function G(e) {
|
|
189
|
+
switch (e.type) {
|
|
190
|
+
case "audioPlayback":
|
|
191
|
+
e.canPlay ? _.problem === "playback-blocked" ? F({
|
|
192
|
+
audioBlocked: !1,
|
|
193
|
+
problem: null
|
|
194
|
+
}) : F({ audioBlocked: !1 }) : F({
|
|
195
|
+
audioBlocked: !0,
|
|
196
|
+
problem: "playback-blocked"
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
case "avatarLeft":
|
|
200
|
+
case "disconnected":
|
|
201
|
+
K();
|
|
202
|
+
return;
|
|
203
|
+
default: return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function K() {
|
|
207
|
+
let e = T;
|
|
208
|
+
if (e === null || _.phase === "ending" || _.phase === "off") return;
|
|
209
|
+
let t = w;
|
|
210
|
+
F({ phase: "ending" });
|
|
211
|
+
let n = null, r = !1;
|
|
212
|
+
try {
|
|
213
|
+
let t = o.client(), i = t === null ? void 0 : await J((n) => t.status(e, n));
|
|
214
|
+
n = i?.endedReason ?? null, r = i !== void 0 && [
|
|
215
|
+
"ended",
|
|
216
|
+
"failed",
|
|
217
|
+
"canceled"
|
|
218
|
+
].includes(i.state);
|
|
219
|
+
} catch {}
|
|
220
|
+
if (w !== t) return;
|
|
221
|
+
let i = f(n);
|
|
222
|
+
r && (T = null, N({
|
|
223
|
+
event: "session-ended",
|
|
224
|
+
reason: "server"
|
|
225
|
+
})), H("client_error", i);
|
|
226
|
+
}
|
|
227
|
+
async function q(e, r) {
|
|
228
|
+
let i = o.client();
|
|
229
|
+
if (i === null) {
|
|
230
|
+
W();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
let a = ++w;
|
|
234
|
+
F({
|
|
235
|
+
phase: "starting",
|
|
236
|
+
problem: null,
|
|
237
|
+
notice: null
|
|
238
|
+
});
|
|
239
|
+
let c;
|
|
240
|
+
try {
|
|
241
|
+
c = await o.openRoom();
|
|
242
|
+
} catch {
|
|
243
|
+
w === a && W();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let u = () => {
|
|
247
|
+
c.disconnect().catch(() => void 0);
|
|
248
|
+
};
|
|
249
|
+
if (w !== a) {
|
|
250
|
+
u();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
let d = s.now(), f;
|
|
254
|
+
try {
|
|
255
|
+
f = await i.create({
|
|
256
|
+
avatarId: e,
|
|
257
|
+
consentVersion: r,
|
|
258
|
+
idempotencyKey: o.newIdempotencyKey()
|
|
259
|
+
});
|
|
260
|
+
} catch (e) {
|
|
261
|
+
u(), w === a && U(e);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (w !== a) {
|
|
265
|
+
u(), i.end(f.sessionId, { reason: k ? "voice_end" : "user" }).catch(() => void 0);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
T = f.sessionId, F({ phase: "connecting" });
|
|
269
|
+
let p = new t({
|
|
270
|
+
room: c,
|
|
271
|
+
avatarIdentity: f.avatarIdentity,
|
|
272
|
+
scheduler: s,
|
|
273
|
+
...o.readinessProbe === void 0 ? {} : { readinessProbe: o.readinessProbe }
|
|
274
|
+
}), g = /* @__PURE__ */ new Set(), _ = {
|
|
275
|
+
session: f,
|
|
276
|
+
transport: p,
|
|
277
|
+
audioElement: o.createAudioElement(),
|
|
278
|
+
meter: null,
|
|
279
|
+
meterAudible: !1,
|
|
280
|
+
meterListeners: g,
|
|
281
|
+
videoTrack: null,
|
|
282
|
+
unsubscribe: null,
|
|
283
|
+
port: n({
|
|
284
|
+
transport: p,
|
|
285
|
+
isEnabled: () => S && !C,
|
|
286
|
+
subscribeMeter: (e) => (g.add(e), () => {
|
|
287
|
+
g.delete(e);
|
|
288
|
+
}),
|
|
289
|
+
isMeterHealthy: () => _.meter?.isRunning() ?? !1
|
|
290
|
+
})
|
|
291
|
+
};
|
|
292
|
+
E = _, _.unsubscribe = p.subscribe(G);
|
|
293
|
+
try {
|
|
294
|
+
await Y(p.connect({
|
|
295
|
+
url: f.livekitUrl,
|
|
296
|
+
token: f.participantToken,
|
|
297
|
+
audioElement: _.audioElement,
|
|
298
|
+
onAvatarAudio: (e) => {
|
|
299
|
+
if (!(e.mediaStreamTrack === null || _.meter !== null)) try {
|
|
300
|
+
_.meter = o.startMeter(e.mediaStreamTrack, (e) => {
|
|
301
|
+
_.meterAudible = e;
|
|
302
|
+
for (let t of [...g]) t(e);
|
|
303
|
+
});
|
|
304
|
+
} catch {
|
|
305
|
+
_.meter = null;
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
onAvatarVideo: (e) => {
|
|
309
|
+
_.videoTrack = e, D !== null && e.attach(D), E === _ && F({ videoReady: !0 });
|
|
310
|
+
}
|
|
311
|
+
}), h);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (w !== a) return;
|
|
314
|
+
N({
|
|
315
|
+
event: "connect-failed",
|
|
316
|
+
error: e instanceof Error ? e.name : "error"
|
|
317
|
+
}), H("client_error", { problem: "connection" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (w !== a || E !== _) return;
|
|
321
|
+
F({
|
|
322
|
+
phase: "ready",
|
|
323
|
+
audioBlocked: p.audioBlocked,
|
|
324
|
+
...p.audioBlocked ? { problem: "playback-blocked" } : {}
|
|
325
|
+
}), z();
|
|
326
|
+
let v = x > 0 ? d + x - l - s.now() : f.endsAt - o.now(), y = Math.max(0, v - m);
|
|
327
|
+
j = s.setTimeout(() => {
|
|
328
|
+
j = null, w === a && (C = !0, R.isBusy() || H("user", { notice: "time-limit" }));
|
|
329
|
+
}, y);
|
|
330
|
+
}
|
|
331
|
+
function J(t) {
|
|
332
|
+
let n = new AbortController();
|
|
333
|
+
return new Promise((r, i) => {
|
|
334
|
+
let a = s.setTimeout(() => {
|
|
335
|
+
n.abort(), i(new e("connection"));
|
|
336
|
+
}, g);
|
|
337
|
+
t(n.signal).then((e) => {
|
|
338
|
+
a(), r(e);
|
|
339
|
+
}, (e) => {
|
|
340
|
+
a(), i(e);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function Y(t, n) {
|
|
345
|
+
return new Promise((r, i) => {
|
|
346
|
+
let a = s.setTimeout(() => i(new e("connection")), n);
|
|
347
|
+
t.then((e) => {
|
|
348
|
+
a(), r(e);
|
|
349
|
+
}, (e) => {
|
|
350
|
+
a(), i(e);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
let X = [];
|
|
355
|
+
X.push(c.subscribe(() => {
|
|
356
|
+
let e = c.getState().avatarEnabled;
|
|
357
|
+
if (e !== S) {
|
|
358
|
+
if (e) {
|
|
359
|
+
c.setAvatarEnabled(!1);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
_.phase !== "off" && _.phase !== "checking" && _.phase !== "choosing" ? H(k ? "voice_end" : "user", {}) : S = !1;
|
|
363
|
+
}
|
|
364
|
+
})), o.subscribeWorkspaceChange && X.push(o.subscribeWorkspaceChange(() => {
|
|
365
|
+
if (y = !1, _.phase === "off") {
|
|
366
|
+
F({
|
|
367
|
+
problem: null,
|
|
368
|
+
avatars: [],
|
|
369
|
+
consentVersion: null
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
H("workspace_switch", { problem: null }), F({
|
|
374
|
+
avatars: [],
|
|
375
|
+
consentVersion: null
|
|
376
|
+
});
|
|
377
|
+
})), o.subscribePageHide && X.push(o.subscribePageHide(() => {
|
|
378
|
+
(T !== null || _.phase !== "off") && H("page_hide", { keepalive: !0 });
|
|
379
|
+
}));
|
|
380
|
+
let Z = {
|
|
381
|
+
subscribe(e) {
|
|
382
|
+
return v.add(e), () => {
|
|
383
|
+
v.delete(e);
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
getSnapshot: () => _,
|
|
387
|
+
setVoiceLive(e) {
|
|
388
|
+
if (k = !e, e) {
|
|
389
|
+
F({
|
|
390
|
+
voiceLive: !0,
|
|
391
|
+
problem: y ? _.problem : null,
|
|
392
|
+
notice: null
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
(T !== null || _.phase !== "off") && (_.phase === "checking" || _.phase === "choosing" ? (w += 1, F({ phase: "off" })) : H("voice_end", {})), F({
|
|
397
|
+
voiceLive: !1,
|
|
398
|
+
notice: null
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
requestOn() {
|
|
402
|
+
if (M || !_.offered || _.phase !== "off") return;
|
|
403
|
+
let e = o.client();
|
|
404
|
+
if (e === null) {
|
|
405
|
+
W();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
let t = ++w;
|
|
409
|
+
F({
|
|
410
|
+
phase: "checking",
|
|
411
|
+
problem: null,
|
|
412
|
+
notice: null
|
|
413
|
+
}), J((t) => e.availability(t)).then((e) => {
|
|
414
|
+
if (w !== t) return;
|
|
415
|
+
if (!e.available) {
|
|
416
|
+
W();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
x = e.maxSessionSeconds * 1e3;
|
|
420
|
+
let n = c.getState().avatarId, r = n !== null && e.avatars.includes(n) ? n : e.avatars[0] ?? null;
|
|
421
|
+
F({
|
|
422
|
+
phase: "choosing",
|
|
423
|
+
avatars: e.avatars,
|
|
424
|
+
selectedAvatarId: r,
|
|
425
|
+
consentVersion: e.consentVersion
|
|
426
|
+
}), b === e.consentVersion && e.avatars.length === 1 && Z.confirm();
|
|
427
|
+
}, (e) => {
|
|
428
|
+
w === t && U(e);
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
confirm() {
|
|
432
|
+
if (_.phase !== "choosing" || !_.voiceLive) return;
|
|
433
|
+
let e = _.selectedAvatarId, t = _.consentVersion;
|
|
434
|
+
e === null || t === null || (b = t, c.setAvatarId(e), S = !0, I(), q(e, t));
|
|
435
|
+
},
|
|
436
|
+
cancelChoice() {
|
|
437
|
+
_.phase === "choosing" && (w += 1, F({ phase: "off" }));
|
|
438
|
+
},
|
|
439
|
+
selectAvatar(e) {
|
|
440
|
+
_.avatars.includes(e) && F({ selectedAvatarId: e });
|
|
441
|
+
},
|
|
442
|
+
switchOff() {
|
|
443
|
+
if (_.phase === "checking" || _.phase === "choosing") {
|
|
444
|
+
w += 1, F({ phase: "off" });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (_.phase === "off" && !S) {
|
|
448
|
+
I();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
H(k ? "voice_end" : "user", {});
|
|
452
|
+
},
|
|
453
|
+
resumeAudio() {
|
|
454
|
+
let e = E;
|
|
455
|
+
e !== null && (e.transport.resumeAudio().catch(() => void 0), e.meter?.resume().catch(() => void 0));
|
|
456
|
+
},
|
|
457
|
+
setStageMounted(e) {
|
|
458
|
+
O = Math.max(0, O + (e ? 1 : -1));
|
|
459
|
+
},
|
|
460
|
+
attachVideo(e) {
|
|
461
|
+
let t = E?.videoTrack ?? null;
|
|
462
|
+
if (e === null) {
|
|
463
|
+
D !== null && t?.detach(), D = null;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
D = e, t?.attach(e);
|
|
467
|
+
},
|
|
468
|
+
route(e, t) {
|
|
469
|
+
if (_.phase !== "ready" || C || !S || O === 0 || E === null || E.port.isAudioBlocked() || e.fallback === !1) return "speakers";
|
|
470
|
+
let n = e.voice;
|
|
471
|
+
return n !== null && n.source !== "hosted" || !t.listVoices().some((e) => e.source === "hosted") ? (N({
|
|
472
|
+
event: "notice",
|
|
473
|
+
notice: { kind: "needs-hosted-voice" }
|
|
474
|
+
}), F({ problem: "needs-hosted-voice" }), "speakers") : (_.problem === "needs-hosted-voice" && F({ problem: null }), "avatar");
|
|
475
|
+
},
|
|
476
|
+
createOutput: () => R.createOutput(),
|
|
477
|
+
dispose() {
|
|
478
|
+
if (!M) {
|
|
479
|
+
M = !0, (T !== null || _.phase !== "off") && H("user", {});
|
|
480
|
+
for (let e of X.splice(0)) e();
|
|
481
|
+
v.clear();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
return Z;
|
|
486
|
+
}
|
|
487
|
+
//#endregion
|
|
488
|
+
export { u as LIVE_AVATAR_CONNECT_TIMEOUT_MS, s as LIVE_AVATAR_END_BEFORE_DEADLINE_MS, o as LIVE_AVATAR_IDLE_MS, c as LIVE_AVATAR_REQUEST_TIMEOUT_MS, m as createLiveAvatarController };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AvatarCopy } from '../useAvatarCopy';
|
|
2
|
+
import { LiveAvatarSnapshot } from './liveAvatarController';
|
|
3
|
+
/** The words for the avatar's state line: what went wrong, what it is doing, or null. */
|
|
4
|
+
export declare function liveAvatarStatusText(snapshot: LiveAvatarSnapshot, copy: AvatarCopy): string | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/shared/assistant/avatar/live/liveAvatarStatus.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
if (e.problem === "playback-blocked" && e.audioBlocked) return null;
|
|
4
|
+
if (e.problem !== null) return t.problem(e.problem);
|
|
5
|
+
if (e.phase === "checking") return t.chooserLoading;
|
|
6
|
+
switch (e.notice) {
|
|
7
|
+
case "interrupted": return t.stateInterrupted;
|
|
8
|
+
case "voice-only": return t.stateVoiceOnly;
|
|
9
|
+
case "time-limit": return t.stateTimeLimit;
|
|
10
|
+
default: return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { e as liveAvatarStatusText };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AvatarRoom } from './avatarPorts';
|
|
2
|
+
/**
|
|
3
|
+
* Loads the SDK and returns a new room behind the `AvatarRoom` port. Rejects when the host does not ship the optional peer (the caller
|
|
4
|
+
* treats any rejection as "the avatar is unavailable here").
|
|
5
|
+
*/
|
|
6
|
+
export declare function openLiveKitAvatarRoom(): Promise<AvatarRoom>;
|