@burdenoff/fe-libs 2026.1028.2 → 2026.1028.4

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.
@@ -4,33 +4,37 @@ import { readHandsFreeOverride as r, resolveHandsFreeEnabled as ee } from "../vo
4
4
  import { silencePlaybackReference as te } from "../voice/handsfree/playbackReference.js";
5
5
  import { primeHostedPlayback as ne } from "../speech/hostedTtsSpeechEngine.js";
6
6
  import { speechFeed as re } from "../speech/speechFeed.js";
7
- import { INITIAL_SESSION as ie, INTERRUPT_MAX_MS as ae, NOTHING_HEARD as i, advanceHeardClock as oe, isCapturing as a, isSubmittable as se, planListeningHold as ce, planReplyConsumption as le, planVoiceAnnouncement as ue, shouldRecognise as de, spokenSoFar as o, voiceSessionReducer as fe } from "../voice/voiceSession.js";
8
- import { useHandsFreeInterrupt as pe } from "../voice/handsfree/useHandsFreeInterrupt.js";
9
- import { isTurnSuperseded as me } from "../voice/supersededTurns.js";
10
- import { captionChunks as he } from "../voice/speechPlayback.js";
11
- import { turnClockMs as s } from "../turnTimeline.js";
12
- import { useAssistantSpeech as ge } from "./AssistantSpeechProvider.js";
13
- import { useLiveAvatarOn as _e } from "./assistantLiveAvatarSeam.js";
14
- import { useCallback as c, useEffect as l, useLayoutEffect as ve, useMemo as ye, useReducer as be, useRef as u, useState as d } from "react";
7
+ import { INITIAL_SESSION as ie, INTERRUPT_MAX_MS as ae, NOTHING_HEARD as oe, advanceHeardClock as se, isCapturing as ce, isSubmittable as le, planListeningHold as ue, planReplyConsumption as de, planVoiceAnnouncement as fe, shouldRecognise as pe, spokenSoFar as i, voiceSessionReducer as me } from "../voice/voiceSession.js";
8
+ import { useHandsFreeInterrupt as he } from "../voice/handsfree/useHandsFreeInterrupt.js";
9
+ import { isTurnSuperseded as ge } from "../voice/supersededTurns.js";
10
+ import { captionChunks as _e } from "../voice/speechPlayback.js";
11
+ import { turnClockMs as a } from "../turnTimeline.js";
12
+ import { useAssistantSpeech as ve } from "./AssistantSpeechProvider.js";
13
+ import { useLiveAvatarOn as ye } from "./assistantLiveAvatarSeam.js";
14
+ import { useCallback as o, useEffect as s, useLayoutEffect as be, useMemo as xe, useReducer as Se, useRef as c, useState as l } from "react";
15
15
  //#region src/shared/assistant/ui/useVoiceSession.ts
