@burdenoff/website-sdk 2026.1105.2 → 2026.1105.3

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.
@@ -1,62 +1,3 @@
1
- /** What the worklet posts for each frame. */
2
- interface CaptureFrameMessage {
3
- /** s16le mono 16 kHz, `CAPTURE_FRAME_SAMPLES` samples. */
4
- pcm: ArrayBuffer;
5
- /** Microphone level over the frame, dBFS. */
6
- rmsDb: number;
7
- /** Reference-bus level over the same samples, dBFS (about -90 when silent, as it always is here). */
8
- refDb: number;
9
- /** Zero crossings per sample. */
10
- zcr: number;
11
- /** AudioContext time at the end of the frame, seconds. */
12
- ctxTime: number;
13
- seq: number;
14
- }
15
-
16
- /**
17
- * BOFF-9797 (Stage C). The Avatar tab's own microphone capture: `getUserMedia`, through the capture worklet
18
- * (`capture-worklet.ts`), into 16 kHz PCM frames.
19
- *
20
- * **Ported, not shared.** From `src/shared/assistant/voice/handsfree/micCapture.ts` in Algoshred/fe-libs at the
21
- * commit `capture-worklet.ts` names (`CAPTURE_WORKLET_PORTED_FROM`; blob `8486c630`), with one difference: fe-libs
22
- * routes everything its assistant plays into the worklet's second input (its playback reference, for barge-in), and the
23
- * website plays nothing through this context (the avatar's voice is the room's own audio element, and the mic is
24
- * offered only while no session plays), so no reference bus is made and the worklet has the microphone as its only
25
- * input. `looksLikeHeadset`, which only fe-libs' barge-in reads, is not ported. `capture-worklet.test.ts` holds the
26
- * parity tests.
27
- *
28
- * It opens `getUserMedia` with echo cancellation, noise suppression and auto gain on. Constraints are plain `true`,
29
- * never `{ exact }`: `exact` throws OverconstrainedError on handsets that cannot honour one. The settings the browser
30
- * actually applied are read back (`track.getSettings()`), because some Android handsets report `true` and apply nothing.
31
- *
32
- * `stop()` stops the track (the browser's recording indicator goes), disconnects the graph and closes the context. The
33
- * caller does that the moment a press ends, whichever way it ends; `pagehide` does it too, whatever the caller does.
34
- */
35
-
36
- interface MicCaptureHandle {
37
- context: AudioContext;
38
- micSource: MediaStreamAudioSourceNode;
39
- track: MediaStreamTrack;
40
- /** What the browser reports it applied. */
41
- settings: MediaTrackSettings;
42
- /** Stop everything and release the microphone. Safe to call more than once. */
43
- stop(): void;
44
- }
45
-
46
- /** What the mic needs of a WebSocket; {@link openBrowserSocket} adapts the browser's. (fe-libs' seam.) */
47
- interface SocketLike {
48
- readonly readyState: number;
49
- send(data: string | ArrayBufferLike | ArrayBufferView): void;
50
- close(code?: number): void;
51
- listen(handlers: SocketHandlers): void;
52
- }
53
- interface SocketHandlers {
54
- open(): void;
55
- message(data: unknown): void;
56
- close(code: number): void;
57
- error(): void;
58
- }
59
-
60
1
  /**
61
2
  * BOFF-9797 (Stage C). The shapes the Avatar tab's mic shares with the live avatar's view.
62
3
  *
@@ -79,7 +20,8 @@ interface AvatarMicOffer {
79
20
  * Why a press did not give text:
80
21
  *
81
22
  * - `denied`: the microphone was refused (the visitor, the browser or the page's permissions);
82
- * - `unsupported`: this browser cannot capture here, or has no microphone to use;
23
+ * - `unsupported`: this browser cannot capture here, or has no microphone to use (with the browser's recognizer: its
24
+ * speech service is off or unreachable here, or it has no language to offer);
83
25
  * - `unavailable`: the server will not take a stream from this page now: no offer, an offer it refuses twice, its mic
84
26
  * notice is another version than this SDK's, or the website avatar is paused;
85
27
  * - `limit`: a limit on voice questions is reached (the visitor's, the network's, or every visitor's together);
@@ -151,9 +93,191 @@ interface AvatarMic {
151
93
  /** The press as it stands. */
152
94
  state(): AvatarMicState;
153
95
  }
