use-voice-control 0.1.86 → 0.1.87

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 (44) hide show
  1. package/README.md +206 -21
  2. package/bin/use-voice-control.mjs +20 -0
  3. package/dist/cli.js +374 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/client/index.d.ts +1 -0
  6. package/dist/client/read-aloud.d.ts +14 -0
  7. package/dist/client.js +132 -157
  8. package/dist/client.js.map +1 -1
  9. package/dist/core/kokoro-node.d.ts +51 -0
  10. package/dist/core/kokoro.d.ts +6 -2
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +33 -76
  13. package/dist/index.js.map +1 -1
  14. package/dist/kokoro-node-DJ_Rxp_N.js +144 -0
  15. package/dist/kokoro-node-DJ_Rxp_N.js.map +1 -0
  16. package/dist/markdown.js +213 -0
  17. package/dist/markdown.js.map +1 -0
  18. package/dist/node/cli.d.ts +52 -0
  19. package/dist/node/document.d.ts +45 -0
  20. package/dist/node/index.d.ts +18 -0
  21. package/dist/node/render.d.ts +30 -0
  22. package/dist/node.js +27 -0
  23. package/dist/node.js.map +1 -0
  24. package/dist/react.js +13 -11
  25. package/dist/react.js.map +1 -1
  26. package/dist/semantic-split-CXhk-k1F.js +44 -0
  27. package/dist/semantic-split-CXhk-k1F.js.map +1 -0
  28. package/dist/types/types.d.ts +14 -1
  29. package/dist/utils/markdown-to-speech.d.ts +76 -0
  30. package/dist/utils/wav.d.ts +27 -0
  31. package/package.json +17 -4
  32. package/speech/client/index.ts +10 -0
  33. package/speech/client/read-aloud.ts +27 -1
  34. package/speech/core/kokoro-node.ts +164 -0
  35. package/speech/core/kokoro.ts +13 -66
  36. package/speech/index.ts +14 -0
  37. package/speech/node/cli.ts +509 -0
  38. package/speech/node/document.ts +126 -0
  39. package/speech/node/index.ts +64 -0
  40. package/speech/node/render.ts +79 -0
  41. package/speech/react/useReadAloud.ts +2 -0
  42. package/speech/types/types.ts +34 -5
  43. package/speech/utils/markdown-to-speech.ts +426 -0
  44. package/speech/utils/wav.ts +89 -0
package/README.md CHANGED
@@ -26,6 +26,7 @@
26
26
  <a href="https://codespaces.new/OpenSourceAGI/qwksearch-research-agent">
27
27
  <img src="https://github.com/codespaces/badge.svg" width="150" height="20" />
28
28
  </a>
29
+ <a href="https://codecov.io/gh/OpenSourceAGI/qwksearch-research-agent"><img src="https://codecov.io/gh/OpenSourceAGI/qwksearch-research-agent/graph/badge.svg?component=package-use-voice-control" alt="Coverage" /></a>
29
30
  </p>
30
31
 
31
32
  <p align="center">
