use-voice-control 0.1.36 → 0.1.38

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.
Files changed (37) hide show
  1. package/README.md +77 -6
  2. package/dist/client/index.d.ts +8 -0
  3. package/dist/client/live-transcriber.d.ts +50 -0
  4. package/dist/client/read-aloud.d.ts +65 -0
  5. package/dist/client.js +301 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/react/SpokenPhraseOverlay.d.ts +18 -0
  8. package/dist/react/index.d.ts +8 -0
  9. package/dist/react/useLiveTranscription.d.ts +19 -0
  10. package/dist/react/useReadAloud.d.ts +22 -0
  11. package/dist/react.js +212 -0
  12. package/dist/react.js.map +1 -0
  13. package/package.json +28 -10
  14. package/speech/api-client.ts +75 -0
  15. package/speech/client/index.ts +21 -0
  16. package/speech/client/live-transcriber.ts +216 -0
  17. package/speech/client/read-aloud.ts +305 -0
  18. package/speech/core/KokoroTTS.js +7 -0
  19. package/speech/core/kokoro.js +8 -0
  20. package/speech/legacy/conversation.js +7 -0
  21. package/speech/legacy/stt.js +7 -0
  22. package/speech/legacy/tts.js +7 -0
  23. package/speech/legacy/worker.js +7 -0
  24. package/speech/react/SpokenPhraseOverlay.tsx +126 -0
  25. package/speech/react/index.ts +27 -0
  26. package/speech/react/useLiveTranscription.ts +142 -0
  27. package/speech/react/useReadAloud.ts +137 -0
  28. package/speech/ui/AudioPlayer.js +6 -0
  29. package/speech/ui/ui.js +7 -0
  30. package/speech/ui/voice-selector.js +3 -0
  31. package/speech/ui/voices.js +7 -0
  32. package/speech/utils/audio-utils.js +7 -0
  33. package/speech/utils/phonemize.js +7 -0
  34. package/speech/utils/semantic-split.d.ts +10 -0
  35. package/speech/utils/semantic-split.js +7 -0
  36. package/speech/utils/sentence-detector.d.ts +13 -0
  37. package/speech/utils/sentence-detector.js +5 -1
package/README.md CHANGED
@@ -3,12 +3,6 @@
3
3
  <br />
4
4
  <a href="https://www.npmjs.com/package/use-voice-control"><img src="https://img.shields.io/npm/dm/use-voice-control.svg" alt="NPM Monthly Downloads"></a>
5
5
  <a href="https://www.npmjs.com/package/use-voice-control"><img src="https://img.shields.io/npm/v/use-voice-control.svg" alt="npm version"></a>
6
- <a href="https://bundlephobia.com/package/use-voice-control" target="_blank" rel="noopener noreferrer">
7
- <img
8
- src="https://img.shields.io/bundlephobia/minzip/use-voice-control?style=flat&label=size"
9
- alt="npm bundle size "
10
- />
11
- </a>
12
6
  <a href="https://discord.gg/SJdBqBz3tV">
13
7
  <img src="https://img.shields.io/discord/1110227955554209923.svg?label=Chat&logo=Discord&colorB=7289da&style=flat"
14
8
  alt="Join Discord" />
