@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/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/index.cjs +2180 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2173 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
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
|