@@ -42,6 +43,12 @@ React hooks and components for seamless voice control and speech I/O (Speech-to-
42
43
  npm install use-voice-control
43
44
  ```
44
45
 
46
+ Or read a document aloud straight from the terminal, no install and no code:
47
+
48
+ ```bash
49
+ npx use-voice-control README.md -o readme.wav
50
+ ```
51
+
45
52
  ---
46
53
 
47
54
  ## 🎤 Features
@@ -58,6 +65,14 @@ npm install use-voice-control
58
65
  - **Server & Client Support**: Run on backend or stream to client
59
66
  - **Multiple Output Formats**: WAV, PCM, or raw audio buffers
60
67
 
68
+ ### Markdown & Files
69
+
70
+ - **Markdown aware**: `#`, `**` and backticks are never read out — headings are
71
+ spoken as headings, code blocks are announced, links keep their text
72
+ - **Files in, audio out**: `.md` and `.txt` files rendered to WAV from the
73
+ [command line](#-command-line) or from Node
74
+ - **Local synthesis**: Kokoro runs on the CPU, so document text never leaves the machine
75
+
61
76
  ### React Integration
62
77
  - **Custom Hooks**: `useVoiceControl()`, `useSpeechRecognition()`, `useSpeechSynthesis()`
63
78
  - **Pre-built Components**: Audio recorder, voice selector, playback controls
@@ -118,6 +133,154 @@ export function VoiceOutput() {
118
133
 
119
134
  ---
120
135
 
136
+ ## 🖥️ Command Line
137
+
138
+ Read a Markdown or text file aloud into an audio file, without writing any code:
139
+
140
+ ```bash
141
+ npx use-voice-control README.md
142
+ # wrote README.wav — 96.4s of audio from README.md in 41.2s
143
+ ```
144
+
145
+ The Markdown is **converted before it is spoken**: `#`, `**`, backticks and the
146
+ rest are never read out. Headings become their own spoken lines with a pause
147
+ after them, list markers are dropped (ordered numbers are kept), links keep their
148
+ text and lose their URL, tables are read row by row, and fenced code blocks are
149
+ announced — "TypeScript code block." — instead of being spelled out character by
150
+ character. See [Markdown → speech](#-markdown--speech) for the rules and how to
151
+ change them.
152
+
153
+ Speech is synthesized locally with [Kokoro](https://huggingface.co/hexgrad/Kokoro-82M)
154
+ on the CPU: nothing is sent to a third-party service. The first run downloads the
155
+ weights (about 90 MB at the default `q8`) into the Hugging Face cache; every run
156
+ after that is offline.
157
+
158
+ ### Common commands
159
+
160
+ ```bash
161
+ # Markdown or plain text in, WAV out (defaults to <input>.wav, next to the input)
162
+ npx use-voice-control notes.md
163
+ npx use-voice-control notes.txt -o spoken.wav
164
+
165
+ # Pick a voice and a speaking rate
166
+ npx use-voice-control notes.md -v am_michael -s 1.15
167
+ npx use-voice-control --list-voices
168
+
169
+ # Speak a string, no file needed
170
+ npx use-voice-control --text "Build finished, three tests failed." -o alert.wav
171
+
172
+ # Read from a pipe, and write the WAV to stdout
173
+ cat notes.md | npx use-voice-control - -o - > notes.wav
174
+ git log -1 --format=%B | npx use-voice-control - -o commit.wav
175
+
176
+ # Check what will be spoken, without downloading the model
177
+ npx use-voice-control README.md --print
178
+ ```
179
+
180
+ `--print` is the fastest way to see the Markdown conversion — it converts the
181
+ document, prints the text, and exits before any model is loaded.
182
+
183
+ ### Options
184
+
185
+ | Option | Meaning |
186
+ | --- | --- |
187
+ | `<file>` | File to read. `-` reads stdin. `.md`, `.markdown`, `.mdx` are read as Markdown; everything else as plain text. |
188
+ | `--text <string>` | Speak this string instead of reading a file. |
189
+ | `-f, --format <fmt>` | `auto` (default), `markdown`, or `text`. Overrides the extension. |
190
+ | `-o, --out <file>` | Audio file to write. Default: the input path with a `.wav` extension, or `out.wav` when the input is stdin or `--text`. `-` writes the WAV to stdout. |
191
+ | `-p, --print` | Print the speakable text and exit — no model, no audio. |
192
+ | `-q, --quiet` | No progress output. |
193
+ | `-v, --voice <id>` | Voice id, default `af_heart`. See `--list-voices`. |
194
+ | `-s, --speed <n>` | Speaking rate between 0.5 and 2. Default 1. |
195
+ | `--headings <mode>` | `text` (default), `announce` ("Heading: Install"), or `skip`. |
196
+ | `--code <mode>` | `announce` (default), `read`, or `skip`. |
197
+ | `--links <mode>` | `text` (default) or `text-and-url`. |
198
+ | `--tables <mode>` | `rows` (default) or `skip`. |
199
+ | `--front-matter` | Read the YAML front matter instead of skipping it. |
200
+ | `--model <id>` | Hugging Face model id. Default `onnx-community/Kokoro-82M-v1.0-ONNX`. |
201
+ | `--dtype <type>` | `fp32`, `fp16`, `q8` (default), `q4`, `q4f16`. |
202
+ | `--device <device>` | `cpu` (default), `wasm`, `webgpu`. |
203
+ | `--chunk <chars>` | Target characters per synthesis chunk. Default 400. |
204
+ | `--gap <ms>` | Silence inserted between chunks. Default 120. |
205
+ | `-h, --help` | Show the full usage text. |
206
+ | `-V, --version` | Print the package version. |
207
+
208
+ Long documents are split on sentence and paragraph boundaries, synthesized chunk
209
+ by chunk, and joined into one file — Kokoro's context is only a few hundred
210
+ phonemes, so this is what makes a whole README work.
211
+
212
+ The command exits `0` on success and `1` on a bad option, a missing file, a
213
+ document with nothing to say, or a failed model load; progress goes to stderr, so
214
+ `-o -` gives you a clean WAV on stdout.
215
+
216
+ ### From code
217
+
218
+ The same thing without the shell:
219
+
220
+ ```ts
221
+ import { renderDocument } from 'use-voice-control/node';
222
+
223
+ const result = await renderDocument({
224
+ file: 'README.md',
225
+ output: 'readme.wav',
226
+ voice: 'af_heart',
227
+ });
228
+
229
+ console.log(`${result.durationSeconds.toFixed(1)}s of audio from ${result.source}`);
230
+ ```
231
+
232
+ `use-voice-control/node` also exports `loadDocument` (file → speakable text),
233
+ `synthesizeSamples` / `synthesizeWav` (text → audio), `runCli`, and the Markdown
234
+ helpers below.
235
+
236
+ ---
237
+
238
+ ## 📝 Markdown → speech
239
+
240
+ Markdown handed straight to a speech engine is read literally: *"hash hash
241
+ Getting started"*, *"star star important star star"*. `markdownToSpeech` parses
242
+ the document instead and emits only the words, keeping the structure the marks
243
+ encoded.
244
+
245
+ ```ts
246
+ import { markdownToSpeech } from 'use-voice-control/markdown';
247
+
248
+ markdownToSpeech('## Install\n\nRun `npm i` and see the [docs](https://x.dev).');
249
+ // "Install.
250
+ //
251
+ // Run npm i and see the docs."
252
+ ```
253
+
254
+ | Markdown | Spoken as |
255
+ | --- | --- |
256
+ | `# Heading` and setext underlines | The heading text on its own, with a pause after it |
257
+ | `**bold**`, `_italic_`, `~~struck~~` | The words, no marks |
258
+ | `` `code span` `` | The code text, no backticks |
259
+ | A fenced code block | "TypeScript code block." (or the code, or nothing) |
260
+ | `[text](url)` | The link text; the URL is dropped |
261
+ | `![alt](src)` | The alt text |
262
+ | `- item`, `1. item`, `- [x] task` | The item; bullets and checkboxes dropped, numbers kept |
263
+ | `> quote` | The quoted words |
264
+ | `\| a \| b \|` | "a, b." — the delimiter row is dropped |
265
+ | `---`, `<!-- … -->`, `[ref]: url` | Nothing |
266
+ | YAML front matter | Nothing, unless `frontMatter: true` |
267
+
268
+ Options: `headings` (`text` \| `announce` \| `skip`), `codeBlocks` (`announce` \|
269
+ `read` \| `skip`), `links` (`text` \| `text-and-url`), `images` (`alt` \| `skip`),
270
+ `tables` (`rows` \| `skip`), `frontMatter`, and `addTerminalPunctuation`.
271
+
272
+ `markdownToSpeechSegments` returns the same content as typed blocks —
273
+ `{ type: 'heading' | 'paragraph' | 'list-item' | 'quote' | 'code' | 'table-row',
274
+ text, level }` — for callers that want to highlight the current heading or skip
275
+ between sections. `stripInlineMarkdown` handles a single line, and
276
+ `looksLikeMarkdown` is the heuristic behind `--format auto`.
277
+
278
+ The same conversion runs in the browser: `ReadAloudController` and `useReadAloud`
279
+ convert text that looks like Markdown before speaking it, so an editor's raw
280
+ document does not have its syntax read back to the listener.
281
+
282
+ ---
283
+
121
284
  ## 🗣️ Read Aloud & Live Dictation
122
285
 
123
286
  Two ready-made browser engines ship from `use-voice-control/client` (framework
@@ -147,8 +310,11 @@ function ReadButton({ text }: { text: string }) {
147
310
  }
148
311
  ```
149
312
 
150
- Options: `provider`, `voice`, `endpoint`, `maxChunkLength`, and `synthesize` to
151
- plug in your own TTS. Controls: `speak`, `pause`, `resume`, `stop`, `toggle`.
313
+ Options: `provider`, `voice`, `endpoint`, `maxChunkLength`, `format`
314
+ (`auto` \| `markdown` \| `text` `auto` converts text that looks like Markdown so
315
+ `##` and `**` are not read out), `markdown` for the conversion options, and
316
+ `synthesize` to plug in your own TTS. Controls: `speak`, `pause`, `resume`,
317
+ `stop`, `toggle`.
152
318
 
153
319
  ### Live dictation
154
320
 
@@ -293,30 +459,35 @@ interface VoiceControlOptions {
293
459
 
294
460
  #### Kokoro Voices
295
461
 
296
- 16 professional voices optimized for natural speech synthesis:
462
+ 28 voices optimized for natural speech synthesis. Run `npx use-voice-control
463
+ --list-voices` to print them with their accent and gender. The `a`/`b` prefix is
464
+ the accent (American / British English) and the letter after it is the gender.
297
465
 
298
- **Female Voices:**
466
+ **American English — female:**
299
467
  ```
300
- af_heart - Warm, caring tone
301
- af_alloy - Neutral, professional
302
- af_aoede - Bright, energetic
303
- af_bella - Soft, gentle
468
+ af_heart - Warm, caring tone af_nicole - Clear, articulate
469
+ af_alloy - Neutral, professional af_nova - Even, unhurried
470
+ af_aoede - Bright, energetic af_river - Calm, soothing
471
+ af_bella - Soft, gentle af_sarah - Warm, approachable
304
472
  af_jessica - Friendly, conversational
305
- af_nicole - Clear, articulate
306
- af_river - Calm, soothing
307
- af_sarah - Warm, approachable
308
- af_sky - Young, vibrant
473
+ af_kore - Steady, measured af_sky - Young, vibrant
309
474
  ```
310
475
 
311
- **Male Voices:**
476
+ **American English — male:**
312
477
  ```
313
- am_adam - Deep, authoritative
314
- am_echo - Resonant, smooth
315
- am_fable - Narrative, engaging
316
- am_fenrir - Bold, strong
478
+ am_adam - Deep, authoritative am_michael - Professional, clear
479
+ am_echo - Resonant, smooth am_onyx - Dark, mysterious
480
+ am_eric - Direct, matter-of-fact am_puck - Playful, quick
481
+ am_fenrir - Bold, strong am_santa - Jovial, avuncular
317
482
  am_liam - Friendly, warm
318
- am_michael - Professional, clear
319
- am_onyx - Dark, mysterious
483
+ ```
484
+
485
+ **British English:**
486
+ ```
487
+ bf_alice - Crisp, precise bm_daniel - Measured, formal
488
+ bf_emma - Warm, unhurried bm_fable - Narrative, engaging
489
+ bf_isabella - Bright, articulate bm_george - Deep, steady
490
+ bf_lily - Light, gentle bm_lewis - Relaxed, conversational
320
491
  ```
321
492
 
322
493
  **Example:**
@@ -400,16 +571,21 @@ Generate speech audio directly from the server or Edge runtime:
400
571
  ```ts
401
572
  import { generateSpeech } from 'use-voice-control/speech';
402
573
 
403
- // Generate Kokoro speech
574
+ // Generate Kokoro speech — runs the model locally on the CPU via `kokoro-js`
404
575
  const audio = await generateSpeech({
405
576
  text: "Hello, world!",
406
577
  provider: 'kokoro',
407
578
  voice: 'af_heart'
408
579
  });
409
580
 
410
- // Returns: { audio: ArrayBuffer, contentType: string }
581
+ // Returns: { audio: ArrayBuffer, contentType: string } — a 16-bit PCM WAV
411
582
  ```
412
583
 
584
+ Text longer than the model's context is chunked on sentence boundaries and joined
585
+ automatically, so a whole document can be passed in one call. Deepgram
586
+ (`provider: 'deepgram'`) goes through a Cloudflare Workers AI binding instead and
587
+ returns MP3; it is not available from the CLI.
588
+
413
589
  ### TypeScript Support
414
590
 
415
591
  Full type definitions included:
@@ -516,6 +692,7 @@ const SpeechWorker = require('use-voice-control/speech/worker.js');
516
692
  ## 🔐 Privacy & Security
517
693
 
518
694
  - **Client-side STT**: Moonshine.js runs entirely in the browser—no audio leaves your device
695
+ - **Local CLI TTS**: `npx use-voice-control` runs Kokoro on your CPU; the document text is never uploaded
519
696
  - **Optional Server TTS**: Choose Kokoro (server-side) or Deepgram (with API key)
520
697
  - **No Tracking**: No analytics or usage telemetry
521
698
  - **HTTPS Required**: Microphone access requires secure context
@@ -548,6 +725,14 @@ const SpeechWorker = require('use-voice-control/speech/worker.js');
548
725
  export { ReadAloudController, LiveTranscriber, isTranscriptionSupported } from 'use-voice-control/client';
549
726
  export { useReadAloud, useLiveTranscription, SpokenPhraseOverlay } from 'use-voice-control/react';
550
727
 
728
+ // Markdown → speech (implemented)
729
+ export { markdownToSpeech, markdownToSpeechSegments, stripInlineMarkdown, looksLikeMarkdown } from 'use-voice-control/markdown';
730
+ export type { MarkdownToSpeechOptions, SpeechSegment } from 'use-voice-control/markdown';
731
+
732
+ // Files → audio, and the CLI (implemented, Node)
733
+ export { renderDocument, loadDocument, synthesizeSamples, synthesizeWav, runCli } from 'use-voice-control/node';
734
+ export type { RenderOptions, RenderResult, KokoroNodeOptions } from 'use-voice-control/node';
735
+
551
736
  // Hooks
552
737
  export { useSpeechRecognition } from 'use-voice-control/hooks';
553
738
  export { useSpeechSynthesis } from 'use-voice-control/hooks';
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview Launcher for the `use-voice-control` command.
4
+ *
5
+ * The command itself is built into `dist/cli.js`; this file exists so the
6
+ * shebang lives in a plain, never-bundled file and so a missing build fails with
7
+ * an instruction rather than a module-resolution error.
8
+ */
9
+ let runCli;
10
+ try {
11
+ ({ runCli } = await import(new URL("../dist/cli.js", import.meta.url).href));
12
+ } catch (error) {
13
+ process.stderr.write(
14
+ "use-voice-control: the package is not built — run `npm run build` in " +
15
+ `packages/use-voice-control first.\n(${error?.message ?? error})\n`
16
+ );
17
+ process.exit(1);
18
+ }
19
+
20
+ process.exitCode = await runCli(process.argv.slice(2));
package/dist/cli.js ADDED
@@ -0,0 +1,374 @@
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