@@ -124,6 +118,79 @@ export function VoiceOutput() {
124
118
 
125
119
  ---
126
120
 
121
+ ## 🗣️ Read Aloud & Live Dictation
122
+
123
+ Two ready-made browser engines ship from `use-voice-control/client` (framework
124
+ agnostic) and `use-voice-control/react` (hooks plus an on-screen phrase display).
125
+ They back the **Read aloud** and **Dictate** tools in the REASON editor and the
126
+ chat composer.
127
+
128
+ ### Read aloud
129
+
130
+ `ReadAloudController` chunks text along sentence and paragraph boundaries,
131
+ synthesizes each chunk (Kokoro via `POST /api/speech/tts` by default) and plays
132
+ them back-to-back while pre-fetching the next — so playback starts after the first
133
+ short chunk instead of after the whole document. If no TTS route answers, it falls
134
+ back to the browser's built-in `speechSynthesis` rather than failing.
135
+
136
+ ```tsx
137
+ import { useReadAloud } from 'use-voice-control/react';
138
+
139
+ function ReadButton({ text }: { text: string }) {
140
+ const { toggle, isActive, currentChunk } = useReadAloud({ voice: 'af_heart' });
141
+
142
+ return (
143
+ <button onClick={() => toggle(text)}>
144
+ {isActive ? `Stop (${currentChunk?.index ?? 0})` : 'Read aloud'}
145
+ </button>
146
+ );
147
+ }
148
+ ```
149
+
150
+ Options: `provider`, `voice`, `endpoint`, `maxChunkLength`, and `synthesize` to
151
+ plug in your own TTS. Controls: `speak`, `pause`, `resume`, `stop`, `toggle`.
152
+
153
+ ### Live dictation
154
+
155
+ `LiveTranscriber` reports two streams: `onPartial`, the guess that keeps changing
156
+ while a phrase is being spoken, and `onCommit`, the phrase the recognizer settled
157
+ on. The first is what lets you type words into an input *as they are said*. It
158
+ prefers the browser's own `SpeechRecognition` (no model download) and falls back to
159
+ on-device Moonshine; Chromium's recognizer stops itself after a pause, so sessions
160
+ restart automatically.
161
+
162
+ ```tsx
163
+ import { useLiveTranscription, SpokenPhraseOverlay } from 'use-voice-control/react';
164
+
165
+ function Dictate() {
166
+ const [text, setText] = useState('');
167
+ const base = useRef('');
168
+
169
+ const { toggle, isListening, lastPhrase, phraseId } = useLiveTranscription({
170
+ onPartial: (phrase) => setText(`${base.current}${phrase}`),
171
+ onCommit: (phrase) => {
172
+ base.current = `${base.current}${phrase} `;
173
+ setText(base.current);
174
+ },
175
+ });
176
+
177
+ return (
178
+ <>
179
+ <textarea value={text} onChange={(e) => setText(e.target.value)} />
180
+ <button onClick={toggle}>{isListening ? 'Stop' : 'Dictate'}</button>
181
+ <SpokenPhraseOverlay phrase={lastPhrase} phraseId={phraseId} visible={isListening} />
182
+ </>
183
+ );
184
+ }
185
+ ```
186
+
187
+ `<SpokenPhraseOverlay />` shows the latest phrase in large type at the centre of the
188
+ viewport and fades out two seconds after the last update, so you can confirm you
189
+ were heard without watching the input. It renders through a portal with pointer
190
+ events disabled and is styled inline, so it looks the same in any app.
191
+
192
+ ---
193
+
127
194
  ## 📚 API Reference
128
195
 
129
196
  ### Hooks
@@ -477,6 +544,10 @@ const SpeechWorker = require('use-voice-control/speech/worker.js');
477
544
  ## 📦 Exports
478
545
 
479
546
  ```ts
547
+ // Read aloud & live dictation (implemented)
548
+ export { ReadAloudController, LiveTranscriber, isTranscriptionSupported } from 'use-voice-control/client';
549
+ export { useReadAloud, useLiveTranscription, SpokenPhraseOverlay } from 'use-voice-control/react';
550
+
480
551
  // Hooks
481
552
  export { useSpeechRecognition } from 'use-voice-control/hooks';
482
553
  export { useSpeechSynthesis } from 'use-voice-control/hooks';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Public entry point for the framework-agnostic browser engines:
3
+ * read-aloud playback and live dictation. React callers should prefer the hooks in
4
+ * `use-voice-control/react`, which wrap these.
5
+ */
6
+ export { ReadAloudController, type ReadAloudChunk, type ReadAloudOptions, type ReadAloudState, type SynthesizeFn, } from './read-aloud';
7
+ export { LiveTranscriber, isTranscriptionSupported, type LiveTranscriberOptions, type TranscriberEngine, } from './live-transcriber';
8
+ export type { TTSProvider, KokoroVoice, DeepgramSpeaker } from '../types/types';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Browser-side live dictation engine.
3
+ *
4
+ * `LiveTranscriber` streams microphone audio to a recognizer and reports two kinds
5
+ * of text: `onPartial`, the in-progress guess that keeps changing while a phrase is
6
+ * being spoken, and `onCommit`, a phrase the recognizer has settled on. Callers use
7
+ * the first to type text into an input as it is being said, and the second to
8
+ * finalize it.
9
+ *
10
+ * Two engines are supported. The browser's own `SpeechRecognition` is preferred when
11
+ * present because it needs no model download; otherwise the package's bundled
12
+ * Moonshine model runs the recognition entirely on-device. Chromium's recognizer
13
+ * stops itself after a pause, so a session that is still meant to be listening is
14
+ * restarted automatically.
15
+ */
16
+ export type TranscriberEngine = "auto" | "webspeech" | "moonshine";
17
+ export interface LiveTranscriberOptions {
18
+ /** Which recognizer to use. Default `auto` (browser first, Moonshine as fallback). */
19
+ engine?: TranscriberEngine;
20
+ /** BCP-47 language tag for the browser recognizer. Default `en-US`. */
21
+ language?: string;
22
+ /** Moonshine model name. Default `model/small`. */
23
+ model?: string;
24
+ /** Fires continuously with the current in-progress phrase. */
25
+ onPartial?: (text: string) => void;
26
+ /** Fires once per phrase the recognizer has settled on. */
27
+ onCommit?: (text: string) => void;
28
+ /** Fires whenever the microphone starts or stops. */
29
+ onStateChange?: (listening: boolean) => void;
30
+ onError?: (error: Error) => void;
31
+ }
32
+ /** True when this browser can dictate at all (either engine). */
33
+ export declare function isTranscriptionSupported(): boolean;
34
+ export declare class LiveTranscriber {
35
+ private options;
36
+ private listening;
37
+ /** Distinguishes a deliberate `stop()` from Chromium's idle auto-stop. */
38
+ private stopRequested;
39
+ private recognition;
40
+ private moonshine;
41
+ constructor(options?: LiveTranscriberOptions);
42
+ setOptions(options: LiveTranscriberOptions): void;
43
+ isListening(): boolean;
44
+ start(): Promise<void>;
45
+ stop(): Promise<void>;
46
+ toggle(): Promise<void>;
47
+ private setListening;
48
+ private startWebSpeech;
49
+ private startMoonshine;
50
+ }
@@ -0,0 +1,65 @@
1
+ import { TTSProvider } from '../types/types';
2
+ export type ReadAloudState = "idle" | "loading" | "speaking" | "paused";
3
+ export interface ReadAloudChunk {
4
+ /** The text of the chunk currently being spoken. */
5
+ text: string;
6
+ /** Zero-based position of this chunk in the queue. */
7
+ index: number;
8
+ /** Total number of chunks queued for this utterance. */
9
+ total: number;
10
+ }
11
+ /** Turns a chunk of text into playable audio. Return `null` to defer to `speechSynthesis`. */
12
+ export type SynthesizeFn = (text: string, signal: AbortSignal) => Promise<Blob | null>;
13
+ export interface ReadAloudOptions {
14
+ /** TTS provider passed to the endpoint. Default `kokoro`. */
15
+ provider?: TTSProvider;
16
+ /** Provider-specific voice id. Default `af_heart`. */
17
+ voice?: string;
18
+ /** HTTP route that synthesizes text. Default `/api/speech/tts`. */
19
+ endpoint?: string;
20
+ /**
21
+ * Target chunk size in characters. Smaller starts talking sooner but makes the
22
+ * seams between chunks more audible. Default 240.
23
+ */
24
+ maxChunkLength?: number;
25
+ /** Override synthesis entirely (tests, a bring-your-own-TTS host app). */
26
+ synthesize?: SynthesizeFn;
27
+ /** Called as each chunk starts playing. */
28
+ onChunk?: (chunk: ReadAloudChunk) => void;
29
+ /** Called on every state transition. */
30
+ onStateChange?: (state: ReadAloudState) => void;
31
+ /** Called when playback ends, either by running out of chunks or by `stop()`. */
32
+ onEnd?: (reason: "finished" | "stopped") => void;
33
+ /** Called when synthesis or playback fails irrecoverably. */
34
+ onError?: (error: Error) => void;
35
+ }
36
+ export declare class ReadAloudController {
37
+ private options;
38
+ private state;
39
+ private abortController;
40
+ private audio;
41
+ private objectUrl;
42
+ /** Set once the endpoint has proved unreachable, so later chunks skip the retry. */
43
+ private endpointUnavailable;
44
+ constructor(options?: ReadAloudOptions);
45
+ /** Replace the options (voice, callbacks, …) without discarding playback state. */
46
+ setOptions(options: ReadAloudOptions): void;
47
+ getState(): ReadAloudState;
48
+ isActive(): boolean;
49
+ /**
50
+ * Speak `text`, cancelling anything already playing. Resolves when playback
51
+ * finishes or is stopped — it never rejects; failures go to `onError`.
52
+ */
53
+ speak(text: string): Promise<void>;
54
+ pause(): void;
55
+ resume(): void;
56
+ /** Stop playback and drop any queued chunks. Safe to call when already idle. */
57
+ stop(): void;
58
+ private setState;
59
+ private cleanup;
60
+ private synthesize;
61
+ private fetchAudio;
62
+ private playChunk;
63
+ private playAudioBlob;
64
+ private playWithSpeechSynthesis;
65
+ }
package/dist/client.js ADDED
@@ -0,0 +1,301 @@
1
+ var U = Object.defineProperty;
2
+ var E = (h, e, i) => e in h ? U(h, e, { enumerable: !0, configurable: !0, writable: !0, value: i }) : h[e] = i;
3
+ var l = (h, e, i) => E(h, typeof e != "symbol" ? e + "" : e, i);
4
+ function C(h, e = 500) {
5
+ const i = h.split(/\n\s*\n/), t = [];
6
+ for (let s of i) {
7
+ if (s.length <= e) {
8
+ t.push(s.trim());
9
+ continue;
10
+ }
11
+ const n = new RegExp(`(?<=[.?!])(?=\\s+["“”'a-z])`, "gi"), a = s.split(n);
12
+ let o = "";
13
+ for (let r of a) {
14
+ if (r = r.trim(), r.length > e) {
15
+ const p = k(r, e);
16
+ for (let c of p)
17
+ (o + " " + c).length > e ? (o && t.push(o.trim()), o = c) : o += (o ? " " : "") + c;
18
+ continue;
19
+ }
20
+ (o + " " + r).length > e ? (o && t.push(o.trim()), o = r) : o += (o ? " " : "") + r;
21
+ }
22
+ o && t.push(o.trim());
23
+ }
24
+ return t;
25
+ }
26
+ function k(h, e) {
27
+ const i = [];
28
+ let t = "";
29
+ const s = h.split(/,\s*/);
30
+ for (let n of s)
31
+ if ((t + ", " + n).length > e)
32
+ if (t && i.push(t.trim()), n.length > e) {
33
+ const a = n.split(/\s+/);
34
+ let o = "";
35
+ for (let r of a)
36
+ (o + " " + r).length > e ? (o && i.push(o.trim()), o = r) : o += (o ? " " : "") + r;
37
+ o && i.push(o.trim()), t = "";
38
+ } else
39
+ t = n;
40
+ else
41
+ t += (t ? ", " : "") + n;
42
+ return t && i.push(t.trim()), i;
43
+ }
44
+ const v = "/api/speech/tts", R = "af_heart", L = 240;
45
+ function w(h) {
46
+ return h.aborted ? Promise.resolve() : new Promise((e) => {
47
+ h.addEventListener("abort", () => e(), { once: !0 });
48
+ });
49
+ }
50
+ class A {
51
+ constructor(e = {}) {
52
+ l(this, "options");
53
+ l(this, "state", "idle");
54
+ l(this, "abortController", null);
55
+ l(this, "audio", null);
56
+ l(this, "objectUrl", null);
57
+ /** Set once the endpoint has proved unreachable, so later chunks skip the retry. */
58
+ l(this, "endpointUnavailable", !1);
59
+ this.options = e;
60
+ }
61
+ /** Replace the options (voice, callbacks, …) without discarding playback state. */
62
+ setOptions(e) {
63
+ this.options = e;
64
+ }
65
+ getState() {
66
+ return this.state;
67
+ }
68
+ isActive() {
69
+ return this.state === "speaking" || this.state === "paused" || this.state === "loading";
70
+ }
71
+ /**
72
+ * Speak `text`, cancelling anything already playing. Resolves when playback
73
+ * finishes or is stopped — it never rejects; failures go to `onError`.
74
+ */
75
+ async speak(e) {
76
+ var a, o, r, p, c, g, d, y;
77
+ this.stop();
78
+ const i = this.options.maxChunkLength ?? L, t = C(e ?? "", i).map((u) => u.trim()).filter((u) => u.length > 0);
79
+ if (t.length === 0) return;
80
+ const s = new AbortController();
81
+ this.abortController = s;
82
+ const { signal: n } = s;
83
+ this.setState("loading");
84
+ try {
85
+ let u = this.synthesize(t[0], n);
86
+ for (let f = 0; f < t.length; f += 1) {
87
+ const m = u;
88
+ u = f + 1 < t.length ? this.synthesize(t[f + 1], n) : Promise.resolve(null);
89
+ const S = await m;
90
+ if (n.aborted || (this.setState("speaking"), (o = (a = this.options).onChunk) == null || o.call(a, { text: t[f], index: f, total: t.length }), n.aborted) || (await this.playChunk(S, t[f], n), n.aborted)) return;
91
+ }
92
+ this.cleanup(), this.setState("idle"), (p = (r = this.options).onEnd) == null || p.call(r, "finished");
93
+ } catch (u) {
94
+ if (n.aborted) return;
95
+ this.cleanup(), this.setState("idle"), (g = (c = this.options).onError) == null || g.call(
96
+ c,
97
+ u instanceof Error ? u : new Error(String(u))
98
+ ), (y = (d = this.options).onEnd) == null || y.call(d, "stopped");
99
+ } finally {
100
+ this.abortController === s && (this.abortController = null);
101
+ }
102
+ }
103
+ pause() {
104
+ this.state === "speaking" && (this.audio ? this.audio.pause() : typeof speechSynthesis < "u" && speechSynthesis.pause(), this.setState("paused"));
105
+ }
106
+ resume() {
107
+ this.state === "paused" && (this.audio ? this.audio.play().catch(() => {
108
+ }) : typeof speechSynthesis < "u" && speechSynthesis.resume(), this.setState("speaking"));
109
+ }
110
+ /** Stop playback and drop any queued chunks. Safe to call when already idle. */
111
+ stop() {
112
+ var i, t, s;
113
+ const e = this.isActive();
114
+ (i = this.abortController) == null || i.abort(), this.abortController = null, this.cleanup(), e && (this.setState("idle"), (s = (t = this.options).onEnd) == null || s.call(t, "stopped"));
115
+ }
116
+ setState(e) {
117
+ var i, t;
118
+ this.state !== e && (this.state = e, (t = (i = this.options).onStateChange) == null || t.call(i, e));
119
+ }
120
+ cleanup() {
121
+ this.audio && (this.audio.pause(), this.audio.src = "", this.audio = null), this.objectUrl && (URL.revokeObjectURL(this.objectUrl), this.objectUrl = null), typeof speechSynthesis < "u" && speechSynthesis.cancel();
122
+ }
123
+ synthesize(e, i) {
124
+ const t = this.options.synthesize ? this.options.synthesize(e, i) : this.fetchAudio(e, i);
125
+ return t.catch(() => {
126
+ }), t;
127
+ }
128
+ async fetchAudio(e, i) {
129
+ if (this.endpointUnavailable) return null;
130
+ const t = this.options.endpoint ?? v;
131
+ try {
132
+ const s = await fetch(t, {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({
136
+ text: e,
137
+ provider: this.options.provider ?? "kokoro",
138
+ voice: this.options.voice ?? R
139
+ }),
140
+ signal: i
141
+ });
142
+ if (!s.ok) throw new Error(`TTS failed: ${s.status}`);
143
+ const n = s.headers.get("Content-Type") || "audio/wav";
144
+ return new Blob([await s.arrayBuffer()], { type: n });
145
+ } catch (s) {
146
+ if (i.aborted) throw s;
147
+ return this.endpointUnavailable = !0, null;
148
+ }
149
+ }
150
+ playChunk(e, i, t) {
151
+ return e ? this.playAudioBlob(e, t) : this.playWithSpeechSynthesis(i, t);
152
+ }
153
+ async playAudioBlob(e, i) {
154
+ const t = URL.createObjectURL(e), s = new Audio(t);
155
+ this.audio = s, this.objectUrl = t;
156
+ const n = new Promise((a, o) => {
157
+ s.addEventListener("ended", () => a(), { once: !0 }), s.addEventListener(
158
+ "error",
159
+ () => o(new Error("Audio playback failed")),
160
+ { once: !0 }
161
+ );
162
+ });
163
+ try {
164
+ await s.play(), await Promise.race([n, w(i)]);
165
+ } finally {
166
+ this.audio === s && (this.audio = null), this.objectUrl === t && (this.objectUrl = null), s.pause(), URL.revokeObjectURL(t);
167
+ }
168
+ }
169
+ async playWithSpeechSynthesis(e, i) {
170
+ if (typeof speechSynthesis > "u" || typeof SpeechSynthesisUtterance > "u")
171
+ throw new Error("No speech synthesis available in this browser");
172
+ const t = new SpeechSynthesisUtterance(e), s = new Promise((n, a) => {
173
+ t.onend = () => n(), t.onerror = () => i.aborted ? n() : a(new Error("Speech synthesis failed"));
174
+ });
175
+ speechSynthesis.speak(t), await Promise.race([s, w(i)]);
176
+ }
177
+ }
178
+ function b() {
179
+ return typeof window > "u" ? null : window.SpeechRecognition || window.webkitSpeechRecognition || null;
180
+ }
181
+ function P() {
182
+ var h;
183
+ return typeof window > "u" ? !1 : b() ? !0 : !!((h = navigator.mediaDevices) != null && h.getUserMedia);
184
+ }
185
+ class j {
186
+ constructor(e = {}) {
187
+ l(this, "options");
188
+ l(this, "listening", !1);
189
+ /** Distinguishes a deliberate `stop()` from Chromium's idle auto-stop. */
190
+ l(this, "stopRequested", !1);
191
+ l(this, "recognition", null);
192
+ l(this, "moonshine", null);
193
+ this.options = e;
194
+ }
195
+ setOptions(e) {
196
+ this.options = e;
197
+ }
198
+ isListening() {
199
+ return this.listening;
200
+ }
201
+ async start() {
202
+ var t, s;
203
+ if (this.listening) return;
204
+ this.stopRequested = !1;
205
+ const e = this.options.engine ?? "auto", i = b();
206
+ try {
207
+ if (e !== "moonshine" && i) {
208
+ this.startWebSpeech(i);
209
+ return;
210
+ }
211
+ if (e === "webspeech")
212
+ throw new Error("Speech recognition is not available in this browser");
213
+ await this.startMoonshine();
214
+ } catch (n) {
215
+ this.setListening(!1), (s = (t = this.options).onError) == null || s.call(
216
+ t,
217
+ n instanceof Error ? n : new Error(String(n))
218
+ );
219
+ }
220
+ }
221
+ async stop() {
222
+ var e, i;
223
+ if (this.stopRequested = !0, this.recognition) {
224
+ try {
225
+ this.recognition.stop();
226
+ } catch {
227
+ }
228
+ this.recognition = null;
229
+ }
230
+ if (this.moonshine) {
231
+ try {
232
+ await ((i = (e = this.moonshine).stop) == null ? void 0 : i.call(e));
233
+ } catch {
234
+ }
235
+ this.moonshine = null;
236
+ }
237
+ this.setListening(!1);
238
+ }
239
+ async toggle() {
240
+ this.listening ? await this.stop() : await this.start();
241
+ }
242
+ setListening(e) {
243
+ var i, t;
244
+ this.listening !== e && (this.listening = e, (t = (i = this.options).onStateChange) == null || t.call(i, e));
245
+ }
246
+ startWebSpeech(e) {
247
+ const i = new e();
248
+ i.continuous = !0, i.interimResults = !0, i.lang = this.options.language ?? "en-US", i.onstart = () => this.setListening(!0), i.onresult = (t) => {
249
+ var n, a, o, r, p;
250
+ let s = "";
251
+ for (let c = t.resultIndex; c < t.results.length; c += 1) {
252
+ const g = t.results[c], d = String(((n = g[0]) == null ? void 0 : n.transcript) ?? "").trim();
253
+ d && (g.isFinal ? (o = (a = this.options).onCommit) == null || o.call(a, d) : s += `${s ? " " : ""}${d}`);
254
+ }
255
+ s && ((p = (r = this.options).onPartial) == null || p.call(r, s));
256
+ }, i.onerror = (t) => {
257
+ var n, a;
258
+ const s = t == null ? void 0 : t.error;
259
+ s === "no-speech" || s === "aborted" || (this.stopRequested = !0, (a = (n = this.options).onError) == null || a.call(n, new Error(`Speech recognition error: ${s}`)));
260
+ }, i.onend = () => {
261
+ if (this.stopRequested || this.recognition !== i) {
262
+ this.recognition = null, this.setListening(!1);
263
+ return;
264
+ }
265
+ try {
266
+ i.start();
267
+ } catch {
268
+ this.recognition = null, this.setListening(!1);
269
+ }
270
+ }, this.recognition = i, i.start();
271
+ }
272
+ async startMoonshine() {
273
+ const e = await import("@moonshine-ai/moonshine-js"), i = new e.MicrophoneTranscriber(
274
+ this.options.model ?? "model/small",
275
+ {
276
+ onTranscriptionUpdated: (t) => {
277
+ var s, n;
278
+ (n = (s = this.options).onPartial) == null || n.call(s, String(t ?? "").trim());
279
+ },
280
+ onTranscriptionCommitted: (t) => {
281
+ var n, a, o, r;
282
+ const s = String(t ?? "").trim();
283
+ s ? (a = (n = this.options).onCommit) == null || a.call(n, s) : (r = (o = this.options).onPartial) == null || r.call(o, "");
284
+ }
285
+ },
286
+ !1
287
+ // streaming mode
288
+ );
289
+ if (this.moonshine = i, await i.start(), this.stopRequested) {
290
+ await this.stop();
291
+ return;
292
+ }
293
+ this.setListening(!0);
294
+ }
295
+ }
296
+ export {
297
+ j as LiveTranscriber,
298
+ A as ReadAloudController,
299
+ P as isTranscriptionSupported
300
+ };
301
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sources":["../speech/utils/semantic-split.js","../speech/client/read-aloud.ts","../speech/client/live-transcriber.ts"],"sourcesContent":["/**\n * @fileoverview Splits long text into TTS-sized chunks along paragraph, sentence, comma, and word boundaries so speech synthesis can process it incrementally.\n *\n * Exports `splitTextSmart` (primary chunker, falling back to `splitLongSentence` for\n * oversized sentences) and `splitLongSentence`. Also retains an older, unused\n * `splitTextSmartOld` variant that lacks the long-sentence fallback.\n */\nexport function splitTextSmart(text, maxChunkLength = 500) {\n const paragraphChunks = text.split(/\\n\\s*\\n/);\n const finalChunks = [];\n\n for (let para of paragraphChunks) {\n if (para.length <= maxChunkLength) {\n finalChunks.push(para.trim());\n continue;\n }\n\n const sentenceRegex = /(?<=[.?!])(?=\\s+[\"“”'a-z])/gi;\n const sentences = para.split(sentenceRegex);\n\n let chunk = '';\n for (let sentence of sentences) {\n sentence = sentence.trim();\n\n if (sentence.length > maxChunkLength) {\n // Sentence too long — fallback split\n const subChunks = splitLongSentence(sentence, maxChunkLength);\n for (let sub of subChunks) {\n if ((chunk + ' ' + sub).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sub;\n } else {\n chunk += (chunk ? ' ' : '') + sub;\n }\n }\n continue;\n }\n\n if ((chunk + ' ' + sentence).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sentence;\n } else {\n chunk += (chunk ? ' ' : '') + sentence;\n }\n }\n if (chunk) finalChunks.push(chunk.trim());\n }\n\n return finalChunks;\n}\n\nexport function splitLongSentence(sentence, maxLen) {\n const chunks = [];\n let current = '';\n\n const commaParts = sentence.split(/,\\s*/);\n for (let part of commaParts) {\n if ((current + ', ' + part).length > maxLen) {\n if (current) chunks.push(current.trim());\n if (part.length > maxLen) {\n const words = part.split(/\\s+/);\n let wordChunk = '';\n for (let word of words) {\n if ((wordChunk + ' ' + word).length > maxLen) {\n if (wordChunk) chunks.push(wordChunk.trim());\n wordChunk = word;\n } else {\n wordChunk += (wordChunk ? ' ' : '') + word;\n }\n }\n if (wordChunk) chunks.push(wordChunk.trim());\n current = '';\n } else {\n current = part;\n }\n } else {\n current += (current ? ', ' : '') + part;\n }\n }\n if (current) chunks.push(current.trim());\n\n return chunks;\n}\n\n\nfunction splitTextSmartOld(text, maxChunkLength = 500) {\n const paragraphChunks = text.split(/\\n\\s*\\n/); // Step 1: split on double returns\n const finalChunks = [];\n\n for (let para of paragraphChunks) {\n if (para.length <= maxChunkLength) {\n finalChunks.push(para.trim());\n continue;\n }\n\n // Step 2: Further split on sentence boundaries if too long\n const sentenceRegex = /(?<=[.?!])(?=\\s+[\"“”'a-z])/gi;\n const sentences = para.split(sentenceRegex);\n\n let chunk = '';\n for (let sentence of sentences) {\n sentence = sentence.trim();\n if ((chunk + ' ' + sentence).length > maxChunkLength) {\n if (chunk) finalChunks.push(chunk.trim());\n chunk = sentence;\n } else {\n chunk += (chunk ? ' ' : '') + sentence;\n }\n }\n if (chunk) finalChunks.push(chunk.trim());\n }\n\n return finalChunks;\n}\n","/**\n * @fileoverview Browser-side \"read aloud\" playback engine.\n *\n * `ReadAloudController` takes a block of text, chunks it along sentence/paragraph\n * boundaries with `splitTextSmart`, synthesizes each chunk (Kokoro by default, via\n * the package's `/api/speech/tts` route) and plays the chunks back-to-back while\n * pre-fetching the next one, so playback starts after the first short chunk rather\n * than after the whole document. Pause/resume/stop are supported throughout, and\n * each chunk is reported to `onChunk` so callers can highlight what is being spoken.\n *\n * When no TTS endpoint is reachable — a host app that has not mounted the route, or\n * an offline build — the controller falls back to the browser's built-in\n * `speechSynthesis` rather than failing, so the feature still works everywhere.\n */\nimport type { TTSProvider } from \"../types/types\";\nimport { splitTextSmart } from \"../utils/semantic-split.js\";\n\nexport type ReadAloudState = \"idle\" | \"loading\" | \"speaking\" | \"paused\";\n\nexport interface ReadAloudChunk {\n /** The text of the chunk currently being spoken. */\n text: string;\n /** Zero-based position of this chunk in the queue. */\n index: number;\n /** Total number of chunks queued for this utterance. */\n total: number;\n}\n\n/** Turns a chunk of text into playable audio. Return `null` to defer to `speechSynthesis`. */\nexport type SynthesizeFn = (\n text: string,\n signal: AbortSignal\n) => Promise<Blob | null>;\n\nexport interface ReadAloudOptions {\n /** TTS provider passed to the endpoint. Default `kokoro`. */\n provider?: TTSProvider;\n /** Provider-specific voice id. Default `af_heart`. */\n voice?: string;\n /** HTTP route that synthesizes text. Default `/api/speech/tts`. */\n endpoint?: string;\n /**\n * Target chunk size in characters. Smaller starts talking sooner but makes the\n * seams between chunks more audible. Default 240.\n */\n maxChunkLength?: number;\n /** Override synthesis entirely (tests, a bring-your-own-TTS host app). */\n synthesize?: SynthesizeFn;\n /** Called as each chunk starts playing. */\n onChunk?: (chunk: ReadAloudChunk) => void;\n /** Called on every state transition. */\n onStateChange?: (state: ReadAloudState) => void;\n /** Called when playback ends, either by running out of chunks or by `stop()`. */\n onEnd?: (reason: \"finished\" | \"stopped\") => void;\n /** Called when synthesis or playback fails irrecoverably. */\n onError?: (error: Error) => void;\n}\n\nconst DEFAULT_ENDPOINT = \"/api/speech/tts\";\nconst DEFAULT_VOICE = \"af_heart\";\nconst DEFAULT_MAX_CHUNK = 240;\n\n/** Resolves once `signal` aborts. Used to race playback against `stop()`. */\nfunction abortPromise(signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n signal.addEventListener(\"abort\", () => resolve(), { once: true });\n });\n}\n\nexport class ReadAloudController {\n private options: ReadAloudOptions;\n private state: ReadAloudState = \"idle\";\n private abortController: AbortController | null = null;\n private audio: HTMLAudioElement | null = null;\n private objectUrl: string | null = null;\n /** Set once the endpoint has proved unreachable, so later chunks skip the retry. */\n private endpointUnavailable = false;\n\n constructor(options: ReadAloudOptions = {}) {\n this.options = options;\n }\n\n /** Replace the options (voice, callbacks, …) without discarding playback state. */\n setOptions(options: ReadAloudOptions): void {\n this.options = options;\n }\n\n getState(): ReadAloudState {\n return this.state;\n }\n\n isActive(): boolean {\n return this.state === \"speaking\" || this.state === \"paused\" || this.state === \"loading\";\n }\n\n /**\n * Speak `text`, cancelling anything already playing. Resolves when playback\n * finishes or is stopped — it never rejects; failures go to `onError`.\n */\n async speak(text: string): Promise<void> {\n this.stop();\n\n const maxChunkLength = this.options.maxChunkLength ?? DEFAULT_MAX_CHUNK;\n const chunks = splitTextSmart(text ?? \"\", maxChunkLength)\n .map((chunk) => chunk.trim())\n .filter((chunk) => chunk.length > 0);\n\n if (chunks.length === 0) return;\n\n const abortController = new AbortController();\n this.abortController = abortController;\n const { signal } = abortController;\n this.setState(\"loading\");\n\n try {\n // Kick off the first chunk, then always keep exactly one chunk in flight\n // ahead of the one playing.\n let upcoming = this.synthesize(chunks[0], signal);\n\n for (let index = 0; index < chunks.length; index += 1) {\n const current = upcoming;\n upcoming =\n index + 1 < chunks.length\n ? this.synthesize(chunks[index + 1], signal)\n : Promise.resolve(null);\n\n const blob = await current;\n if (signal.aborted) return;\n\n // Announce the state before the chunk so a listener reacting to\n // `onChunk` sees the controller already speaking.\n this.setState(\"speaking\");\n this.options.onChunk?.({ text: chunks[index], index, total: chunks.length });\n if (signal.aborted) return;\n await this.playChunk(blob, chunks[index], signal);\n if (signal.aborted) return;\n }\n\n this.cleanup();\n this.setState(\"idle\");\n this.options.onEnd?.(\"finished\");\n } catch (error) {\n if (signal.aborted) return;\n this.cleanup();\n this.setState(\"idle\");\n this.options.onError?.(\n error instanceof Error ? error : new Error(String(error))\n );\n this.options.onEnd?.(\"stopped\");\n } finally {\n if (this.abortController === abortController) {\n this.abortController = null;\n }\n }\n }\n\n pause(): void {\n if (this.state !== \"speaking\") return;\n if (this.audio) {\n this.audio.pause();\n } else if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.pause();\n }\n this.setState(\"paused\");\n }\n\n resume(): void {\n if (this.state !== \"paused\") return;\n if (this.audio) {\n void this.audio.play().catch(() => undefined);\n } else if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.resume();\n }\n this.setState(\"speaking\");\n }\n\n /** Stop playback and drop any queued chunks. Safe to call when already idle. */\n stop(): void {\n const wasActive = this.isActive();\n this.abortController?.abort();\n this.abortController = null;\n this.cleanup();\n if (wasActive) {\n this.setState(\"idle\");\n this.options.onEnd?.(\"stopped\");\n }\n }\n\n private setState(state: ReadAloudState): void {\n if (this.state === state) return;\n this.state = state;\n this.options.onStateChange?.(state);\n }\n\n private cleanup(): void {\n if (this.audio) {\n this.audio.pause();\n this.audio.src = \"\";\n this.audio = null;\n }\n if (this.objectUrl) {\n URL.revokeObjectURL(this.objectUrl);\n this.objectUrl = null;\n }\n if (typeof speechSynthesis !== \"undefined\") {\n speechSynthesis.cancel();\n }\n }\n\n private synthesize(text: string, signal: AbortSignal): Promise<Blob | null> {\n const promise = this.options.synthesize\n ? this.options.synthesize(text, signal)\n : this.fetchAudio(text, signal);\n // Pre-fetched chunks are awaited a beat later; mark them handled now so a\n // stop() mid-flight does not surface as an unhandled rejection.\n promise.catch(() => undefined);\n return promise;\n }\n\n private async fetchAudio(text: string, signal: AbortSignal): Promise<Blob | null> {\n if (this.endpointUnavailable) return null;\n\n const endpoint = this.options.endpoint ?? DEFAULT_ENDPOINT;\n try {\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n text,\n provider: this.options.provider ?? \"kokoro\",\n voice: this.options.voice ?? DEFAULT_VOICE,\n }),\n signal,\n });\n\n if (!response.ok) throw new Error(`TTS failed: ${response.status}`);\n\n const contentType = response.headers.get(\"Content-Type\") || \"audio/wav\";\n return new Blob([await response.arrayBuffer()], { type: contentType });\n } catch (error) {\n if (signal.aborted) throw error;\n // No usable endpoint in this host app — remember it and let every\n // remaining chunk go straight to the browser's own synthesizer.\n this.endpointUnavailable = true;\n return null;\n }\n }\n\n private playChunk(\n blob: Blob | null,\n text: string,\n signal: AbortSignal\n ): Promise<void> {\n return blob\n ? this.playAudioBlob(blob, signal)\n : this.playWithSpeechSynthesis(text, signal);\n }\n\n private async playAudioBlob(blob: Blob, signal: AbortSignal): Promise<void> {\n const url = URL.createObjectURL(blob);\n const audio = new Audio(url);\n this.audio = audio;\n this.objectUrl = url;\n\n const finished = new Promise<void>((resolve, reject) => {\n audio.addEventListener(\"ended\", () => resolve(), { once: true });\n audio.addEventListener(\n \"error\",\n () => reject(new Error(\"Audio playback failed\")),\n { once: true }\n );\n });\n\n try {\n await audio.play();\n await Promise.race([finished, abortPromise(signal)]);\n } finally {\n if (this.audio === audio) this.audio = null;\n if (this.objectUrl === url) this.objectUrl = null;\n audio.pause();\n URL.revokeObjectURL(url);\n }\n }\n\n private async playWithSpeechSynthesis(\n text: string,\n signal: AbortSignal\n ): Promise<void> {\n if (typeof speechSynthesis === \"undefined\" || typeof SpeechSynthesisUtterance === \"undefined\") {\n throw new Error(\"No speech synthesis available in this browser\");\n }\n\n const utterance = new SpeechSynthesisUtterance(text);\n const finished = new Promise<void>((resolve, reject) => {\n utterance.onend = () => resolve();\n // `cancel()` fires an error event; a stop is not a failure.\n utterance.onerror = () =>\n signal.aborted ? resolve() : reject(new Error(\"Speech synthesis failed\"));\n });\n\n speechSynthesis.speak(utterance);\n await Promise.race([finished, abortPromise(signal)]);\n }\n}\n","/**\n * @fileoverview Browser-side live dictation engine.\n *\n * `LiveTranscriber` streams microphone audio to a recognizer and reports two kinds\n * of text: `onPartial`, the in-progress guess that keeps changing while a phrase is\n * being spoken, and `onCommit`, a phrase the recognizer has settled on. Callers use\n * the first to type text into an input as it is being said, and the second to\n * finalize it.\n *\n * Two engines are supported. The browser's own `SpeechRecognition` is preferred when\n * present because it needs no model download; otherwise the package's bundled\n * Moonshine model runs the recognition entirely on-device. Chromium's recognizer\n * stops itself after a pause, so a session that is still meant to be listening is\n * restarted automatically.\n */\n\nexport type TranscriberEngine = \"auto\" | \"webspeech\" | \"moonshine\";\n\nexport interface LiveTranscriberOptions {\n /** Which recognizer to use. Default `auto` (browser first, Moonshine as fallback). */\n engine?: TranscriberEngine;\n /** BCP-47 language tag for the browser recognizer. Default `en-US`. */\n language?: string;\n /** Moonshine model name. Default `model/small`. */\n model?: string;\n /** Fires continuously with the current in-progress phrase. */\n onPartial?: (text: string) => void;\n /** Fires once per phrase the recognizer has settled on. */\n onCommit?: (text: string) => void;\n /** Fires whenever the microphone starts or stops. */\n onStateChange?: (listening: boolean) => void;\n onError?: (error: Error) => void;\n}\n\ntype SpeechRecognitionCtor = new () => any;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof window === \"undefined\") return null;\n return (\n (window as any).SpeechRecognition ||\n (window as any).webkitSpeechRecognition ||\n null\n );\n}\n\n/** True when this browser can dictate at all (either engine). */\nexport function isTranscriptionSupported(): boolean {\n if (typeof window === \"undefined\") return false;\n if (getSpeechRecognition()) return true;\n return !!navigator.mediaDevices?.getUserMedia;\n}\n\nexport class LiveTranscriber {\n private options: LiveTranscriberOptions;\n private listening = false;\n /** Distinguishes a deliberate `stop()` from Chromium's idle auto-stop. */\n private stopRequested = false;\n private recognition: any = null;\n private moonshine: any = null;\n\n constructor(options: LiveTranscriberOptions = {}) {\n this.options = options;\n }\n\n setOptions(options: LiveTranscriberOptions): void {\n this.options = options;\n }\n\n isListening(): boolean {\n return this.listening;\n }\n\n async start(): Promise<void> {\n if (this.listening) return;\n this.stopRequested = false;\n\n const engine = this.options.engine ?? \"auto\";\n const Recognition = getSpeechRecognition();\n\n try {\n if (engine !== \"moonshine\" && Recognition) {\n this.startWebSpeech(Recognition);\n return;\n }\n if (engine === \"webspeech\") {\n throw new Error(\"Speech recognition is not available in this browser\");\n }\n await this.startMoonshine();\n } catch (error) {\n this.setListening(false);\n this.options.onError?.(\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n\n async stop(): Promise<void> {\n this.stopRequested = true;\n\n if (this.recognition) {\n try {\n this.recognition.stop();\n } catch {\n /* already stopped */\n }\n this.recognition = null;\n }\n\n if (this.moonshine) {\n try {\n await this.moonshine.stop?.();\n } catch {\n /* already stopped */\n }\n this.moonshine = null;\n }\n\n this.setListening(false);\n }\n\n async toggle(): Promise<void> {\n if (this.listening) {\n await this.stop();\n } else {\n await this.start();\n }\n }\n\n private setListening(listening: boolean): void {\n if (this.listening === listening) return;\n this.listening = listening;\n this.options.onStateChange?.(listening);\n }\n\n private startWebSpeech(Recognition: SpeechRecognitionCtor): void {\n const recognition = new Recognition();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = this.options.language ?? \"en-US\";\n\n recognition.onstart = () => this.setListening(true);\n\n recognition.onresult = (event: any) => {\n let interim = \"\";\n for (let i = event.resultIndex; i < event.results.length; i += 1) {\n const result = event.results[i];\n const text = String(result[0]?.transcript ?? \"\").trim();\n if (!text) continue;\n if (result.isFinal) {\n // No `onPartial(\"\")` first: a commit supersedes the interim phrase, and\n // clearing it separately would make consumers that write the interim\n // into a document erase and re-insert the same words.\n this.options.onCommit?.(text);\n } else {\n interim += `${interim ? \" \" : \"\"}${text}`;\n }\n }\n if (interim) this.options.onPartial?.(interim);\n };\n\n recognition.onerror = (event: any) => {\n const code = event?.error;\n // `no-speech` and `aborted` are routine during a long dictation session;\n // `onend` restarts the recognizer for us.\n if (code === \"no-speech\" || code === \"aborted\") return;\n this.stopRequested = true;\n this.options.onError?.(new Error(`Speech recognition error: ${code}`));\n };\n\n recognition.onend = () => {\n if (this.stopRequested || this.recognition !== recognition) {\n this.recognition = null;\n this.setListening(false);\n return;\n }\n // Chromium ends the session after a pause — start a fresh one so the user\n // can keep dictating without touching the button again.\n try {\n recognition.start();\n } catch {\n this.recognition = null;\n this.setListening(false);\n }\n };\n\n this.recognition = recognition;\n recognition.start();\n }\n\n private async startMoonshine(): Promise<void> {\n const Moonshine = await import(\"@moonshine-ai/moonshine-js\");\n\n const transcriber = new Moonshine.MicrophoneTranscriber(\n this.options.model ?? \"model/small\",\n {\n onTranscriptionUpdated: (text: string) => {\n this.options.onPartial?.(String(text ?? \"\").trim());\n },\n onTranscriptionCommitted: (text: string) => {\n const committed = String(text ?? \"\").trim();\n if (committed) this.options.onCommit?.(committed);\n else this.options.onPartial?.(\"\");\n },\n },\n false // streaming mode\n );\n\n this.moonshine = transcriber;\n await transcriber.start();\n if (this.stopRequested) {\n await this.stop();\n return;\n }\n this.setListening(true);\n }\n}\n"],"names":["splitTextSmart","text","maxChunkLength","paragraphChunks","finalChunks","para","sentenceRegex","sentences","chunk","sentence","subChunks","splitLongSentence","sub","maxLen","chunks","current","commaParts","part","words","wordChunk","word","DEFAULT_ENDPOINT","DEFAULT_VOICE","DEFAULT_MAX_CHUNK","abortPromise","signal","resolve","ReadAloudController","options","__publicField","_a","_b","_c","_d","_e","_f","_g","_h","abortController","upcoming","index","blob","error","wasActive","state","promise","endpoint","response","contentType","url","audio","finished","reject","utterance","getSpeechRecognition","isTranscriptionSupported","LiveTranscriber","engine","Recognition","listening","recognition","event","interim","i","result","code","Moonshine","transcriber","committed"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export interface SpokenPhraseOverlayProps {
2
+ /** The words to display. An empty string hides the overlay. */
3
+ phrase: string;
4
+ /**
5
+ * Bump this whenever `phrase` is refreshed to restart the hide timer, including
6
+ * when the same words are repeated. `useLiveTranscription` supplies it.
7
+ */
8
+ phraseId?: number;
9
+ /** How long the phrase stays up after the last update. Default 2000ms. */
10
+ durationMs?: number;
11
+ /** Set false to suppress the overlay entirely (e.g. once dictation stops). */
12
+ visible?: boolean;
13
+ /** Stacking order. Default 2147483000, above typical app chrome and modals. */
14
+ zIndex?: number;
15
+ className?: string;
16
+ }
17
+ export declare function SpokenPhraseOverlay({ phrase, phraseId, durationMs, visible, zIndex, className, }: SpokenPhraseOverlayProps): import('react').ReactPortal;
18
+ export default SpokenPhraseOverlay;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Public entry point for the React layer: the read-aloud and live
3
+ * dictation hooks, plus the centred overlay that echoes the phrase just spoken.
4
+ */
5
+ export { useReadAloud, type UseReadAloudOptions, type UseReadAloudReturn } from './useReadAloud';
6
+ export { useLiveTranscription, type UseLiveTranscriptionOptions, type UseLiveTranscriptionReturn, } from './useLiveTranscription';
7
+ export { SpokenPhraseOverlay, type SpokenPhraseOverlayProps, } from './SpokenPhraseOverlay';
8
+ export { ReadAloudController, LiveTranscriber, isTranscriptionSupported, type ReadAloudChunk, type ReadAloudOptions, type ReadAloudState, type LiveTranscriberOptions, type TranscriberEngine, } from '../client';
@@ -0,0 +1,19 @@
1
+ import { LiveTranscriberOptions } from '../client/live-transcriber';
2
+ export interface UseLiveTranscriptionOptions extends Omit<LiveTranscriberOptions, "onStateChange"> {
3
+ }
4
+ export interface UseLiveTranscriptionReturn {
5
+ isListening: boolean;
6
+ /** True when this browser can dictate at all. */
7
+ isSupported: boolean;
8
+ /** The in-progress phrase, updating as it is spoken. Empty between phrases. */
9
+ partial: string;
10
+ /** The most recent thing heard, partial or committed. */
11
+ lastPhrase: string;
12
+ /** Increments on every `lastPhrase` update, including repeats of the same words. */
13
+ phraseId: number;
14
+ error: Error | null;
15
+ start: () => Promise<void>;
16
+ stop: () => Promise<void>;
17
+ toggle: () => Promise<void>;
18
+ }
19
+ export declare function useLiveTranscription(options?: UseLiveTranscriptionOptions): UseLiveTranscriptionReturn;