@burdenoff/fe-libs 2026.1013.9 → 2026.1013.10

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.
@@ -44,6 +44,7 @@ export * from './speech/browserSpeechEngine';
44
44
  export * from './speech/hostedTtsSpeechEngine';
45
45
  export * from './speech/routingSpeechEngine';
46
46
  export * from './speech/hostedVoiceCatalogue';
47
+ export * from './speech/pieceSink';
47
48
  export * from './speech/speechFeed';
48
49
  export * from './voice/useVoiceInput';
49
50
  export * from './voice/voiceStageState';
@@ -1,23 +1,24 @@
1
1
  import { tapPlaybackElement as e } from "../voice/handsfree/playbackReference.js";
2
2
  import { HOSTED_TTS_TIER as t, HOSTED_VOICES as n, hostedLocaleFor as r } from "./hostedVoiceCatalogue.js";
3
3
  import { buildRequest as i, classifyHttpFailure as a, decodeAudioResponse as o, disableDurationMs as s, shouldDisableEngine as c, speakableChunks as l } from "./hostedTtsProtocol.js";
4
+ import { createPieceSink as u } from "./pieceSink.js";
4
5
  //#region src/shared/assistant/speech/hostedTtsSpeechEngine.ts
5
- function u(e) {
6
+ function d(e) {
6
7
  let t = Object.entries(e).map(([e, t]) => `${e.toLowerCase()}:${t}`).sort().join("\n"), n = 5381;
7
8
  for (let e = 0; e < t.length; e += 1) n = (n << 5) + n + t.charCodeAt(e) | 0;
8
9
  return `${t.length}:${n >>> 0}`;
9
10
  }
10
- var d = new Set([
11
+ var f = new Set([
11
12
  429,
12
13
  502,
13
14
  503,
14
15
  504
15
- ]), f = 3, p = 400, m = 2e3;
16
- function h(e, t) {
16
+ ]), p = 3, m = 400, h = 2e3;
17
+ function g(e, t) {
17
18
  let n = e === null ? NaN : Number(e);
18
- return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : p * 2 ** (t - 1), m);
19
+ return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : m * 2 ** (t - 1), h);
19
20
  }
