@burdenoff/fe-libs 2026.1013.12 → 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.
Files changed (44) hide show
  1. package/dist/shared/assistant/avatar/live/LiveAvatarStage.d.ts +15 -0
  2. package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +139 -0
  3. package/dist/shared/assistant/avatar/live/avatarAudioDecode.d.ts +40 -0
  4. package/dist/shared/assistant/avatar/live/avatarAudioDecode.js +72 -0
  5. package/dist/shared/assistant/avatar/live/avatarAudioErrors.d.ts +25 -0
  6. package/dist/shared/assistant/avatar/live/avatarAudioErrors.js +27 -0
  7. package/dist/shared/assistant/avatar/live/avatarAudioMeter.d.ts +37 -0
  8. package/dist/shared/assistant/avatar/live/avatarAudioMeter.js +62 -0
  9. package/dist/shared/assistant/avatar/live/avatarMp3Inspect.d.ts +30 -0
  10. package/dist/shared/assistant/avatar/live/avatarMp3Inspect.js +168 -0
  11. package/dist/shared/assistant/avatar/live/avatarPcm16.d.ts +21 -0
  12. package/dist/shared/assistant/avatar/live/avatarPcm16.js +54 -0
  13. package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.d.ts +181 -0
  14. package/dist/shared/assistant/avatar/live/avatarPlaybackTracker.js +221 -0
  15. package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +66 -0
  16. package/dist/shared/assistant/avatar/live/avatarPorts.js +44 -0
  17. package/dist/shared/assistant/avatar/live/avatarProtocol.d.ts +22 -0
  18. package/dist/shared/assistant/avatar/live/avatarProtocol.js +27 -0
  19. package/dist/shared/assistant/avatar/live/avatarReplyOutputs.d.ts +170 -0
  20. package/dist/shared/assistant/avatar/live/avatarReplyOutputs.js +525 -0
  21. package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +96 -0
  22. package/dist/shared/assistant/avatar/live/avatarSessionClient.js +261 -0
  23. package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +270 -0
  24. package/dist/shared/assistant/avatar/live/avatarTransport.js +333 -0
  25. package/dist/shared/assistant/avatar/live/avatarTransportPort.d.ts +17 -0
  26. package/dist/shared/assistant/avatar/live/avatarTransportPort.js +27 -0
  27. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +44 -0
  28. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +124 -0
  29. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +133 -0
  30. package/dist/shared/assistant/avatar/live/liveAvatarController.js +488 -0
  31. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +4 -0
  32. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +14 -0
  33. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.d.ts +6 -0
  34. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +86 -0
  35. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +2 -0
  36. package/dist/shared/assistant/avatar/useAvatarCopy.js +1 -0
  37. package/dist/shared/assistant/i18nManifest.d.ts +1 -0
  38. package/dist/shared/assistant/i18nManifest.js +1 -0
  39. package/dist/shared/assistant/index.d.ts +17 -0
  40. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +37 -0
  41. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -63
  42. package/dist/shared/assistant/speech/types.d.ts +14 -2
  43. package/dist/shared-assistant.js +250 -233
  44. package/package.json +2 -1