96
+ /**
97
+ * The mic through the browser's own speech recognition (Stage C, `mic.mode: "browser"` in availability): the browser
98
+ * turns the voice into text, and nothing of the press reaches the server. Same press, same states, same failures as
99
+ * the stream's ({@link AvatarMic}, {@link AvatarMicState}); it never reports `unavailable`, `limit` or `busy`.
100
+ */
101
+ interface BrowserAvatarMicOptions {
102
+ /** The recognizer's language, a BCP-47 tag (`en-IN`). */
103
+ lang: string;
104
+ /** Tried once, in the same press, when the browser does not recognise `lang` (`en-US`). */
105
+ fallbackLang: string;
106
+ /** The longest press, from the moment the browser starts listening: thirty seconds. */
107
+ maxPressMs: number;
108
+ /** Every change of the press's state. */
109
+ onState: (state: AvatarMicState) => void;
110
+ }
154
111
  /** What the mic's chunk (`dist/avatar-mic/`) exports. */
155
112
  interface AvatarMicModule {
156
113
  createAvatarMic: (options: AvatarMicOptions) => AvatarMic;
114
+ createBrowserAvatarMic: (options: BrowserAvatarMicOptions) => AvatarMic;
115
+ }
116
+
117
+ /**
118
+ * BOFF-9797 (Stage C, the browser's own recognizer). The Avatar tab's mic through the browser's speech recognition
119
+ * (`SpeechRecognition`, `webkitSpeechRecognition`), as the product assistant listens: tap to start, tap to stop,
120
+ * thirty seconds at most. The browser turns the voice into text (its own speech service: Google's for Chrome, Apple's
121
+ * for Safari, Microsoft's for Edge); this module never sends anything anywhere, and the text goes only to the view,
122
+ * which puts it in the box for the visitor to check and Send.
123
+ *
124
+ * The phones' quirks are the ones the SDK's Chat dictation (`src/hooks/use-speech-input.ts`, live on the sites) met
125
+ * there, handled the same way: the permission prompt raised by opening the microphone first, a pause before the
126
+ * recognizer opens its own, one quiet retry after a fresh grant, and one utterance per recognizer on a touch device.
127
+ *
128
+ * A press, in order:
129
+ *
130
+ * 1. **The tap** (`start`, called inside the tap where the view can). Where the browser reports the microphone granted
131
+ * (or reports nothing, and a press on this page has had it), the recognizer starts inside the tap. Otherwise the
132
+ * microphone is opened and released at once, inside the tap (`getUserMedia`): that is what raises the browser's
133
+ * permission prompt on phones, where a recognizer's own `start()` is refused (`not-allowed`) without asking anyone.
134
+ * Nothing of that capture is kept, played or sent. Once it is granted, the recognizer starts
135
+ * {@link BROWSER_PRIME_SETTLE_MS} later, so it does not race the released capture for the microphone. The
136
+ * recognizer: interim results, one alternative, the press's language, and `continuous`, except on a touch device,
137
+ * whose recognizer hears one utterance whatever it is asked (and, asked for more, fails sooner). A press still
138
+ * finishing is superseded (aborted, its text never reported; the view lets no tap through meanwhile).
139
+ * 2. **The browser starts listening** (`onstart`, the honest signal: never while a prompt is up): `listening`, and the
140
+ * press's limit counts from here ({@link BrowserAvatarMicOptions.maxPressMs}).
141
+ * 3. **Words as they are recognised**: rebuilt from the whole result list each time (finals, then the words still
142
+ * being recognised), so a result sent again never doubles up, and a final that repeats the one before it with more
143
+ * words replaces it (Android's cumulative finals).
144
+ * 4. **The engine ending by itself** (a pause, a phone's one utterance, a quiet spell's `no-speech`) does NOT end the
145
+ * press: tap to stop holds on every platform. A new recognizer takes over in the same press, its words after the
146
+ * ones before: at once on a computer, after {@link BROWSER_TOUCH_RESTART_MS} on a touch device, and after
147
+ * {@link BROWSER_RESTART_COOLDOWN_MS} when one has just thrown on `start()` or, on a computer, ended as it began.
148
+ * Bounded against a recognizer that never listens: after {@link BROWSER_MAX_QUICK_ENDS} ends in a row as they
149
+ * began, or {@link BROWSER_MAX_RESTARTS} recognizers in all, the press gives what it has. Events of a recognizer that
150
+ * has been replaced are dropped.
151
+ * 5. **The stop** (the visitor's tap, or the limit): `stop()`, not `abort()`, as browsers deliver the last words after
152
+ * it; up to {@link BROWSER_FINAL_WAIT_MS} for its end. A tap while it is still starting (the prompt included) calls
153
+ * the press off.
154
+ * 6. **The text** (`done`): what was heard, final or not (an engine that never marks a result final, iOS Safari, still
155
+ * gives its words: the visitor checks them in the box anyway). Empty when nothing was heard.
156
+ *
157
+ * **Released on every way out.** `cancel()` (the view going, the page hidden, the Chat tab), a failure, a supersede and
158
+ * the end of a press all detach the recognizer's handlers first and then `abort()` it, so the browser's recording
159
+ * indicator goes and nothing it says later is reported. A microphone opened for the prompt is released as soon as it
160
+ * opens, whether or not the press is still there.
161
+ *
162
+ * Errors. Opening the microphone for the prompt: refused (`NotAllowedError`, `SecurityError`) is `denied`; no
163
+ * microphone (`NotFoundError`) is `unsupported`; anything else is `failed`. The recognizer's
164
+ * (`SpeechRecognitionErrorEvent.error`): `not-allowed` or `service-not-allowed` while the microphone is confirmed for
165
+ * the press (the browser reports it granted, or opening it just worked) is tried once more after
166
+ * {@link BROWSER_GRANT_RETRY_MS}, quietly (Android refuses the first start after a fresh grant and takes the next), and
167
+ * after that it is `unsupported`: the speech service refusing, not a permission the visitor can give. Unconfirmed,
168
+ * `not-allowed` is `denied` (and the next tap asks the browser again) and `service-not-allowed` (iOS with Siri and
169
+ * Dictation off, Edge's policy) is `unsupported`; `audio-capture` (no microphone) is `unsupported`;
170
+ * `language-not-supported` tries the fallback language once, then `unsupported`; `network` is `failed`, and
171
+ * `unsupported` the second time running on the page; `no-speech` is a quiet spell (a new recognizer); anything else is
172
+ * `failed`. A failure once words were heard, whatever it is, gives those words instead (`done`).
173
+ */
174
+
175
+ /** One alternative of a recognition result: what the browser heard. */
176
+ interface RecognitionAlternativeLike {
177
+ readonly transcript: string;
178
+ }
179
+ /** One recognition result: final, or still being recognised. */
180
+ interface RecognitionResultLike {
181
+ readonly isFinal: boolean;
182
+ readonly length: number;
183
+ readonly [index: number]: RecognitionAlternativeLike | undefined;
184
+ }
185
+ /** The whole result list of a recognizer, as each `result` event carries it. */
186
+ interface RecognitionResultListLike {
187
+ readonly length: number;
188
+ readonly [index: number]: RecognitionResultLike | undefined;
189
+ }
190
+ /** What this module uses of a browser's `SpeechRecognition` (declared here: TypeScript's DOM library has none). */
191
+ interface RecognitionLike {
192
+ lang: string;
193
+ continuous: boolean;
194
+ interimResults: boolean;
195
+ maxAlternatives: number;
196
+ onstart: (() => void) | null;
197
+ onresult: ((event: {
198
+ readonly results: RecognitionResultListLike;
199
+ }) => void) | null;
200
+ onerror: ((event: {
201
+ readonly error: string;
202
+ }) => void) | null;
203
+ onend: (() => void) | null;
204
+ start(): void;
205
+ stop(): void;
206
+ abort(): void;
207
+ }
208
+ /** What a press needs from the browser. The browser's own by default; a test hands in fakes. */
209
+ interface BrowserAvatarMicSeams {
210
+ /** A new recognizer, or null where the browser has none. */
211
+ createRecognition: () => RecognitionLike | null;
212
+ /** A monotonic clock, in milliseconds, for the cooldowns. */
213
+ now: () => number;
214
+ /** {@link primeBrowserMicrophone}: called inside the tap, where the microphone is not known to be granted. */
215
+ primeMicrophone: () => Promise<void> | null;
216
+ /** {@link watchBrowserMicPermission}: called once, as the mic is made. */
217
+ watchMicPermission: (onState: (state: PermissionState) => void) => void;
218
+ /** {@link hasCoarsePointer}: read at each tap. */
219
+ touch: () => boolean;
220
+ }
221
+ /** The Avatar tab's mic through the browser's own recognizer. See the file's comment for what a press does. */
222
+ declare function createBrowserAvatarMic(options: BrowserAvatarMicOptions, seams?: Partial<BrowserAvatarMicSeams>): AvatarMic;
223
+
224
+ /** What the worklet posts for each frame. */
225
+ interface CaptureFrameMessage {
226
+ /** s16le mono 16 kHz, `CAPTURE_FRAME_SAMPLES` samples. */
227
+ pcm: ArrayBuffer;
228
+ /** Microphone level over the frame, dBFS. */
229
+ rmsDb: number;
230
+ /** Reference-bus level over the same samples, dBFS (about -90 when silent, as it always is here). */
231
+ refDb: number;
232
+ /** Zero crossings per sample. */
233
+ zcr: number;
234
+ /** AudioContext time at the end of the frame, seconds. */
235
+ ctxTime: number;
236
+ seq: number;
237
+ }
238
+
239
+ /**
240
+ * BOFF-9797 (Stage C). The Avatar tab's own microphone capture: `getUserMedia`, through the capture worklet
241
+ * (`capture-worklet.ts`), into 16 kHz PCM frames.
242
+ *
243
+ * **Ported, not shared.** From `src/shared/assistant/voice/handsfree/micCapture.ts` in Algoshred/fe-libs at the
244
+ * commit `capture-worklet.ts` names (`CAPTURE_WORKLET_PORTED_FROM`; blob `8486c630`), with one difference: fe-libs
245
+ * routes everything its assistant plays into the worklet's second input (its playback reference, for barge-in), and the
246
+ * website plays nothing through this context (the avatar's voice is the room's own audio element, and the mic is
247
+ * offered only while no session plays), so no reference bus is made and the worklet has the microphone as its only
248
+ * input. `looksLikeHeadset`, which only fe-libs' barge-in reads, is not ported. `capture-worklet.test.ts` holds the
249
+ * parity tests.
250
+ *
251
+ * It opens `getUserMedia` with echo cancellation, noise suppression and auto gain on. Constraints are plain `true`,
252
+ * never `{ exact }`: `exact` throws OverconstrainedError on handsets that cannot honour one. The settings the browser
253
+ * actually applied are read back (`track.getSettings()`), because some Android handsets report `true` and apply nothing.
254
+ *
255
+ * `stop()` stops the track (the browser's recording indicator goes), disconnects the graph and closes the context. The
256
+ * caller does that the moment a press ends, whichever way it ends; `pagehide` does it too, whatever the caller does.
257
+ */
258
+
259
+ interface MicCaptureHandle {
260
+ context: AudioContext;
261
+ micSource: MediaStreamAudioSourceNode;
262
+ track: MediaStreamTrack;
263
+ /** What the browser reports it applied. */
264
+ settings: MediaTrackSettings;
265
+ /** Stop everything and release the microphone. Safe to call more than once. */
266
+ stop(): void;
267
+ }
268
+
269
+ /** What the mic needs of a WebSocket; {@link openBrowserSocket} adapts the browser's. (fe-libs' seam.) */
270
+ interface SocketLike {
271
+ readonly readyState: number;
272
+ send(data: string | ArrayBufferLike | ArrayBufferView): void;
273
+ close(code?: number): void;
274
+ listen(handlers: SocketHandlers): void;
275
+ }
276
+ interface SocketHandlers {
277
+ open(): void;
278
+ message(data: unknown): void;
279
+ close(code: number): void;
280
+ error(): void;
157
281
  }
