@voiceinput/core 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2173 @@
1
+ import { VoiceInputError, VoiceInputError as VoiceInputError$1 } from "@voiceinput/provider";
2
+ //#region src/audio-worklet-source.ts
3
+ const VOICE_INPUT_PROCESSOR_NAME = "voiceinput-pcm16";
4
+ /** Self-contained so the emitted function can also be loaded as a worklet. */
5
+ function registerVoiceInputPcm16Processor(ProcessorBase, sourceSampleRate, register, processorName) {
6
+ class VoiceInputPcm16Processor extends ProcessorBase {
7
+ #frameSamples;
8
+ #ratio;
9
+ #input = [];
10
+ #filter;
11
+ #filterHistory;
12
+ #position = 0;
13
+ #frame;
14
+ #frameOffset = 0;
15
+ #filterIndex = 0;
16
+ constructor(options) {
17
+ super();
18
+ const processorOptions = options.processorOptions ?? {};
19
+ const targetSampleRate = processorOptions.targetSampleRate ?? 16e3;
20
+ this.#frameSamples = processorOptions.frameSamples ?? 320;
21
+ this.#ratio = sourceSampleRate / targetSampleRate;
22
+ this.#filter = this.#createLowPassFilter();
23
+ this.#filterHistory = new Float32Array(this.#filter.length);
24
+ this.#frame = new Int16Array(this.#frameSamples);
25
+ this.port.onmessage = (event) => {
26
+ if (typeof event.data === "object" && event.data !== null && "type" in event.data && event.data.type === "flush") {
27
+ this.#flush();
28
+ this.port.postMessage({ type: "flushed" });
29
+ }
30
+ };
31
+ }
32
+ process(inputs) {
33
+ const channels = inputs[0];
34
+ const sampleCount = channels?.[0]?.length ?? 0;
35
+ if (channels === void 0 || channels.length === 0 || sampleCount === 0) return true;
36
+ for (let index = 0; index < sampleCount; index += 1) {
37
+ let monoSample = 0;
38
+ for (const channel of channels) monoSample += channel[index] ?? 0;
39
+ this.#input.push(this.#filterSample(monoSample / channels.length));
40
+ }
41
+ this.#drain();
42
+ return true;
43
+ }
44
+ #createLowPassFilter() {
45
+ if (this.#ratio <= 1) return [1];
46
+ const tapCount = 31;
47
+ const center = 15;
48
+ const cutoff = .45 / this.#ratio;
49
+ const coefficients = [];
50
+ let sum = 0;
51
+ for (let index = 0; index < tapCount; index += 1) {
52
+ const offset = index - center;
53
+ const coefficient = (offset === 0 ? 2 * cutoff : Math.sin(2 * Math.PI * cutoff * offset) / (Math.PI * offset)) * (.42 - .5 * Math.cos(2 * Math.PI * index / 30) + .08 * Math.cos(4 * Math.PI * index / 30));
54
+ coefficients.push(coefficient);
55
+ sum += coefficient;
56
+ }
57
+ return coefficients.map((coefficient) => coefficient / sum);
58
+ }
59
+ #filterSample(sample) {
60
+ this.#filterHistory[this.#filterIndex] = sample;
61
+ let filtered = 0;
62
+ for (let tap = 0; tap < this.#filter.length; tap += 1) {
63
+ const historyIndex = (this.#filterIndex - tap + this.#filter.length) % this.#filter.length;
64
+ filtered += (this.#filter[tap] ?? 0) * (this.#filterHistory[historyIndex] ?? 0);
65
+ }
66
+ this.#filterIndex = (this.#filterIndex + 1) % this.#filter.length;
67
+ return filtered;
68
+ }
69
+ #drain() {
70
+ while (this.#position + 1 < this.#input.length) {
71
+ const lower = Math.floor(this.#position);
72
+ const fraction = this.#position - lower;
73
+ const first = this.#input[lower] ?? 0;
74
+ const sample = first + ((this.#input[lower + 1] ?? first) - first) * fraction;
75
+ const clamped = Math.max(-1, Math.min(1, sample));
76
+ this.#frame[this.#frameOffset] = clamped < 0 ? clamped * 32768 : clamped * 32767;
77
+ this.#frameOffset += 1;
78
+ if (this.#frameOffset === this.#frame.length) {
79
+ this.#emitFrame(this.#frame);
80
+ this.#frame = new Int16Array(this.#frameSamples);
81
+ this.#frameOffset = 0;
82
+ }
83
+ this.#position += this.#ratio;
84
+ }
85
+ const consumed = Math.min(Math.floor(this.#position), Math.max(0, this.#input.length - 1));
86
+ if (consumed > 0) {
87
+ this.#input.splice(0, consumed);
88
+ this.#position -= consumed;
89
+ }
90
+ }
91
+ #flush() {
92
+ const lastSample = this.#input.at(-1);
93
+ if (lastSample !== void 0) {
94
+ this.#input.push(lastSample);
95
+ this.#drain();
96
+ }
97
+ if (this.#frameOffset > 0) this.#emitFrame(this.#frame.slice(0, this.#frameOffset));
98
+ this.#input.length = 0;
99
+ this.#position = 0;
100
+ this.#frame = new Int16Array(this.#frameSamples);
101
+ this.#frameOffset = 0;
102
+ }
103
+ #emitFrame(frame) {
104
+ const buffer = frame.buffer;
105
+ this.port.postMessage(buffer, [buffer]);
106
+ }
107
+ }
108
+ register(processorName, VoiceInputPcm16Processor);
109
+ }
110
+ const AUDIO_WORKLET_SOURCE = `(${registerVoiceInputPcm16Processor.toString()})(AudioWorkletProcessor, sampleRate, registerProcessor, ${JSON.stringify(VOICE_INPUT_PROCESSOR_NAME)});`;
111
+ //#endregion
112
+ //#region src/browser-audio.ts
113
+ const DEFAULT_FRAME_DURATION_MS = 20;
114
+ const FLUSH_TIMEOUT_MS = 500;
115
+ function getBrowserVoiceInputSupport() {
116
+ const missingCapabilities = [];
117
+ const browser = globalThis;
118
+ if (globalThis.isSecureContext !== true) missingCapabilities.push("secure-context");
119
+ if (typeof navigator === "undefined" || navigator.mediaDevices === void 0) missingCapabilities.push("media-devices");
120
+ else if (typeof navigator.mediaDevices.getUserMedia !== "function") missingCapabilities.push("get-user-media");
121
+ if (browser.AudioContext === void 0 && browser.webkitAudioContext === void 0) missingCapabilities.push("audio-context");
122
+ else {
123
+ const AudioContextConstructor = browser.AudioContext ?? browser.webkitAudioContext;
124
+ if (AudioContextConstructor === void 0 || !("audioWorklet" in AudioContextConstructor.prototype) || typeof globalThis.AudioWorkletNode !== "function") missingCapabilities.push("audio-worklet");
125
+ }
126
+ return Object.freeze({
127
+ isSupported: missingCapabilities.length === 0,
128
+ missingCapabilities: Object.freeze(missingCapabilities)
129
+ });
130
+ }
131
+ function createBrowserAudioSource(options = {}) {
132
+ const frameDurationMs = options.frameDurationMs ?? DEFAULT_FRAME_DURATION_MS;
133
+ if (!Number.isFinite(frameDurationMs) || frameDurationMs <= 0) throw new VoiceInputError$1({
134
+ code: "invalid-configuration",
135
+ message: "frameDurationMs must be a positive finite number."
136
+ });
137
+ const workletModuleUrl = options.workletModuleUrl === void 0 ? void 0 : String(options.workletModuleUrl);
138
+ if (workletModuleUrl !== void 0 && workletModuleUrl.trim().length === 0) throw new VoiceInputError$1({
139
+ code: "invalid-configuration",
140
+ message: "workletModuleUrl must be a non-empty URL."
141
+ });
142
+ return { async prepare(prepareOptions) {
143
+ assertBrowserSupport();
144
+ assertUserActivation();
145
+ return prepareBrowserAudio(prepareOptions, {
146
+ constraints: options.constraints,
147
+ frameDurationMs,
148
+ workletModuleUrl
149
+ });
150
+ } };
151
+ }
152
+ async function prepareBrowserAudio(prepareOptions, options) {
153
+ const { abortSignal, sampleRate } = prepareOptions;
154
+ throwIfAborted(abortSignal);
155
+ let mediaStream;
156
+ let audioContext;
157
+ let sourceNode;
158
+ let workletNode;
159
+ let silentOutput;
160
+ let streamController;
161
+ let closeContextPromise;
162
+ let flushTimer;
163
+ let resolveFlush;
164
+ let started = false;
165
+ let closed = false;
166
+ let tracksStopped = false;
167
+ const stream = new ReadableStream({ start(controller) {
168
+ streamController = controller;
169
+ } }, {
170
+ highWaterMark: sampleRate * 15,
171
+ size: (chunk) => chunk.length
172
+ });
173
+ const cleanup = (error) => {
174
+ if (closed) return;
175
+ closed = true;
176
+ abortSignal.removeEventListener("abort", handleAbort);
177
+ safely$1(() => sourceNode?.disconnect());
178
+ safely$1(() => workletNode?.disconnect());
179
+ safely$1(() => silentOutput?.disconnect());
180
+ safely$1(() => workletNode?.port.close());
181
+ stopTracks();
182
+ if (audioContext !== void 0 && audioContext.state !== "closed") closeContextPromise ??= audioContext.close().catch(() => {});
183
+ if (streamController !== void 0) {
184
+ if (error === void 0) safely$1(() => streamController?.close());
185
+ else safely$1(() => streamController?.error(error));
186
+ }
187
+ if (flushTimer !== void 0) {
188
+ clearTimeout(flushTimer);
189
+ flushTimer = void 0;
190
+ }
191
+ resolveFlush?.();
192
+ resolveFlush = void 0;
193
+ };
194
+ function stopTracks() {
195
+ if (tracksStopped || mediaStream === void 0) return;
196
+ tracksStopped = true;
197
+ for (const track of mediaStream.getTracks()) safely$1(() => track.stop());
198
+ }
199
+ const handleAbort = () => cleanup();
200
+ abortSignal.addEventListener("abort", handleAbort, { once: true });
201
+ try {
202
+ mediaStream = await navigator.mediaDevices.getUserMedia({
203
+ audio: {
204
+ ...options.constraints,
205
+ channelCount: 1
206
+ },
207
+ video: false
208
+ });
209
+ if (closed || abortSignal.aborted) stopTracks();
210
+ throwIfAborted(abortSignal);
211
+ prepareOptions.onAcquired?.();
212
+ throwIfAborted(abortSignal);
213
+ audioContext = createAudioContext(getAudioContextConstructor(), sampleRate);
214
+ if (audioContext.audioWorklet === void 0) throw unsupportedBrowser(["audio-worklet"]);
215
+ try {
216
+ await loadWorklet(audioContext, options.workletModuleUrl);
217
+ } catch (cause) {
218
+ throwIfAborted(abortSignal);
219
+ throw new VoiceInputError$1({
220
+ code: "audio-error",
221
+ message: "The microphone AudioWorklet could not be loaded. Check workletModuleUrl and the page's Content Security Policy.",
222
+ retryable: true,
223
+ cause
224
+ });
225
+ }
226
+ throwIfAborted(abortSignal);
227
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
228
+ workletNode = new AudioWorkletNode(audioContext, VOICE_INPUT_PROCESSOR_NAME, {
229
+ numberOfInputs: 1,
230
+ numberOfOutputs: 1,
231
+ outputChannelCount: [1],
232
+ channelCount: 1,
233
+ channelCountMode: "explicit",
234
+ channelInterpretation: "speakers",
235
+ processorOptions: {
236
+ frameSamples: Math.max(1, Math.round(sampleRate * options.frameDurationMs / 1e3)),
237
+ targetSampleRate: sampleRate
238
+ }
239
+ });
240
+ silentOutput = audioContext.createGain();
241
+ silentOutput.gain.value = 0;
242
+ workletNode.connect(silentOutput);
243
+ silentOutput.connect(audioContext.destination);
244
+ workletNode.port.onmessage = (event) => {
245
+ if (closed || streamController === void 0) return;
246
+ const data = event.data;
247
+ if (data instanceof ArrayBuffer || data instanceof Int16Array) {
248
+ const chunk = data instanceof ArrayBuffer ? new Int16Array(data) : data;
249
+ if ((streamController.desiredSize ?? 0) < chunk.length) {
250
+ cleanup(new VoiceInputError$1({
251
+ code: "audio-error",
252
+ retryable: true,
253
+ message: "The application stopped consuming microphone audio."
254
+ }));
255
+ return;
256
+ }
257
+ streamController.enqueue(chunk);
258
+ } else if (typeof data === "object" && data !== null && "type" in data && data.type === "flushed") {
259
+ resolveFlush?.();
260
+ resolveFlush = void 0;
261
+ }
262
+ };
263
+ workletNode.port.onmessageerror = (event) => {
264
+ cleanup(new VoiceInputError$1({
265
+ code: "audio-error",
266
+ message: "The browser could not read microphone audio frames.",
267
+ retryable: true,
268
+ cause: event
269
+ }));
270
+ };
271
+ workletNode.addEventListener("processorerror", (event) => {
272
+ cleanup(new VoiceInputError$1({
273
+ code: "audio-error",
274
+ message: "The microphone audio processor stopped unexpectedly.",
275
+ retryable: true,
276
+ cause: event
277
+ }));
278
+ }, { once: true });
279
+ for (const track of mediaStream.getAudioTracks()) track.addEventListener("ended", () => {
280
+ if (!closed) cleanup(new VoiceInputError$1({
281
+ code: "audio-error",
282
+ message: "The microphone stopped unexpectedly.",
283
+ retryable: true
284
+ }));
285
+ }, { once: true });
286
+ if (audioContext.state !== "running") await audioContext.resume();
287
+ throwIfAborted(abortSignal);
288
+ audioContext.addEventListener("statechange", () => {
289
+ if (started && !closed && audioContext?.state !== "running") cleanup(new VoiceInputError$1({
290
+ code: "audio-error",
291
+ retryable: true,
292
+ message: "Microphone capture was interrupted. Start a new recording to continue."
293
+ }));
294
+ });
295
+ return {
296
+ stream,
297
+ start() {
298
+ if (closed || started) return;
299
+ started = true;
300
+ sourceNode?.connect(workletNode);
301
+ },
302
+ async stop() {
303
+ stopTracks();
304
+ if (!closed && started && workletNode !== void 0) {
305
+ safely$1(() => sourceNode?.disconnect());
306
+ started = false;
307
+ await new Promise((resolve) => {
308
+ resolveFlush = resolve;
309
+ flushTimer = setTimeout(resolve, FLUSH_TIMEOUT_MS);
310
+ workletNode?.port.postMessage({ type: "flush" });
311
+ });
312
+ }
313
+ cleanup();
314
+ await closeContextPromise;
315
+ },
316
+ abort() {
317
+ cleanup();
318
+ }
319
+ };
320
+ } catch (error) {
321
+ cleanup();
322
+ if (VoiceInputError$1.isInstance(error)) throw error;
323
+ throw normalizeBrowserAudioError(error);
324
+ }
325
+ }
326
+ function normalizeBrowserAudioError(error) {
327
+ const name = getErrorName(error);
328
+ if (name === "NotAllowedError" || name === "SecurityError") return new VoiceInputError$1({
329
+ code: "permission-denied",
330
+ message: "Microphone permission was denied.",
331
+ cause: error
332
+ });
333
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") return new VoiceInputError$1({
334
+ code: "device-not-found",
335
+ message: "No microphone is available.",
336
+ cause: error
337
+ });
338
+ if (name === "NotReadableError" || name === "TrackStartError" || name === "AbortError") return new VoiceInputError$1({
339
+ code: "device-busy",
340
+ message: "The microphone is unavailable or in use by another application.",
341
+ retryable: true,
342
+ cause: error
343
+ });
344
+ return new VoiceInputError$1({
345
+ code: "audio-error",
346
+ message: "The browser audio pipeline failed.",
347
+ retryable: true,
348
+ cause: error
349
+ });
350
+ }
351
+ function assertBrowserSupport() {
352
+ const support = getBrowserVoiceInputSupport();
353
+ if (!support.isSupported) throw unsupportedBrowser(support.missingCapabilities);
354
+ }
355
+ function assertUserActivation() {
356
+ if (navigator.userActivation !== void 0 && navigator.userActivation.isActive === false) throw new VoiceInputError$1({
357
+ code: "permission-denied",
358
+ message: "Microphone access must be started directly from a user interaction."
359
+ });
360
+ }
361
+ function unsupportedBrowser(missingCapabilities) {
362
+ return new VoiceInputError$1({
363
+ code: "unsupported-browser",
364
+ message: `Voice input is unavailable because the browser is missing: ${missingCapabilities.join(", ")}.`
365
+ });
366
+ }
367
+ function getAudioContextConstructor() {
368
+ const browser = globalThis;
369
+ const AudioContextConstructor = browser.AudioContext ?? browser.webkitAudioContext;
370
+ if (AudioContextConstructor === void 0) throw unsupportedBrowser(["audio-context"]);
371
+ return AudioContextConstructor;
372
+ }
373
+ function createAudioContext(AudioContextConstructor, sampleRate) {
374
+ try {
375
+ return new AudioContextConstructor({
376
+ latencyHint: "interactive",
377
+ sampleRate
378
+ });
379
+ } catch (error) {
380
+ const name = getErrorName(error);
381
+ if (name !== "NotSupportedError" && name !== "TypeError") throw error;
382
+ return new AudioContextConstructor({ latencyHint: "interactive" });
383
+ }
384
+ }
385
+ async function loadWorklet(context, moduleUrl) {
386
+ if (moduleUrl !== void 0) {
387
+ await context.audioWorklet.addModule(moduleUrl);
388
+ return;
389
+ }
390
+ const url = URL.createObjectURL(new Blob([AUDIO_WORKLET_SOURCE], { type: "text/javascript" }));
391
+ try {
392
+ await context.audioWorklet.addModule(url);
393
+ } finally {
394
+ URL.revokeObjectURL(url);
395
+ }
396
+ }
397
+ function throwIfAborted(signal) {
398
+ if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
399
+ }
400
+ function getErrorName(error) {
401
+ return typeof error === "object" && error !== null && "name" in error ? String(error.name) : void 0;
402
+ }
403
+ function safely$1(operation) {
404
+ try {
405
+ operation();
406
+ } catch {}
407
+ }
408
+ //#endregion
409
+ //#region src/audio-queue.ts
410
+ /** A bounded FIFO shared by startup buffering and a slow provider transport. */
411
+ var AudioQueue = class {
412
+ maximumSamples;
413
+ #chunks = [];
414
+ #samples = 0;
415
+ #closed = false;
416
+ #wake;
417
+ constructor(maximumSamples) {
418
+ this.maximumSamples = maximumSamples;
419
+ }
420
+ push(chunk) {
421
+ if (this.#closed) return;
422
+ if (this.#samples + chunk.length > this.maximumSamples) throw new VoiceInputError$1({
423
+ code: "network-error",
424
+ retryable: true,
425
+ message: "Audio could not be sent fast enough. Recording stopped before the audio buffer overflowed."
426
+ });
427
+ this.#chunks.push(chunk.slice());
428
+ this.#samples += chunk.length;
429
+ this.#wake?.();
430
+ this.#wake = void 0;
431
+ }
432
+ async read() {
433
+ while (!this.#closed && this.#chunks.length === 0) await new Promise((resolve) => {
434
+ this.#wake = resolve;
435
+ });
436
+ const chunk = this.#chunks.shift();
437
+ if (chunk) this.#samples -= chunk.length;
438
+ return chunk;
439
+ }
440
+ close(discard = false) {
441
+ this.#closed = true;
442
+ if (discard) {
443
+ this.#chunks = [];
444
+ this.#samples = 0;
445
+ }
446
+ this.#wake?.();
447
+ this.#wake = void 0;
448
+ }
449
+ };
450
+ //#endregion
451
+ //#region src/transcript-boundary.ts
452
+ const WORD_CHARACTER = /[\p{L}\p{N}]/u;
453
+ const NO_SPACE_CHARACTER = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
454
+ const WHITESPACE_CHARACTER = /\s/u;
455
+ const OPENING_PUNCTUATION = /[(\u005b{<\u2018\u201c([{〈《「『【〔]/u;
456
+ const CLOSING_PUNCTUATION = /[.,!?;:%)\]}\u003e\u2019\u201d\u2026。、,.!?:;)]}〉》」』】〕]/u;
457
+ function appendTranscriptPart(current, part) {
458
+ return `${current}${normalizeTranscriptInsertion(current, "", part)}`;
459
+ }
460
+ function normalizeTranscriptInsertion(left, right, text) {
461
+ const core = text.replace(/^\s+/u, "").replace(/\s+$/u, "");
462
+ if (core.length === 0) return "";
463
+ return `${needsBoundarySpace(left.at(-1), core.at(0), "left") ? " " : ""}${core}${needsBoundarySpace(core.at(-1), right.at(0), "right") ? " " : ""}`;
464
+ }
465
+ function needsBoundarySpace(left, right, side) {
466
+ if (left === void 0 || right === void 0 || WHITESPACE_CHARACTER.test(left) || WHITESPACE_CHARACTER.test(right) || OPENING_PUNCTUATION.test(left) || CLOSING_PUNCTUATION.test(right) || NO_SPACE_CHARACTER.test(left) && NO_SPACE_CHARACTER.test(right)) return false;
467
+ return WORD_CHARACTER.test(left) || WORD_CHARACTER.test(right) || side === "right" && CLOSING_PUNCTUATION.test(left);
468
+ }
469
+ //#endregion
470
+ //#region src/session.ts
471
+ const DEFAULT_MAX_DURATION_MS = 3e5;
472
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 15e3;
473
+ const DURATION_WARNING_MS = 3e4;
474
+ const FINALIZATION_TIMEOUT_MS = 5e3;
475
+ function createVoiceInputSession(options) {
476
+ assertProvider(options.provider);
477
+ assertAudioSource(options.audioSource);
478
+ if (options.textEngine !== void 0) assertTextEngine(options.textEngine);
479
+ const configuration = validateSessionConfiguration(options);
480
+ return new VoiceInputSessionController(options.provider, options.audioSource, options.textEngine, configuration);
481
+ }
482
+ var VoiceInputSessionController = class {
483
+ #listeners = /* @__PURE__ */ new Set();
484
+ #provider;
485
+ #audioSource;
486
+ #textEngine;
487
+ #configuration;
488
+ #snapshot = Object.freeze({
489
+ status: "idle",
490
+ transcript: "",
491
+ interimTranscript: "",
492
+ finalTranscript: "",
493
+ error: null
494
+ });
495
+ #activeRun;
496
+ #nextOptions;
497
+ updateOptions(options) {
498
+ this.#nextOptions = options;
499
+ }
500
+ constructor(provider, audioSource, textEngine, configuration) {
501
+ this.#provider = provider;
502
+ this.#audioSource = audioSource;
503
+ this.#textEngine = textEngine;
504
+ this.#configuration = configuration;
505
+ }
506
+ getSnapshot() {
507
+ return this.#snapshot;
508
+ }
509
+ subscribe(listener) {
510
+ this.#listeners.add(listener);
511
+ return () => this.#listeners.delete(listener);
512
+ }
513
+ async start() {
514
+ if (this.#snapshot.status !== "idle" && this.#snapshot.status !== "error") return;
515
+ try {
516
+ if (this.#nextOptions) {
517
+ const options = this.#nextOptions;
518
+ assertProvider(options.provider);
519
+ assertAudioSource(options.audioSource);
520
+ this.#provider = options.provider;
521
+ this.#audioSource = options.audioSource;
522
+ this.#configuration = validateSessionConfiguration(options);
523
+ }
524
+ } catch (error) {
525
+ this.#setPreflightError(this.#normalizeValidationError(error));
526
+ return;
527
+ }
528
+ this.#setSnapshot({
529
+ transcript: "",
530
+ interimTranscript: "",
531
+ finalTranscript: "",
532
+ error: null
533
+ });
534
+ const transcriptionOptions = getTranscriptionOptions(this.#configuration);
535
+ try {
536
+ this.#provider.validateOptions(transcriptionOptions);
537
+ } catch (error) {
538
+ this.#setPreflightError(this.#normalizeValidationError(error));
539
+ return;
540
+ }
541
+ const run = {
542
+ abortController: new AbortController(),
543
+ queue: new AudioQueue(this.#provider.sampleRate * 15),
544
+ closedSegments: /* @__PURE__ */ new Set(),
545
+ implicitSegment: 0,
546
+ cleanup: []
547
+ };
548
+ this.#textEngine?.begin();
549
+ this.#activeRun = run;
550
+ if (this.#textEngine) run.cleanup.push(this.#textEngine.subscribe((event) => {
551
+ if (!this.#isActive(run)) return;
552
+ if (event.type === "text-limit") this.#emit(event);
553
+ const reason = event.type === "text-limit" ? "max-length" : event.type === "reset" ? "replaced" : "target-unavailable";
554
+ queueMicrotask(() => {
555
+ if (this.#isActive(run)) this.stop(reason);
556
+ });
557
+ }));
558
+ if (typeof document !== "undefined") {
559
+ const onVisibility = () => {
560
+ if (document.hidden && this.#isActive(run)) this.stop("backgrounded");
561
+ };
562
+ document.addEventListener("visibilitychange", onVisibility);
563
+ run.cleanup.push(() => document.removeEventListener("visibilitychange", onVisibility));
564
+ }
565
+ this.#transition("requesting-permission");
566
+ if (!this.#isActive(run)) return;
567
+ const startConnectionDeadline = () => {
568
+ if (this.#isActive(run)) this.#scheduleConnectionDeadline(run);
569
+ };
570
+ let audio;
571
+ try {
572
+ const preparation = Promise.resolve(this.#audioSource.prepare({
573
+ sampleRate: this.#provider.sampleRate,
574
+ abortSignal: run.abortController.signal,
575
+ onAcquired: startConnectionDeadline
576
+ }));
577
+ preparation.then((lateAudio) => {
578
+ if (!this.#isActive(run)) safely(() => lateAudio.abort(run.abortController.signal.reason ?? "stale-session"));
579
+ }, () => {});
580
+ audio = await untilAborted(preparation, run.abortController.signal);
581
+ } catch (error) {
582
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "audio-error"));
583
+ return;
584
+ }
585
+ if (!this.#isActive(run)) {
586
+ safely(() => audio.abort("stale-session"));
587
+ return;
588
+ }
589
+ run.audio = audio;
590
+ startConnectionDeadline();
591
+ run.captureTask = this.#captureAudio(run, audio.stream);
592
+ this.#scheduleDurationLimit(run);
593
+ try {
594
+ await untilAborted(Promise.resolve(audio.start()), run.abortController.signal);
595
+ } catch (error) {
596
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "audio-error"));
597
+ return;
598
+ }
599
+ if (!this.#isActive(run)) return;
600
+ this.#transition("connecting");
601
+ if (!this.#isActive(run)) return;
602
+ let providerSession;
603
+ try {
604
+ const opening = Promise.resolve(this.#provider.doOpen({
605
+ ...transcriptionOptions,
606
+ abortSignal: run.abortController.signal
607
+ }));
608
+ opening.then((lateSession) => {
609
+ if (!this.#isActive(run)) safely(() => lateSession.abort(run.abortController.signal.reason ?? "stale-session"));
610
+ }, () => {});
611
+ providerSession = await untilAborted(opening, run.abortController.signal);
612
+ } catch (error) {
613
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "provider-error"));
614
+ return;
615
+ }
616
+ if (!this.#isActive(run)) {
617
+ safely(() => providerSession.abort("stale-session"));
618
+ safely(() => audio.abort("stale-session"));
619
+ return;
620
+ }
621
+ run.providerSession = providerSession;
622
+ run.providerTask = this.#consumeProviderStream(run, providerSession);
623
+ run.audioTask = this.#pumpAudio(run, providerSession);
624
+ this.#clearConnectionTimer(run);
625
+ this.#transition("listening");
626
+ }
627
+ async stop(reason = "user") {
628
+ const run = this.#activeRun;
629
+ if (run === void 0) return;
630
+ if (run.stopPromise === void 0) run.stopPromise = this.#performStop(run, reason);
631
+ await run.stopPromise;
632
+ }
633
+ async cancel() {
634
+ const run = this.#activeRun;
635
+ if (run === void 0) return;
636
+ this.#activeRun = void 0;
637
+ this.#abortRun(run, "cancelled");
638
+ this.#textEngine?.cancel();
639
+ this.#setSnapshot({
640
+ transcript: this.#snapshot.finalTranscript,
641
+ interimTranscript: "",
642
+ error: null
643
+ });
644
+ this.#transition("idle");
645
+ this.#emit({ type: "cancel" });
646
+ }
647
+ async toggle() {
648
+ if (this.#activeRun === void 0) await this.start();
649
+ else await this.stop();
650
+ }
651
+ async #performStop(run, reason) {
652
+ this.#clearRunTimers(run);
653
+ this.#transition("stopping");
654
+ if (!this.#isActive(run)) return;
655
+ const audio = run.audio;
656
+ const providerSession = run.providerSession;
657
+ const audioTask = run.audioTask;
658
+ const providerTask = run.providerTask;
659
+ if (providerSession === void 0) {
660
+ if (!await this.#completeTextEngine(run)) return;
661
+ this.#activeRun = void 0;
662
+ this.#abortRun(run, reason);
663
+ this.#transition("idle");
664
+ this.#emit({
665
+ type: "stop",
666
+ reason
667
+ });
668
+ return;
669
+ }
670
+ try {
671
+ await withTimeout((async () => {
672
+ await audio?.stop();
673
+ await run.captureTask;
674
+ await audioTask;
675
+ if (!this.#isActive(run)) return;
676
+ await providerSession.finish();
677
+ await providerTask;
678
+ })(), FINALIZATION_TIMEOUT_MS, this.#provider.provider);
679
+ if (!this.#isActive(run)) return;
680
+ if (!await this.#completeTextEngine(run)) return;
681
+ this.#activeRun = void 0;
682
+ this.#clearRunTimers(run);
683
+ for (const cleanup of run.cleanup.splice(0)) cleanup();
684
+ run.queue.close(true);
685
+ this.#transition("idle");
686
+ this.#emit({
687
+ type: "stop",
688
+ reason
689
+ });
690
+ } catch (error) {
691
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "provider-error"));
692
+ }
693
+ }
694
+ async #consumeProviderStream(run, session) {
695
+ const reader = session.stream.getReader();
696
+ try {
697
+ while (this.#isActive(run)) {
698
+ const result = await reader.read();
699
+ if (result.done || !this.#isActive(run)) break;
700
+ if (this.#handleProviderPart(run, result.value)) return;
701
+ }
702
+ if (this.#isActive(run) && this.#snapshot.status !== "stopping" && this.#snapshot.status !== "error") this.#failRun(run, new VoiceInputError({
703
+ code: "provider-error",
704
+ message: `${this.#provider.provider} ended the transcription stream unexpectedly.`,
705
+ provider: this.#provider.provider,
706
+ retryable: true
707
+ }));
708
+ } catch (error) {
709
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "provider-error"));
710
+ } finally {
711
+ reader.releaseLock();
712
+ }
713
+ }
714
+ #handleProviderPart(run, part) {
715
+ if (!this.#isActive(run)) return true;
716
+ const segmentId = part.type === "interim" || part.type === "final" ? part.segmentId ?? `legacy:${run.implicitSegment}` : "";
717
+ if (part.type === "interim" || part.type === "final") {
718
+ if (typeof segmentId !== "string" || segmentId.length === 0) throw new TypeError("Transcription segmentId must be a nonempty string.");
719
+ if (run.closedSegments.has(segmentId)) return false;
720
+ if (part.type === "final") {
721
+ run.closedSegments.add(segmentId);
722
+ run.implicitSegment++;
723
+ }
724
+ }
725
+ switch (part.type) {
726
+ case "interim": {
727
+ this.#textEngine?.applyInterim(part.text, segmentId);
728
+ const previousTranscript = this.#snapshot.transcript;
729
+ const transcript = appendTranscriptPart(this.#snapshot.finalTranscript, part.text);
730
+ this.#setSnapshot({
731
+ interimTranscript: part.text,
732
+ transcript
733
+ });
734
+ this.#emit({
735
+ type: "interim",
736
+ text: part.text,
737
+ segmentId,
738
+ transcript,
739
+ transcriptChanged: transcript !== previousTranscript
740
+ });
741
+ return false;
742
+ }
743
+ case "final": {
744
+ this.#textEngine?.applyFinal(part.text, segmentId);
745
+ const previousTranscript = this.#snapshot.transcript;
746
+ const previousFinalTranscript = this.#snapshot.finalTranscript;
747
+ const finalTranscript = appendTranscriptPart(previousFinalTranscript, part.text);
748
+ this.#setSnapshot({
749
+ finalTranscript,
750
+ interimTranscript: "",
751
+ transcript: finalTranscript
752
+ });
753
+ this.#emit({
754
+ type: "final",
755
+ text: part.text,
756
+ segmentId,
757
+ transcript: finalTranscript,
758
+ transcriptChanged: finalTranscript !== previousTranscript,
759
+ finalTranscriptChanged: finalTranscript !== previousFinalTranscript
760
+ });
761
+ return false;
762
+ }
763
+ case "error":
764
+ this.#failRun(run, VoiceInputError.isInstance(part.error) ? part.error : this.#normalizeError(part.error, "provider-error"));
765
+ return true;
766
+ case "speech-start":
767
+ this.#emit({ type: "speech-start" });
768
+ return false;
769
+ case "speech-end":
770
+ this.#emit({ type: "speech-end" });
771
+ return false;
772
+ }
773
+ }
774
+ async #captureAudio(run, stream) {
775
+ const reader = stream.getReader();
776
+ const cancelReader = () => {
777
+ reader.cancel().catch(() => {});
778
+ };
779
+ run.abortController.signal.addEventListener("abort", cancelReader, { once: true });
780
+ try {
781
+ while (this.#isActive(run)) {
782
+ const result = await reader.read();
783
+ if (result.done || !this.#isActive(run)) break;
784
+ if (!(result.value instanceof Int16Array)) throw new VoiceInputError({
785
+ code: "audio-error",
786
+ message: "The audio source emitted a non-PCM16 audio chunk."
787
+ });
788
+ run.queue.push(result.value);
789
+ }
790
+ } catch (error) {
791
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "audio-error"));
792
+ } finally {
793
+ run.abortController.signal.removeEventListener("abort", cancelReader);
794
+ reader.releaseLock();
795
+ run.queue.close();
796
+ }
797
+ }
798
+ async #pumpAudio(run, session) {
799
+ try {
800
+ while (this.#isActive(run)) {
801
+ const chunk = await run.queue.read();
802
+ if (!chunk || !this.#isActive(run)) return;
803
+ await untilAborted(Promise.resolve(session.sendAudio(chunk)), run.abortController.signal);
804
+ }
805
+ } catch (error) {
806
+ if (this.#isActive(run)) this.#failRun(run, this.#normalizeError(error, "audio-error"));
807
+ }
808
+ }
809
+ #scheduleDurationLimit(run) {
810
+ const { maxDurationMs } = this.#configuration;
811
+ const warningDelayMs = maxDurationMs - DURATION_WARNING_MS;
812
+ const warn = () => {
813
+ if (this.#isActive(run) && this.#snapshot.status !== "stopping") this.#emit({
814
+ type: "duration-warning",
815
+ remainingMs: Math.min(DURATION_WARNING_MS, maxDurationMs),
816
+ maxDurationMs
817
+ });
818
+ };
819
+ if (warningDelayMs <= 0) warn();
820
+ else run.warningTimer = setTimeout(warn, warningDelayMs);
821
+ run.durationTimer = setTimeout(() => {
822
+ if (this.#isActive(run) && this.#snapshot.status !== "stopping") this.stop("max-duration");
823
+ }, maxDurationMs);
824
+ }
825
+ #scheduleConnectionDeadline(run) {
826
+ if (run.connectionDeadlineStarted === true) return;
827
+ run.connectionDeadlineStarted = true;
828
+ const { connectionTimeoutMs } = this.#configuration;
829
+ run.connectionTimer = setTimeout(() => {
830
+ if (!this.#isActive(run)) return;
831
+ this.#failRun(run, new VoiceInputError({
832
+ code: "network-error",
833
+ message: `${this.#provider.provider} did not connect within ${connectionTimeoutMs} ms. Check the network and try again.`,
834
+ provider: this.#provider.provider,
835
+ retryable: true
836
+ }));
837
+ }, connectionTimeoutMs);
838
+ }
839
+ #setPreflightError(error) {
840
+ this.#setSnapshot({
841
+ transcript: "",
842
+ interimTranscript: "",
843
+ finalTranscript: "",
844
+ error
845
+ });
846
+ this.#transition("error");
847
+ this.#emit({
848
+ type: "error",
849
+ error
850
+ });
851
+ }
852
+ #failRun(run, error) {
853
+ if (!this.#isActive(run)) return;
854
+ this.#activeRun = void 0;
855
+ this.#abortRun(run, error);
856
+ this.#textEngine?.cancel();
857
+ this.#setSnapshot({
858
+ transcript: this.#snapshot.finalTranscript,
859
+ interimTranscript: "",
860
+ error
861
+ });
862
+ this.#transition("error");
863
+ this.#emit({
864
+ type: "error",
865
+ error
866
+ });
867
+ }
868
+ #abortRun(run, reason) {
869
+ this.#clearRunTimers(run);
870
+ for (const cleanup of run.cleanup.splice(0)) cleanup();
871
+ run.queue.close(true);
872
+ safely(() => run.audio?.abort(reason));
873
+ safely(() => run.abortController.abort(reason));
874
+ safely(() => run.providerSession?.abort(reason));
875
+ }
876
+ #normalizeValidationError(error) {
877
+ if (VoiceInputError.isInstance(error)) return error;
878
+ return new VoiceInputError({
879
+ code: "invalid-configuration",
880
+ message: `The ${this.#provider.provider} provider could not validate the session options.`,
881
+ provider: this.#provider.provider,
882
+ cause: error
883
+ });
884
+ }
885
+ #normalizeError(error, code) {
886
+ if (VoiceInputError.isInstance(error)) return error;
887
+ return new VoiceInputError({
888
+ code,
889
+ message: code === "audio-error" ? "The audio source failed." : `${this.#provider.provider} failed during the transcription session.`,
890
+ ...code === "provider-error" ? { provider: this.#provider.provider } : {},
891
+ retryable: true,
892
+ cause: error
893
+ });
894
+ }
895
+ #isActive(run) {
896
+ return this.#activeRun === run;
897
+ }
898
+ #transition(status) {
899
+ const previousStatus = this.#snapshot.status;
900
+ if (previousStatus === status) return;
901
+ this.#setSnapshot({ status });
902
+ this.#emit({
903
+ type: "status-change",
904
+ previousStatus,
905
+ status
906
+ });
907
+ }
908
+ #setSnapshot(patch) {
909
+ this.#snapshot = Object.freeze({
910
+ ...this.#snapshot,
911
+ ...patch
912
+ });
913
+ }
914
+ #emit(event) {
915
+ for (const listener of this.#listeners) try {
916
+ listener(event);
917
+ } catch (error) {
918
+ reportUnhandledError$1(error);
919
+ }
920
+ }
921
+ #clearRunTimers(run) {
922
+ this.#clearConnectionTimer(run);
923
+ if (run.warningTimer !== void 0) {
924
+ clearTimeout(run.warningTimer);
925
+ delete run.warningTimer;
926
+ }
927
+ if (run.durationTimer !== void 0) {
928
+ clearTimeout(run.durationTimer);
929
+ delete run.durationTimer;
930
+ }
931
+ }
932
+ #clearConnectionTimer(run) {
933
+ if (run.connectionTimer !== void 0) {
934
+ clearTimeout(run.connectionTimer);
935
+ delete run.connectionTimer;
936
+ }
937
+ }
938
+ async #completeTextEngine(run) {
939
+ const completion = this.#textEngine?.complete();
940
+ if (completion === void 0) return this.#isActive(run);
941
+ if (completion.processing) this.#transition("processing");
942
+ const errors = await completion.result;
943
+ if (!this.#isActive(run)) return false;
944
+ for (const error of errors) {
945
+ this.#setSnapshot({ error });
946
+ this.#emit({
947
+ type: "error",
948
+ error
949
+ });
950
+ }
951
+ return true;
952
+ }
953
+ };
954
+ function getTranscriptionOptions(options) {
955
+ return {
956
+ ...options.language === void 0 ? {} : { language: options.language },
957
+ ...options.vocabulary === void 0 ? {} : { vocabulary: options.vocabulary },
958
+ ...options.endpointing === void 0 ? {} : { endpointing: options.endpointing }
959
+ };
960
+ }
961
+ function isValidLanguage(language) {
962
+ if (language.length === 0 || language !== language.trim()) return false;
963
+ try {
964
+ return Intl.getCanonicalLocales(language).length === 1;
965
+ } catch {
966
+ return false;
967
+ }
968
+ }
969
+ function validateSessionConfiguration(options) {
970
+ const issues = [];
971
+ const language = options.language;
972
+ const vocabulary = options.vocabulary;
973
+ const endpointing = options.endpointing;
974
+ const maxDurationMs = options.maxDurationMs;
975
+ const connectionTimeoutMs = options.connectionTimeoutMs;
976
+ let validatedLanguage;
977
+ let validatedVocabulary;
978
+ let validatedEndpointing;
979
+ if (language !== void 0) {
980
+ if (typeof language !== "string" || !isValidLanguage(language)) issues.push("language: must be a valid BCP 47 language tag without whitespace.");
981
+ else validatedLanguage = language;
982
+ }
983
+ if (vocabulary !== void 0) {
984
+ if (!Array.isArray(vocabulary)) issues.push("vocabulary: must be an array of strings.");
985
+ else {
986
+ const copy = [...vocabulary];
987
+ for (const [index, term] of copy.entries()) if (typeof term !== "string" || term.length === 0 || term !== term.trim()) issues.push(`vocabulary.${index}: must be a non-empty string with no outer whitespace.`);
988
+ validatedVocabulary = Object.freeze(copy);
989
+ }
990
+ }
991
+ if (endpointing === false) validatedEndpointing = false;
992
+ else if (endpointing !== void 0) {
993
+ if (!isObject(endpointing) || !hasOnlySilenceMs(endpointing)) issues.push("endpointing: must be false or an object containing only silenceMs.");
994
+ else {
995
+ const silenceMs = endpointing["silenceMs"];
996
+ if (!isPositiveInteger(silenceMs)) issues.push("endpointing.silenceMs: must be a positive safe integer.");
997
+ else validatedEndpointing = { silenceMs };
998
+ }
999
+ }
1000
+ if (maxDurationMs !== void 0 && !isPositiveInteger(maxDurationMs)) issues.push("maxDurationMs: must be a positive safe integer.");
1001
+ if (connectionTimeoutMs !== void 0 && !isPositiveInteger(connectionTimeoutMs)) issues.push("connectionTimeoutMs: must be a positive safe integer.");
1002
+ if (issues.length > 0) {
1003
+ const cause = new TypeError(issues.join("; "));
1004
+ throw new VoiceInputError({
1005
+ code: "invalid-configuration",
1006
+ message: cause.message,
1007
+ cause
1008
+ });
1009
+ }
1010
+ return {
1011
+ ...getTranscriptionOptions({
1012
+ language: validatedLanguage,
1013
+ vocabulary: validatedVocabulary,
1014
+ endpointing: validatedEndpointing
1015
+ }),
1016
+ maxDurationMs: maxDurationMs ?? DEFAULT_MAX_DURATION_MS,
1017
+ connectionTimeoutMs: connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS
1018
+ };
1019
+ }
1020
+ function isObject(value) {
1021
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1022
+ }
1023
+ function hasOnlySilenceMs(value) {
1024
+ let count = 0;
1025
+ for (const key in value) if (key !== "silenceMs" || ++count > 1) return false;
1026
+ return count === 1 && Object.hasOwn(value, "silenceMs");
1027
+ }
1028
+ function isPositiveInteger(value) {
1029
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1030
+ }
1031
+ function assertProvider(provider) {
1032
+ if (typeof provider !== "object" || provider === null || provider.specificationVersion !== "v1" || typeof provider.provider !== "string" || provider.provider.length === 0 || typeof provider.modelId !== "string" || provider.modelId.length === 0 || !Number.isInteger(provider.sampleRate) || provider.sampleRate <= 0 || typeof provider.validateOptions !== "function" || typeof provider.doOpen !== "function") throw new VoiceInputError({
1033
+ code: "invalid-configuration",
1034
+ message: "provider must implement the VoiceInputProviderV1 specification."
1035
+ });
1036
+ }
1037
+ function assertAudioSource(audioSource) {
1038
+ if (typeof audioSource !== "object" || audioSource === null || typeof audioSource.prepare !== "function") throw new VoiceInputError({
1039
+ code: "invalid-configuration",
1040
+ message: "audioSource must implement the VoiceAudioSource interface."
1041
+ });
1042
+ }
1043
+ function assertTextEngine(textEngine) {
1044
+ if (typeof textEngine !== "object" || textEngine === null || typeof textEngine.begin !== "function" || typeof textEngine.applyInterim !== "function" || typeof textEngine.applyFinal !== "function" || typeof textEngine.complete !== "function" || typeof textEngine.cancel !== "function") throw new VoiceInputError({
1045
+ code: "invalid-configuration",
1046
+ message: "textEngine must implement the VoiceInputTextEngine interface."
1047
+ });
1048
+ }
1049
+ function safely(operation) {
1050
+ try {
1051
+ operation();
1052
+ } catch (error) {
1053
+ reportUnhandledError$1(error);
1054
+ }
1055
+ }
1056
+ function reportUnhandledError$1(error) {
1057
+ const reportError = globalThis.reportError;
1058
+ if (typeof reportError === "function") reportError(error);
1059
+ else queueMicrotask(() => {
1060
+ throw error;
1061
+ });
1062
+ }
1063
+ async function untilAborted(promise, signal) {
1064
+ if (signal.aborted) throw signal.reason;
1065
+ let onAbort;
1066
+ try {
1067
+ return await Promise.race([promise, new Promise((_resolve, reject) => {
1068
+ onAbort = () => reject(signal.reason);
1069
+ signal.addEventListener("abort", onAbort, { once: true });
1070
+ })]);
1071
+ } finally {
1072
+ if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
1073
+ }
1074
+ }
1075
+ async function withTimeout(promise, timeoutMs, provider) {
1076
+ let timer;
1077
+ try {
1078
+ await Promise.race([promise, new Promise((_resolve, reject) => {
1079
+ timer = setTimeout(() => {
1080
+ reject(new VoiceInputError({
1081
+ code: "provider-error",
1082
+ message: `${provider} did not finish the transcription session in time.`,
1083
+ provider,
1084
+ retryable: true
1085
+ }));
1086
+ }, timeoutMs);
1087
+ })]);
1088
+ } finally {
1089
+ if (timer !== void 0) clearTimeout(timer);
1090
+ }
1091
+ }
1092
+ //#endregion
1093
+ //#region src/text-engine/history.ts
1094
+ /** History belongs to one attachment, never to a provider or recording. */
1095
+ var TextHistory = class {
1096
+ #past = [];
1097
+ #future = [];
1098
+ #group = 0;
1099
+ clear() {
1100
+ this.#past = [];
1101
+ this.#future = [];
1102
+ this.breakGroup();
1103
+ }
1104
+ breakGroup() {
1105
+ this.#group += 1;
1106
+ }
1107
+ record(before, after, key) {
1108
+ if (before.value === after.value) return;
1109
+ const now = Date.now();
1110
+ const groupedKey = `${this.#group}:${key}`;
1111
+ const last = this.#past.at(-1);
1112
+ if (last?.key === groupedKey && last.after.value === before.value && (key.startsWith("voice:") || key === "composition" || now - last.at < 1e3) && last) {
1113
+ last.after = after;
1114
+ last.at = now;
1115
+ if (last.before.value === last.after.value) this.#past.pop();
1116
+ } else this.#past.push({
1117
+ before,
1118
+ after,
1119
+ key: groupedKey,
1120
+ at: now
1121
+ });
1122
+ this.#future = [];
1123
+ while (this.#past.length > 100 || this.#retainedBytes() > 2097152) this.#past.shift();
1124
+ }
1125
+ undo() {
1126
+ this.breakGroup();
1127
+ const entry = this.#past.pop();
1128
+ if (!entry) return void 0;
1129
+ this.#future.push(entry);
1130
+ return entry.before;
1131
+ }
1132
+ redo() {
1133
+ this.breakGroup();
1134
+ const entry = this.#future.pop();
1135
+ if (!entry) return void 0;
1136
+ this.#past.push(entry);
1137
+ return entry.after;
1138
+ }
1139
+ #retainedBytes() {
1140
+ return this.#past.reduce((bytes, item) => bytes + 2 * (item.before.value.length + item.after.value.length), 0);
1141
+ }
1142
+ };
1143
+ //#endregion
1144
+ //#region src/text-engine/dom-target.ts
1145
+ const SUPPORTED_INPUT_TYPES = /* @__PURE__ */ new Set([
1146
+ "text",
1147
+ "search",
1148
+ "url",
1149
+ "tel"
1150
+ ]);
1151
+ var TextTargetAdapter = class {
1152
+ #controlled;
1153
+ #callbacks;
1154
+ #target = null;
1155
+ #writeDepth = 0;
1156
+ #observer;
1157
+ #form = null;
1158
+ #composing = false;
1159
+ constructor(controlled, callbacks) {
1160
+ this.#controlled = controlled;
1161
+ this.#callbacks = callbacks;
1162
+ }
1163
+ get target() {
1164
+ return this.#target;
1165
+ }
1166
+ get isControlled() {
1167
+ return this.#controlled !== void 0;
1168
+ }
1169
+ attach(target) {
1170
+ assertSupportedTarget(target);
1171
+ this.detach();
1172
+ this.#target = target;
1173
+ const value = this.#controlled?.getValue() ?? target.value;
1174
+ this.#withGuard(() => {
1175
+ if (target.value !== value) target.value = value;
1176
+ });
1177
+ target.addEventListener("beforeinput", this.#handleBeforeInput);
1178
+ target.addEventListener("keydown", this.#handleKeyDown);
1179
+ target.addEventListener("compositionstart", this.#handleCompositionStart);
1180
+ target.addEventListener("compositionend", this.#handleCompositionEnd);
1181
+ this.#form = target.form;
1182
+ this.#form?.addEventListener("reset", this.#handleReset);
1183
+ this.#observer = new MutationObserver(() => this.#callbacks.onAvailability());
1184
+ this.#observer.observe(target, {
1185
+ attributes: true,
1186
+ attributeFilter: [
1187
+ "disabled",
1188
+ "readonly",
1189
+ "type",
1190
+ "maxlength"
1191
+ ]
1192
+ });
1193
+ for (let ancestor = target.parentElement; ancestor; ancestor = ancestor.parentElement) this.#observer.observe(ancestor, {
1194
+ attributes: true,
1195
+ attributeFilter: ["disabled"]
1196
+ });
1197
+ target.addEventListener("input", this.#handleInput);
1198
+ target.addEventListener("select", this.#handleSelectionChange);
1199
+ target.ownerDocument.addEventListener("selectionchange", this.#handleSelectionChange);
1200
+ return value;
1201
+ }
1202
+ detach() {
1203
+ const target = this.#target;
1204
+ if (target === null) return;
1205
+ target.removeEventListener("beforeinput", this.#handleBeforeInput);
1206
+ target.removeEventListener("keydown", this.#handleKeyDown);
1207
+ target.removeEventListener("compositionstart", this.#handleCompositionStart);
1208
+ target.removeEventListener("compositionend", this.#handleCompositionEnd);
1209
+ this.#form?.removeEventListener("reset", this.#handleReset);
1210
+ this.#form = null;
1211
+ this.#observer?.disconnect();
1212
+ this.#observer = void 0;
1213
+ this.#composing = false;
1214
+ target.removeEventListener("input", this.#handleInput);
1215
+ target.removeEventListener("select", this.#handleSelectionChange);
1216
+ target.ownerDocument.removeEventListener("selectionchange", this.#handleSelectionChange);
1217
+ this.#target = null;
1218
+ }
1219
+ isWritable() {
1220
+ return this.#target !== null && isSupportedTarget(this.#target) && !this.#target.matches(":disabled") && !this.#target.readOnly;
1221
+ }
1222
+ readValue() {
1223
+ return this.#target?.value ?? "";
1224
+ }
1225
+ readSelection() {
1226
+ return this.#target === null || !isSupportedTarget(this.#target) ? null : readSelection(this.#target);
1227
+ }
1228
+ readSelectionWhenValueIs(value) {
1229
+ return this.#target !== null && isSupportedTarget(this.#target) && this.#target.value === value ? readSelection(this.#target) : null;
1230
+ }
1231
+ applyMutation(mutation) {
1232
+ const target = this.#target;
1233
+ if (target === null || !this.isWritable() || this.#composing) return;
1234
+ this.#withGuard(() => {
1235
+ setNativeValue(target, mutation.value);
1236
+ restoreSelection(target, mutation.selection);
1237
+ if (!mutation.changed) return;
1238
+ try {
1239
+ if (!this.#controlled?.dispatchInput) this.#controlled?.onValueChange(mutation.value);
1240
+ } catch (error) {
1241
+ this.#callbacks.onUnhandledError(error);
1242
+ }
1243
+ if (this.#controlled === void 0 || this.#controlled.dispatchInput) {
1244
+ const InputEventConstructor = target.ownerDocument.defaultView?.InputEvent ?? InputEvent;
1245
+ target.dispatchEvent(new InputEventConstructor("input", {
1246
+ bubbles: true,
1247
+ inputType: "insertText"
1248
+ }));
1249
+ }
1250
+ });
1251
+ }
1252
+ synchronize(value, selection) {
1253
+ const target = this.#target;
1254
+ if (target === null || !isSupportedTarget(target) || this.#composing) return;
1255
+ this.#withGuard(() => {
1256
+ if (target.value !== value) target.value = value;
1257
+ restoreSelection(target, selection);
1258
+ });
1259
+ }
1260
+ #handleBeforeInput = (event) => {
1261
+ if (this.#writeDepth !== 0) return;
1262
+ const inputType = event.inputType ?? "insertText";
1263
+ if (inputType === "historyUndo" || inputType === "historyRedo") {
1264
+ if (event.cancelable && !this.#composing) {
1265
+ event.preventDefault();
1266
+ this.#callbacks.onHistory(inputType === "historyRedo");
1267
+ }
1268
+ return;
1269
+ }
1270
+ this.#callbacks.onBeforeInput(inputType);
1271
+ };
1272
+ #handleKeyDown = (rawEvent) => {
1273
+ const event = rawEvent;
1274
+ if (event.defaultPrevented || event.isComposing || this.#composing || !this.isWritable()) return;
1275
+ const modifier = event.ctrlKey || event.metaKey;
1276
+ const key = event.key.toLowerCase();
1277
+ if (modifier && !event.altKey && (key === "z" || event.ctrlKey && key === "y")) {
1278
+ event.preventDefault();
1279
+ this.#callbacks.onHistory(key === "y" || event.shiftKey);
1280
+ }
1281
+ };
1282
+ #handleCompositionStart = () => {
1283
+ this.#composing = true;
1284
+ this.#callbacks.onComposition(true);
1285
+ };
1286
+ #handleCompositionEnd = () => {
1287
+ this.#composing = false;
1288
+ const target = this.#target;
1289
+ queueMicrotask(() => {
1290
+ if (target === this.#target) this.#callbacks.onComposition(false);
1291
+ });
1292
+ };
1293
+ #handleReset = (event) => {
1294
+ const target = this.#target;
1295
+ queueMicrotask(() => {
1296
+ if (!event.defaultPrevented && target === this.#target) this.#callbacks.onReset();
1297
+ });
1298
+ };
1299
+ #handleInput = () => {
1300
+ if (this.#writeDepth === 0) this.#callbacks.onInput();
1301
+ };
1302
+ #handleSelectionChange = () => {
1303
+ if (this.#writeDepth === 0) this.#callbacks.onSelectionChange();
1304
+ };
1305
+ #withGuard(operation) {
1306
+ this.#writeDepth += 1;
1307
+ try {
1308
+ operation();
1309
+ } finally {
1310
+ this.#writeDepth -= 1;
1311
+ }
1312
+ }
1313
+ };
1314
+ function assertSupportedTarget(target) {
1315
+ if (!isSupportedTarget(target)) throw invalidTarget();
1316
+ }
1317
+ function isSupportedTarget(target) {
1318
+ if (typeof target !== "object" || target === null) return false;
1319
+ if (target.tagName === "TEXTAREA") return true;
1320
+ return target.tagName === "INPUT" && SUPPORTED_INPUT_TYPES.has(target.type);
1321
+ }
1322
+ function readSelection(target) {
1323
+ const start = target.selectionStart;
1324
+ const end = target.selectionEnd;
1325
+ if (start === null || end === null) throw invalidTarget("The text target does not expose a selection.");
1326
+ const direction = target.selectionDirection;
1327
+ return {
1328
+ start,
1329
+ end,
1330
+ direction: direction === "forward" || direction === "backward" ? direction : "none"
1331
+ };
1332
+ }
1333
+ function restoreSelection(target, selection) {
1334
+ if (selection === null) return;
1335
+ const start = Math.min(selection.start, target.value.length);
1336
+ const end = Math.min(selection.end, target.value.length);
1337
+ target.setSelectionRange(start, end, selection.direction);
1338
+ }
1339
+ function invalidTarget(message = "Text targets must be <textarea> elements or <input> elements of type text, search, url, or tel.") {
1340
+ return new VoiceInputError$1({
1341
+ code: "invalid-configuration",
1342
+ message
1343
+ });
1344
+ }
1345
+ function setNativeValue(target, value) {
1346
+ const view = target.ownerDocument.defaultView;
1347
+ const prototype = target.tagName === "TEXTAREA" ? view?.HTMLTextAreaElement.prototype : view?.HTMLInputElement.prototype;
1348
+ const descriptor = prototype && Object.getOwnPropertyDescriptor(prototype, "value");
1349
+ if (descriptor?.set) descriptor.set.call(target, value);
1350
+ else target.value = value;
1351
+ }
1352
+ //#endregion
1353
+ //#region src/text-engine/ownership-model.ts
1354
+ var TextOwnershipModel = class {
1355
+ #interimBehavior;
1356
+ #maxLength = -1;
1357
+ #source = "final";
1358
+ #limit;
1359
+ #value = "";
1360
+ #selection = null;
1361
+ #spans = [];
1362
+ #provisional;
1363
+ #currentFinalSpan;
1364
+ #interimTranscript = "";
1365
+ #runId = 0;
1366
+ #runActive = false;
1367
+ #nextSpanId = 1;
1368
+ #nextGeneration = 1;
1369
+ constructor(interimBehavior) {
1370
+ this.#interimBehavior = interimBehavior;
1371
+ }
1372
+ configureMutation(maxLength, source) {
1373
+ this.#maxLength = maxLength;
1374
+ this.#source = source;
1375
+ this.#limit = void 0;
1376
+ }
1377
+ takeLimit() {
1378
+ const limit = this.#limit;
1379
+ this.#limit = void 0;
1380
+ return limit;
1381
+ }
1382
+ #limitReplacement(start, end, text) {
1383
+ if (this.#maxLength < 0) return text;
1384
+ const available = Math.max(0, this.#maxLength - (this.#value.length - (end - start)));
1385
+ if (text.length < available || text.length === 0) return text;
1386
+ let insertedText = "";
1387
+ for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(text)) {
1388
+ if (insertedText.length + segment.length > available) break;
1389
+ insertedText += segment;
1390
+ }
1391
+ this.#limit = {
1392
+ type: "text-limit",
1393
+ maxLength: this.#maxLength,
1394
+ text,
1395
+ insertedText,
1396
+ source: this.#source
1397
+ };
1398
+ return insertedText;
1399
+ }
1400
+ setInterimBehavior(behavior) {
1401
+ this.#interimBehavior = behavior;
1402
+ }
1403
+ get value() {
1404
+ return this.#value;
1405
+ }
1406
+ get selection() {
1407
+ return this.#selection === null ? null : {
1408
+ start: this.#selection.start,
1409
+ end: this.#selection.end,
1410
+ direction: this.#selection.direction
1411
+ };
1412
+ }
1413
+ get hasSelection() {
1414
+ return this.#selection !== null;
1415
+ }
1416
+ get isRunActive() {
1417
+ return this.#runActive;
1418
+ }
1419
+ getSnapshot() {
1420
+ const selection = this.#selection === null ? null : Object.freeze({
1421
+ start: this.#selection.start,
1422
+ end: this.#selection.end,
1423
+ direction: this.#selection.direction
1424
+ });
1425
+ const spans = this.#spans.map((span) => Object.freeze({
1426
+ id: span.id,
1427
+ start: span.start,
1428
+ end: span.end,
1429
+ text: this.#value.slice(span.start, span.end),
1430
+ state: span.state
1431
+ }));
1432
+ return Object.freeze({
1433
+ value: this.#value,
1434
+ selection,
1435
+ interimTranscript: this.#interimTranscript,
1436
+ spans: Object.freeze(spans)
1437
+ });
1438
+ }
1439
+ replaceTarget(value) {
1440
+ this.freezeForTargetReplacement();
1441
+ this.#value = value;
1442
+ this.#selection = null;
1443
+ this.#spans = [];
1444
+ this.#provisional = void 0;
1445
+ this.#currentFinalSpan = void 0;
1446
+ }
1447
+ captureSelection(selection) {
1448
+ this.#freezeProvisional();
1449
+ this.#currentFinalSpan = void 0;
1450
+ this.#selection = toMutableSelection(selection);
1451
+ }
1452
+ begin() {
1453
+ this.#runId += 1;
1454
+ this.#runActive = true;
1455
+ this.#interimTranscript = "";
1456
+ this.#provisional = void 0;
1457
+ this.#currentFinalSpan = void 0;
1458
+ }
1459
+ applyInterim(text, canInsert) {
1460
+ if (!this.#runActive || !canInsert) return null;
1461
+ this.#interimTranscript = text;
1462
+ if (this.#interimBehavior === "expose") return null;
1463
+ if (this.#provisional !== void 0) {
1464
+ if (this.proveSpan(this.#provisional)) return this.#replaceOwnedSpan(this.#provisional, text, "provisional").mutation;
1465
+ this.#abandonProvisional();
1466
+ }
1467
+ if (text.length === 0 || !canInsert) return null;
1468
+ const inserted = this.#insertAtAnchor(text, "provisional");
1469
+ this.#provisional = inserted?.span;
1470
+ return inserted?.mutation ?? null;
1471
+ }
1472
+ applyFinal(text, canInsert) {
1473
+ if (!this.#runActive || !canInsert) return null;
1474
+ this.#interimTranscript = "";
1475
+ let finalized;
1476
+ let mutation = null;
1477
+ if (this.#provisional !== void 0) {
1478
+ if (this.proveSpan(this.#provisional)) {
1479
+ const replaced = this.#replaceOwnedSpan(this.#provisional, text, "finalized");
1480
+ finalized = replaced?.span;
1481
+ mutation = replaced?.mutation ?? null;
1482
+ } else this.#abandonProvisional();
1483
+ this.#provisional = void 0;
1484
+ }
1485
+ if (finalized === void 0 && text.length > 0 && canInsert) {
1486
+ const inserted = this.#insertAtAnchor(text, "finalized");
1487
+ finalized = inserted?.span;
1488
+ mutation = inserted?.mutation ?? mutation;
1489
+ }
1490
+ if (finalized !== void 0) this.#mergeFinalizedSpan(finalized);
1491
+ return mutation;
1492
+ }
1493
+ complete(canInsert) {
1494
+ let mutation = null;
1495
+ if (this.#runActive && this.#provisional !== void 0) {
1496
+ if (this.proveSpan(this.#provisional)) {
1497
+ this.#provisional.state = "finalized";
1498
+ delete this.#provisional.replacedText;
1499
+ this.#mergeFinalizedSpan(this.#provisional);
1500
+ } else this.#abandonProvisional();
1501
+ this.#provisional = void 0;
1502
+ } else if (this.#runActive && this.#interimBehavior === "expose" && this.#interimTranscript.length > 0 && canInsert) {
1503
+ const inserted = this.#insertAtAnchor(this.#interimTranscript, "finalized");
1504
+ if (inserted !== void 0) {
1505
+ this.#mergeFinalizedSpan(inserted.span);
1506
+ mutation = inserted.mutation;
1507
+ }
1508
+ }
1509
+ this.#interimTranscript = "";
1510
+ const runId = this.#runId;
1511
+ const spans = this.#spans.filter((span) => span.runId === runId && span.state === "finalized");
1512
+ this.#runActive = false;
1513
+ this.#currentFinalSpan = void 0;
1514
+ return {
1515
+ mutation,
1516
+ runId,
1517
+ spans
1518
+ };
1519
+ }
1520
+ cancel() {
1521
+ let mutation = null;
1522
+ if (this.#provisional !== void 0) {
1523
+ if (this.proveSpan(this.#provisional)) mutation = this.#removeOwnedSpan(this.#provisional);
1524
+ else this.#abandonProvisional();
1525
+ }
1526
+ this.#provisional = void 0;
1527
+ this.#interimTranscript = "";
1528
+ this.#currentFinalSpan = void 0;
1529
+ this.#runActive = false;
1530
+ return mutation;
1531
+ }
1532
+ freezeForTargetReplacement() {
1533
+ this.#freezeProvisional();
1534
+ for (const span of this.#spans) if (span.state === "finalized") {
1535
+ span.state = "frozen";
1536
+ span.generation = this.#nextGeneration++;
1537
+ }
1538
+ this.#currentFinalSpan = void 0;
1539
+ }
1540
+ destroy() {
1541
+ this.freezeForTargetReplacement();
1542
+ this.#selection = null;
1543
+ this.#interimTranscript = "";
1544
+ this.#runActive = false;
1545
+ }
1546
+ beforeInput() {
1547
+ this.#freezeProvisional();
1548
+ this.#currentFinalSpan = void 0;
1549
+ }
1550
+ reconcileExternalValue(value, committedSelection) {
1551
+ if (value === this.#value) {
1552
+ if (committedSelection !== null && !sameSelection$1(this.#selection, committedSelection)) {
1553
+ this.#freezeProvisional();
1554
+ this.#currentFinalSpan = void 0;
1555
+ this.#selection = toMutableSelection(committedSelection);
1556
+ }
1557
+ return;
1558
+ }
1559
+ const edit = findSingleEdit(this.#value, value);
1560
+ const delta = edit.newEnd - edit.oldEnd;
1561
+ const adjustedSelection = adjustSelectionForEdit(this.#selection, edit, delta);
1562
+ const provisional = this.#provisional;
1563
+ const provisionalOverlaps = provisional !== void 0 && spanOverlapsEdit(provisional, edit);
1564
+ this.#adjustSpansForEdit(edit.oldStart, edit.oldEnd, delta);
1565
+ this.#value = value;
1566
+ this.#selection = adjustedSelection;
1567
+ this.#currentFinalSpan = void 0;
1568
+ if (provisionalOverlaps) {
1569
+ this.#interimTranscript = "";
1570
+ this.#provisional = void 0;
1571
+ } else if (this.#provisional !== void 0 && !this.proveSpan(this.#provisional)) this.#abandonProvisional();
1572
+ if (committedSelection !== null && !sameSelection$1(this.#selection, committedSelection)) {
1573
+ this.#freezeProvisional();
1574
+ this.#selection = toMutableSelection(committedSelection);
1575
+ }
1576
+ }
1577
+ selectionChanged(selection) {
1578
+ if (sameSelection$1(this.#selection, selection)) return;
1579
+ this.#freezeProvisional();
1580
+ this.#currentFinalSpan = void 0;
1581
+ this.#selection = toMutableSelection(selection);
1582
+ }
1583
+ proveSpan(span) {
1584
+ return this.#spans.includes(span) && span.start >= 0 && span.end >= span.start && span.end <= this.#value.length && this.#value.slice(span.start, span.end) === span.expectedText;
1585
+ }
1586
+ getSpanText(span) {
1587
+ return this.#value.slice(span.start, span.end);
1588
+ }
1589
+ canApplyTransform(span, generation, originalText) {
1590
+ return span.generation === generation && span.state === "finalized" && this.proveSpan(span) && this.#value.slice(span.start, span.end) === originalText;
1591
+ }
1592
+ applyTransform(span, text) {
1593
+ const replacement = this.#limitReplacement(span.start, span.end, normalizeInsertion(this.#value.slice(0, span.start), this.#value.slice(span.end), text));
1594
+ const changed = this.#replaceText(span.start, span.end, replacement, span.id);
1595
+ if (replacement.length === 0) {
1596
+ this.#spans = this.#spans.filter((candidate) => candidate !== span);
1597
+ this.#selection = {
1598
+ start: span.start,
1599
+ end: span.start,
1600
+ direction: "none",
1601
+ replacePending: false
1602
+ };
1603
+ } else {
1604
+ span.end = span.start + replacement.length;
1605
+ span.state = "transformed";
1606
+ span.generation = this.#nextGeneration++;
1607
+ span.expectedText = replacement;
1608
+ this.#selection = {
1609
+ start: span.end,
1610
+ end: span.end,
1611
+ direction: "none",
1612
+ replacePending: false
1613
+ };
1614
+ }
1615
+ return this.#createMutation(changed);
1616
+ }
1617
+ freezeFailedTransform(span) {
1618
+ if (span.state === "finalized" && this.proveSpan(span)) {
1619
+ span.state = "frozen";
1620
+ span.generation = this.#nextGeneration++;
1621
+ }
1622
+ }
1623
+ #insertAtAnchor(text, state) {
1624
+ const selection = this.#selection;
1625
+ if (selection === null) return;
1626
+ const start = selection.start;
1627
+ const end = selection.replacePending ? selection.end : selection.start;
1628
+ const replacement = this.#limitReplacement(start, end, normalizeInsertion(this.#value.slice(0, start), this.#value.slice(end), text));
1629
+ if (replacement.length === 0) return;
1630
+ selection.replacePending = false;
1631
+ const replacedText = this.#value.slice(start, end);
1632
+ const changed = this.#replaceText(start, end, replacement);
1633
+ const span = {
1634
+ id: this.#nextSpanId++,
1635
+ start,
1636
+ end: start + replacement.length,
1637
+ state,
1638
+ runId: this.#runId,
1639
+ generation: this.#nextGeneration++,
1640
+ expectedText: replacement,
1641
+ ...end > start ? { replacedText } : {}
1642
+ };
1643
+ this.#spans.push(span);
1644
+ this.#selection = {
1645
+ start: span.end,
1646
+ end: span.end,
1647
+ direction: "none",
1648
+ replacePending: false
1649
+ };
1650
+ return {
1651
+ mutation: this.#createMutation(changed),
1652
+ span
1653
+ };
1654
+ }
1655
+ #replaceOwnedSpan(span, text, state) {
1656
+ const replacement = this.#limitReplacement(span.start, span.end, normalizeInsertion(this.#value.slice(0, span.start), this.#value.slice(span.end), text));
1657
+ if (replacement.length === 0) return { mutation: this.#removeOwnedSpan(span) };
1658
+ const changed = this.#replaceText(span.start, span.end, replacement, span.id);
1659
+ span.end = span.start + replacement.length;
1660
+ span.state = state;
1661
+ span.generation = this.#nextGeneration++;
1662
+ span.expectedText = replacement;
1663
+ if (state === "finalized") delete span.replacedText;
1664
+ this.#selection = {
1665
+ start: span.end,
1666
+ end: span.end,
1667
+ direction: "none",
1668
+ replacePending: false
1669
+ };
1670
+ return {
1671
+ mutation: this.#createMutation(changed),
1672
+ span
1673
+ };
1674
+ }
1675
+ #removeOwnedSpan(span) {
1676
+ const start = span.start;
1677
+ const replacement = span.replacedText ?? "";
1678
+ const changed = this.#replaceText(start, span.end, replacement, span.id);
1679
+ this.#spans = this.#spans.filter((candidate) => candidate !== span);
1680
+ this.#selection = {
1681
+ start,
1682
+ end: start + replacement.length,
1683
+ direction: "none",
1684
+ replacePending: replacement.length > 0
1685
+ };
1686
+ return this.#createMutation(changed);
1687
+ }
1688
+ #replaceText(start, end, replacement, excludedSpanId) {
1689
+ const previousValue = this.#value;
1690
+ const nextValue = `${previousValue.slice(0, start)}${replacement}${previousValue.slice(end)}`;
1691
+ if (nextValue === previousValue) return false;
1692
+ const delta = replacement.length - (end - start);
1693
+ this.#adjustSpansForEdit(start, end, delta, excludedSpanId);
1694
+ this.#value = nextValue;
1695
+ return true;
1696
+ }
1697
+ #mergeFinalizedSpan(span) {
1698
+ const previous = this.#currentFinalSpan;
1699
+ if (previous !== void 0 && previous !== span && previous.state === "finalized" && previous.runId === span.runId && previous.end === span.start && this.proveSpan(previous) && this.proveSpan(span)) {
1700
+ previous.end = span.end;
1701
+ previous.generation = this.#nextGeneration++;
1702
+ previous.expectedText = this.#value.slice(previous.start, previous.end);
1703
+ this.#spans = this.#spans.filter((candidate) => candidate !== span);
1704
+ this.#currentFinalSpan = previous;
1705
+ return;
1706
+ }
1707
+ this.#currentFinalSpan = span;
1708
+ }
1709
+ #freezeProvisional() {
1710
+ if (this.#provisional !== void 0) {
1711
+ this.#provisional.state = "frozen";
1712
+ this.#provisional.generation = this.#nextGeneration++;
1713
+ this.#provisional = void 0;
1714
+ }
1715
+ this.#interimTranscript = "";
1716
+ this.#currentFinalSpan = void 0;
1717
+ }
1718
+ #abandonProvisional() {
1719
+ const provisional = this.#provisional;
1720
+ if (provisional !== void 0) {
1721
+ this.#spans = this.#spans.filter((span) => span !== provisional);
1722
+ this.#provisional = void 0;
1723
+ }
1724
+ this.#interimTranscript = "";
1725
+ this.#currentFinalSpan = void 0;
1726
+ }
1727
+ #adjustSpansForEdit(start, end, delta, excludedSpanId) {
1728
+ const retained = [];
1729
+ for (const span of this.#spans) if (span.id === excludedSpanId) retained.push(span);
1730
+ else if (span.end <= start) retained.push(span);
1731
+ else if (span.start >= end) {
1732
+ span.start += delta;
1733
+ span.end += delta;
1734
+ retained.push(span);
1735
+ } else {
1736
+ if (span === this.#provisional) this.#provisional = void 0;
1737
+ if (span === this.#currentFinalSpan) this.#currentFinalSpan = void 0;
1738
+ }
1739
+ this.#spans = retained;
1740
+ }
1741
+ #createMutation(changed) {
1742
+ return {
1743
+ changed,
1744
+ selection: this.selection,
1745
+ value: this.#value
1746
+ };
1747
+ }
1748
+ };
1749
+ function normalizeInsertion(left, right, text) {
1750
+ return normalizeTranscriptInsertion(left, right, text);
1751
+ }
1752
+ function findSingleEdit(previous, next) {
1753
+ let prefix = 0;
1754
+ const maximumPrefix = Math.min(previous.length, next.length);
1755
+ while (prefix < maximumPrefix && previous[prefix] === next[prefix]) prefix += 1;
1756
+ let suffix = 0;
1757
+ const maximumSuffix = Math.min(previous.length - prefix, next.length - prefix);
1758
+ while (suffix < maximumSuffix && previous[previous.length - suffix - 1] === next[next.length - suffix - 1]) suffix += 1;
1759
+ return {
1760
+ oldStart: prefix,
1761
+ oldEnd: previous.length - suffix,
1762
+ newEnd: next.length - suffix
1763
+ };
1764
+ }
1765
+ function adjustSelectionForEdit(selection, edit, delta) {
1766
+ if (selection === null) return null;
1767
+ if (selection.end <= edit.oldStart) return selection;
1768
+ if (selection.start >= edit.oldEnd) return {
1769
+ ...selection,
1770
+ start: selection.start + delta,
1771
+ end: selection.end + delta
1772
+ };
1773
+ return {
1774
+ start: edit.newEnd,
1775
+ end: edit.newEnd,
1776
+ direction: "none",
1777
+ replacePending: false
1778
+ };
1779
+ }
1780
+ function spanOverlapsEdit(span, edit) {
1781
+ return !(span.end <= edit.oldStart || span.start >= edit.oldEnd);
1782
+ }
1783
+ function toMutableSelection(selection) {
1784
+ return {
1785
+ ...selection,
1786
+ replacePending: selection.start !== selection.end
1787
+ };
1788
+ }
1789
+ function sameSelection$1(current, next) {
1790
+ return current !== null && current.start === next.start && current.end === next.end && (current.start === current.end || current.direction === next.direction);
1791
+ }
1792
+ //#endregion
1793
+ //#region src/text-engine/transform.ts
1794
+ var TransformTimeoutError = class extends Error {
1795
+ constructor() {
1796
+ super("Transcript transform timed out.");
1797
+ this.name = "TransformTimeoutError";
1798
+ }
1799
+ };
1800
+ async function runTransformWithTimeout(transform, timeoutMs) {
1801
+ let timer;
1802
+ try {
1803
+ const result = transform();
1804
+ return await Promise.race([Promise.resolve(result), new Promise((_resolve, reject) => {
1805
+ timer = setTimeout(() => reject(new TransformTimeoutError()), timeoutMs);
1806
+ })]);
1807
+ } finally {
1808
+ if (timer !== void 0) clearTimeout(timer);
1809
+ }
1810
+ }
1811
+ //#endregion
1812
+ //#region src/text-engine/controller.ts
1813
+ var VoiceInputTextEngineController = class {
1814
+ #controlled;
1815
+ #transformTranscript;
1816
+ #transformTimeoutMs;
1817
+ #model;
1818
+ #target;
1819
+ #nextOptions;
1820
+ updateOptions(options) {
1821
+ if (options.interimBehavior !== void 0 && options.interimBehavior !== "inline" && options.interimBehavior !== "expose") throw invalidConfiguration$1("interimBehavior must be inline or expose.");
1822
+ if (options.transformTimeoutMs !== void 0 && (!Number.isInteger(options.transformTimeoutMs) || options.transformTimeoutMs <= 0)) throw invalidConfiguration$1("transformTimeoutMs must be a positive finite integer.");
1823
+ if (options.transformTranscript !== void 0 && typeof options.transformTranscript !== "function") throw invalidConfiguration$1("transformTranscript must be a function.");
1824
+ this.#nextOptions = options;
1825
+ }
1826
+ #completionGeneration = 0;
1827
+ #history = new TextHistory();
1828
+ #listeners = /* @__PURE__ */ new Set();
1829
+ #suppressed = /* @__PURE__ */ new Set();
1830
+ #closedSegments = /* @__PURE__ */ new Set();
1831
+ #implicitSegment = 0;
1832
+ #currentSegment;
1833
+ #limitedSegment;
1834
+ #composing = false;
1835
+ #beforeInput;
1836
+ #inputType = "insertText";
1837
+ subscribe(listener) {
1838
+ this.#listeners.add(listener);
1839
+ return () => this.#listeners.delete(listener);
1840
+ }
1841
+ isWritable() {
1842
+ return this.#target.isWritable();
1843
+ }
1844
+ undo() {
1845
+ this.#restoreHistory(false);
1846
+ }
1847
+ redo() {
1848
+ this.#restoreHistory(true);
1849
+ }
1850
+ #restoreHistory(redo) {
1851
+ if (!this.isWritable() || this.#composing) return;
1852
+ const state = redo ? this.#history.redo() : this.#history.undo();
1853
+ if (!state) return;
1854
+ this.#takeOwnership();
1855
+ this.#invalidateCompletion();
1856
+ this.#model.replaceTarget(state.value);
1857
+ if (state.selection) this.#model.captureSelection(state.selection);
1858
+ this.#target.applyMutation({
1859
+ ...state,
1860
+ changed: true
1861
+ });
1862
+ }
1863
+ #takeOwnership() {
1864
+ if (this.#currentSegment !== void 0) this.#suppressed.add(this.#currentSegment);
1865
+ this.#model.beforeInput();
1866
+ this.#history.breakGroup();
1867
+ }
1868
+ #emit(event) {
1869
+ for (const listener of this.#listeners) try {
1870
+ listener(event);
1871
+ } catch (error) {
1872
+ reportUnhandledError(error);
1873
+ }
1874
+ }
1875
+ #state() {
1876
+ return {
1877
+ value: this.#model.value,
1878
+ selection: this.#target.readSelection() ?? this.#model.selection
1879
+ };
1880
+ }
1881
+ #availabilityChanged() {
1882
+ if (!this.isWritable()) {
1883
+ this.#takeOwnership();
1884
+ this.#invalidateCompletion();
1885
+ this.#emit({ type: "target-unavailable" });
1886
+ }
1887
+ }
1888
+ #reset() {
1889
+ this.#beforeInput = void 0;
1890
+ this.#takeOwnership();
1891
+ this.#invalidateCompletion();
1892
+ this.#model.replaceTarget(this.#target.readValue());
1893
+ this.#model.cancel();
1894
+ this.#history.clear();
1895
+ this.#emit({ type: "reset" });
1896
+ }
1897
+ constructor(options) {
1898
+ this.#controlled = options.controlled;
1899
+ this.#transformTranscript = options.transformTranscript;
1900
+ this.#transformTimeoutMs = options.transformTimeoutMs;
1901
+ this.#model = new TextOwnershipModel(options.interimBehavior);
1902
+ this.#target = new TextTargetAdapter(options.controlled, {
1903
+ onBeforeInput: (inputType) => {
1904
+ this.#beforeInput = this.#state();
1905
+ this.#inputType = inputType;
1906
+ if (!this.#composing) {
1907
+ if (this.#currentSegment !== void 0) this.#suppressed.add(this.#currentSegment);
1908
+ this.#model.beforeInput();
1909
+ }
1910
+ },
1911
+ onHistory: (redo) => this.#restoreHistory(redo),
1912
+ onComposition: (active) => {
1913
+ if (active) this.#takeOwnership();
1914
+ this.#composing = active;
1915
+ if (!active) {
1916
+ this.#handleInput();
1917
+ this.#history.breakGroup();
1918
+ }
1919
+ },
1920
+ onReset: () => this.#reset(),
1921
+ onAvailability: () => this.#availabilityChanged(),
1922
+ onInput: () => this.#handleInput(),
1923
+ onSelectionChange: () => this.#handleSelectionChange(),
1924
+ onUnhandledError: reportUnhandledError
1925
+ });
1926
+ }
1927
+ getSnapshot() {
1928
+ return this.#model.getSnapshot();
1929
+ }
1930
+ setTarget(target) {
1931
+ if (target === this.#target.target) return;
1932
+ const wasAttached = this.#target.target !== null;
1933
+ this.#takeOwnership();
1934
+ this.#history.clear();
1935
+ this.#beforeInput = void 0;
1936
+ this.#composing = false;
1937
+ this.#invalidateCompletion();
1938
+ this.#model.cancel();
1939
+ if (wasAttached) this.#emit({ type: "reset" });
1940
+ if (target === null) {
1941
+ this.#target.detach();
1942
+ this.#model.replaceTarget("");
1943
+ return;
1944
+ }
1945
+ const value = this.#target.attach(target);
1946
+ this.#model.replaceTarget(value);
1947
+ this.#target.synchronize(this.#model.value, this.#model.selection);
1948
+ }
1949
+ captureSelection() {
1950
+ if (!this.#target.isWritable()) return null;
1951
+ this.#reconcileUncontrolledDomValue();
1952
+ const selection = this.#target.readSelection();
1953
+ if (selection === null) return null;
1954
+ if (!sameSelection(this.#model.selection, selection)) this.#takeOwnership();
1955
+ this.#model.captureSelection(selection);
1956
+ return Object.freeze({ ...selection });
1957
+ }
1958
+ reconcileControlledValue(value) {
1959
+ if (this.#controlled === void 0) throw invalidConfiguration$1("reconcileControlledValue is only available for controlled text engines.");
1960
+ if (typeof value !== "string") throw invalidConfiguration$1("A controlled value must be a string.");
1961
+ if (this.#composing) return;
1962
+ if (value !== this.#model.value) this.#history.clear();
1963
+ const committedSelection = this.#target.readSelectionWhenValueIs(value);
1964
+ this.#model.reconcileExternalValue(value, committedSelection);
1965
+ if (this.#currentSegment !== void 0 && !this.#model.getSnapshot().spans.some((span) => span.state === "provisional")) this.#suppressed.add(this.#currentSegment);
1966
+ this.#target.synchronize(this.#model.value, this.#model.selection);
1967
+ }
1968
+ begin() {
1969
+ if (this.#nextOptions) {
1970
+ this.#model.setInterimBehavior(this.#nextOptions.interimBehavior ?? "inline");
1971
+ this.#transformTranscript = this.#nextOptions.transformTranscript;
1972
+ this.#transformTimeoutMs = this.#nextOptions.transformTimeoutMs ?? 1e4;
1973
+ }
1974
+ this.#invalidateCompletion();
1975
+ this.#model.begin();
1976
+ this.#suppressed.clear();
1977
+ this.#closedSegments.clear();
1978
+ this.#implicitSegment = 0;
1979
+ this.#currentSegment = void 0;
1980
+ this.#limitedSegment = void 0;
1981
+ this.#history.breakGroup();
1982
+ if (!this.#model.hasSelection) this.captureSelection();
1983
+ }
1984
+ applyInterim(text, segmentId) {
1985
+ this.#applyTranscript(text, segmentId, false);
1986
+ }
1987
+ applyFinal(text, segmentId) {
1988
+ this.#applyTranscript(text, segmentId, true);
1989
+ }
1990
+ #applyTranscript(text, segmentId, final) {
1991
+ if (typeof text !== "string" || !this.#model.isRunActive) return;
1992
+ const id = segmentId ?? `implicit:${this.#implicitSegment}`;
1993
+ if (this.#closedSegments.has(id)) return;
1994
+ this.#reconcileUncontrolledDomValue();
1995
+ if (this.#currentSegment !== void 0 && this.#currentSegment !== id) this.#takeOwnership();
1996
+ if (this.#currentSegment !== id) this.#history.breakGroup();
1997
+ this.#currentSegment = id;
1998
+ if (this.#composing || !this.isWritable() || this.#limitedSegment !== void 0 && this.#limitedSegment !== id) {
1999
+ this.#suppressed.add(id);
2000
+ this.#model.beforeInput();
2001
+ }
2002
+ if (!this.#suppressed.has(id)) {
2003
+ const before = this.#state();
2004
+ this.#model.configureMutation(this.#target.target?.maxLength ?? -1, final ? "final" : "interim");
2005
+ const mutation = final ? this.#model.applyFinal(text, true) : this.#model.applyInterim(text, true);
2006
+ this.#applyMutation(mutation, before, `voice:${id}`);
2007
+ const limit = this.#model.takeLimit();
2008
+ if (limit) {
2009
+ const firstLimit = this.#limitedSegment === void 0;
2010
+ this.#limitedSegment = id;
2011
+ if (firstLimit) this.#emit(limit);
2012
+ }
2013
+ }
2014
+ if (final) {
2015
+ this.#closedSegments.add(id);
2016
+ this.#currentSegment = void 0;
2017
+ this.#implicitSegment += 1;
2018
+ this.#history.breakGroup();
2019
+ }
2020
+ }
2021
+ complete() {
2022
+ if (!this.#model.isRunActive) return {
2023
+ processing: false,
2024
+ result: Promise.resolve([])
2025
+ };
2026
+ const before = this.#state();
2027
+ this.#model.configureMutation(this.#target.target?.maxLength ?? -1, "final");
2028
+ const completion = this.#model.complete(this.#target.isWritable() && !this.#composing);
2029
+ this.#applyMutation(completion.mutation, before, `voice:${this.#currentSegment}`);
2030
+ const limit = this.#model.takeLimit();
2031
+ if (limit) this.#emit(limit);
2032
+ this.#history.breakGroup();
2033
+ const processing = this.#transformTranscript !== void 0 && completion.spans.length > 0;
2034
+ const completionGeneration = ++this.#completionGeneration;
2035
+ if (!processing || this.#transformTranscript === void 0) return {
2036
+ processing: false,
2037
+ result: Promise.resolve([])
2038
+ };
2039
+ return {
2040
+ processing: true,
2041
+ result: Promise.all(completion.spans.map((span) => this.#transformSpan(span, completionGeneration))).then((errors) => errors.filter(isVoiceInputError))
2042
+ };
2043
+ }
2044
+ cancel() {
2045
+ this.#invalidateCompletion();
2046
+ const before = this.#state();
2047
+ if (!this.isWritable() || this.#composing) this.#takeOwnership();
2048
+ this.#applyMutation(this.#model.cancel(), before, `voice:${this.#currentSegment}`);
2049
+ this.#currentSegment = void 0;
2050
+ this.#history.breakGroup();
2051
+ }
2052
+ destroy() {
2053
+ this.#invalidateCompletion();
2054
+ this.#model.destroy();
2055
+ this.#target.detach();
2056
+ this.#history.clear();
2057
+ this.#listeners.clear();
2058
+ }
2059
+ async #transformSpan(span, completionGeneration) {
2060
+ const transform = this.#transformTranscript;
2061
+ if (transform === void 0 || !this.#model.proveSpan(span)) return null;
2062
+ const generation = span.generation;
2063
+ const originalText = this.#model.getSpanText(span);
2064
+ const transcript = originalText.trim();
2065
+ try {
2066
+ const transformed = await runTransformWithTimeout(() => transform(transcript), this.#transformTimeoutMs);
2067
+ if (typeof transformed !== "string") throw new TypeError("transformTranscript must resolve to a string.");
2068
+ if (!this.isWritable() || this.#composing || completionGeneration !== this.#completionGeneration || !this.#model.canApplyTransform(span, generation, originalText)) return null;
2069
+ const before = this.#state();
2070
+ this.#model.configureMutation(this.#target.target?.maxLength ?? -1, "transform");
2071
+ this.#history.breakGroup();
2072
+ this.#applyMutation(this.#model.applyTransform(span, transformed), before, `transform:${span.id}`);
2073
+ const limit = this.#model.takeLimit();
2074
+ if (limit) this.#emit(limit);
2075
+ return null;
2076
+ } catch (cause) {
2077
+ if (completionGeneration !== this.#completionGeneration) return null;
2078
+ this.#model.freezeFailedTransform(span);
2079
+ return new VoiceInputError$1({
2080
+ code: "transform-error",
2081
+ message: cause instanceof TransformTimeoutError ? `Transcript transform timed out after ${this.#transformTimeoutMs}ms.` : "Transcript transform failed.",
2082
+ cause
2083
+ });
2084
+ }
2085
+ }
2086
+ #handleInput() {
2087
+ const selection = this.#target.readSelection();
2088
+ const before = this.#beforeInput ?? {
2089
+ value: this.#model.value,
2090
+ selection: this.#model.selection
2091
+ };
2092
+ this.#beforeInput = void 0;
2093
+ this.#model.reconcileExternalValue(this.#target.readValue(), selection);
2094
+ const key = this.#composing ? "composition" : this.#inputType;
2095
+ if (![
2096
+ "insertText",
2097
+ "deleteContentBackward",
2098
+ "deleteContentForward",
2099
+ "composition"
2100
+ ].includes(key)) this.#history.breakGroup();
2101
+ this.#history.record(before, this.#state(), key);
2102
+ }
2103
+ #handleSelectionChange() {
2104
+ const selection = this.#target.readSelection();
2105
+ if (selection !== null && !this.#composing) {
2106
+ if (!sameSelection(selection, this.#model.selection)) this.#takeOwnership();
2107
+ this.#model.selectionChanged(selection);
2108
+ }
2109
+ }
2110
+ #reconcileUncontrolledDomValue() {
2111
+ if (this.#target.isControlled || this.#target.target === null) return;
2112
+ const value = this.#target.readValue();
2113
+ if (value !== this.#model.value) {
2114
+ this.#takeOwnership();
2115
+ this.#history.clear();
2116
+ this.#model.reconcileExternalValue(value, this.#target.readSelection());
2117
+ }
2118
+ }
2119
+ #applyMutation(mutation, before, key = "voice") {
2120
+ if (mutation !== null) {
2121
+ if (before && mutation.changed) this.#history.record(before, mutation, key);
2122
+ this.#target.applyMutation(mutation);
2123
+ }
2124
+ }
2125
+ #invalidateCompletion() {
2126
+ this.#completionGeneration += 1;
2127
+ }
2128
+ };
2129
+ function invalidConfiguration$1(message) {
2130
+ return new VoiceInputError$1({
2131
+ code: "invalid-configuration",
2132
+ message
2133
+ });
2134
+ }
2135
+ function isVoiceInputError(error) {
2136
+ return error !== null;
2137
+ }
2138
+ function reportUnhandledError(error) {
2139
+ const reportError = globalThis.reportError;
2140
+ if (typeof reportError === "function") reportError(error);
2141
+ else queueMicrotask(() => {
2142
+ throw error;
2143
+ });
2144
+ }
2145
+ function sameSelection(left, right) {
2146
+ return left?.start === right?.start && left?.end === right?.end && (left?.start === left?.end || left?.direction === right?.direction);
2147
+ }
2148
+ //#endregion
2149
+ //#region src/text-engine.ts
2150
+ const DEFAULT_TRANSFORM_TIMEOUT_MS = 1e4;
2151
+ function createVoiceInputTextEngine(options = {}) {
2152
+ const interimBehavior = options.interimBehavior ?? "inline";
2153
+ if (interimBehavior !== "inline" && interimBehavior !== "expose") throw invalidConfiguration("interimBehavior must be either \"inline\" or \"expose\".");
2154
+ if (options.transformTimeoutMs !== void 0 && (!Number.isFinite(options.transformTimeoutMs) || !Number.isInteger(options.transformTimeoutMs) || options.transformTimeoutMs <= 0)) throw invalidConfiguration("transformTimeoutMs must be a positive finite integer.");
2155
+ if (options.controlled !== void 0 && (typeof options.controlled.getValue !== "function" || typeof options.controlled.onValueChange !== "function")) throw invalidConfiguration("controlled must provide getValue and onValueChange functions.");
2156
+ if (options.transformTranscript !== void 0 && typeof options.transformTranscript !== "function") throw invalidConfiguration("transformTranscript must be a function.");
2157
+ return new VoiceInputTextEngineController({
2158
+ interimBehavior,
2159
+ controlled: options.controlled,
2160
+ transformTranscript: options.transformTranscript,
2161
+ transformTimeoutMs: options.transformTimeoutMs ?? DEFAULT_TRANSFORM_TIMEOUT_MS
2162
+ });
2163
+ }
2164
+ function invalidConfiguration(message) {
2165
+ return new VoiceInputError$1({
2166
+ code: "invalid-configuration",
2167
+ message
2168
+ });
2169
+ }
2170
+ //#endregion
2171
+ export { AUDIO_WORKLET_SOURCE as VOICE_INPUT_AUDIO_WORKLET_SOURCE, VoiceInputError, createBrowserAudioSource, createVoiceInputSession, createVoiceInputTextEngine, getBrowserVoiceInputSupport, normalizeBrowserAudioError };
2172
+
2173
+ //# sourceMappingURL=index.js.map