use-voice-control 0.1.95 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,374 +1,2 @@
1
- import { looksLikeMarkdown as g, markdownToSpeech as S } from "./markdown.js";
2
- import { a as $, e as y, w as M, K as x, d as T } from "./kokoro-node-DJ_Rxp_N.js";
3
- const E = [".md", ".markdown", ".mdown", ".mkd", ".mdx"], O = [".txt", ".text", ""];
4
- function v(t) {
5
- const e = t.split(/[\\/]/).pop() ?? "", s = e.lastIndexOf(".");
6
- return s > 0 ? e.slice(s).toLowerCase() : "";
7
- }
8
- function h(t, e, s = "auto") {
9
- if (s !== "auto") return s;
10
- if (e) {
11
- const o = v(e);
12
- if (E.includes(o)) return "markdown";
13
- if (O.includes(o)) return "text";
14
- }
15
- return g(t) ? "markdown" : "text";
16
- }
17
- function m(t, e, s = {}) {
18
- return e === "text" ? t.replace(/\r\n?/g, `
19
- `).trim() : S(t, s);
20
- }
21
- async function w(t) {
22
- const { file: e, text: s, format: o = "auto", markdown: r = {} } = t;
23
- if (s !== void 0) {
24
- const n = h(s, void 0, o);
25
- return { text: m(s, n, r), format: n, source: "--text" };
26
- }
27
- if (!e) throw new Error("No input: pass a file path, `-` for stdin, or --text");
28
- if (e === "-") {
29
- const n = t.readStdin ? await t.readStdin() : await q(), c = h(n, void 0, o);
30
- return { text: m(n, c, r), format: c, source: "stdin" };
31
- }
32
- const { readFile: i } = await import("node:fs/promises"), a = await i(e, "utf8"), l = h(a, e, o);
33
- return { text: m(a, l, r), format: l, source: e };
34
- }
35
- async function q() {
36
- const t = [];
37
- for await (const e of process.stdin)
38
- t.push(Buffer.isBuffer(e) ? e : Buffer.from(e));
39
- return Buffer.concat(t).toString("utf8");
40
- }
41
- function D(t) {
42
- const e = v(t);
43
- return `${e ? t.slice(0, -e.length) : t}.wav`;
44
- }
45
- async function N(t) {
46
- const e = await w(t);
47
- if (!e.text.trim())
48
- throw new Error(`Nothing to speak: ${e.source} has no readable text`);
49
- const s = t.synthesize ?? $, { samples: o, sampleRate: r } = await s(e.text, t), i = y(o, r), a = t.output ?? (t.file && t.file !== "-" ? D(t.file) : "out.wav");
50
- if (a === "-")
51
- process.stdout.write(Buffer.from(i));
52
- else {
53
- const { writeFile: l } = await import("node:fs/promises");
54
- await l(a, Buffer.from(i));
55
- }
56
- return {
57
- output: a,
58
- text: e.text,
59
- format: e.format,
60
- source: e.source,
61
- durationSeconds: M(o, r),
62
- bytes: i.byteLength
63
- };
64
- }
65
- const F = {
66
- auto: "auto",
67
- markdown: "markdown",
68
- md: "markdown",
69
- text: "text",
70
- txt: "text",
71
- plain: "text"
72
- }, V = ["fp32", "fp16", "q8", "q4", "q4f16"], B = ["wasm", "webgpu", "cpu"], C = `use-voice-control — read a Markdown or text file aloud into an audio file
73
-
74
- Usage
75
- npx use-voice-control <file.md|file.txt|-> [options]
76
- npx use-voice-control --text "Hello there" -o hello.wav
77
- cat notes.md | npx use-voice-control - -o notes.wav
78
-
79
- Markdown is converted before it is spoken: "#", "**" and the rest are not read
80
- out, headings become their own spoken lines, links keep their text, and fenced
81
- code blocks are announced instead of being spelled out.
82
-
83
- Input
84
- <file> File to read. Use "-" to read stdin.
85
- --text <string> Speak this string instead of reading a file.
86
- -f, --format <fmt> auto (default), markdown, or text.
87
-
88
- Output
89
- -o, --out <file> Audio file to write. Default: the input path with a
90
- .wav extension. Use "-" to write the WAV to stdout.
91
- -p, --print Print the speakable text and exit — no model, no audio.
92
- Useful for checking the Markdown conversion.
93
- -q, --quiet No progress output.
94
-
95
- Voice
96
- -v, --voice <id> Voice id. Default af_heart. See --list-voices.
97
- -s, --speed <n> Speaking rate, 0.5-2. Default 1.
98
- --list-voices Print the available voices and exit.
99
-
100
- Markdown handling
101
- --headings <mode> text (default) | announce | skip
102
- --code <mode> announce (default) | read | skip
103
- --links <mode> text (default) | text-and-url
104
- --tables <mode> rows (default) | skip
105
- --front-matter Read the YAML front matter instead of skipping it.
106
-
107
- Model
108
- --model <id> Hugging Face model id.
109
- Default onnx-community/Kokoro-82M-v1.0-ONNX.
110
- --dtype <type> fp32 | fp16 | q8 (default) | q4 | q4f16
111
- --device <device> cpu (default) | wasm | webgpu
112
- --chunk <chars> Target characters per synthesis chunk. Default 400.
113
- --gap <ms> Silence between chunks. Default 120.
114
-
115
- Other
116
- -h, --help Show this help.
117
- -V, --version Print the package version.
118
-
119
- The first run downloads the Kokoro weights (about 90 MB at the default q8) into
120
- the Hugging Face cache; later runs are offline. Speech is synthesized locally —
121
- no text leaves the machine.`;
122
- function d(t, e, s, o) {
123
- const r = t[e + 1];
124
- return r === void 0 || r.startsWith("-") ? (o.push(`${s} needs a value`), { next: e + 1 }) : { value: r, next: e + 1 };
125
- }
126
- function k(t, e, s) {
127
- if (t === void 0) return;
128
- const o = Number(t);
129
- if (!Number.isFinite(o)) {
130
- s.push(`${e} expects a number, got "${t}"`);
131
- return;
132
- }
133
- return o;
134
- }
135
- function f(t, e, s, o) {
136
- if (t !== void 0) {
137
- if (!e.includes(t)) {
138
- o.push(`${s} expects one of ${e.join(", ")} — got "${t}"`);
139
- return;
140
- }
141
- return t;
142
- }
143
- }
144
- function L(t) {
145
- const e = [], s = {}, o = {
146
- command: "speak",
147
- format: "auto",
148
- voice: "af_heart",
149
- speed: 1,
150
- markdown: s,
151
- quiet: !1,
152
- errors: e
153
- };
154
- let r = !1;
155
- for (let i = 0; i < t.length; i += 1) {
156
- const a = t[i];
157
- if (r || !a.startsWith("-") || a === "-") {
158
- o.input !== void 0 ? e.push(`unexpected extra input "${a}" — pass one file at a time`) : o.input = a;
159
- continue;
160
- }
161
- if (a === "--") {
162
- r = !0;
163
- continue;
164
- }
165
- const l = a.indexOf("=");
166
- if (l > 1) {
167
- t.splice(i, 1, a.slice(0, l), a.slice(l + 1)), i -= 1;
168
- continue;
169
- }
170
- switch (a) {
171
- case "-h":
172
- case "--help":
173
- return o.command = "help", o;
174
- case "-V":
175
- case "--version":
176
- return o.command = "version", o;
177
- case "--list-voices":
178
- return o.command = "list-voices", o;
179
- case "-p":
180
- case "--print":
181
- case "--dry-run":
182
- o.command = "print";
183
- break;
184
- case "-q":
185
- case "--quiet":
186
- o.quiet = !0;
187
- break;
188
- case "--front-matter":
189
- s.frontMatter = !0;
190
- break;
191
- case "-o":
192
- case "--out":
193
- case "--output": {
194
- const { value: n, next: c } = d(t, i, a, e);
195
- o.output = n, i = c;
196
- break;
197
- }
198
- case "--text": {
199
- const n = t[i + 1];
200
- n === void 0 ? e.push("--text needs a value") : o.text = n, i += 1;
201
- break;
202
- }
203
- case "-f":
204
- case "--format": {
205
- const { value: n, next: c } = d(t, i, a, e);
206
- if (i = c, n !== void 0) {
207
- const u = F[n.toLowerCase()];
208
- u ? o.format = u : e.push(`--format expects auto, markdown or text — got "${n}"`);
209
- }
210
- break;
211
- }
212
- case "-v":
213
- case "--voice": {
214
- const { value: n, next: c } = d(t, i, a, e);
215
- i = c, n !== void 0 && (x.includes(n) ? o.voice = n : e.push(`unknown voice "${n}" — run --list-voices to see them all`));
216
- break;
217
- }
218
- case "-s":
219
- case "--speed": {
220
- const { value: n, next: c } = d(t, i, a, e);
221
- i = c;
222
- const u = k(n, a, e);
223
- u !== void 0 && (u < 0.5 || u > 2 ? e.push("--speed must be between 0.5 and 2") : o.speed = u);
224
- break;
225
- }
226
- case "--model": {
227
- const { value: n, next: c } = d(t, i, a, e);
228
- o.model = n, i = c;
229
- break;
230
- }
231
- case "--dtype": {
232
- const { value: n, next: c } = d(t, i, a, e);
233
- i = c, o.dtype = f(n, V, a, e) ?? o.dtype;
234
- break;
235
- }
236
- case "--device": {
237
- const { value: n, next: c } = d(t, i, a, e);
238
- i = c, o.device = f(n, B, a, e) ?? o.device;
239
- break;
240
- }
241
- case "--chunk":
242
- case "--chunk-length": {
243
- const { value: n, next: c } = d(t, i, a, e);
244
- i = c;
245
- const u = k(n, a, e);
246
- u !== void 0 && (u < 40 ? e.push("--chunk must be at least 40 characters") : o.maxChunkLength = u);
247
- break;
248
- }
249
- case "--gap": {
250
- const { value: n, next: c } = d(t, i, a, e);
251
- i = c;
252
- const u = k(n, a, e);
253
- u !== void 0 && (u < 0 ? e.push("--gap cannot be negative") : o.gapMs = u);
254
- break;
255
- }
256
- case "--headings": {
257
- const { value: n, next: c } = d(t, i, a, e);
258
- i = c, s.headings = f(n, ["text", "announce", "skip"], a, e) ?? s.headings;
259
- break;
260
- }
261
- case "--code":
262
- case "--code-blocks": {
263
- const { value: n, next: c } = d(t, i, a, e);
264
- i = c, s.codeBlocks = f(n, ["announce", "read", "skip"], a, e) ?? s.codeBlocks;
265
- break;
266
- }
267
- case "--links": {
268
- const { value: n, next: c } = d(t, i, a, e);
269
- i = c, s.links = f(n, ["text", "text-and-url"], a, e) ?? s.links;
270
- break;
271
- }
272
- case "--tables": {
273
- const { value: n, next: c } = d(t, i, a, e);
274
- i = c, s.tables = f(n, ["rows", "skip"], a, e) ?? s.tables;
275
- break;
276
- }
277
- default:
278
- e.push(`unknown option "${a}" — run --help to see the options`);
279
- }
280
- }
281
- return o.text !== void 0 && o.input !== void 0 && e.push("pass either a file or --text, not both"), o;
282
- }
283
- function I() {
284
- const t = x.map((e) => {
285
- const s = T(e);
286
- return ` ${e.padEnd(14)}${s.name.padEnd(12)}${s.gender.padEnd(8)}${s.accent}`;
287
- });
288
- return [`${x.length} Kokoro voices:`, ...t].join(`
289
- `);
290
- }
291
- async function K() {
292
- try {
293
- const { readFile: t } = await import("node:fs/promises");
294
- for (const e of ["../package.json", "../../package.json"])
295
- try {
296
- const s = new URL(e, import.meta.url), o = JSON.parse(await t(s, "utf8"));
297
- if ((o == null ? void 0 : o.name) === "use-voice-control" && o.version) return o.version;
298
- } catch {
299
- }
300
- } catch {
301
- }
302
- return "unknown";
303
- }
304
- async function z(t, e = {}) {
305
- const s = e.log ?? ((n) => process.stdout.write(`${n}
306
- `)), o = e.error ?? ((n) => process.stderr.write(`${n}
307
- `)), r = L([...t]);
308
- if (r.command === "help")
309
- return s(C), 0;
310
- if (r.command === "version")
311
- return s(await K()), 0;
312
- if (r.command === "list-voices")
313
- return s(I()), 0;
314
- if (r.errors.length > 0)
315
- return r.errors.forEach((n) => o(`use-voice-control: ${n}`)), o("Run `npx use-voice-control --help` for usage."), 1;
316
- const i = e.stdinIsTTY ?? !!process.stdin.isTTY, a = r.input ?? (r.text === void 0 && !i ? "-" : void 0);
317
- if (a === void 0 && r.text === void 0)
318
- return o("use-voice-control: no input — pass a file, pipe text in, or use --text"), o("Run `npx use-voice-control --help` for usage."), 1;
319
- const l = {
320
- file: a,
321
- text: r.text,
322
- format: r.format,
323
- markdown: r.markdown,
324
- readStdin: e.readStdin
325
- };
326
- try {
327
- if (r.command === "print") {
328
- const u = await w(l);
329
- return s(u.text), 0;
330
- }
331
- const n = Date.now(), c = await N({
332
- ...l,
333
- output: r.output,
334
- voice: r.voice,
335
- speed: r.speed,
336
- model: r.model,
337
- dtype: r.dtype,
338
- device: r.device,
339
- maxChunkLength: r.maxChunkLength,
340
- gapMs: r.gapMs,
341
- synthesize: e.synthesize,
342
- onModelProgress: r.quiet ? void 0 : (u) => {
343
- if ((u == null ? void 0 : u.status) === "progress" && u.file && u.total) {
344
- const p = Math.round((u.loaded ?? 0) / u.total * 100);
345
- o(`downloading ${u.file}: ${p}%`);
346
- }
347
- },
348
- onChunk: r.quiet ? void 0 : ({ index: u, total: p }) => o(`speaking chunk ${u + 1}/${p}`)
349
- });
350
- if (!r.quiet) {
351
- const u = c.durationSeconds.toFixed(1), p = ((Date.now() - n) / 1e3).toFixed(1), b = c.output === "-" ? "stdout" : c.output;
352
- o(`wrote ${b} — ${u}s of audio from ${c.source} in ${p}s`);
353
- }
354
- return 0;
355
- } catch (n) {
356
- return o(`use-voice-control: ${n instanceof Error ? n.message : String(n)}`), 1;
357
- }
358
- }
359
- export {
360
- E as M,
361
- O as T,
362
- C as USAGE,
363
- h as a,
364
- N as b,
365
- D as d,
366
- v as e,
367
- I as formatVoiceList,
368
- w as l,
369
- L as parseArgs,
370
- q as r,
371
- z as runCli,
372
- m as t
373
- };
374
- //# sourceMappingURL=cli.js.map
1
+ import { i as e, n as t, r as n, t as r } from "./cli-CXzTD4W4.js";
2
+ export { r as USAGE, t as formatVoiceList, n as parseArgs, e as runCli };
@@ -0,0 +1,229 @@
1
+ import { t as e } from "./semantic-split-DF26uo-Z.js";
2
+ import { looksLikeMarkdown as t, markdownToSpeech as n } from "./markdown.js";
3
+ //#region speech/client/read-aloud.ts
4
+ var r = "/api/speech/tts", i = "af_heart", a = 240;
5
+ function o(e) {
6
+ return e.aborted ? Promise.resolve() : new Promise((t) => {
7
+ e.addEventListener("abort", () => t(), { once: !0 });
8
+ });
9
+ }
10
+ var s = class {
11
+ options;
12
+ state = "idle";
13
+ abortController = null;
14
+ audio = null;
15
+ objectUrl = null;
16
+ endpointUnavailable = !1;
17
+ constructor(e = {}) {
18
+ this.options = e;
19
+ }
20
+ setOptions(e) {
21
+ this.options = e;
22
+ }
23
+ getState() {
24
+ return this.state;
25
+ }
26
+ isActive() {
27
+ return this.state === "speaking" || this.state === "paused" || this.state === "loading";
28
+ }
29
+ async speak(t) {
30
+ this.stop();
31
+ let n = this.options.maxChunkLength ?? a, r = e(this.toSpeakableText(t ?? ""), n).map((e) => e.trim()).filter((e) => e.length > 0);
32
+ if (r.length === 0) return;
33
+ let i = new AbortController();
34
+ this.abortController = i;
35
+ let { signal: o } = i;
36
+ this.setState("loading");
37
+ try {
38
+ let e = this.synthesize(r[0], o);
39
+ for (let t = 0; t < r.length; t += 1) {
40
+ let n = e;
41
+ e = t + 1 < r.length ? this.synthesize(r[t + 1], o) : Promise.resolve(null);
42
+ let i = await n;
43
+ if (o.aborted || (this.setState("speaking"), this.options.onChunk?.({
44
+ text: r[t],
45
+ index: t,
46
+ total: r.length
47
+ }), o.aborted) || (await this.playChunk(i, r[t], o), o.aborted)) return;
48
+ }
49
+ this.cleanup(), this.setState("idle"), this.options.onEnd?.("finished");
50
+ } catch (e) {
51
+ if (o.aborted) return;
52
+ this.cleanup(), this.setState("idle"), this.options.onError?.(e instanceof Error ? e : Error(String(e))), this.options.onEnd?.("stopped");
53
+ } finally {
54
+ this.abortController === i && (this.abortController = null);
55
+ }
56
+ }
57
+ pause() {
58
+ this.state === "speaking" && (this.audio ? this.audio.pause() : typeof speechSynthesis < "u" && speechSynthesis.pause(), this.setState("paused"));
59
+ }
60
+ resume() {
61
+ this.state === "paused" && (this.audio ? this.audio.play().catch(() => void 0) : typeof speechSynthesis < "u" && speechSynthesis.resume(), this.setState("speaking"));
62
+ }
63
+ stop() {
64
+ let e = this.isActive();
65
+ this.abortController?.abort(), this.abortController = null, this.cleanup(), e && (this.setState("idle"), this.options.onEnd?.("stopped"));
66
+ }
67
+ toSpeakableText(e) {
68
+ let r = this.options.format ?? "auto";
69
+ return r === "text" ? e : r === "markdown" || t(e) ? n(e, this.options.markdown) : e;
70
+ }
71
+ setState(e) {
72
+ this.state !== e && (this.state = e, this.options.onStateChange?.(e));
73
+ }
74
+ cleanup() {
75
+ this.audio &&= (this.audio.pause(), this.audio.src = "", null), this.objectUrl &&= (URL.revokeObjectURL(this.objectUrl), null), typeof speechSynthesis < "u" && speechSynthesis.cancel();
76
+ }
77
+ synthesize(e, t) {
78
+ let n = this.options.synthesize ? this.options.synthesize(e, t) : this.fetchAudio(e, t);
79
+ return n.catch(() => void 0), n;
80
+ }
81
+ async fetchAudio(e, t) {
82
+ if (this.endpointUnavailable) return null;
83
+ let n = this.options.endpoint ?? r;
84
+ try {
85
+ let r = await fetch(n, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({
89
+ text: e,
90
+ provider: this.options.provider ?? "kokoro",
91
+ voice: this.options.voice ?? i
92
+ }),
93
+ signal: t
94
+ });
95
+ if (!r.ok) throw Error(`TTS failed: ${r.status}`);
96
+ let a = r.headers.get("Content-Type") || "audio/wav";
97
+ return new Blob([await r.arrayBuffer()], { type: a });
98
+ } catch (e) {
99
+ if (t.aborted) throw e;
100
+ return this.endpointUnavailable = !0, null;
101
+ }
102
+ }
103
+ playChunk(e, t, n) {
104
+ return e ? this.playAudioBlob(e, n) : this.playWithSpeechSynthesis(t, n);
105
+ }
106
+ async playAudioBlob(e, t) {
107
+ let n = URL.createObjectURL(e), r = new Audio(n);
108
+ this.audio = r, this.objectUrl = n;
109
+ let i = new Promise((e, t) => {
110
+ r.addEventListener("ended", () => e(), { once: !0 }), r.addEventListener("error", () => t(/* @__PURE__ */ Error("Audio playback failed")), { once: !0 });
111
+ });
112
+ try {
113
+ await r.play(), await Promise.race([i, o(t)]);
114
+ } finally {
115
+ this.audio === r && (this.audio = null), this.objectUrl === n && (this.objectUrl = null), r.pause(), URL.revokeObjectURL(n);
116
+ }
117
+ }
118
+ async playWithSpeechSynthesis(e, t) {
119
+ if (typeof speechSynthesis > "u" || typeof SpeechSynthesisUtterance > "u") throw Error("No speech synthesis available in this browser");
120
+ let n = new SpeechSynthesisUtterance(e), r = new Promise((e, r) => {
121
+ n.onend = () => e(), n.onerror = () => t.aborted ? e() : r(/* @__PURE__ */ Error("Speech synthesis failed"));
122
+ });
123
+ speechSynthesis.speak(n), await Promise.race([r, o(t)]);
124
+ }
125
+ };
126
+ //#endregion
127
+ //#region speech/client/live-transcriber.ts
128
+ function c() {
129
+ return typeof window > "u" ? null : window.SpeechRecognition || window.webkitSpeechRecognition || null;
130
+ }
131
+ function l() {
132
+ return typeof window > "u" ? !1 : c() ? !0 : !!navigator.mediaDevices?.getUserMedia;
133
+ }
134
+ var u = class {
135
+ options;
136
+ listening = !1;
137
+ stopRequested = !1;
138
+ recognition = null;
139
+ moonshine = null;
140
+ constructor(e = {}) {
141
+ this.options = e;
142
+ }
143
+ setOptions(e) {
144
+ this.options = e;
145
+ }
146
+ isListening() {
147
+ return this.listening;
148
+ }
149
+ async start() {
150
+ if (this.listening) return;
151
+ this.stopRequested = !1;
152
+ let e = this.options.engine ?? "auto", t = c();
153
+ try {
154
+ if (e !== "moonshine" && t) {
155
+ this.startWebSpeech(t);
156
+ return;
157
+ }
158
+ if (e === "webspeech") throw Error("Speech recognition is not available in this browser");
159
+ await this.startMoonshine();
160
+ } catch (e) {
161
+ this.setListening(!1), this.options.onError?.(e instanceof Error ? e : Error(String(e)));
162
+ }
163
+ }
164
+ async stop() {
165
+ if (this.stopRequested = !0, this.recognition) {
166
+ try {
167
+ this.recognition.stop();
168
+ } catch {}
169
+ this.recognition = null;
170
+ }
171
+ if (this.moonshine) {
172
+ try {
173
+ await this.moonshine.stop?.();
174
+ } catch {}
175
+ this.moonshine = null;
176
+ }
177
+ this.setListening(!1);
178
+ }
179
+ async toggle() {
180
+ this.listening ? await this.stop() : await this.start();
181
+ }
182
+ setListening(e) {
183
+ this.listening !== e && (this.listening = e, this.options.onStateChange?.(e));
184
+ }
185
+ startWebSpeech(e) {
186
+ let t = new e();
187
+ t.continuous = !0, t.interimResults = !0, t.lang = this.options.language ?? "en-US", t.onstart = () => this.setListening(!0), t.onresult = (e) => {
188
+ let t = "";
189
+ for (let n = e.resultIndex; n < e.results.length; n += 1) {
190
+ let r = e.results[n], i = String(r[0]?.transcript ?? "").trim();
191
+ i && (r.isFinal ? this.options.onCommit?.(i) : t += `${t ? " " : ""}${i}`);
192
+ }
193
+ t && this.options.onPartial?.(t);
194
+ }, t.onerror = (e) => {
195
+ let t = e?.error;
196
+ t !== "no-speech" && t !== "aborted" && (this.stopRequested = !0, this.options.onError?.(/* @__PURE__ */ Error(`Speech recognition error: ${t}`)));
197
+ }, t.onend = () => {
198
+ if (this.stopRequested || this.recognition !== t) {
199
+ this.recognition = null, this.setListening(!1);
200
+ return;
201
+ }
202
+ try {
203
+ t.start();
204
+ } catch {
205
+ this.recognition = null, this.setListening(!1);
206
+ }
207
+ }, this.recognition = t, t.start();
208
+ }
209
+ async startMoonshine() {
210
+ let e = new (await (import("@moonshine-ai/moonshine-js"))).MicrophoneTranscriber(this.options.model ?? "model/small", {
211
+ onTranscriptionUpdated: (e) => {
212
+ this.options.onPartial?.(String(e ?? "").trim());
213
+ },
214
+ onTranscriptionCommitted: (e) => {
215
+ let t = String(e ?? "").trim();
216
+ t ? this.options.onCommit?.(t) : this.options.onPartial?.("");
217
+ }
218
+ }, !1);
219
+ if (this.moonshine = e, await e.start(), this.stopRequested) {
220
+ await this.stop();
221
+ return;
222
+ }
223
+ this.setListening(!0);
224
+ }
225
+ };
226
+ //#endregion
227
+ export { l as n, s as r, u as t };
228
+
229
+ //# sourceMappingURL=client-CwHmea9T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-CwHmea9T.js","names":[],"sources":["../speech/client/read-aloud.ts","../speech/client/live-transcriber.ts"],"sourcesContent":["/**\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 {\n looksLikeMarkdown,\n markdownToSpeech,\n type MarkdownToSpeechOptions,\n} from \"../utils/markdown-to-speech\";\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 /**\n * How to read the text handed to `speak()`. `auto` (default) converts text\n * that looks like Markdown so \"##\" and \"**\" are not read out; `markdown`\n * always converts; `text` never does.\n */\n format?: \"auto\" | \"markdown\" | \"text\";\n /** Markdown conversion options, when the text is read as Markdown. */\n markdown?: MarkdownToSpeechOptions;\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(this.toSpeakableText(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 /**\n * Converts Markdown to spoken words before chunking, so a document read out\n * of an editor does not have its syntax read back to the listener.\n */\n private toSpeakableText(text: string): string {\n const format = this.options.format ?? \"auto\";\n if (format === \"text\") return text;\n if (format === \"markdown\" || looksLikeMarkdown(text)) {\n return markdownToSpeech(text, this.options.markdown);\n }\n return text;\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"],"mappings":"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"}