158
282
 
159
283
  /**
@@ -176,7 +300,8 @@ interface AvatarMicModule {
176
300
  *
177
301
  * **Released on every way out.** The microphone's track is stopped the moment the recording stops, and also when the
178
302
  * press fails, is cancelled (`cancel()`: the view going, the page hidden, the Chat tab) or superseded (a new press while
179
- * one is still finishing: `cancel` for the old one, so it stops being paid for, and its text is never reported). A
303
+ * one is still finishing, a tap the view lets through for this stream mic only: `cancel` for the old one, so it stops
304
+ * being paid for, and its text is never reported). A
180
305
  * microphone that opens after its press has gone is closed as it opens. The stream is closed whenever no press needs
181
306
  * it: one connection serves a press and the press that supersedes it, and no other.
182
307
  */
@@ -190,4 +315,4 @@ interface AvatarMicSeams {
190
315
  /** The mic of one Avatar tab. See the file's comment for what a press does. */
191
316
  declare function createAvatarMic(options: AvatarMicOptions, seams?: Partial<AvatarMicSeams>): AvatarMic;
192
317
 
193
- export { type AvatarMic, type AvatarMicFailure, type AvatarMicModule, type AvatarMicOffer, type AvatarMicOptions, type AvatarMicState, createAvatarMic };
318
+ export { type AvatarMic, type AvatarMicFailure, type AvatarMicModule, type AvatarMicOffer, type AvatarMicOptions, type AvatarMicState, type BrowserAvatarMicOptions, createAvatarMic, createBrowserAvatarMic };
@@ -1,62 +1,3 @@
1
- /** What the worklet posts for each frame. */
2
- interface CaptureFrameMessage {
3
- /** s16le mono 16 kHz, `CAPTURE_FRAME_SAMPLES` samples. */
4
- pcm: ArrayBuffer;
5
- /** Microphone level over the frame, dBFS. */
6
- rmsDb: number;
7
- /** Reference-bus level over the same samples, dBFS (about -90 when silent, as it always is here). */
8
- refDb: number;
9
- /** Zero crossings per sample. */
10
- zcr: number;
11
- /** AudioContext time at the end of the frame, seconds. */
12
- ctxTime: number;
13
- seq: number;
14
- }
15
-
16
- /**
17
- * BOFF-9797 (Stage C). The Avatar tab's own microphone capture: `getUserMedia`, through the capture worklet
18
- * (`capture-worklet.ts`), into 16 kHz PCM frames.
19
- *
20
- * **Ported, not shared.** From `src/shared/assistant/voice/handsfree/micCapture.ts` in Algoshred/fe-libs at the
21
- * commit `capture-worklet.ts` names (`CAPTURE_WORKLET_PORTED_FROM`; blob `8486c630`), with one difference: fe-libs
22
- * routes everything its assistant plays into the worklet's second input (its playback reference, for barge-in), and the
23
- * website plays nothing through this context (the avatar's voice is the room's own audio element, and the mic is
24
- * offered only while no session plays), so no reference bus is made and the worklet has the microphone as its only
25
- * input. `looksLikeHeadset`, which only fe-libs' barge-in reads, is not ported. `capture-worklet.test.ts` holds the
26
- * parity tests.
27
- *
28
- * It opens `getUserMedia` with echo cancellation, noise suppression and auto gain on. Constraints are plain `true`,
29
- * never `{ exact }`: `exact` throws OverconstrainedError on handsets that cannot honour one. The settings the browser
30
- * actually applied are read back (`track.getSettings()`), because some Android handsets report `true` and apply nothing.
31
- *
32
- * `stop()` stops the track (the browser's recording indicator goes), disconnects the graph and closes the context. The
33
- * caller does that the moment a press ends, whichever way it ends; `pagehide` does it too, whatever the caller does.
34
- */
35
-
36
- interface MicCaptureHandle {
37
- context: AudioContext;
38
- micSource: MediaStreamAudioSourceNode;
39
- track: MediaStreamTrack;
40
- /** What the browser reports it applied. */
41
- settings: MediaTrackSettings;
42
- /** Stop everything and release the microphone. Safe to call more than once. */
43
- stop(): void;
44
- }
45
-
46
- /** What the mic needs of a WebSocket; {@link openBrowserSocket} adapts the browser's. (fe-libs' seam.) */
47
- interface SocketLike {
48
- readonly readyState: number;
49
- send(data: string | ArrayBufferLike | ArrayBufferView): void;
50
- close(code?: number): void;
51
- listen(handlers: SocketHandlers): void;
52
- }
53
- interface SocketHandlers {
54
- open(): void;
55
- message(data: unknown): void;
56
- close(code: number): void;
57
- error(): void;
58
- }
59
-
60
1
  /**
61
2
  * BOFF-9797 (Stage C). The shapes the Avatar tab's mic shares with the live avatar's view.
62
3
  *
@@ -79,7 +20,8 @@ interface AvatarMicOffer {
79
20
  * Why a press did not give text:
80
21
  *
81
22
  * - `denied`: the microphone was refused (the visitor, the browser or the page's permissions);
82
- * - `unsupported`: this browser cannot capture here, or has no microphone to use;
23
+ * - `unsupported`: this browser cannot capture here, or has no microphone to use (with the browser's recognizer: its
24
+ * speech service is off or unreachable here, or it has no language to offer);
83
25
  * - `unavailable`: the server will not take a stream from this page now: no offer, an offer it refuses twice, its mic
84
26
  * notice is another version than this SDK's, or the website avatar is paused;
85
27
  * - `limit`: a limit on voice questions is reached (the visitor's, the network's, or every visitor's together);
@@ -151,9 +93,191 @@ interface AvatarMic {
151
93
  /** The press as it stands. */
152
94
  state(): AvatarMicState;
153
95
  }