@@ -0,0 +1,54 @@
1
+ import { PcmFormatError as e } from "./avatarAudioErrors.js";
2
+ //#region src/shared/assistant/avatar/live/avatarPcm16.ts
3
+ var t = 16e3, n = 2;
4
+ function r(e) {
5
+ let t = new Int16Array(e.length);
6
+ for (let n = 0; n < e.length; n += 1) {
7
+ let r = e[n] ?? 0;
8
+ Number.isNaN(r) ? t[n] = 0 : r >= 1 ? t[n] = 32767 : r <= -1 ? t[n] = -32768 : t[n] = Math.round(r < 0 ? r * 32768 : r * 32767);
9
+ }
10
+ return t;
11
+ }
12
+ function i(e) {
13
+ let t = new Uint8Array(e.length * 2), n = new DataView(t.buffer);
14
+ for (let t = 0; t < e.length; t += 1) n.setInt16(t * 2, e[t] ?? 0, !0);
15
+ return t;
16
+ }
17
+ function a(e) {
18
+ return i(r(e));
19
+ }
20
+ function o(e) {
21
+ s(e);
22
+ let t = new Int16Array(e.length / 2), n = new DataView(e.buffer, e.byteOffset, e.byteLength);
23
+ for (let e = 0; e < t.length; e += 1) t[e] = n.getInt16(e * 2, !0);
24
+ return t;
25
+ }
26
+ function s(t) {
27
+ if (t.byteLength % 2 != 0) throw new e(`PCM16 byte length must be even, got ${t.byteLength}`);
28
+ }
29
+ function c(n, r = t) {
30
+ let i = typeof n == "number" ? n : n.byteLength;
31
+ if (!Number.isFinite(i) || i < 0 || i % 2 != 0) throw new e(`PCM16 byte length must be a non-negative even number, got ${i}`);
32
+ if (!(r > 0)) throw new e(`sample rate must be positive, got ${r}`);
33
+ return i / 2 / r;
34
+ }
35
+ function l(n, r = t) {
36
+ if (!Number.isFinite(n) || n <= 0) throw new e(`chunk duration must be positive, got ${n}`);
37
+ return Math.max(1, Math.round(r * n / 1e3));
38
+ }
39
+ function u(e, n, r = t) {
40
+ s(e);
41
+ let i = l(n, r) * 2, a = [];
42
+ for (let t = 0; t < e.byteLength; t += i) a.push(e.subarray(t, Math.min(e.byteLength, t + i)));
43
+ return a;
44
+ }
45
+ function d(e) {
46
+ let t = e[0];
47
+ if (!t) return new Float32Array();
48
+ if (e.length === 1) return t.slice();
49
+ let n = new Float32Array(t.length), r = 1 / e.length;
50
+ for (let t of e) for (let e = 0; e < n.length; e += 1) n[e] = (n[e] ?? 0) + (t[e] ?? 0) * r;
51
+ return n;
52
+ }
53
+ //#endregion
54
+ export { t as AVATAR_SAMPLE_RATE, n as PCM16_BYTES_PER_SAMPLE, o as bytesToInt16LE, u as chunkPcm16, d as downmixToMono, r as floatToInt16, a as floatToPcm16Bytes, i as int16ToBytesLE, c as pcm16DurationOf, l as pcm16SamplesPerChunk };
@@ -0,0 +1,181 @@
1
+ import { AvatarScheduler } from './avatarPorts';
2
+ export interface AvatarTrackerConfig {
3
+ /** After the provider says "started" (or none arrived), how long to wait for the meter to hear audio. */
4
+ readonly audibleGraceMs: number;
5
+ /** Time allowed between the first write and audible playback. */
6
+ readonly startLatencyAllowanceMs: number;
7
+ /** Fixed slack added to the expected duration before "no finish" is declared. */
8
+ readonly finishSlackMs: number;
9
+ /** Extra slack proportional to the expected duration. */
10
+ readonly finishSlackRatio: number;
11
+ /** How long to wait after the provider's finished before onEnd (jitter buffer and output latency). */
12
+ readonly tailMs: number;
13
+ /**
14
+ * While the meter still hears audio after finished, extend the tail by at most this much. Still heard then: the output is silenced
15
+ * before the reply is reported ended (the "completed" outcome's `mutedAtEnd`).
16
+ */
17
+ readonly maxTailExtraMs: number;
18
+ /** Provider position may undershoot the expected duration by this much (or 5 percent) before it is "early". */
19
+ readonly positionToleranceSec: number;
20
+ /** Meter quiet this long, after the expected end, without a provider finish: declare uncertain early. */
21
+ readonly quietGraceMs: number;
22
+ /** false when no meter can be built (then meter-based checks are skipped, never assumed). */
23
+ readonly meterSupported: boolean;
24
+ }
25
+ export declare const DEFAULT_AVATAR_TRACKER_CONFIG: AvatarTrackerConfig;
26
+ export type AvatarFailureCause = 'not-allowed' | 'unsupported' | 'unknown';
27
+ export type AvatarUncertainReason = 'no-finish' | 'no-finish-quiet' | 'early-finish' | 'provider-interrupted' | 'bad-payload' | 'avatar-left' | 'disconnected' | 'silent-playback' | 'stall' | 'synth-failed'
28
+ /** The avatar reported playing part of a reply we were about to retry: something may have been heard, so it is not replayed. */
29
+ | 'played-before-retry';
30
+ export type AvatarBeforeAudioReason =
31
+ /** The avatar still refuses our stream (its answer to our clear was "reject" or "method not supported"). */
32
+ 'not-accepted' | 'not-ready' | 'audio-blocked' | 'avatar-left' | 'disconnected' | 'stall' | 'synth-failed' | 'stream-error'
33
+ /** We wrote audio, but neither the provider nor the meter showed any playback in time: the avatar is not responding. */
34
+ | 'no-start';
35
+ export type AvatarReplyOutcome =
36
+ /**
37
+ * The provider reported the reply played out. `mutedAtEnd`: a meter that can hear still heard audio when the tail wait after that report
38
+ * ran out (tailMs plus maxTailExtraMs), so the output was silenced (stopOutput) BEFORE onEnd. The report claims only what the provider
39
+ * confirmed, that the reply played out; it does not claim that its audio ended by itself, since that end was cut, not heard.
40
+ */
41
+ {
42
+ readonly kind: 'completed';
43
+ readonly mutedAtEnd?: true;
44
+ } | {
45
+ readonly kind: 'cancelled';
46
+ }
47
+ /** Nothing was heard: safe to speak the same reply with the normal voice. */
48
+ | {
49
+ readonly kind: 'failed-before-audio';
50
+ readonly reason: AvatarBeforeAudioReason;
51
+ readonly cause: AvatarFailureCause;
52
+ }
53
+ /** Something may have been heard: never replay automatically; show an interrupted/unavailable state. */
54
+ | {
55
+ readonly kind: 'uncertain';
56
+ readonly reason: AvatarUncertainReason;
57
+ readonly heardAudio: boolean;
58
+ readonly cause: AvatarFailureCause;
59
+ }
60
+ /**
61
+ * Nothing of this reply was written: the avatar's EARLIER output could not be established as stopped (operator decision 7, 2026-10-09).
62
+ * While that output may still play, the reply must not start anywhere: no voice fallback, no retry. It ends as an error.
63
+ */
64
+ | {
65
+ readonly kind: 'output-not-cleared';
66
+ };
67
+ export interface AvatarTrackerCallbacks {
68
+ onStart(): void;
69
+ onChunkStart(index: number): void;
70
+ onEnd(): void;
71
+ /**
72
+ * Stop delivery, silence the output and request the provider clear. Called BEFORE any failure/uncertain outcome is reported, and before a
73
+ * completion whose audible end was cut (`mutedAtEnd`). `overdue`: the tracker stops because the provider's start or finish did not come
74
+ * in time (see isAvatarReportOverdue); a report of this reply that still arrives is presumed lost.
75
+ */
76
+ stopOutput(overdue: boolean): void;
77
+ onOutcome(outcome: AvatarReplyOutcome): void;
78
+ }
79
+ /**
80
+ * The verdicts that mean the provider's report of the reply did not come in time: no start (nothing heard or signalled within the start
81
+ * allowance and the audible grace), or no finish (by the finish deadline, or quiet long after the expected end). A report that still arrives
82
+ * after such a verdict is presumed lost (see AvatarClearOptions.finishOverdue in avatarTransport.ts).
83
+ */
84
+ export declare function isAvatarReportOverdue(reason: AvatarUncertainReason | AvatarBeforeAudioReason): boolean;
85
+ export declare class ReplyPlaybackTracker {
86
+ private readonly callbacks;
87
+ private readonly scheduler;
88
+ private readonly config;
89
+ private done;
90
+ /** The provider confirmed completion: only the tail wait remains; failures and safety nets no longer apply. */
91
+ private finishing;
92
+ private firstWriteAt;
93
+ /** A write completed: bytes really left for the avatar. */
94
+ private deliveredAt;
95
+ private startedAt;
96
+ private streamClosedAt;
97
+ private expectedTotalSec;
98
+ private readonly durations;
99
+ private lastChunkStart;
100
+ private meterAudible;
101
+ private meterEverAudible;
102
+ /**
103
+ * The meter still heard audio when this reply began writing: an earlier reply's tail, which it cannot tell apart from ours. From then
104
+ * on it is ignored for this reply, exactly as if there were none: it neither starts the reply nor proves it silent.
105
+ */
106
+ private meterIgnored;
107
+ /** When the meter first heard audio after our first write. */
108
+ private firstAudibleAt;
109
+ private lastAudibleAt;
110
+ private heardByRpc;
111
+ /** The bound on an unproven start (see firstWrite), until streamEnding arms the reply's own deadline. */
112
+ private cancelStartWait;
113
+ private readonly cancels;
114
+ constructor(callbacks: AvatarTrackerCallbacks, scheduler: AvatarScheduler, config?: AvatarTrackerConfig);
115
+ get isDone(): boolean;
116
+ get heard(): boolean;
117
+ /**
118
+ * Nothing of this reply can have been heard so far, by the same proof a failure uses (see provablySilent). Read-only, added for the
119
+ * avatar output (BOFF-8822 P8): when the person switches the avatar off mid-reply, a reply that provably played nothing hands every
120
+ * piece to the speakers, and one that may have been heard hands over only the pieces that cannot have begun.
121
+ */
122
+ get nothingHeard(): boolean;
123
+ /**
124
+ * The avatar's start latency as this reply measured it: from the first write until the meter first heard audio (the meter's attack and the
125
+ * audio's own leading silence included). Null until then, and for a reply whose meter is ignored (see meterIgnored).
126
+ */
127
+ get audibleAfterMs(): number | null;
128
+ /** The meter's evidence counts for this reply: there is one that can hear, and it was not still hearing earlier audio when we began. */
129
+ private get meterUsable();
130
+ firstWrite(): void;
131
+ /** A write completed (call after awaiting it). From now on silence must be PROVEN before a reply may go to the voice. */
132
+ delivered(): void;
133
+ chunkReady(index: number, durationSec: number): void;
134
+ /**
135
+ * Call BEFORE awaiting segment.close(): the provider can report finished before the close promise resolves,
136
+ * and a finished that arrives while the stream still looks open would be misread as an early finish.
137
+ */
138
+ streamEnding(totalDurationSec: number): void;
139
+ providerStarted(): void;
140
+ /** Provisional: a level meter on the avatar's received audio. Never used to report completion. */
141
+ meter(audible: boolean): void;
142
+ providerFinished(positionSec: number | null, interrupted: boolean | null): void;
143
+ avatarLeft(): void;
144
+ disconnected(): void;
145
+ audioBlocked(): void;
146
+ /** The next chunk did not arrive in time. */
147
+ stalled(): void;
148
+ /** Synthesis or the stream failed. */
149
+ failed(reason: 'synth-failed' | 'stream-error' | 'not-ready' | 'not-accepted', cause?: AvatarFailureCause): void;
150
+ /**
151
+ * Before this reply wrote anything: the avatar's earlier output could not be established as stopped, so the reply must not start (see
152
+ * the "output-not-cleared" outcome).
153
+ */
154
+ outputNotCleared(): void;
155
+ cancel(): void;
156
+ /**
157
+ * How long the provider gets to finish `durationSec` of audio once it was handed over: the start allowance, the audio itself, and the
158
+ * fixed and proportional finish slack. The one budget for streamEnding's deadline and for an unproven start (see firstWrite).
159
+ */
160
+ private noFinishBudgetMs;
161
+ /** The audio of every piece announced so far (chunkReady), in seconds: at the first write, the piece being written. */
162
+ private handedSec;
163
+ /**
164
+ * Nothing can have been heard: nothing reached the avatar, or a meter that can really hear stayed silent and the provider signalled nothing.
165
+ * Without that proof a failure is reported as uncertain, so the voice never repeats speech the user may have heard.
166
+ */
167
+ private provablySilent;
168
+ private failOrUncertain;
169
+ private markStarted;
170
+ private emitChunkStart;
171
+ private scheduleChunkStart;
172
+ private finishAfterTail;
173
+ /** After the expected end, a quiet meter and no finish from the provider means we cannot confirm completion. */
174
+ private pollQuiet;
175
+ private becomeUncertain;
176
+ private becomeFailedBeforeAudio;
177
+ /** Runs `fn` after `ms` unless the reply ends first; returns a function that cancels it sooner. */
178
+ private later;
179
+ private finish;
180
+ private clearTimers;
181
+ }
@@ -0,0 +1,221 @@
1
+ //#region src/shared/assistant/avatar/live/avatarPlaybackTracker.ts
2
+ var e = {
3
+ audibleGraceMs: 3e3,
4
+ startLatencyAllowanceMs: 3e3,
5
+ finishSlackMs: 3e3,
6
+ finishSlackRatio: .25,
7
+ tailMs: 350,
8
+ maxTailExtraMs: 1e3,
9
+ positionToleranceSec: .5,
10
+ quietGraceMs: 2e3,
11
+ meterSupported: !0
12
+ };
13
+ function t(e) {
14
+ return e === "no-start" || e === "no-finish" || e === "no-finish-quiet";
15
+ }
16
+ var n = class {
17
+ callbacks;
18
+ scheduler;
19
+ config;
20
+ done = !1;
21
+ finishing = !1;
22
+ firstWriteAt = null;
23
+ deliveredAt = null;
24
+ startedAt = null;
25
+ streamClosedAt = null;
26
+ expectedTotalSec = null;
27
+ durations = [];
28
+ lastChunkStart = -1;
29
+ meterAudible = !1;
30
+ meterEverAudible = !1;
31
+ meterIgnored = !1;
32
+ firstAudibleAt = null;
33
+ lastAudibleAt = null;
34
+ heardByRpc = !1;
35
+ cancelStartWait = null;
36
+ cancels = /* @__PURE__ */ new Set();
37
+ constructor(t, n, r = e) {
38
+ this.callbacks = t, this.scheduler = n, this.config = r;
39
+ }
40
+ get isDone() {
41
+ return this.done;
42
+ }
43
+ get heard() {
44
+ return this.startedAt !== null;
45
+ }
46
+ get nothingHeard() {
47
+ return this.provablySilent();
48
+ }
49
+ get audibleAfterMs() {
50
+ return this.firstAudibleAt === null || this.firstWriteAt === null ? null : this.firstAudibleAt - this.firstWriteAt;
51
+ }
52
+ get meterUsable() {
53
+ return this.config.meterSupported && !this.meterIgnored;
54
+ }
55
+ firstWrite() {
56
+ if (!(this.done || this.firstWriteAt !== null)) {
57
+ if (this.firstWriteAt = this.scheduler.now(), this.meterAudible &&= (this.meterIgnored = !0, !1), this.meterUsable) {
58
+ this.later(this.config.startLatencyAllowanceMs + this.config.audibleGraceMs, () => {
59
+ this.startedAt === null && this.becomeFailedBeforeAudio("no-start", "unknown");
60
+ });
61
+ return;
62
+ }
63
+ this.cancelStartWait = this.later(this.noFinishBudgetMs(this.handedSec()), () => {
64
+ this.startedAt === null && this.becomeUncertain("no-finish");
65
+ });
66
+ }
67
+ }
68
+ delivered() {
69
+ this.done || this.deliveredAt !== null || (this.deliveredAt = this.scheduler.now());
70
+ }
71
+ chunkReady(e, t) {
72
+ this.done || (this.durations[e] = t, this.startedAt !== null && this.scheduleChunkStart(e));
73
+ }
74
+ streamEnding(e) {
75
+ this.done || this.streamClosedAt !== null || (this.streamClosedAt = this.scheduler.now(), this.expectedTotalSec = e, this.cancelStartWait?.(), this.cancelStartWait = null, this.later(this.noFinishBudgetMs(e), () => {
76
+ this.meterUsable && this.startedAt === null && !this.meterEverAudible ? this.becomeFailedBeforeAudio("no-start", "unknown") : this.becomeUncertain("no-finish");
77
+ }), this.pollQuiet());
78
+ }
79
+ providerStarted() {
80
+ this.done || this.firstWriteAt === null || (this.heardByRpc = !0, this.markStarted("rpc"));
81
+ }
82
+ meter(e) {
83
+ this.done || this.meterIgnored || (this.meterAudible = e, e && this.firstWriteAt !== null && (this.meterEverAudible = !0, this.lastAudibleAt = this.scheduler.now(), this.firstAudibleAt ??= this.lastAudibleAt, this.markStarted("meter")));
84
+ }
85
+ providerFinished(e, t) {
86
+ if (!this.done) {
87
+ if (e === null || t === null) return this.becomeUncertain("bad-payload");
88
+ if (t) return this.becomeUncertain("provider-interrupted");
89
+ if (this.streamClosedAt === null || this.expectedTotalSec === null || e + Math.max(this.config.positionToleranceSec, .05 * this.expectedTotalSec) < this.expectedTotalSec) return this.becomeUncertain("early-finish");
90
+ this.markStarted("finish"), this.finishing = !0, this.clearTimers(), this.finishAfterTail();
91
+ }
92
+ }
93
+ avatarLeft() {
94
+ this.failOrUncertain("avatar-left");
95
+ }
96
+ disconnected() {
97
+ this.failOrUncertain("disconnected");
98
+ }
99
+ audioBlocked() {
100
+ if (!this.done) {
101
+ if (this.startedAt === null) return this.becomeFailedBeforeAudio("audio-blocked", "unknown");
102
+ this.becomeUncertain("silent-playback");
103
+ }
104
+ }
105
+ stalled() {
106
+ this.failOrUncertain("stall");
107
+ }
108
+ failed(e, t = "unknown") {
109
+ if (!this.done) {
110
+ if (this.provablySilent()) return this.becomeFailedBeforeAudio(e, t);
111
+ this.becomeUncertain(e === "synth-failed" ? "synth-failed" : "stall", t);
112
+ }
113
+ }
114
+ outputNotCleared() {
115
+ this.done || (this.finish(), this.callbacks.stopOutput(!1), this.callbacks.onOutcome({ kind: "output-not-cleared" }));
116
+ }
117
+ cancel() {
118
+ this.done || (this.finish(), this.callbacks.stopOutput(!1), this.callbacks.onOutcome({ kind: "cancelled" }));
119
+ }
120
+ noFinishBudgetMs(e) {
121
+ let t = this.config.finishSlackMs + this.config.finishSlackRatio * e * 1e3;
122
+ return this.config.startLatencyAllowanceMs + e * 1e3 + t;
123
+ }
124
+ handedSec() {
125
+ return this.durations.reduce((e, t) => e + t, 0);
126
+ }
127
+ provablySilent() {
128
+ return this.startedAt !== null || this.heardByRpc || this.meterEverAudible ? !1 : this.deliveredAt === null ? !0 : this.meterUsable;
129
+ }
130
+ failOrUncertain(e) {
131
+ if (!(this.done || this.finishing)) {
132
+ if (this.provablySilent()) return this.becomeFailedBeforeAudio(e, "unknown");
133
+ this.becomeUncertain(e);
134
+ }
135
+ }
136
+ markStarted(e) {
137
+ if (!(this.done || this.startedAt !== null)) {
138
+ this.startedAt = this.scheduler.now(), this.emitChunkStart(0), this.callbacks.onStart();
139
+ for (let e = 1; e < this.durations.length; e += 1) this.scheduleChunkStart(e);
140
+ e === "rpc" && this.meterUsable && !this.meterEverAudible && this.later(this.config.audibleGraceMs, () => {
141
+ this.meterEverAudible || this.becomeUncertain("silent-playback");
142
+ });
143
+ }
144
+ }
145
+ emitChunkStart(e) {
146
+ this.done || e <= this.lastChunkStart || (this.lastChunkStart = e, this.callbacks.onChunkStart(e));
147
+ }
148
+ scheduleChunkStart(e) {
149
+ if (this.startedAt === null || e === 0 || this.durations[e] === void 0) return;
150
+ let t = 0;
151
+ for (let n = 0; n < e; n += 1) t += this.durations[n] ?? 0;
152
+ let n = this.startedAt + t * 1e3 - this.scheduler.now();
153
+ this.later(Math.max(0, n), () => this.emitChunkStart(e));
154
+ }
155
+ finishAfterTail() {
156
+ let e = this.scheduler.now(), t = () => {
157
+ if (this.done) return;
158
+ let n = this.scheduler.now() - e;
159
+ if (!(this.meterUsable && this.meterAudible)) {
160
+ this.finish(), this.callbacks.onEnd(), this.callbacks.onOutcome({ kind: "completed" });
161
+ return;
162
+ }
163
+ if (n >= this.config.tailMs + this.config.maxTailExtraMs) {
164
+ this.finish(), this.callbacks.stopOutput(!1), this.callbacks.onEnd(), this.callbacks.onOutcome({
165
+ kind: "completed",
166
+ mutedAtEnd: !0
167
+ });
168
+ return;
169
+ }
170
+ this.later(100, t);
171
+ };
172
+ this.later(this.config.tailMs, t);
173
+ }
174
+ pollQuiet() {
175
+ if (this.done || !this.meterUsable) return;
176
+ let e = () => {
177
+ if (this.done) return;
178
+ let t = this.scheduler.now(), n = (this.startedAt ?? (this.firstWriteAt ?? t) + this.config.startLatencyAllowanceMs) + (this.expectedTotalSec ?? 0) * 1e3, r = this.meterAudible ? 0 : t - (this.lastAudibleAt ?? this.firstWriteAt ?? t);
179
+ if (this.startedAt !== null && t >= n && r >= this.config.quietGraceMs) {
180
+ this.becomeUncertain("no-finish-quiet");
181
+ return;
182
+ }
183
+ this.later(250, e);
184
+ };
185
+ this.later(250, e);
186
+ }
187
+ becomeUncertain(e, n = "unknown") {
188
+ if (this.done || this.finishing) return;
189
+ let r = this.startedAt !== null || this.heardByRpc || this.meterEverAudible;
190
+ this.finish(), this.callbacks.stopOutput(t(e)), this.callbacks.onOutcome({
191
+ kind: "uncertain",
192
+ reason: e,
193
+ heardAudio: r,
194
+ cause: n
195
+ });
196
+ }
197
+ becomeFailedBeforeAudio(e, n) {
198
+ this.done || this.finishing || (this.finish(), this.callbacks.stopOutput(t(e)), this.callbacks.onOutcome({
199
+ kind: "failed-before-audio",
200
+ reason: e,
201
+ cause: n
202
+ }));
203
+ }
204
+ later(e, t) {
205
+ let n = this.scheduler.setTimeout(() => {
206
+ this.cancels.delete(n), t();
207
+ }, e);
208
+ return this.cancels.add(n), () => {
209
+ this.cancels.delete(n), n();
210
+ };
211
+ }
212
+ finish() {
213
+ this.done = !0, this.clearTimers();
214
+ }
215
+ clearTimers() {
216
+ for (let e of [...this.cancels]) e();
217
+ this.cancels.clear();
218
+ }
219
+ };
220
+ //#endregion
221
+ export { e as DEFAULT_AVATAR_TRACKER_CONFIG, n as ReplyPlaybackTracker, t as isAvatarReportOverdue };
@@ -0,0 +1,66 @@
1
+ export type AvatarUnsubscribe = () => void;
2
+ export interface AvatarScheduler {
3
+ /** Milliseconds, monotonic. */
4
+ now(): number;
5
+ /** Returns a cancel function. */
6
+ setTimeout(fn: () => void, ms: number): () => void;
7
+ }
8
+ export declare const realAvatarScheduler: AvatarScheduler;
9
+ /**
10
+ * @internal A fake clock for tests: timers only fire when advance() is called. Exported for the avatar suites (the assistant barrel
11
+ * star-exports this module); production code uses `realAvatarScheduler`.
12
+ */
13
+ export declare class FakeAvatarScheduler implements AvatarScheduler {
14
+ private current;
15
+ private nextId;
16
+ private readonly timers;
17
+ now(): number;
18
+ setTimeout(fn: () => void, ms: number): () => void;
19
+ /** Advance time, firing due timers in order (timers scheduled while advancing are honoured). */
20
+ advance(ms: number): void;
21
+ pendingTimers(): number;
22
+ }
23
+ export interface AvatarByteWriter {
24
+ write(chunk: Uint8Array): Promise<void>;
25
+ close(): Promise<void>;
26
+ }
27
+ export interface AvatarRemoteTrack {
28
+ readonly identity: string;
29
+ readonly kind: 'audio' | 'video';
30
+ attach(element: HTMLMediaElement): void;
31
+ detach(): void;
32
+ /** The raw track for WebAudio metering; null when the platform does not expose it. */
33
+ readonly mediaStreamTrack: MediaStreamTrack | null;
34
+ }
35
+ export declare class AvatarRpcRejectedError extends Error {
36
+ constructor(message: string);
37
+ }
38
+ /** Everything the transport needs from a LiveKit room. */
39
+ export interface AvatarRoom {
40
+ connect(url: string, token: string): Promise<void>;
41
+ disconnect(): Promise<void>;
42
+ isConnected(): boolean;
43
+ hasParticipant(identity: string): boolean;
44
+ hasVideoTrack(identity: string): boolean;
45
+ onParticipantConnected(handler: (identity: string) => void): AvatarUnsubscribe;
46
+ onParticipantDisconnected(handler: (identity: string) => void): AvatarUnsubscribe;
47
+ onTrackSubscribed(handler: (track: AvatarRemoteTrack) => void): AvatarUnsubscribe;
48
+ onDisconnected(handler: () => void): AvatarUnsubscribe;
49
+ onReconnecting(handler: () => void): AvatarUnsubscribe;
50
+ onReconnected(handler: () => void): AvatarUnsubscribe;
51
+ onAudioPlaybackChanged(handler: (canPlay: boolean) => void): AvatarUnsubscribe;
52
+ /** The handler may throw AvatarRpcRejectedError to reject the call. Returns the response payload. */
53
+ registerRpc(method: string, handler: (callerIdentity: string, payload: string) => string): void;
54
+ unregisterRpc(method: string): void;
55
+ performRpc(destinationIdentity: string, method: string, payload: string, timeoutMs: number): Promise<string>;
56
+ openByteStream(options: {
57
+ topic: string;
58
+ name: string;
59
+ destinationIdentity: string;
60
+ attributes: Record<string, string>;
61
+ }): Promise<AvatarByteWriter>;
62
+ /** Resume audio playback after a user gesture. */
63
+ startAudio(): Promise<void>;
64
+ /** Whether the browser currently lets remote audio play (false until a user gesture while autoplay is blocked). */
65
+ canPlaybackAudio(): boolean;
66
+ }
@@ -0,0 +1,44 @@
1
+ //#region src/shared/assistant/avatar/live/avatarPorts.ts
2
+ var e = {
3
+ now: () => performance.now(),
4
+ setTimeout: (e, t) => {
5
+ let n = setTimeout(e, t);
6
+ return () => clearTimeout(n);
7
+ }
8
+ }, t = class {
9
+ current = 0;
10
+ nextId = 1;
11
+ timers = /* @__PURE__ */ new Map();
12
+ now() {
13
+ return this.current;
14
+ }
15
+ setTimeout(e, t) {
16
+ let n = this.nextId++;
17
+ return this.timers.set(n, {
18
+ at: this.current + Math.max(0, t),
19
+ fn: e
20
+ }), () => {
21
+ this.timers.delete(n);
22
+ };
23
+ }
24
+ advance(e) {
25
+ let t = this.current + e;
26
+ for (;;) {
27
+ let e = null, n = Infinity;
28
+ for (let [r, i] of this.timers) i.at <= t && i.at < n && (n = i.at, e = r);
29
+ if (e === null) break;
30
+ let r = this.timers.get(e);
31
+ this.timers.delete(e), r && (this.current = Math.max(this.current, r.at), r.fn());
32
+ }
33
+ this.current = t;
34
+ }
35
+ pendingTimers() {
36
+ return this.timers.size;
37
+ }
38
+ }, n = class extends Error {
39
+ constructor(e) {
40
+ super(e), this.name = "AvatarRpcRejectedError";
41
+ }
42
+ };
43
+ //#endregion
44
+ export { n as AvatarRpcRejectedError, t as FakeAvatarScheduler, e as realAvatarScheduler };
@@ -0,0 +1,22 @@
1
+ export declare const AVATAR_AUDIO_STREAM_TOPIC = "lk.audio_stream";
2
+ export declare const AVATAR_RPC_CLEAR_BUFFER = "lk.clear_buffer";
3
+ export declare const AVATAR_RPC_PLAYBACK_FINISHED = "lk.playback_finished";
4
+ export declare const AVATAR_RPC_PLAYBACK_STARTED = "lk.playback_started";
5
+ export declare const AVATAR_PCM_SAMPLE_RATE = 16000;
6
+ export declare const AVATAR_PCM_CHANNELS = 1;
7
+ /** PCM16 mono at 16 kHz. */
8
+ export declare const AVATAR_PCM_BYTES_PER_SECOND: number;
9
+ /** The reference sender waits this long for playback_finished(interrupted) after clear_buffer. */
10
+ export declare const AVATAR_CLEAR_BUFFER_TIMEOUT_MS = 2000;
11
+ export declare function avatarPcmDurationSec(byteLength: number): number;
12
+ /** Attributes the receiver REQUIRES on every audio byte stream (strings). */
13
+ export declare function avatarSegmentAttributes(): Record<string, string>;
14
+ export interface AvatarPlaybackFinishedPayload {
15
+ readonly positionSec: number;
16
+ readonly interrupted: boolean;
17
+ }
18
+ /**
19
+ * Parse the lk.playback_finished RPC payload. Returns null when it is not the
20
+ * documented shape: callers must treat that as "completion unknown", never as success.
21
+ */
22
+ export declare function parseAvatarPlaybackFinished(payload: string): AvatarPlaybackFinishedPayload | null;
@@ -0,0 +1,27 @@
1
+ //#region src/shared/assistant/avatar/live/avatarProtocol.ts
2
+ var e = "lk.audio_stream", t = "lk.clear_buffer", n = "lk.playback_finished", r = "lk.playback_started", i = 16e3, a = 1, o = i * 1 * 2, s = 2e3;
3
+ function c(e) {
4
+ return e / o;
5
+ }
6
+ function l() {
7
+ return {
8
+ sample_rate: String(i),
9
+ num_channels: "1"
10
+ };
11
+ }
12
+ function u(e) {
13
+ let t;
14
+ try {
15
+ t = JSON.parse(e);
16
+ } catch {
17
+ return null;
18
+ }
19
+ if (typeof t != "object" || !t) return null;
20
+ let n = t, r = n.playback_position, i = n.interrupted;
21
+ return typeof r != "number" || !Number.isFinite(r) || r < 0 || typeof i != "boolean" ? null : {
22
+ positionSec: r,
23
+ interrupted: i
24
+ };
25
+ }
26
+ //#endregion
27
+ export { e as AVATAR_AUDIO_STREAM_TOPIC, s as AVATAR_CLEAR_BUFFER_TIMEOUT_MS, o as AVATAR_PCM_BYTES_PER_SECOND, a as AVATAR_PCM_CHANNELS, i as AVATAR_PCM_SAMPLE_RATE, t as AVATAR_RPC_CLEAR_BUFFER, n as AVATAR_RPC_PLAYBACK_FINISHED, r as AVATAR_RPC_PLAYBACK_STARTED, c as avatarPcmDurationSec, l as avatarSegmentAttributes, u as parseAvatarPlaybackFinished };