20
- function g(e, t) {
21
+ function _(e, t) {
21
22
  return new Promise((n) => {
22
23
  if (t.aborted) {
23
24
  n();
@@ -32,44 +33,44 @@ function g(e, t) {
32
33
  t.addEventListener("abort", i, { once: !0 });
33
34
  });
34
35
  }
35
- function _(p) {
36
- let m = p.fetchImpl ?? ((...e) => fetch(...e)), _ = p.now ?? (() => Date.now()), v = null, y = null, b = 0, x = null, S = 0, C = null, w = null, T = () => {
36
+ function v(m) {
37
+ let h = m.fetchImpl ?? ((...e) => fetch(...e)), v = m.now ?? (() => Date.now()), y = null, b = null, x = 0, S = null, C = null, w = 0, T = null, E = null, D = () => {
37
38
  try {
38
- return p.getHeaders?.() ?? {};
39
+ return m.getHeaders?.() ?? {};
39
40
  } catch {
40
41
  return {};
41
42
  }
42
- }, E = () => _() >= S ? !1 : w === null ? !0 : u(T()) === w, D = /* @__PURE__ */ new Set(), O = () => {
43
- for (let e of [...D]) e();
44
- }, k = () => {
45
- C !== null && clearTimeout(C), C = null;
46
- }, A = (e) => {
47
- k(), C = setTimeout(() => {
48
- C = null;
49
- let e = S - _();
43
+ }, O = () => v() >= w ? !1 : E === null ? !0 : d(D()) === E, k = /* @__PURE__ */ new Set(), A = () => {
44
+ for (let e of [...k]) e();
45
+ }, j = () => {
46
+ T !== null && clearTimeout(T), T = null;
47
+ }, M = (e) => {
48
+ j(), T = setTimeout(() => {
49
+ T = null;
50
+ let e = w - v();
50
51
  if (e > 0) {
51
- A(e);
52
+ M(e);
52
53
  return;
53
54
  }
54
- O();
55
+ A();
55
56
  }, e);
56
- }, j = (e, t) => {
57
- S = _() + e, w = t, A(e), O();
58
- }, M = () => {
59
- y &&= (URL.revokeObjectURL(y), null);
60
- }, N = () => {
61
- if (v) {
62
- v.onended = null, v.onerror = null;
57
+ }, N = (e, t) => {
58
+ w = v() + e, E = t, M(e), A();
59
+ }, P = () => {
60
+ b &&= (URL.revokeObjectURL(b), null);
61
+ }, F = () => {
62
+ if (y) {
63
+ y.onended = null, y.onerror = null;
63
64
  try {
64
- v.pause();
65
+ y.pause();
65
66
  } catch {}
66
- v = null;
67
+ y = null;
67
68
  }
68
- M();
69
+ P();
69
70
  };
70
- async function P(e, n, s) {
71
- let c = i(e, r(n.lang) ?? n.lang, t, n.voice?.name ?? null, n.rate), l = p.getHeaders?.() ?? {}, _;
72
- for (let e = 1; _ = await m(p.endpoint, {
71
+ async function I(e, n, s) {
72
+ let c = i(e, r(n.lang) ?? n.lang, t, n.voice?.name ?? null, n.rate), l = m.getHeaders?.() ?? {}, u;
73
+ for (let e = 1; u = await h(m.endpoint, {
73
74
  method: "POST",
74
75
  signal: s,
75
76
  headers: {
@@ -77,42 +78,42 @@ function _(p) {
77
78
  ...l
78
79
  },
79
80
  body: JSON.stringify(c)
80
- }), !_.ok; e += 1) {
81
- if (!(d.has(_.status) && e < f && !s.aborted)) {
82
- let e = /* @__PURE__ */ Error(`hosted tts ${_.status}`);
83
- throw e.reason = a(_.status), e.headers = u(l), e;
81
+ }), !u.ok; e += 1) {
82
+ if (!(f.has(u.status) && e < p && !s.aborted)) {
83
+ let e = /* @__PURE__ */ Error(`hosted tts ${u.status}`);
84
+ throw e.reason = a(u.status), e.headers = d(l), e;
84
85
  }
85
- await g(h(_.headers.get("retry-after"), e), s);
86
+ await _(g(u.headers.get("retry-after"), e), s);
86
87
  }
87
- let v = _.headers.get("content-type"), y = o(v, v?.includes("json") ? await _.json() : await _.arrayBuffer());
88
+ let v = u.headers.get("content-type"), y = o(v, v?.includes("json") ? await u.json() : await u.arrayBuffer());
88
89
  if (!y) {
89
90
  let e = /* @__PURE__ */ Error("hosted tts returned no audio");
90
91
  throw e.reason = "unknown", e;
91
92
  }
92
93
  return y;
93
94
  }
94
- function F(t, n, r) {
95
+ function L(t, n, r) {
95
96
  return new Promise((i, a) => {
96
- if (n !== b) {
97
+ if (n !== x) {
97
98
  i();
98
99
  return;
99
100
  }
100
101
  let o = new Blob([t.slice().buffer], { type: "audio/mpeg" });
101
- M(), y = URL.createObjectURL(o);
102
- let s = new Audio(y);
103
- e(s), v = s, s.onended = () => {
104
- n === b && i();
102
+ P(), b = URL.createObjectURL(o);
103
+ let s = new Audio(b);
104
+ e(s), y = s, s.onended = () => {
105
+ n === x && i();
105
106
  }, s.onerror = () => {
106
- if (n !== b) {
107
+ if (n !== x) {
107
108
  i();
108
109
  return;
109
110
  }
110
111
  let e = /* @__PURE__ */ Error("audio playback failed");
111
112
  e.reason = "unknown", a(e);
112
113
  }, s.play().then(() => {
113
- n === b && r?.();
114
+ n === x && r?.();
114
115
  }, (e) => {
115
- if (n !== b) {
116
+ if (n !== x) {
116
117
  i();
117
118
  return;
118
119
  }
@@ -124,14 +125,14 @@ function _(p) {
124
125
  return {
125
126
  id: "hosted-tts",
126
127
  isSupported() {
127
- return E() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
128
+ return O() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
128
129
  },
129
130
  listVoices() {
130
- return E() ? [] : [...n];
131
+ return O() ? [] : [...n];
131
132
  },
132
133
  subscribeVoices(e) {
133
- return D.add(e), () => {
134
- D.delete(e);
134
+ return k.add(e), () => {
135
+ k.delete(e);
135
136
  };
136
137
  },
137
138
  speak(e, t, n = {}) {
@@ -140,43 +141,61 @@ function _(p) {
140
141
  n.onEnd?.();
141
142
  return;
142
143
  }
143
- b += 1;
144
- let i = b;
145
- N();
144
+ x += 1;
145
+ let i = x;
146
+ F(), C?.cancel();
146
147
  let a = new AbortController();
147
- x = a;
148
- let o = !1, u = () => {
148
+ S = a;
149
+ let o = !1, d = () => {
149
150
  o || (o = !0, n.onStart?.());
150
- }, d = (e) => {
151
- let n = P(e, t, a.signal);
151
+ }, f = (e) => {
152
+ let n = I(e, t, a.signal);
152
153
  return n.catch(() => {}), n;
153
- };
154
- (async () => {
154
+ }, p = t.audioOutput ? u(t.audioOutput, {
155
+ play: (e) => L(e, i, d),
156
+ isCurrent: () => i === x,
157
+ onChunkStart: (e) => n.onChunkStart?.(e),
158
+ onStart: d
159
+ }) : null;
160
+ C = p, (async () => {
155
161
  try {
156
- let e = d(r[0]);
162
+ let e = f(r[0]);
157
163
  for (let t = 0; t < r.length; t += 1) {
158
- if (i !== b) return;
164
+ if (i !== x) return;
159
165
  let a = await e;
160
- if (i !== b) return;
166
+ if (i !== x) return;
161
167
  let o = r[t + 1];
162
- o !== void 0 && (e = d(o)), n.onChunkStart?.(t), await F(a, i, u);
168
+ if (o !== void 0 && (e = f(o)), p) {
169
+ await p.write(a);
170
+ continue;
171
+ }
172
+ n.onChunkStart?.(t), await L(a, i, d);
163
173
  }
164
- i === b && n.onEnd?.();
174
+ if (p) {
175
+ let e = await p.drain();
176
+ if (i !== x) return;
177
+ if (!e) throw Object.assign(/* @__PURE__ */ Error("audio output did not finish"), { reason: "unknown" });
178
+ }
179
+ i === x && n.onEnd?.();
165
180
  } catch (e) {
166
- if (a.abort(), i !== b) return;
167
- b += 1, N();
181
+ if (a.abort(), i !== x) return;
182
+ if (p) {
183
+ if (await p.drain().catch(() => !1), i !== x) return;
184
+ p.cancel();
185
+ }
186
+ x += 1, F();
168
187
  let t = e?.reason;
169
- c(t, e?.transient === !0) && j(s(t), t === "not-allowed" ? e.headers ?? null : null), n.onError?.(t === "not-allowed" || t === "unsupported" || t === "cancelled" ? t : "unknown");
188
+ c(t, e?.transient === !0) && N(s(t), t === "not-allowed" ? e.headers ?? null : null), n.onError?.(t === "not-allowed" || t === "unsupported" || t === "cancelled" ? t : "unknown");
170
189
  }
171
190
  })();
172
191
  },
173
192
  headersChanged() {
174
- _() >= S || w === null || E() || (S = 0, w = null, k(), O());
193
+ v() >= w || E === null || O() || (w = 0, E = null, j(), A());
175
194
  },
176
195
  cancel() {
177
- b += 1, x?.abort(), x = null, N();
196
+ x += 1, S?.abort(), S = null, F(), C?.cancel(), C = null;
178
197
  }
179
198
  };
180
199
  }
181
200
  //#endregion
182
- export { _ as createHostedTtsEngine, h as retryDelayMs };
201
+ export { v as createHostedTtsEngine, g as retryDelayMs };
@@ -0,0 +1,43 @@
1
+ import { SpeechAudioOutput } from './types';
2
+ /** @internal What the sink needs from the reply it belongs to. */
3
+ export interface PieceSinkReply {
4
+ /** Plays one piece on the speakers and resolves when it has finished: the engine's own `play`. */
5
+ play(bytes: Uint8Array): Promise<void>;
6
+ /** False once the reply was cancelled or replaced: nothing more is played. */
7
+ isCurrent(): boolean;
8
+ /** Piece `index` begins: the live caption's sync signal. */
9
+ onChunkStart(index: number): void;
10
+ /** The reply's first sound (the engine announces it once per reply). */
11
+ onStart(): void;
12
+ }
13
+ /** @internal The engine's side of one reply's audio output. */
14
+ export interface PieceSink {
15
+ /**
16
+ * Hand over the reply's next piece. Pieces are numbered 0, 1, 2… by ONE
17
+ * counter for the whole reply: a queue's count continues across appends and
18
+ * across drains, and a retracted number is never given again. Resolves when
19
+ * the next piece may follow; rejects as `drain` does once the speakers fail.
20
+ */
21
+ write(bytes: Uint8Array): Promise<void>;
22
+ /**
23
+ * Resolves once everything written so far has finished playing: `true`, or
24
+ * `false` when the output FAILED after every piece had begun, so the end was
25
+ * never confirmed (a leave is not a failure: `true`). Callable any number of
26
+ * times; writing may go on after it, even while it is pending (a piece written
27
+ * meanwhile is not part of it). Never rejects with the output's reason, only
28
+ * with the speakers' own failure, which the engine reports as it always has.
29
+ * Once the speakers have failed, every later or queued `write` and `drain`
30
+ * rejects with that same failure until `cancel()`: nothing more plays, and
31
+ * nothing is confirmed.
32
+ */
33
+ drain(): Promise<boolean>;
34
+ /**
35
+ * Withdraw every piece from `fromIndex` on that has not begun, on the output
36
+ * and in the record a failure replays from; the one playing finishes.
37
+ */
38
+ retract(fromIndex: number): void;
39
+ /** The hard stop: silence the output and drop everything. Nothing is replayed. */
40
+ cancel(): void;
41
+ }
42
+ /** @internal One reply's piece sink, playing on `output` until it fails or leaves. */
43
+ export declare function createPieceSink(output: SpeechAudioOutput, reply: PieceSinkReply): PieceSink;
@@ -0,0 +1,84 @@
1
+ import { SPEECH_OUTPUT_LEFT as e } from "./types.js";
2
+ //#region src/shared/assistant/speech/pieceSink.ts
3
+ var t = (t) => t?.reason === e;
4
+ function n(e, n) {
5
+ let r = e, i = !1, a = 0, o = /* @__PURE__ */ new Map(), s = () => !i && n.isCurrent(), c = () => {
6
+ let e = r;
7
+ r = null;
8
+ try {
9
+ e?.cancel();
10
+ } catch {}
11
+ }, l = (e, t) => () => {
12
+ if (!(r !== e || !s() || !o.has(t))) {
13
+ for (let e of o.keys()) {
14
+ if (e > t) break;
15
+ o.delete(e);
16
+ }
17
+ n.onChunkStart(t), n.onStart();
18
+ }
19
+ }, u = !1, d = async () => {
20
+ for (let [e, t] of o) {
21
+ if (!s()) return;
22
+ o.delete(e), u = !0, n.onChunkStart(e);
23
+ try {
24
+ await n.play(t);
25
+ } catch (e) {
26
+ throw o.clear(), e;
27
+ }
28
+ }
29
+ }, f = Promise.resolve(), p = () => {
30
+ if (!s()) return Promise.resolve();
31
+ let e = f.then(d);
32
+ return f = e, e;
33
+ };
34
+ return {
35
+ async write(e) {
36
+ let t = a;
37
+ if (a += 1, !s()) return;
38
+ o.set(t, e);
39
+ let n = r;
40
+ if (n) try {
41
+ await n.write(e, t, l(n, t));
42
+ } catch {
43
+ if (!s()) return;
44
+ r === n && c();
45
+ }
46
+ r === null && await p();
47
+ },
48
+ async drain() {
49
+ let e = r;
50
+ if (e && s()) {
51
+ let n = a;
52
+ try {
53
+ if (await e.drain(), r === e) {
54
+ for (let e of o.keys()) {
55
+ if (e >= n) break;
56
+ o.delete(e);
57
+ }
58
+ return !0;
59
+ }
60
+ } catch (n) {
61
+ if (!s()) return !0;
62
+ let i = t(n) || o.size > 0;
63
+ return r === e && c(), await p(), i || u;
64
+ }
65
+ }
66
+ return await p(), !0;
67
+ },
68
+ retract(e) {
69
+ if (!s()) return;
70
+ let t = r;
71
+ if (t) try {
72
+ t.retract(e);
73
+ } catch {
74
+ r === t && c();
75
+ }
76
+ for (let t of o.keys()) t >= e && o.delete(t);
77
+ },
78
+ cancel() {
79
+ i = !0, o.clear(), c();
80
+ }
81
+ };
82
+ }
83
+ //#endregion
84
+ export { n as createPieceSink };
@@ -17,7 +17,80 @@ export interface SpeechSpeakOptions {
17
17
  * heard (BOFF-7283). Omitted means fallback is allowed.
18
18
  */
19
19
  fallback?: boolean;
20
+ /**
21
+ * Play this reply's hosted audio HERE instead of on the speakers: the
22
+ * optional live avatar (BOFF-8822). Attached per reply by the panel that
23
+ * shows the avatar, so no other caller of the shared engine can reach it.
24
+ * An engine with no audio data to hand over (the device voice) ignores it.
25
+ *
26
+ * It belongs to the whole reply: one output, one piece count and one hard
27
+ * stop, whether the reply is one `speak()` or a queue that grows by appends.
28
+ */
29
+ audioOutput?: SpeechAudioOutput;
30
+ }
31
+ /**
32
+ * Somewhere other than the speakers to play a reply's hosted audio
33
+ * (BOFF-8822).
34
+ *
35
+ * ★★ The hosted engine keeps everything else: the request (voice, language,
36
+ * rate), the retries, the one-piece prefetch, the refusal latch, `cancel()`.
37
+ * Each piece is synthesised ONCE. If the output fails, the pieces it never
38
+ * began play on the speakers from the bytes already fetched, so falling back
39
+ * never synthesises anything twice, and an output's failure never latches the
40
+ * engine off.
41
+ *
42
+ * ★ Leaving is not stopping. When the output stops being the reply's
43
+ * destination while the reply goes on (the user switched the avatar off), it
44
+ * rejects the pending `write` or `drain` (or the next one, if none is pending)
45
+ * and every later one with an error whose `reason` is `SPEECH_OUTPUT_LEFT`. The
46
+ * rest of the reply then plays on the speakers, exactly as after a failure, and
47
+ * still ends with `onEnd`. Switching it off never goes through the engine's
48
+ * `cancel()`, which silences the reply, nor through `retract()`.
49
+ *
50
+ * One object serves exactly one reply: attach a fresh one to each, so neither
51
+ * a leave nor the hard stop carries over to the next reply.
52
+ */
53
+ export interface SpeechAudioOutput {
54
+ /**
55
+ * Take piece `index` (the bytes the proxy returned). `index` counts the
56
+ * reply's pieces from 0 with ONE counter for the whole reply: for a queue it
57
+ * continues across appends, and a retracted number is never given again.
58
+ *
59
+ * Resolve when the next piece may follow: once this one is queued, pieces
60
+ * play back to back; resolving when it BEGINS keeps synthesis one piece
61
+ * ahead, as on the speakers. Call `began()` when its sound begins, and,
62
+ * before rejecting, for any piece that MAY have been heard: the speakers
63
+ * resume after the last piece that began, so nothing is heard twice. A piece
64
+ * retracted before it began resolves its `write` without `began()`. Writing
65
+ * may go on after a `drain()`, until `cancel()` or the reply's end.
66
+ */
67
+ write(bytes: Uint8Array, index: number, began: () => void): Promise<void>;
68
+ /**
69
+ * Resolve once everything written SO FAR has finished playing; reject as
70
+ * `write` does. Callable more than once: a queue may drain when its speech
71
+ * pauses and write again afterwards, even before that drain settles (a piece
72
+ * written meanwhile is not part of it). The reply's last drain follows its
73
+ * end and its last write.
74
+ */
75
+ drain(): Promise<void>;
76
+ /**
77
+ * Drop every piece with `index >= fromIndex` that has not begun; the piece
78
+ * playing, if any, finishes. Call `began()` first for any of them that MAY
79
+ * have been heard: it is then the one playing, and stays. Queued speech is
80
+ * withdrawn this way; `cancel()` remains the hard stop.
81
+ */
82
+ retract(fromIndex: number): void;
83
+ /**
84
+ * The hard stop (a stop, a new reply, a failure): silence at once and drop
85
+ * everything queued. Pending `write`/`drain` calls still settle, either way.
86
+ */
87
+ cancel(): void;
20
88
  }
89
+ /**
90
+ * The `reason` a `SpeechAudioOutput` rejects with once it has LEFT the reply
91
+ * (switched off while the reply goes on). Any other rejection is a failure.
92
+ */
93
+ export declare const SPEECH_OUTPUT_LEFT = "left";
21
94
  /**
22
95
  * Why playback stopped. `cancelled` is not a failure: it is what every queued
23
96
  * utterance reports when we deliberately cancel (new playback, panel close,
@@ -0,0 +1,4 @@
1
+ //#region src/shared/assistant/speech/types.ts
2
+ var e = "left";
3
+ //#endregion
4
+ export { e as SPEECH_OUTPUT_LEFT };