96
+ /**
97
+ * The mic through the browser's own speech recognition (Stage C, `mic.mode: "browser"` in availability): the browser
98
+ * turns the voice into text, and nothing of the press reaches the server. Same press, same states, same failures as
99
+ * the stream's ({@link AvatarMic}, {@link AvatarMicState}); it never reports `unavailable`, `limit` or `busy`.
100
+ */
101
+ interface BrowserAvatarMicOptions {
102
+ /** The recognizer's language, a BCP-47 tag (`en-IN`). */
103
+ lang: string;
104
+ /** Tried once, in the same press, when the browser does not recognise `lang` (`en-US`). */
105
+ fallbackLang: string;
106
+ /** The longest press, from the moment the browser starts listening: thirty seconds. */
107
+ maxPressMs: number;
108
+ /** Every change of the press's state. */
109
+ onState: (state: AvatarMicState) => void;
110
+ }
154
111
  /** What the mic's chunk (`dist/avatar-mic/`) exports. */
155
112
  interface AvatarMicModule {
156
113
  createAvatarMic: (options: AvatarMicOptions) => AvatarMic;
114
+ createBrowserAvatarMic: (options: BrowserAvatarMicOptions) => AvatarMic;
115
+ }
116
+
117
+ /**
118
+ * BOFF-9797 (Stage C, the browser's own recognizer). The Avatar tab's mic through the browser's speech recognition
119
+ * (`SpeechRecognition`, `webkitSpeechRecognition`), as the product assistant listens: tap to start, tap to stop,
120
+ * thirty seconds at most. The browser turns the voice into text (its own speech service: Google's for Chrome, Apple's
121
+ * for Safari, Microsoft's for Edge); this module never sends anything anywhere, and the text goes only to the view,
122
+ * which puts it in the box for the visitor to check and Send.
123
+ *
124
+ * The phones' quirks are the ones the SDK's Chat dictation (`src/hooks/use-speech-input.ts`, live on the sites) met
125
+ * there, handled the same way: the permission prompt raised by opening the microphone first, a pause before the
126
+ * recognizer opens its own, one quiet retry after a fresh grant, and one utterance per recognizer on a touch device.
127
+ *
128
+ * A press, in order:
129
+ *
130
+ * 1. **The tap** (`start`, called inside the tap where the view can). Where the browser reports the microphone granted
131
+ * (or reports nothing, and a press on this page has had it), the recognizer starts inside the tap. Otherwise the
132
+ * microphone is opened and released at once, inside the tap (`getUserMedia`): that is what raises the browser's
133
+ * permission prompt on phones, where a recognizer's own `start()` is refused (`not-allowed`) without asking anyone.
134
+ * Nothing of that capture is kept, played or sent. Once it is granted, the recognizer starts
135
+ * {@link BROWSER_PRIME_SETTLE_MS} later, so it does not race the released capture for the microphone. The
136
+ * recognizer: interim results, one alternative, the press's language, and `continuous`, except on a touch device,
137
+ * whose recognizer hears one utterance whatever it is asked (and, asked for more, fails sooner). A press still
138
+ * finishing is superseded (aborted, its text never reported; the view lets no tap through meanwhile).
139
+ * 2. **The browser starts listening** (`onstart`, the honest signal: never while a prompt is up): `listening`, and the
140
+ * press's limit counts from here ({@link BrowserAvatarMicOptions.maxPressMs}).
141
+ * 3. **Words as they are recognised**: rebuilt from the whole result list each time (finals, then the words still
142
+ * being recognised), so a result sent again never doubles up, and a final that repeats the one before it with more
143
+ * words replaces it (Android's cumulative finals).
144
+ * 4. **The engine ending by itself** (a pause, a phone's one utterance, a quiet spell's `no-speech`) does NOT end the
145
+ * press: tap to stop holds on every platform. A new recognizer takes over in the same press, its words after the
146
+ * ones before: at once on a computer, after {@link BROWSER_TOUCH_RESTART_MS} on a touch device, and after
147
+ * {@link BROWSER_RESTART_COOLDOWN_MS} when one has just thrown on `start()` or, on a computer, ended as it began.
148
+ * Bounded against a recognizer that never listens: after {@link BROWSER_MAX_QUICK_ENDS} ends in a row as they
149
+ * began, or {@link BROWSER_MAX_RESTARTS} recognizers in all, the press gives what it has. Events of a recognizer that
150
+ * has been replaced are dropped.
151
+ * 5. **The stop** (the visitor's tap, or the limit): `stop()`, not `abort()`, as browsers deliver the last words after
152
+ * it; up to {@link BROWSER_FINAL_WAIT_MS} for its end. A tap while it is still starting (the prompt included) calls
153
+ * the press off.
154
+ * 6. **The text** (`done`): what was heard, final or not (an engine that never marks a result final, iOS Safari, still
155
+ * gives its words: the visitor checks them in the box anyway). Empty when nothing was heard.
156
+ *
157
+ * **Released on every way out.** `cancel()` (the view going, the page hidden, the Chat tab), a failure, a supersede and
158
+ * the end of a press all detach the recognizer's handlers first and then `abort()` it, so the browser's recording
159
+ * indicator goes and nothing it says later is reported. A microphone opened for the prompt is released as soon as it
160
+ * opens, whether or not the press is still there.
161
+ *
162
+ * Errors. Opening the microphone for the prompt: refused (`NotAllowedError`, `SecurityError`) is `denied`; no
163
+ * microphone (`NotFoundError`) is `unsupported`; anything else is `failed`. The recognizer's
164
+ * (`SpeechRecognitionErrorEvent.error`): `not-allowed` or `service-not-allowed` while the microphone is confirmed for
165
+ * the press (the browser reports it granted, or opening it just worked) is tried once more after
166
+ * {@link BROWSER_GRANT_RETRY_MS}, quietly (Android refuses the first start after a fresh grant and takes the next), and
167
+ * after that it is `unsupported`: the speech service refusing, not a permission the visitor can give. Unconfirmed,
168
+ * `not-allowed` is `denied` (and the next tap asks the browser again) and `service-not-allowed` (iOS with Siri and
169
+ * Dictation off, Edge's policy) is `unsupported`; `audio-capture` (no microphone) is `unsupported`;
170
+ * `language-not-supported` tries the fallback language once, then `unsupported`; `network` is `failed`, and
171
+ * `unsupported` the second time running on the page; `no-speech` is a quiet spell (a new recognizer); anything else is
172
+ * `failed`. A failure once words were heard, whatever it is, gives those words instead (`done`).
173
+ */
174
+
175
+ /** One alternative of a recognition result: what the browser heard. */
176
+ interface RecognitionAlternativeLike {
177
+ readonly transcript: string;
178
+ }
179
+ /** One recognition result: final, or still being recognised. */
180
+ interface RecognitionResultLike {
181
+ readonly isFinal: boolean;
182
+ readonly length: number;
183
+ readonly [index: number]: RecognitionAlternativeLike | undefined;
184
+ }
185
+ /** The whole result list of a recognizer, as each `result` event carries it. */
186
+ interface RecognitionResultListLike {
187
+ readonly length: number;
188
+ readonly [index: number]: RecognitionResultLike | undefined;
189
+ }
190
+ /** What this module uses of a browser's `SpeechRecognition` (declared here: TypeScript's DOM library has none). */
191
+ interface RecognitionLike {
192
+ lang: string;
193
+ continuous: boolean;
194
+ interimResults: boolean;
195
+ maxAlternatives: number;
196
+ onstart: (() => void) | null;
197
+ onresult: ((event: {
198
+ readonly results: RecognitionResultListLike;
199
+ }) => void) | null;
200
+ onerror: ((event: {
201
+ readonly error: string;
202
+ }) => void) | null;
203
+ onend: (() => void) | null;
204
+ start(): void;
205
+ stop(): void;
206
+ abort(): void;
207
+ }
208
+ /** What a press needs from the browser. The browser's own by default; a test hands in fakes. */
209
+ interface BrowserAvatarMicSeams {
210
+ /** A new recognizer, or null where the browser has none. */
211
+ createRecognition: () => RecognitionLike | null;
212
+ /** A monotonic clock, in milliseconds, for the cooldowns. */
213
+ now: () => number;
214
+ /** {@link primeBrowserMicrophone}: called inside the tap, where the microphone is not known to be granted. */
215
+ primeMicrophone: () => Promise<void> | null;
216
+ /** {@link watchBrowserMicPermission}: called once, as the mic is made. */
217
+ watchMicPermission: (onState: (state: PermissionState) => void) => void;
218
+ /** {@link hasCoarsePointer}: read at each tap. */
219
+ touch: () => boolean;
220
+ }
221
+ /** The Avatar tab's mic through the browser's own recognizer. See the file's comment for what a press does. */
222
+ declare function createBrowserAvatarMic(options: BrowserAvatarMicOptions, seams?: Partial<BrowserAvatarMicSeams>): AvatarMic;
223
+
224
+ /** What the worklet posts for each frame. */
225
+ interface CaptureFrameMessage {
226
+ /** s16le mono 16 kHz, `CAPTURE_FRAME_SAMPLES` samples. */
227
+ pcm: ArrayBuffer;
228
+ /** Microphone level over the frame, dBFS. */
229
+ rmsDb: number;
230
+ /** Reference-bus level over the same samples, dBFS (about -90 when silent, as it always is here). */
231
+ refDb: number;
232
+ /** Zero crossings per sample. */
233
+ zcr: number;
234
+ /** AudioContext time at the end of the frame, seconds. */
235
+ ctxTime: number;
236
+ seq: number;
237
+ }
238
+
239
+ /**
240
+ * BOFF-9797 (Stage C). The Avatar tab's own microphone capture: `getUserMedia`, through the capture worklet
241
+ * (`capture-worklet.ts`), into 16 kHz PCM frames.
242
+ *
243
+ * **Ported, not shared.** From `src/shared/assistant/voice/handsfree/micCapture.ts` in Algoshred/fe-libs at the
244
+ * commit `capture-worklet.ts` names (`CAPTURE_WORKLET_PORTED_FROM`; blob `8486c630`), with one difference: fe-libs
245
+ * routes everything its assistant plays into the worklet's second input (its playback reference, for barge-in), and the
246
+ * website plays nothing through this context (the avatar's voice is the room's own audio element, and the mic is
247
+ * offered only while no session plays), so no reference bus is made and the worklet has the microphone as its only
248
+ * input. `looksLikeHeadset`, which only fe-libs' barge-in reads, is not ported. `capture-worklet.test.ts` holds the
249
+ * parity tests.
250
+ *
251
+ * It opens `getUserMedia` with echo cancellation, noise suppression and auto gain on. Constraints are plain `true`,
252
+ * never `{ exact }`: `exact` throws OverconstrainedError on handsets that cannot honour one. The settings the browser
253
+ * actually applied are read back (`track.getSettings()`), because some Android handsets report `true` and apply nothing.
254
+ *
255
+ * `stop()` stops the track (the browser's recording indicator goes), disconnects the graph and closes the context. The
256
+ * caller does that the moment a press ends, whichever way it ends; `pagehide` does it too, whatever the caller does.
257
+ */
258
+
259
+ interface MicCaptureHandle {
260
+ context: AudioContext;
261
+ micSource: MediaStreamAudioSourceNode;
262
+ track: MediaStreamTrack;
263
+ /** What the browser reports it applied. */
264
+ settings: MediaTrackSettings;
265
+ /** Stop everything and release the microphone. Safe to call more than once. */
266
+ stop(): void;
267
+ }
268
+
269
+ /** What the mic needs of a WebSocket; {@link openBrowserSocket} adapts the browser's. (fe-libs' seam.) */
270
+ interface SocketLike {
271
+ readonly readyState: number;
272
+ send(data: string | ArrayBufferLike | ArrayBufferView): void;
273
+ close(code?: number): void;
274
+ listen(handlers: SocketHandlers): void;
275
+ }
276
+ interface SocketHandlers {
277
+ open(): void;
278
+ message(data: unknown): void;
279
+ close(code: number): void;
280
+ error(): void;
157
281
  }