16
- var xe = 4e3, Se = 2500, Ce = 5e3, we = 12e3;
17
- function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersede: De, handsFreeInterrupt: Oe, handsFreeStreamNoticeAccepted: ke, onAcceptHandsFreeStreamNotice: Ae, onBargeIn: je }) {
18
- let Me = n(Ee), m = u(null), [h, g] = be(c((e, t) => fe(e, t, m.current), []), ie), _ = ge(), { setSessionSpeaking: v, setSessionMicOpen: y, speakingMessageId: b, stop: x } = _, S = u(_.toggle), C = _.toggle;
19
- ve(() => {
20
- S.current = C;
21
- }, [C]);
22
- let w = u(null), T = u({}), E = u(i), D = u(null), O = u(!1), k = u(null), [Ne, A] = d(null), [j, M] = d(!1), [Pe, N] = d(!1), [P, F] = d(!1), I = u(h);
23
- I.current = h;
24
- let L = u([]), R = u(null), z = u(!1), B = c(() => {
16
+ var Ce = 4e3, we = 2500, Te = 5e3, Ee = 12e3;
17
+ function De({ onSubmit: De, latestReply: u, language: d, graceMs: Oe, onSupersede: ke, handsFreeInterrupt: Ae, handsFreeStreamNoticeAccepted: je, onAcceptHandsFreeStreamNotice: Me, onBargeIn: Ne }) {
18
+ let f = n(Oe), p = c(null), [m, h] = Se(o((e, t) => me(e, t, p.current), []), ie), g = ve(), { setSessionSpeaking: _, setSessionMicOpen: v, speakingMessageId: y, stop: b } = g, x = c(g.toggle), Pe = g.toggle;
19
+ be(() => {
20
+ x.current = Pe;
21
+ }, [Pe]);
22
+ let S = c(null), C = c({}), w = c(oe), T = c(void 0), E = c(void 0);
23
+ s(() => {
24
+ (m.phase === "idle" || m.phase === "ended") && (T.current = void 0, E.current = void 0);
25
+ }, [m.phase]);
26
+ let D = c(null), O = c(!1), k = c(null), [Fe, A] = l(null), [j, M] = l(!1), [Ie, N] = l(!1), [P, F] = l(!1), I = c(m);
27
+ I.current = m;
28
+ let L = c([]), R = c(null), z = c(!1), B = o(() => {
25
29
  L.current = [], R.current = null, z.current = !1;
26
- }, []), V = c(() => {
30
+ }, []), V = o(() => {
27
31
  for (;;) {
28
32
  let e = L.current.shift();
29
33
  if (!e) return !1;
30
34
  if (!(e.stillWanted && !e.stillWanted())) {
31
- R.current = e, m.current = m.current ? `${m.current} ${e.text}` : e.text;
35
+ R.current = e, p.current = p.current ? `${p.current} ${e.text}` : e.text;
32
36
  try {
33
- S.current(e.id, e.text, e.lang);
37
+ x.current(e.id, e.text, e.lang);
34
38
  } catch {
35
39
  R.current = null;
36
40
  continue;
@@ -38,50 +42,50 @@ function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersed
38
42
  return !0;
39
43
  }
40
44
  }
41
- }, []), H = u(!1), Fe = c((e) => {
42
- let t = ce({
45
+ }, []), H = c(!1), Le = o((e) => {
46
+ let t = ue({
43
47
  requested: e,
44
48
  held: H.current,
45
49
  muted: I.current.muted
46
50
  });
47
- H.current = t.held, t.mute !== null && g({
51
+ H.current = t.held, t.mute !== null && h({
48
52
  type: "mute",
49
53
  muted: t.mute
50
54
  });
51
- }, []), U = c((e) => {
52
- let t = ue(I.current);
53
- t !== "drop" && (R.current?.id === e.id || L.current.some((t) => t.id === e.id) || (L.current.push(e), t === "start" && !z.current && (z.current = !0, g({ type: "announce" }))));
54
- }, []), Ie = c((e) => {
55
+ }, []), U = o((e) => {
56
+ let t = fe(I.current);
57
+ t !== "drop" && (R.current?.id === e.id || L.current.some((t) => t.id === e.id) || (L.current.push(e), t === "start" && !z.current && (z.current = !0, h({ type: "announce" }))));
58
+ }, []), Re = o((e) => {
55
59
  if (I.current.phase !== "thinking") {
56
60
  U(e);
57
61
  return;
58
62
  }
59
- L.current.push(e), z.current = !0, g({ type: "answered-locally" });
60
- }, [U]), Le = u(f);
61
- Le.current = f, l(() => {
62
- if (h.phase === "speaking") {
63
+ L.current.push(e), z.current = !0, h({ type: "answered-locally" });
64
+ }, [U]), ze = c(u);
65
+ ze.current = u, s(() => {
66
+ if (m.phase === "speaking") {
63
67
  let e = k.current;
64
- m.current = e && D.current === e.id && f?.id !== e.id ? e.text : f?.text ?? null;
68
+ p.current = e && D.current === e.id && u?.id !== e.id ? e.text : u?.text ?? null;
65
69
  return;
66
70
  }
67
- if (m.current === null) return;
71
+ if (p.current === null) return;
68
72
  let e = setTimeout(() => {
69
- m.current = null;
70
- }, Se);
73
+ p.current = null;
74
+ }, we);
71
75
  return () => clearTimeout(e);
72
- }, [h.phase, f]);
73
- let W = u(p);
74
- W.current = p;
75
- let G = c((e, t) => {
76
- g({
76
+ }, [m.phase, u]);
77
+ let W = c(d);
78
+ W.current = d;
79
+ let G = o((e, t) => {
80
+ h({
77
81
  type: "speech",
78
82
  transcript: e,
79
83
  final: t,
80
84
  at: Date.now(),
81
85
  language: W.current
82
86
  });
83
- }, []), Re = c((e, t, n) => {
84
- g({
87
+ }, []), Be = o((e, t, n) => {
88
+ h({
85
89
  type: "speech",
86
90
  transcript: e,
87
91
  final: t,
@@ -89,176 +93,185 @@ function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersed
89
93
  language: n ?? W.current,
90
94
  gated: !0
91
95
  });
92
- }, []), K = u(0), ze = c(() => {
93
- B(), x(), K.current = Date.now(), M(!0), g({ type: "interrupt" });
94
- }, [x, B]), q = e({
95
- onFinalize: c((e) => G(e, !0), [G]),
96
- language: p
97
- }), Be = u(je);
98
- Be.current = je;
99
- let Ve = c(() => {
100
- B(), te(), x(), K.current = Date.now(), M(!0), g({ type: "interrupt" }), Be.current?.();
101
- }, [x, B]), He = _e(), J = pe({
102
- enabled: ee(Oe, r(), He),
103
- phase: h.phase,
104
- muted: h.muted,
105
- language: p,
106
- streamNoticeAccepted: ke ?? null,
107
- onAcceptStreamNotice: Ae,
108
- onBargeIn: Ve,
109
- onTranscript: Re
110
- }), Ue = J.mode === "notice", Y = q.isListening || J.suppressWebSpeech && J.mode === "active";
111
- l(() => {
96
+ }, []), K = c(0), Ve = o(() => {
97
+ B(), b(), K.current = Date.now(), M(!0), h({ type: "interrupt" });
98
+ }, [b, B]), q = e({
99
+ onFinalize: o((e) => G(e, !0), [G]),
100
+ language: d,
101
+ onSpeechEnd: (e) => {
102
+ I.current.phase === "listening" && (E.current = e);
103
+ }
104
+ }), He = c(Ne);
105
+ He.current = Ne;
106
+ let Ue = o(() => {
107
+ B(), te(), b(), K.current = Date.now(), M(!0), h({ type: "interrupt" }), He.current?.();
108
+ }, [b, B]), We = ye(), J = he({
109
+ enabled: ee(Ae, r(), We),
110
+ phase: m.phase,
111
+ muted: m.muted,
112
+ language: d,
113
+ streamNoticeAccepted: je ?? null,
114
+ onAcceptStreamNotice: Me,
115
+ onBargeIn: Ue,
116
+ onTranscript: Be,
117
+ onSpeechFrame: (e) => {
118
+ I.current.phase === "listening" && !I.current.muted && (T.current = e);
119
+ }
120
+ }), Ge = J.mode === "notice", Y = q.isListening || J.suppressWebSpeech && J.mode === "active";
121
+ s(() => {
112
122
  q.interim && G(q.interim, !1);
113
- }, [q.interim, G]), l(() => {
114
- w.current && clearTimeout(w.current), N(!1), E.current = oe(E.current, h, s());
123
+ }, [q.interim, G]), s(() => {
124
+ S.current && clearTimeout(S.current), N(!1), w.current = se(w.current, m, a());
115
125
  let e = t({
116
- phase: h.phase,
117
- muted: h.muted,
118
- spoken: o(h),
126
+ phase: m.phase,
127
+ muted: m.muted,
128
+ spoken: i(m),
119
129
  holding: P,
120
- graceMs: Me
130
+ graceMs: f
121
131
  });
122
132
  if (!e.arm) return;
123
- let n = setTimeout(() => N(!0), Math.min(800, e.delayMs)), r = s();
124
- return w.current = setTimeout(() => {
125
- T.current = {
126
- speechEnded: E.current.at ?? r,
133
+ let n = setTimeout(() => N(!0), Math.min(800, e.delayMs)), r = a();
134
+ return S.current = setTimeout(() => {
135
+ C.current = {
136
+ speechEnded: w.current.at ?? r,
137
+ lastSpeechFrame: T.current,
138
+ recognizerSpeechEnded: E.current,
139
+ selectedGraceMs: f,
127
140
  graceStarted: r,
128
- graceEnded: s()
129
- }, g({
141
+ graceEnded: a()
142
+ }, h({
130
143
  type: "silence",
131
144
  at: Date.now()
132
145
  });
133
146
  }, e.delayMs), () => {
134
- clearTimeout(n), w.current && clearTimeout(w.current);
147
+ clearTimeout(n), S.current && clearTimeout(S.current);
135
148
  };
136
149
  }, [
137
- h,
150
+ m,
138
151
  P,
139
- Me
140
- ]), l(() => {
141
- (h.phase !== "listening" || h.muted) && F(!1);
142
- }, [h.phase, h.muted]), l(() => {
152
+ f
153
+ ]), s(() => {
154
+ (m.phase !== "listening" || m.muted) && F(!1);
155
+ }, [m.phase, m.muted]), s(() => {
143
156
  if (!j) return;
144
157
  let e = Date.now() - K.current, t = Y ? Math.max(0, 500 - e) : Math.max(0, ae - e), n = setTimeout(() => M(!1), t);
145
158
  return () => clearTimeout(n);
146
159
  }, [j, Y]);
147
- let We = u("");
148
- h.phase === "listening" && (We.current = o(h)), l(() => {
149
- if (h.phase !== "thinking") return;
150
- y(!1), B(), A((e) => typeof e == "object" && e ? null : e);
151
- let e = We.current.trim();
152
- e && Te(e, Q.current ?? `vs-orphan-${Date.now().toString(36)}`, T.current, { correction: I.current.correcting }), T.current = {}, E.current = i;
153
- }, [h.phase]), l(() => {
154
- let e = le(f?.id, h.phase, D.current, me(f?.id));
155
- e.claim && (D.current = f?.id ?? null, e.advance && f && (f.refused ? (B(), A({
160
+ let Ke = c("");
161
+ m.phase === "listening" && (Ke.current = i(m)), s(() => {
162
+ if (m.phase !== "thinking") return;
163
+ v(!1), B(), A((e) => typeof e == "object" && e ? null : e);
164
+ let e = Ke.current.trim();
165
+ e && De(e, Q.current ?? `vs-orphan-${Date.now().toString(36)}`, C.current, { correction: I.current.correcting }), C.current = {}, w.current = oe, T.current = void 0, E.current = void 0;
166
+ }, [m.phase]), s(() => {
167
+ let e = de(u?.id, m.phase, D.current, ge(u?.id));
168
+ e.claim && (D.current = u?.id ?? null, e.advance && u && (u.refused ? (B(), A({
156
169
  kind: "turn-refused",
157
- text: f.text
158
- }), g({ type: "turn-refused" })) : g({
170
+ text: u.text
171
+ }), h({ type: "turn-refused" })) : h({
159
172
  type: "reply",
160
- text: f.text
173
+ text: u.text
161
174
  })));
162
175
  }, [
163
- f,
164
- h.phase,
176
+ u,
177
+ m.phase,
165
178
  B
166
- ]), l(() => {
167
- if (b === null || h.phase !== "thinking" || D.current === b) return;
168
- if (me(b)) {
169
- x();
179
+ ]), s(() => {
180
+ if (y === null || m.phase !== "thinking" || D.current === y) return;
181
+ if (ge(y)) {
182
+ b();
170
183
  return;
171
184
  }
172
- let e = re.peek(b);
173
- !e || e.channel !== "voice" || e.voiceSessionId === void 0 || e.voiceSessionId !== Q.current || (D.current = b, k.current = {
174
- id: b,
185
+ let e = re.peek(y);
186
+ !e || e.channel !== "voice" || e.voiceSessionId === void 0 || e.voiceSessionId !== Q.current || (D.current = y, k.current = {
187
+ id: y,
175
188
  text: e.text
176
- }, g({
189
+ }, h({
177
190
  type: "reply-audio-started",
178
- replyId: b
191
+ replyId: y
179
192
  }));
180
193
  }, [
181
- b,
182
- h.phase,
183
- x
184
- ]), l(() => {
185
- let e = b !== null;
186
- O.current && !e && (R.current = null, (I.current.phase !== "speaking" || !V()) && g({ type: "speech-ended" })), O.current = e;
187
- }, [b, V]), l(() => {
188
- h.phase !== "listening" && A((e) => e === "nothing-heard" ? null : e);
189
- }, [h.phase]), l(() => {
190
- if (h.phase !== "speaking" || b !== null) return;
194
+ y,
195
+ m.phase,
196
+ b
197
+ ]), s(() => {
198
+ let e = y !== null;
199
+ O.current && !e && (R.current = null, (I.current.phase !== "speaking" || !V()) && h({ type: "speech-ended" })), O.current = e;
200
+ }, [y, V]), s(() => {
201
+ m.phase !== "listening" && A((e) => e === "nothing-heard" ? null : e);
202
+ }, [m.phase]), s(() => {
203
+ if (m.phase !== "speaking" || y !== null) return;
191
204
  let e = setTimeout(() => {
192
- O.current || (B(), g({ type: "speech-ended" }));
193
- }, xe);
205
+ O.current || (B(), h({ type: "speech-ended" }));
206
+ }, Ce);
194
207
  return () => clearTimeout(e);
195
208
  }, [
196
- h.phase,
197
- b,
209
+ m.phase,
210
+ y,
198
211
  B
199
212
  ]);
200
- let X = _.outputNotCleared;
201
- l(() => {
213
+ let X = g.outputNotCleared;
214
+ s(() => {
202
215
  if (X === null || D.current !== X.messageId) return;
203
216
  let { phase: e } = I.current;
204
217
  e === "idle" || e === "ended" || e === "thinking" || A("reply-not-heard");
205
- }, [X]), l(() => {
206
- h.phase === "listening" || h.phase === "thinking" || A((e) => e === "reply-not-heard" ? null : e);
207
- }, [h.phase]);
208
- let Z = u(De);
209
- Z.current = De;
210
- let Ge = u(0);
211
- l(() => {
212
- let e = h.superseded;
213
- e !== Ge.current && (Ge.current = e, e !== 0 && (B(), x(), Z.current?.()));
218
+ }, [X]), s(() => {
219
+ m.phase === "listening" || m.phase === "thinking" || A((e) => e === "reply-not-heard" ? null : e);
220
+ }, [m.phase]);
221
+ let Z = c(ke);
222
+ Z.current = ke;
223
+ let qe = c(0);
224
+ s(() => {
225
+ let e = m.superseded;
226
+ e !== qe.current && (qe.current = e, e !== 0 && (B(), b(), Z.current?.()));
214
227
  }, [
215
- h.superseded,
228
+ m.superseded,
216
229
  B,
217
- x
218
- ]), l(() => {
219
- y(a(h)), de(h) && !J.suppressWebSpeech ? q.isActive || q.start("auto") : q.isActive && q.stop();
230
+ b
231
+ ]), s(() => {
232
+ v(ce(m)), pe(m) && !J.suppressWebSpeech ? q.isActive || q.start("auto") : q.isActive && q.stop();
220
233
  }, [
221
- h,
234
+ m,
222
235
  q,
223
- y,
236
+ v,
224
237
  J.suppressWebSpeech
225
- ]), l(() => {
226
- h.phase !== "speaking" || !z.current || (z.current = !1, V() || g({ type: "speech-ended" }));
227
- }, [h.phase, V]), l(() => {
228
- q.error === "not-allowed" || q.error === "audio-capture" ? g({
238
+ ]), s(() => {
239
+ m.phase !== "speaking" || !z.current || (z.current = !1, V() || h({ type: "speech-ended" }));
240
+ }, [m.phase, V]), s(() => {
241
+ q.error === "not-allowed" || q.error === "audio-capture" ? h({
229
242
  type: "fail",
230
243
  error: "mic-denied"
231
- }) : q.error === "language-not-supported" ? g({
244
+ }) : q.error === "language-not-supported" ? h({
232
245
  type: "fail",
233
246
  error: "no-speech-support"
234
247
  }) : q.error === "network" ? A("network") : q.error === "unknown" && A("recognition-failed");
235
- }, [q.error]), l(() => {
248
+ }, [q.error]), s(() => {
236
249
  q.error !== null || !Y || A((e) => e === "network" || e === "recognition-failed" ? null : e);
237
- }, [q.error, Y]), l(() => {
238
- if (Y && A((e) => e === "not-capturing" ? null : e), !a(h) || Y || Ue) return;
239
- let e = setTimeout(() => A("not-capturing"), Ce);
250
+ }, [q.error, Y]), s(() => {
251
+ if (Y && A((e) => e === "not-capturing" ? null : e), !ce(m) || Y || Ge) return;
252
+ let e = setTimeout(() => A("not-capturing"), Te);
240
253
  return () => clearTimeout(e);
241
254
  }, [
242
- h,
255
+ m,
243
256
  Y,
244
- Ue
245
- ]), l(() => {
246
- if (h.phase !== "listening" || h.muted || !Y || o(h) !== "") return;
247
- let e = setTimeout(() => A("nothing-heard"), we);
257
+ Ge
258
+ ]), s(() => {
259
+ if (m.phase !== "listening" || m.muted || !Y || i(m) !== "") return;
260
+ let e = setTimeout(() => A("nothing-heard"), Ee);
248
261
  return () => clearTimeout(e);
249
- }, [h, Y]), l(() => {
250
- o(h) !== "" && A(null);
251
- }, [h]);
252
- let Q = u(null), Ke = c(() => {
262
+ }, [m, Y]), s(() => {
263
+ i(m) !== "" && A(null);
264
+ }, [m]);
265
+ let Q = c(null), Je = o(() => {
253
266
  if (M(!1), N(!1), F(!1), B(), H.current = !1, !q.isSupported) {
254
- g({
267
+ h({
255
268
  type: "fail",
256
269
  error: "no-speech-support"
257
270
  });
258
271
  return;
259
272
  }
260
- if (_.isBlocked) {
261
- g({
273
+ if (g.isBlocked) {
274
+ h({
262
275
  type: "fail",
263
276
  error: "no-voice-support"
264
277
  });
@@ -271,54 +284,57 @@ function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersed
271
284
  t.volume = 0, e.speak(t);
272
285
  }
273
286
  } catch {}
274
- ne(), v(!0), A(null), D.current = Le.current?.id ?? null, k.current = null, Q.current = `vs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, g({ type: "start" });
287
+ ne(), _(!0), A(null), D.current = ze.current?.id ?? null, k.current = null, Q.current = `vs-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, h({ type: "start" });
275
288
  }, [
276
289
  q.isSupported,
277
- _.isBlocked,
278
- v,
290
+ g.isBlocked,
291
+ _,
279
292
  B
280
- ]), qe = c(() => {
281
- M(!1), N(!1), F(!1), B(), H.current = !1, Q.current = null, g({ type: "end" }), x(), v(!1);
293
+ ]), Ye = o(() => {
294
+ M(!1), N(!1), F(!1), B(), H.current = !1, Q.current = null, h({ type: "end" }), b(), _(!1);
282
295
  }, [
283
- x,
284
- v,
296
+ b,
297
+ _,
285
298
  B
286
- ]), Je = c((e) => Q.current === e, []), Ye = c(() => {
299
+ ]), Xe = o((e) => Q.current === e, []), Ze = o(() => {
287
300
  let e = I.current;
288
301
  e.phase !== "listening" || e.muted || F(!0);
289
- }, []), Xe = c(() => {
302
+ }, []), Qe = o(() => {
290
303
  F(!1);
291
304
  let e = I.current;
292
- if (e.phase !== "listening" || e.muted || !se(o(e))) return;
293
- let t = s();
294
- T.current = {
295
- speechEnded: E.current.at ?? t,
305
+ if (e.phase !== "listening" || e.muted || !le(i(e))) return;
306
+ let t = a();
307
+ C.current = {
308
+ speechEnded: w.current.at ?? t,
309
+ lastSpeechFrame: T.current,
310
+ recognizerSpeechEnded: E.current,
311
+ selectedGraceMs: f,
296
312
  graceStarted: t,
297
313
  graceEnded: t
298
- }, g({
314
+ }, h({
299
315
  type: "silence",
300
316
  at: Date.now()
301
317
  });
302
- }, []), Ze = c(() => {
303
- g({
318
+ }, [f]), $e = o(() => {
319
+ h({
304
320
  type: "mute",
305
321
  muted: !I.current.muted
306
322
  });
307
323
  }, []);
308
- l(() => () => {
309
- v(!1), y(!1);
310
- }, [v, y]);
311
- let $ = he(_.spokenChunks, h.phase, b);
312
- return ye(() => ({
313
- session: h,
314
- notice: Ne ?? (J.fallbackReason !== null && J.fallbackReason !== "notice-declined" ? "hands-free-fallback" : J.nothingHeard ? "hands-free-nothing-heard" : null),
324
+ s(() => () => {
325
+ _(!1), v(!1);
326
+ }, [_, v]);
327
+ let $ = _e(g.spokenChunks, m.phase, y);
328
+ return xe(() => ({
329
+ session: m,
330
+ notice: Fe ?? (J.fallbackReason !== null && J.fallbackReason !== "notice-declined" ? "hands-free-fallback" : J.nothingHeard ? "hands-free-nothing-heard" : null),
315
331
  spokenChunks: $,
316
332
  capturing: Y,
317
- active: h.phase !== "idle" && (h.phase !== "ended" || h.error !== null),
333
+ active: m.phase !== "idle" && (m.phase !== "ended" || m.error !== null),
318
334
  interrupting: j,
319
- awaitingEndpoint: Pe,
335
+ awaitingEndpoint: Ie,
320
336
  holding: P,
321
- correcting: h.correcting,
337
+ correcting: m.correcting,
322
338
  handsFree: {
323
339
  mode: J.mode,
324
340
  nothingHeard: J.nothingHeard,
@@ -326,22 +342,22 @@ function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersed
326
342
  fallbackReason: J.fallbackReason,
327
343
  privacyNotice: J.privacyNotice
328
344
  },
329
- available: q.isSupported && !_.isBlocked,
330
- start: Ke,
331
- end: qe,
332
- isCurrentSession: Je,
333
- toggleMute: Ze,
334
- interrupt: ze,
335
- letMeFinish: Ye,
336
- imDone: Xe,
345
+ available: q.isSupported && !g.isBlocked,
346
+ start: Je,
347
+ end: Ye,
348
+ isCurrentSession: Xe,
349
+ toggleMute: $e,
350
+ interrupt: Ve,
351
+ letMeFinish: Ze,
352
+ imDone: Qe,
337
353
  announce: U,
338
- holdListening: Fe,
339
- answerLocally: Ie
354
+ holdListening: Le,
355
+ answerLocally: Re
340
356
  }), [
341
- h,
342
- Ne,
357
+ m,
358
+ Fe,
343
359
  j,
344
- Pe,
360
+ Ie,
345
361
  P,
346
362
  Y,
347
363
  J.mode,
@@ -350,19 +366,19 @@ function Te({ onSubmit: Te, latestReply: f, language: p, graceMs: Ee, onSupersed
350
366
  J.fallbackReason,
351
367
  J.privacyNotice,
352
368
  q.isSupported,
353
- _.isBlocked,
369
+ g.isBlocked,
354
370
  $,
355
- Ke,
356
- qe,
357
371
  Je,
358
- Ze,
359
- ze,
360
372
  Ye,
361
373
  Xe,
374
+ $e,
375
+ Ve,
376
+ Ze,
377
+ Qe,
362
378
  U,
363
- Fe,
364
- Ie
379
+ Le,
380
+ Re
365
381
  ]);
366
382
  }
367
383
  //#endregion
368
- export { Te as useVoiceSession };
384
+ export { De as useVoiceSession };
@@ -33,6 +33,9 @@ export interface ControllerDeps {
33
33
  onFallback: (reason: FallbackReason) => void;
34
34
  /** A barge-in that produced no words within `NOTHING_HEARD_MS`. */
35
35
  onNothingHeard?: () => void;
36
+ /** Timing only: end of the latest echo-approved voiced capture frame. */
37
+ onSpeechFrame?: (at: number) => void;
38
+ now?: () => number;
36
39
  isEnabled: () => boolean;
37
40
  prerollMs?: number;
38
41
  setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
@@ -66,7 +66,11 @@ function u(u) {
66
66
  rmsDb: e.rmsDb,
67
67
  zcr: e.zcr
68
68
  }, t.pass);
69
- r.event === "onset" ? M(t.reason) : b && r.inSegment && u.transport.pushFrame(n), r.event === "end" && j();
69
+ if (r.event === "onset" && M(t.reason), b && r.voiced && t.pass) {
70
+ let t = u.now?.() ?? performance.now(), n = _?.context ? Math.max(0, (_.context.currentTime - e.ctxTime) * 1e3) : 0;
71
+ u.onSpeechFrame?.(t - n);
72
+ }
73
+ r.event !== "onset" && b && r.inSegment && u.transport.pushFrame(n), r.event === "end" && j();
70
74
  }, P = () => {
71
75
  if (!v && !_) return;
72
76
  v = !1, k(), b && u.transport.cancelUtterance(b.id), b = null, g.invalidate(), u.transport.stop(), h.clear(), p.reset(), m.reset();
@@ -24,6 +24,7 @@ export interface UseHandsFreeInterruptOptions {
24
24
  language: string;
25
25
  /** The user's voice started while the assistant was audible. */
26
26
  onBargeIn: () => void;
27
+ onSpeechFrame?: (at: number) => void;
27
28
  onTranscript: (text: string, final: boolean, language: string | undefined) => void;
28
29
  /**
29
30
  * The backend stream's privacy-notice version this person accepted (it is
@@ -57,6 +57,7 @@ function b(s) {
57
57
  onTranscript: (e, t, n) => {
58
58
  D(!1), C.current.onTranscript(e, t, n);
59
59
  },
60
+ onSpeechFrame: (e) => C.current.onSpeechFrame?.(e),
60
61
  onFallback: T,
61
62
  onNothingHeard: () => D(!0),
62
63
  isEnabled: () => C.current.enabled
@@ -94,6 +94,8 @@ export interface UseVoiceInputOptions {
94
94
  * here) means the user can freely edit while dictation continues.
95
95
  */
96
96
  onFinalize: (text: string) => void;
97
+ /** Browser endpoint notification time; not an acoustic timestamp. */
98
+ onSpeechEnd?: (at: number) => void;
97
99
  /**
98
100
  * BCP-47 tag to listen for. Omit to follow the app locale, which is what this
99
101
  * hook did unconditionally before (BOFF-7108).
@@ -137,4 +139,4 @@ export interface UseVoiceInputResult {
137
139
  toggle: () => void;
138
140
  clearError: () => void;
139
141
  }
140
- export declare function useVoiceInput({ onFinalize, language: requested, }: UseVoiceInputOptions): UseVoiceInputResult;
142
+ export declare function useVoiceInput({ onFinalize, onSpeechEnd, language: requested, }: UseVoiceInputOptions): UseVoiceInputResult;