158
282
 
159
283
  /**
@@ -176,7 +300,8 @@ interface AvatarMicModule {
176
300
  *
177
301
  * **Released on every way out.** The microphone's track is stopped the moment the recording stops, and also when the
178
302
  * press fails, is cancelled (`cancel()`: the view going, the page hidden, the Chat tab) or superseded (a new press while
179
- * one is still finishing: `cancel` for the old one, so it stops being paid for, and its text is never reported). A
303
+ * one is still finishing, a tap the view lets through for this stream mic only: `cancel` for the old one, so it stops
304
+ * being paid for, and its text is never reported). A
180
305
  * microphone that opens after its press has gone is closed as it opens. The stream is closed whenever no press needs
181
306
  * it: one connection serves a press and the press that supersedes it, and no other.
182
307
  */
@@ -190,4 +315,4 @@ interface AvatarMicSeams {
190
315
  /** The mic of one Avatar tab. See the file's comment for what a press does. */
191
316
  declare function createAvatarMic(options: AvatarMicOptions, seams?: Partial<AvatarMicSeams>): AvatarMic;
192
317
 
193
- export { type AvatarMic, type AvatarMicFailure, type AvatarMicModule, type AvatarMicOffer, type AvatarMicOptions, type AvatarMicState, createAvatarMic };
318
+ export { type AvatarMic, type AvatarMicFailure, type AvatarMicModule, type AvatarMicOffer, type AvatarMicOptions, type AvatarMicState, type BrowserAvatarMicOptions, createAvatarMic, createBrowserAvatarMic };