use-voice-control 0.1.85 → 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
@@ -0,0 +1,213 @@
1
+ const A = {
2
+ headings: "text",
3
+ codeBlocks: "announce",
4
+ links: "text",
5
+ images: "alt",
6
+ frontMatter: !1,
7
+ tables: "rows",
8
+ addTerminalPunctuation: !0
9
+ }, x = {
10
+ js: "JavaScript",
11
+ jsx: "JavaScript",
12
+ mjs: "JavaScript",
13
+ cjs: "JavaScript",
14
+ ts: "TypeScript",
15
+ tsx: "TypeScript",
16
+ py: "Python",
17
+ rb: "Ruby",
18
+ rs: "Rust",
19
+ sh: "shell",
20
+ bash: "shell",
21
+ zsh: "shell",
22
+ shell: "shell",
23
+ console: "shell",
24
+ yml: "YAML",
25
+ yaml: "YAML",
26
+ md: "Markdown",
27
+ json: "JSON",
28
+ html: "HTML",
29
+ css: "CSS",
30
+ sql: "SQL",
31
+ go: "Go",
32
+ java: "Java",
33
+ c: "C",
34
+ cpp: "C plus plus",
35
+ cs: "C sharp",
36
+ php: "PHP",
37
+ swift: "Swift",
38
+ kt: "Kotlin",
39
+ diff: "diff",
40
+ text: "",
41
+ txt: "",
42
+ plaintext: ""
43
+ }, T = {
44
+ amp: "&",
45
+ lt: "<",
46
+ gt: ">",
47
+ quot: '"',
48
+ apos: "'",
49
+ nbsp: " ",
50
+ mdash: "—",
51
+ ndash: "–",
52
+ hellip: "…",
53
+ ldquo: '"',
54
+ rdquo: '"',
55
+ lsquo: "'",
56
+ rsquo: "'"
57
+ }, L = /^ {0,3}(#{1,6})(?:\s+(.*?))?\s*$/, R = /^ {0,3}(=+|-+)\s*$/, C = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/, I = /^ {0,3}(`{3,}|~{3,})\s*([^`]*)$/, N = /^ *(?:[-+*]|\d{1,9}[.)])(?:\s+(.*))?$/, w = /^ *(\d{1,9})[.)]\s+(.*)$/, P = /^\[([ xX])\]\s+/, b = /^ {0,3}\[[^\]]+\]:\s*\S+.*$/, y = /^[\s|:-]*-[\s|:-]*$/, M = "\0", O = "", j = /\u0000(\d+)\u0001/g;
58
+ function D(i) {
59
+ return /[.!?:;,…]["')\]]?$/.test(i) ? i : `${i}.`;
60
+ }
61
+ function H(i) {
62
+ return i.replace(/&#(\d{1,7});/g, (r, t) => String.fromCodePoint(Number(t))).replace(
63
+ /&#[xX]([0-9a-fA-F]{1,6});/g,
64
+ (r, t) => String.fromCodePoint(parseInt(t, 16))
65
+ ).replace(/&([a-zA-Z]+);/g, (r, t) => T[t.toLowerCase()] ?? r);
66
+ }
67
+ function _(i, r = {}) {
68
+ const t = { ...A, ...r }, a = [], l = (o) => `${M}${a.push(o) - 1}${O}`;
69
+ let e = i;
70
+ return e = e.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, (o, n) => l(n)), e = e.replace(
71
+ /(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/g,
72
+ (o, n, p) => l(String(p).trim())
73
+ ), e = e.replace(/<!--[\s\S]*?-->/g, ""), e = e.replace(
74
+ /!\[([^\]]*)\]\([^)]*\)/g,
75
+ (o, n) => t.images === "skip" ? "" : String(n)
76
+ ), e = e.replace(
77
+ /!\[([^\]]*)\]\[[^\]]*\]/g,
78
+ (o, n) => t.images === "skip" ? "" : String(n)
79
+ ), e = e.replace(/\[\^[^\]]+\]/g, ""), e = e.replace(
80
+ /\[([^\]]*)\]\(\s*<?([^\s)]*)>?[^)]*\)/g,
81
+ (o, n, p) => t.links === "text-and-url" && p ? `${n}, ${p}` : String(n)
82
+ ), e = e.replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1"), e = e.replace(
83
+ /<((?:https?|mailto):[^>\s]+)>/gi,
84
+ (o, n) => t.links === "text-and-url" ? String(n) : String(n).replace(/^mailto:/i, "")
85
+ ), e = e.replace(/<\/?[a-zA-Z][^>]*>/g, ""), e = e.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, "$1"), e = e.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "$1"), e = e.replace(new RegExp("(?<![A-Za-z0-9_])__(?=\\S)([\\s\\S]*?\\S)__(?![A-Za-z0-9_])", "g"), "$1"), e = e.replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1"), e = e.replace(/\*(?=\S)([^*\n]*?\S)\*/g, "$1"), e = e.replace(new RegExp("(?<![A-Za-z0-9_])_(?=\\S)([^_\\n]*?\\S)_(?![A-Za-z0-9_])", "g"), "$1"), e = H(e), e = e.replace(
86
+ j,
87
+ (o, n) => a[Number(n)] ?? ""
88
+ ), e.replace(/[ \t]+/g, " ").trim();
89
+ }
90
+ function q(i) {
91
+ var a;
92
+ const r = ((a = i.trim().split(/[\s,{]/)[0]) == null ? void 0 : a.toLowerCase()) ?? "", t = r in x ? x[r] : r;
93
+ return t ? `${t} code block.` : "Code block.";
94
+ }
95
+ function z(i, r = {}) {
96
+ const t = { ...A, ...r }, a = [];
97
+ let l = (i ?? "").replace(/\r\n?/g, `
98
+ `).replace(/\t/g, " ");
99
+ l = l.replace(/<!--[\s\S]*?-->/g, "");
100
+ const e = l.split(`
101
+ `);
102
+ let o = 0;
103
+ if (!t.frontMatter && /^---\s*$/.test(e[0] ?? "")) {
104
+ const s = e.findIndex((u, f) => f > 0 && /^(---|\.\.\.)\s*$/.test(u));
105
+ s > 0 && (o = s + 1);
106
+ }
107
+ let n = [], p = !1;
108
+ const d = (s, u, f) => {
109
+ const S = u.trim();
110
+ if (!S) return;
111
+ const m = t.addTerminalPunctuation ? D(S) : S;
112
+ a.push(f === void 0 ? { type: s, text: m } : { type: s, text: m, level: f });
113
+ }, g = () => {
114
+ if (n.length === 0) return;
115
+ const s = _(n.join(" "), t);
116
+ n = [];
117
+ const u = p ? "quote" : "paragraph";
118
+ p = !1, d(u, s);
119
+ };
120
+ for (; o < e.length; o += 1) {
121
+ let s = e[o];
122
+ const u = I.exec(s);
123
+ if (u) {
124
+ g();
125
+ const c = u[1], h = u[2] ?? "", E = new RegExp(`^ {0,3}\\${c[0]}{${c.length},}\\s*$`), k = [];
126
+ for (o += 1; o < e.length && !E.test(e[o]); o += 1)
127
+ k.push(e[o]);
128
+ t.codeBlocks === "announce" ? d("code", q(h)) : t.codeBlocks === "read" && d("code", k.join(" ").replace(/\s+/g, " "));
129
+ continue;
130
+ }
131
+ if (s.trim() === "") {
132
+ g();
133
+ continue;
134
+ }
135
+ let f = !1;
136
+ for (; /^ {0,3}>\s?/.test(s); )
137
+ s = s.replace(/^ {0,3}>\s?/, ""), f = !0;
138
+ if (f && (n.length === 0 && (p = !0), s.trim() === "")) {
139
+ g();
140
+ continue;
141
+ }
142
+ if (C.test(s)) {
143
+ g();
144
+ continue;
145
+ }
146
+ const S = L.exec(s);
147
+ if (S) {
148
+ if (g(), t.headings === "skip") continue;
149
+ const c = _((S[2] ?? "").replace(/\s+#+$/, ""), t);
150
+ if (!c) continue;
151
+ d("heading", t.headings === "announce" ? `Heading: ${c}` : c, S[1].length);
152
+ continue;
153
+ }
154
+ const m = R.exec(s);
155
+ if (m && n.length > 0 && t.headings !== "skip") {
156
+ const c = _(n.join(" "), t);
157
+ n = [], p = !1, c && d(
158
+ "heading",
159
+ t.headings === "announce" ? `Heading: ${c}` : c,
160
+ m[1].startsWith("=") ? 1 : 2
161
+ );
162
+ continue;
163
+ }
164
+ if (b.test(s) && n.length === 0) continue;
165
+ if (s.trimStart().startsWith("|")) {
166
+ if (g(), t.tables === "skip" || y.test(s)) continue;
167
+ const c = s.trim().replace(/^\|/, "").replace(/\|$/, "").split(new RegExp("(?<!\\\\)\\|")).map((h) => _(h, t)).filter((h) => h.length > 0);
168
+ c.length > 0 && d("table-row", c.join(", "));
169
+ continue;
170
+ }
171
+ const $ = N.exec(s);
172
+ if ($) {
173
+ g();
174
+ const c = w.exec(s), h = (c ? c[2] : $[1] ?? "").replace(P, ""), E = _(h, t);
175
+ if (!E) continue;
176
+ d("list-item", c ? `${c[1]}. ${E}` : E);
177
+ continue;
178
+ }
179
+ n.push(s.trim());
180
+ }
181
+ return g(), a;
182
+ }
183
+ function v(i, r = {}) {
184
+ const t = z(i, r);
185
+ return t.reduce((a, l, e) => {
186
+ if (e === 0) return l.text;
187
+ const o = t[e - 1].type, n = (l.type === "list-item" || l.type === "table-row") && o === l.type;
188
+ return `${a}${n ? `
189
+ ` : `
190
+
191
+ `}${l.text}`;
192
+ }, "");
193
+ }
194
+ function Z(i) {
195
+ if (!i) return !1;
196
+ const r = i.slice(0, 2e4);
197
+ return [
198
+ /^ {0,3}#{1,6}\s+\S/m,
199
+ /^ {0,3}(?:```|~~~)/m,
200
+ /^ {0,3}[-+*]\s+\S/m,
201
+ /^ {0,3}>\s+\S/m,
202
+ /\[[^\]]+\]\([^)]+\)/,
203
+ new RegExp("(?<![A-Za-z0-9])\\*\\*\\S[\\s\\S]*?\\S\\*\\*"),
204
+ /^ {0,3}\|.*\|\s*$/m
205
+ ].filter((a) => a.test(r)).length >= 2;
206
+ }
207
+ export {
208
+ Z as looksLikeMarkdown,
209
+ v as markdownToSpeech,
210
+ z as markdownToSpeechSegments,
211
+ _ as stripInlineMarkdown
212
+ };
213
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sources":["../speech/utils/markdown-to-speech.ts"],"sourcesContent":["/**\n * @fileoverview Turns Markdown into text a speech synthesizer can read.\n *\n * Markdown handed straight to a TTS engine is read literally: \"hash hash Getting\n * started\", \"star star important star star\". This module parses the document\n * instead and emits only the words, keeping the *structure* the marks encoded —\n * a heading becomes its own spoken segment ending in a full stop so the\n * synthesizer pauses, a list item becomes one sentence, a fenced code block is\n * announced rather than spelled out.\n *\n * It is deliberately dependency-free and runs in the browser as well as Node, so\n * `ReadAloudController` and the CLI can share it.\n */\n\n/** Kind of block the converter recognised. */\nexport type SpeechSegmentType =\n | \"heading\"\n | \"paragraph\"\n | \"list-item\"\n | \"quote\"\n | \"code\"\n | \"table-row\";\n\nexport interface SpeechSegment {\n type: SpeechSegmentType;\n /** Spoken text of the block, with every Markdown mark already removed. */\n text: string;\n /** Heading level 1-6. Only set on `heading` segments. */\n level?: number;\n}\n\nexport interface MarkdownToSpeechOptions {\n /**\n * How headings are spoken. `text` (default) reads the heading words on their\n * own so they land between pauses; `announce` prefixes them with \"Heading:\";\n * `skip` drops them entirely.\n */\n headings?: \"text\" | \"announce\" | \"skip\";\n /**\n * What to do with fenced code blocks. `announce` (default) replaces the block\n * with a short spoken note, `read` reads the code verbatim, `skip` drops it.\n */\n codeBlocks?: \"announce\" | \"read\" | \"skip\";\n /** `text` (default) speaks the link text and drops the URL; `text-and-url` reads both. */\n links?: \"text\" | \"text-and-url\";\n /** `alt` (default) speaks the image's alt text; `skip` drops images. */\n images?: \"alt\" | \"skip\";\n /** Read the YAML front matter block. Off by default. */\n frontMatter?: boolean;\n /** `rows` (default) reads table rows as comma-separated cells; `skip` drops tables. */\n tables?: \"rows\" | \"skip\";\n /**\n * Append a full stop to blocks that do not end in punctuation, so the\n * synthesizer pauses between them instead of running them together.\n * Default true.\n */\n addTerminalPunctuation?: boolean;\n}\n\ntype ResolvedOptions = Required<MarkdownToSpeechOptions>;\n\nconst DEFAULTS: ResolvedOptions = {\n headings: \"text\",\n codeBlocks: \"announce\",\n links: \"text\",\n images: \"alt\",\n frontMatter: false,\n tables: \"rows\",\n addTerminalPunctuation: true,\n};\n\n/** Spoken names for code fence languages that would otherwise be read as letters. */\nconst LANGUAGE_NAMES: Record<string, string> = {\n js: \"JavaScript\",\n jsx: \"JavaScript\",\n mjs: \"JavaScript\",\n cjs: \"JavaScript\",\n ts: \"TypeScript\",\n tsx: \"TypeScript\",\n py: \"Python\",\n rb: \"Ruby\",\n rs: \"Rust\",\n sh: \"shell\",\n bash: \"shell\",\n zsh: \"shell\",\n shell: \"shell\",\n console: \"shell\",\n yml: \"YAML\",\n yaml: \"YAML\",\n md: \"Markdown\",\n json: \"JSON\",\n html: \"HTML\",\n css: \"CSS\",\n sql: \"SQL\",\n go: \"Go\",\n java: \"Java\",\n c: \"C\",\n cpp: \"C plus plus\",\n cs: \"C sharp\",\n php: \"PHP\",\n swift: \"Swift\",\n kt: \"Kotlin\",\n diff: \"diff\",\n text: \"\",\n txt: \"\",\n plaintext: \"\",\n};\n\nconst ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n mdash: \"—\",\n ndash: \"–\",\n hellip: \"…\",\n ldquo: '\"',\n rdquo: '\"',\n lsquo: \"'\",\n rsquo: \"'\",\n};\n\nconst ATX_HEADING = /^ {0,3}(#{1,6})(?:\\s+(.*?))?\\s*$/;\nconst SETEXT_UNDERLINE = /^ {0,3}(=+|-+)\\s*$/;\nconst THEMATIC_BREAK = /^ {0,3}([-*_])[ \\t]*(?:\\1[ \\t]*){2,}$/;\nconst FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})\\s*([^`]*)$/;\nconst LIST_ITEM = /^ *(?:[-+*]|\\d{1,9}[.)])(?:\\s+(.*))?$/;\nconst ORDERED_ITEM = /^ *(\\d{1,9})[.)]\\s+(.*)$/;\nconst TASK_MARKER = /^\\[([ xX])\\]\\s+/;\nconst LINK_REFERENCE_DEFINITION = /^ {0,3}\\[[^\\]]+\\]:\\s*\\S+.*$/;\nconst TABLE_DELIMITER_ROW = /^[\\s|:-]*-[\\s|:-]*$/;\n\n/** Marks the slot where a protected substring was lifted out of the line. */\nconst PLACEHOLDER_OPEN = \"\\u0000\";\nconst PLACEHOLDER_CLOSE = \"\\u0001\";\nconst PLACEHOLDER_PATTERN = /\\u0000(\\d+)\\u0001/g;\n\n/** Ends a block with a full stop unless it already ends in sentence punctuation. */\nfunction withTerminalPunctuation(text: string): string {\n return /[.!?:;,…][\"')\\]]?$/.test(text) ? text : `${text}.`;\n}\n\n/** Replaces `&amp;`-style entities with the characters they stand for. */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#(\\d{1,7});/g, (_m, code) => String.fromCodePoint(Number(code)))\n .replace(/&#[xX]([0-9a-fA-F]{1,6});/g, (_m, code) =>\n String.fromCodePoint(parseInt(code, 16))\n )\n .replace(/&([a-zA-Z]+);/g, (match, name) => ENTITIES[name.toLowerCase()] ?? match);\n}\n\n/**\n * Strips the inline marks — emphasis, code spans, links, images, raw HTML — from\n * a single line, leaving the words behind.\n */\nexport function stripInlineMarkdown(\n input: string,\n options: MarkdownToSpeechOptions = {}\n): string {\n const opts: ResolvedOptions = { ...DEFAULTS, ...options };\n const protectedRuns: string[] = [];\n const hold = (value: string): string =>\n `${PLACEHOLDER_OPEN}${protectedRuns.push(value) - 1}${PLACEHOLDER_CLOSE}`;\n\n let text = input;\n\n // Backslash escapes and code spans come out first: whatever they contain must\n // survive the emphasis pass untouched.\n text = text.replace(/\\\\([\\\\`*_{}[\\]()#+\\-.!>~|])/g, (_m, char) => hold(char));\n text = text.replace(/(`+)([^`]|[^`][\\s\\S]*?[^`])\\1(?!`)/g, (_m, _ticks, code) =>\n hold(String(code).trim())\n );\n\n text = text.replace(/<!--[\\s\\S]*?-->/g, \"\");\n\n // Images before links: `![alt](src)` is a link whose text starts with `!`.\n text = text.replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, (_m, alt) =>\n opts.images === \"skip\" ? \"\" : String(alt)\n );\n text = text.replace(/!\\[([^\\]]*)\\]\\[[^\\]]*\\]/g, (_m, alt) =>\n opts.images === \"skip\" ? \"\" : String(alt)\n );\n\n // Footnote references carry no spoken meaning.\n text = text.replace(/\\[\\^[^\\]]+\\]/g, \"\");\n\n text = text.replace(/\\[([^\\]]*)\\]\\(\\s*<?([^\\s)]*)>?[^)]*\\)/g, (_m, label, url) =>\n opts.links === \"text-and-url\" && url ? `${label}, ${url}` : String(label)\n );\n text = text.replace(/\\[([^\\]]*)\\]\\[[^\\]]*\\]/g, \"$1\");\n\n // Autolinks: `<https://example.com>` reads better as the bare address.\n text = text.replace(/<((?:https?|mailto):[^>\\s]+)>/gi, (_m, url) =>\n opts.links === \"text-and-url\"\n ? String(url)\n : String(url).replace(/^mailto:/i, \"\")\n );\n\n // Remaining angle brackets are raw HTML tags.\n text = text.replace(/<\\/?[a-zA-Z][^>]*>/g, \"\");\n\n text = text.replace(/\\*\\*\\*(?=\\S)([\\s\\S]*?\\S)\\*\\*\\*/g, \"$1\");\n text = text.replace(/\\*\\*(?=\\S)([\\s\\S]*?\\S)\\*\\*/g, \"$1\");\n text = text.replace(/(?<![A-Za-z0-9_])__(?=\\S)([\\s\\S]*?\\S)__(?![A-Za-z0-9_])/g, \"$1\");\n text = text.replace(/~~(?=\\S)([\\s\\S]*?\\S)~~/g, \"$1\");\n text = text.replace(/\\*(?=\\S)([^*\\n]*?\\S)\\*/g, \"$1\");\n // Underscores are only emphasis between word boundaries, so `snake_case` survives.\n text = text.replace(/(?<![A-Za-z0-9_])_(?=\\S)([^_\\n]*?\\S)_(?![A-Za-z0-9_])/g, \"$1\");\n\n text = decodeEntities(text);\n text = text.replace(\n PLACEHOLDER_PATTERN,\n (_m, index) => protectedRuns[Number(index)] ?? \"\"\n );\n\n return text.replace(/[ \\t]+/g, \" \").trim();\n}\n\n/** Spoken stand-in for a fenced code block, e.g. \"TypeScript code block.\" */\nfunction announceCode(language: string): string {\n const key = language.trim().split(/[\\s,{]/)[0]?.toLowerCase() ?? \"\";\n const name = key in LANGUAGE_NAMES ? LANGUAGE_NAMES[key] : key;\n return name ? `${name} code block.` : \"Code block.\";\n}\n\n/**\n * Parses Markdown into the blocks that should be spoken, in reading order.\n *\n * Use this when the caller wants the structure — to highlight the current\n * heading, say, or to skip to a section. Callers that only need something to\n * feed a synthesizer want {@link markdownToSpeech}.\n */\nexport function markdownToSpeechSegments(\n markdown: string,\n options: MarkdownToSpeechOptions = {}\n): SpeechSegment[] {\n const opts: ResolvedOptions = { ...DEFAULTS, ...options };\n const segments: SpeechSegment[] = [];\n\n let source = (markdown ?? \"\").replace(/\\r\\n?/g, \"\\n\").replace(/\\t/g, \" \");\n source = source.replace(/<!--[\\s\\S]*?-->/g, \"\");\n\n const lines = source.split(\"\\n\");\n let index = 0;\n\n // YAML front matter: only when it opens on the very first line.\n if (!opts.frontMatter && /^---\\s*$/.test(lines[0] ?? \"\")) {\n const closing = lines.findIndex((line, i) => i > 0 && /^(---|\\.\\.\\.)\\s*$/.test(line));\n if (closing > 0) index = closing + 1;\n }\n\n /** Soft-wrapped lines of the paragraph being accumulated. */\n let paragraph: string[] = [];\n let quoting = false;\n\n const push = (type: SpeechSegmentType, text: string, level?: number): void => {\n const trimmed = text.trim();\n if (!trimmed) return;\n const spoken = opts.addTerminalPunctuation ? withTerminalPunctuation(trimmed) : trimmed;\n segments.push(level === undefined ? { type, text: spoken } : { type, text: spoken, level });\n };\n\n const flushParagraph = (): void => {\n if (paragraph.length === 0) return;\n const text = stripInlineMarkdown(paragraph.join(\" \"), opts);\n paragraph = [];\n const type = quoting ? \"quote\" : \"paragraph\";\n quoting = false;\n push(type, text);\n };\n\n for (; index < lines.length; index += 1) {\n let line = lines[index];\n\n // A fence closes any paragraph before it and swallows lines until it ends.\n const fence = FENCE_OPEN.exec(line);\n if (fence) {\n flushParagraph();\n const marker = fence[1];\n const language = fence[2] ?? \"\";\n const closingFence = new RegExp(`^ {0,3}\\\\${marker[0]}{${marker.length},}\\\\s*$`);\n const body: string[] = [];\n index += 1;\n for (; index < lines.length; index += 1) {\n if (closingFence.test(lines[index])) break;\n body.push(lines[index]);\n }\n if (opts.codeBlocks === \"announce\") {\n push(\"code\", announceCode(language));\n } else if (opts.codeBlocks === \"read\") {\n push(\"code\", body.join(\" \").replace(/\\s+/g, \" \"));\n }\n continue;\n }\n\n if (line.trim() === \"\") {\n flushParagraph();\n continue;\n }\n\n // Blockquote markers are stripped so the quoted content is parsed normally.\n let isQuote = false;\n while (/^ {0,3}>\\s?/.test(line)) {\n line = line.replace(/^ {0,3}>\\s?/, \"\");\n isQuote = true;\n }\n if (isQuote) {\n if (paragraph.length === 0) quoting = true;\n if (line.trim() === \"\") {\n flushParagraph();\n continue;\n }\n }\n\n if (THEMATIC_BREAK.test(line)) {\n flushParagraph();\n continue;\n }\n\n const heading = ATX_HEADING.exec(line);\n if (heading) {\n flushParagraph();\n if (opts.headings === \"skip\") continue;\n // A closing run of hashes (`## Title ##`) is decoration, not content.\n const text = stripInlineMarkdown((heading[2] ?? \"\").replace(/\\s+#+$/, \"\"), opts);\n if (!text) continue;\n push(\"heading\", opts.headings === \"announce\" ? `Heading: ${text}` : text, heading[1].length);\n continue;\n }\n\n // Setext heading: the underline applies to the single line above it.\n const underline = SETEXT_UNDERLINE.exec(line);\n if (underline && paragraph.length > 0 && opts.headings !== \"skip\") {\n const text = stripInlineMarkdown(paragraph.join(\" \"), opts);\n paragraph = [];\n quoting = false;\n if (text) {\n push(\n \"heading\",\n opts.headings === \"announce\" ? `Heading: ${text}` : text,\n underline[1].startsWith(\"=\") ? 1 : 2\n );\n }\n continue;\n }\n\n if (LINK_REFERENCE_DEFINITION.test(line) && paragraph.length === 0) continue;\n\n if (line.trimStart().startsWith(\"|\")) {\n flushParagraph();\n if (opts.tables === \"skip\" || TABLE_DELIMITER_ROW.test(line)) continue;\n const cells = line\n .trim()\n .replace(/^\\|/, \"\")\n .replace(/\\|$/, \"\")\n .split(/(?<!\\\\)\\|/)\n .map((cell) => stripInlineMarkdown(cell, opts))\n .filter((cell) => cell.length > 0);\n if (cells.length > 0) push(\"table-row\", cells.join(\", \"));\n continue;\n }\n\n const item = LIST_ITEM.exec(line);\n if (item) {\n flushParagraph();\n const ordered = ORDERED_ITEM.exec(line);\n // Keep the number — a listener needs it — but drop bullets and checkboxes.\n const body = (ordered ? ordered[2] : (item[1] ?? \"\")).replace(TASK_MARKER, \"\");\n const text = stripInlineMarkdown(body, opts);\n if (!text) continue;\n push(\"list-item\", ordered ? `${ordered[1]}. ${text}` : text);\n continue;\n }\n\n paragraph.push(line.trim());\n }\n\n flushParagraph();\n return segments;\n}\n\n/**\n * Converts Markdown to plain text ready for a speech synthesizer.\n *\n * Blocks are separated by blank lines so downstream chunkers (`splitTextSmart`)\n * break on them; runs of list items and table rows stay in one block so a list\n * is not chopped into one request per bullet.\n */\nexport function markdownToSpeech(\n markdown: string,\n options: MarkdownToSpeechOptions = {}\n): string {\n const segments = markdownToSpeechSegments(markdown, options);\n\n return segments.reduce((out, segment, i) => {\n if (i === 0) return segment.text;\n const previous = segments[i - 1].type;\n const runsOn =\n (segment.type === \"list-item\" || segment.type === \"table-row\") &&\n previous === segment.type;\n return `${out}${runsOn ? \"\\n\" : \"\\n\\n\"}${segment.text}`;\n }, \"\");\n}\n\n/**\n * Guesses whether a blob of text is Markdown, for callers with no filename to go\n * on (piped stdin, a paste). Errs towards `false`: prose read as Markdown is\n * mostly unchanged anyway, so only reasonably clear signals count.\n */\nexport function looksLikeMarkdown(text: string): boolean {\n if (!text) return false;\n const sample = text.slice(0, 20000);\n const signals = [\n /^ {0,3}#{1,6}\\s+\\S/m,\n /^ {0,3}(?:```|~~~)/m,\n /^ {0,3}[-+*]\\s+\\S/m,\n /^ {0,3}>\\s+\\S/m,\n /\\[[^\\]]+\\]\\([^)]+\\)/,\n /(?<![A-Za-z0-9])\\*\\*\\S[\\s\\S]*?\\S\\*\\*/,\n /^ {0,3}\\|.*\\|\\s*$/m,\n ];\n return signals.filter((pattern) => pattern.test(sample)).length >= 2;\n}\n"],"names":["DEFAULTS","LANGUAGE_NAMES","ENTITIES","ATX_HEADING","SETEXT_UNDERLINE","THEMATIC_BREAK","FENCE_OPEN","LIST_ITEM","ORDERED_ITEM","TASK_MARKER","LINK_REFERENCE_DEFINITION","TABLE_DELIMITER_ROW","PLACEHOLDER_OPEN","PLACEHOLDER_CLOSE","PLACEHOLDER_PATTERN","withTerminalPunctuation","text","decodeEntities","_m","code","match","name","stripInlineMarkdown","input","options","opts","protectedRuns","hold","value","char","_ticks","alt","label","url","index","announceCode","language","_a","key","markdownToSpeechSegments","markdown","segments","source","lines","closing","line","i","paragraph","quoting","push","type","level","trimmed","spoken","flushParagraph","fence","marker","closingFence","body","isQuote","heading","underline","cells","cell","item","ordered","markdownToSpeech","out","segment","previous","runsOn","looksLikeMarkdown","sample","pattern"],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { KokoroDevice, KokoroDtype } from '../core/kokoro-node';
2
+ import { MarkdownToSpeechOptions } from '../utils/markdown-to-speech';
3
+ import { RequestedFormat } from './document';
4
+ import { RenderOptions } from './render';
5
+ export type CliCommand = "speak" | "print" | "help" | "version" | "list-voices";
6
+ export interface CliOptions {
7
+ command: CliCommand;
8
+ /** Input path, or `-` for stdin. Undefined when `text` is set. */
9
+ input?: string;
10
+ /** Literal text passed with `--text`. */
11
+ text?: string;
12
+ output?: string;
13
+ format: RequestedFormat;
14
+ voice: string;
15
+ speed: number;
16
+ model?: string;
17
+ dtype?: KokoroDtype;
18
+ device?: KokoroDevice;
19
+ maxChunkLength?: number;
20
+ gapMs?: number;
21
+ markdown: MarkdownToSpeechOptions;
22
+ quiet: boolean;
23
+ /** Everything wrong with the command line. Non-empty means do not run. */
24
+ errors: string[];
25
+ }
26
+ export interface CliIO {
27
+ /** Writes a line to stdout. */
28
+ log?: (line: string) => void;
29
+ /** Writes a line to stderr — progress and errors go here so `-o -` stays clean. */
30
+ error?: (line: string) => void;
31
+ /** Overrides synthesis, for tests. */
32
+ synthesize?: RenderOptions["synthesize"];
33
+ /** Overrides reading stdin, for tests. */
34
+ readStdin?: () => Promise<string>;
35
+ /** Whether stdin is a terminal. When false and no input is given, stdin is read. */
36
+ stdinIsTTY?: boolean;
37
+ }
38
+ export declare const USAGE = "use-voice-control \u2014 read a Markdown or text file aloud into an audio file\n\nUsage\n npx use-voice-control <file.md|file.txt|-> [options]\n npx use-voice-control --text \"Hello there\" -o hello.wav\n cat notes.md | npx use-voice-control - -o notes.wav\n\nMarkdown is converted before it is spoken: \"#\", \"**\" and the rest are not read\nout, headings become their own spoken lines, links keep their text, and fenced\ncode blocks are announced instead of being spelled out.\n\nInput\n <file> File to read. Use \"-\" to read stdin.\n --text <string> Speak this string instead of reading a file.\n -f, --format <fmt> auto (default), markdown, or text.\n\nOutput\n -o, --out <file> Audio file to write. Default: the input path with a\n .wav extension. Use \"-\" to write the WAV to stdout.\n -p, --print Print the speakable text and exit \u2014 no model, no audio.\n Useful for checking the Markdown conversion.\n -q, --quiet No progress output.\n\nVoice\n -v, --voice <id> Voice id. Default af_heart. See --list-voices.\n -s, --speed <n> Speaking rate, 0.5-2. Default 1.\n --list-voices Print the available voices and exit.\n\nMarkdown handling\n --headings <mode> text (default) | announce | skip\n --code <mode> announce (default) | read | skip\n --links <mode> text (default) | text-and-url\n --tables <mode> rows (default) | skip\n --front-matter Read the YAML front matter instead of skipping it.\n\nModel\n --model <id> Hugging Face model id.\n Default onnx-community/Kokoro-82M-v1.0-ONNX.\n --dtype <type> fp32 | fp16 | q8 (default) | q4 | q4f16\n --device <device> cpu (default) | wasm | webgpu\n --chunk <chars> Target characters per synthesis chunk. Default 400.\n --gap <ms> Silence between chunks. Default 120.\n\nOther\n -h, --help Show this help.\n -V, --version Print the package version.\n\nThe first run downloads the Kokoro weights (about 90 MB at the default q8) into\nthe Hugging Face cache; later runs are offline. Speech is synthesized locally \u2014\nno text leaves the machine.";
39
+ /**
40
+ * Parses the command line. Pure: it reads nothing and writes nothing, so the
41
+ * tests can assert on the whole option set.
42
+ */
43
+ export declare function parseArgs(argv: string[]): CliOptions;
44
+ /** The `--list-voices` table, derived from the ids so it cannot drift. */
45
+ export declare function formatVoiceList(): string;
46
+ /**
47
+ * Runs the command line and resolves to the process exit code.
48
+ *
49
+ * @param argv Arguments after the executable and script, i.e. `process.argv.slice(2)`.
50
+ * @param io Injection points for output, stdin and synthesis.
51
+ */
52
+ export declare function runCli(argv: string[], io?: CliIO): Promise<number>;
@@ -0,0 +1,45 @@
1
+ import { MarkdownToSpeechOptions } from '../utils/markdown-to-speech';
2
+ export type DocumentFormat = "markdown" | "text";
3
+ /** What the caller asked for; `auto` defers to the extension, then the content. */
4
+ export type RequestedFormat = DocumentFormat | "auto";
5
+ /** Extensions read as Markdown. Everything else is treated as plain text. */
6
+ export declare const MARKDOWN_EXTENSIONS: string[];
7
+ /** Extensions the CLI accepts without complaint. Others still work, with a warning. */
8
+ export declare const TEXT_EXTENSIONS: string[];
9
+ /** Lowercased extension of a path, including the dot (`""` when there is none). */
10
+ export declare function extensionOf(filename: string): string;
11
+ /**
12
+ * Decides how to read a document.
13
+ *
14
+ * An explicit `requested` format always wins — a `.txt` file full of Markdown is
15
+ * still the user's call. Otherwise the extension decides, and content sniffing
16
+ * is the last resort for input that arrived without a name.
17
+ */
18
+ export declare function detectFormat(content: string, filename?: string, requested?: RequestedFormat): DocumentFormat;
19
+ /** Converts document content to the text that should be spoken. */
20
+ export declare function toSpeechText(content: string, format: DocumentFormat, options?: MarkdownToSpeechOptions): string;
21
+ export interface LoadedDocument {
22
+ /** The speakable text, Markdown already converted. */
23
+ text: string;
24
+ /** How the content was read. */
25
+ format: DocumentFormat;
26
+ /** Where it came from, for log lines: a path, `stdin`, or `--text`. */
27
+ source: string;
28
+ }
29
+ export interface LoadDocumentOptions {
30
+ /** Path to read. Use `-` for stdin. Mutually exclusive with `text`. */
31
+ file?: string;
32
+ /** Literal content to speak, instead of reading a file. */
33
+ text?: string;
34
+ format?: RequestedFormat;
35
+ markdown?: MarkdownToSpeechOptions;
36
+ /** Reads stdin. Injected so the CLI tests do not need a real pipe. */
37
+ readStdin?: () => Promise<string>;
38
+ }
39
+ /**
40
+ * Reads a document from a path, stdin, or an inline string and returns the text
41
+ * to synthesize.
42
+ */
43
+ export declare function loadDocument(options: LoadDocumentOptions): Promise<LoadedDocument>;
44
+ /** Collects all of stdin as UTF-8 text. */
45
+ export declare function readStdinToString(): Promise<string>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @fileoverview Server-side entry point: read a document, speak it to a file.
3
+ *
4
+ * ```ts
5
+ * import { renderDocument } from "use-voice-control/node";
6
+ *
7
+ * const result = await renderDocument({ file: "README.md", output: "readme.wav" });
8
+ * console.log(`${result.durationSeconds.toFixed(1)}s of audio`);
9
+ * ```
10
+ *
11
+ * The same code backs the `use-voice-control` command, exported here as
12
+ * `runCli` for hosts that want to wrap it.
13
+ */
14
+ export { detectFormat, extensionOf, loadDocument, readStdinToString, toSpeechText, MARKDOWN_EXTENSIONS, TEXT_EXTENSIONS, type DocumentFormat, type LoadDocumentOptions, type LoadedDocument, type RequestedFormat, } from './document';
15
+ export { defaultOutputPath, renderDocument, type RenderOptions, type RenderResult, } from './render';
16
+ export { formatVoiceList, parseArgs, runCli, USAGE, type CliCommand, type CliIO, type CliOptions, } from './cli';
17
+ export { loadKokoroTTS, resetKokoroCache, synthesizeSamples, synthesizeWav, type KokoroAudio, type KokoroDevice, type KokoroDtype, type KokoroNodeOptions, } from '../core/kokoro-node';
18
+ export { looksLikeMarkdown, markdownToSpeech, markdownToSpeechSegments, stripInlineMarkdown, type MarkdownToSpeechOptions, type SpeechSegment, type SpeechSegmentType, } from '../utils/markdown-to-speech';
@@ -0,0 +1,30 @@
1
+ import { KokoroAudio, KokoroNodeOptions } from '../core/kokoro-node';
2
+ import { LoadDocumentOptions } from './document';
3
+ export interface RenderOptions extends LoadDocumentOptions, KokoroNodeOptions {
4
+ /** Where to write the audio. `-` writes the WAV to stdout. */
5
+ output?: string;
6
+ /**
7
+ * Replace the synthesizer. Used by the tests, and by hosts that already have a
8
+ * model loaded and do not want a second copy.
9
+ */
10
+ synthesize?: (text: string, options: KokoroNodeOptions) => Promise<KokoroAudio>;
11
+ }
12
+ export interface RenderResult {
13
+ /** Path written, or `-` when the audio went to stdout. */
14
+ output: string;
15
+ /** The text that was spoken, after Markdown conversion. */
16
+ text: string;
17
+ format: "markdown" | "text";
18
+ source: string;
19
+ durationSeconds: number;
20
+ bytes: number;
21
+ }
22
+ /** `notes.md` becomes `notes.wav`, next to the original. */
23
+ export declare function defaultOutputPath(input: string): string;
24
+ /**
25
+ * Reads a document, synthesizes it, and writes a WAV file.
26
+ *
27
+ * Returns the spoken text alongside the file details so a caller can show what
28
+ * was read without converting the document a second time.
29
+ */
30
+ export declare function renderDocument(options: RenderOptions): Promise<RenderResult>;
package/dist/node.js ADDED
@@ -0,0 +1,27 @@
1
+ import { M as o, T as s, USAGE as r, d as t, a as n, e as S, formatVoiceList as d, l as i, parseArgs as m, r as T, b as p, runCli as l, t as c } from "./cli.js";
2
+ import { l as h, r as f, a as u, s as E } from "./kokoro-node-DJ_Rxp_N.js";
3
+ import { looksLikeMarkdown as N, markdownToSpeech as O, markdownToSpeechSegments as w, stripInlineMarkdown as M } from "./markdown.js";
4
+ export {
5
+ o as MARKDOWN_EXTENSIONS,
6
+ s as TEXT_EXTENSIONS,
7
+ r as USAGE,
8
+ t as defaultOutputPath,
9
+ n as detectFormat,
10
+ S as extensionOf,
11
+ d as formatVoiceList,
12
+ i as loadDocument,
13
+ h as loadKokoroTTS,
14
+ N as looksLikeMarkdown,
15
+ O as markdownToSpeech,
16
+ w as markdownToSpeechSegments,
17
+ m as parseArgs,
18
+ T as readStdinToString,
19
+ p as renderDocument,
20
+ f as resetKokoroCache,
21
+ l as runCli,
22
+ M as stripInlineMarkdown,
23
+ u as synthesizeSamples,
24
+ E as synthesizeWav,
25
+ c as toSpeechText
26
+ };
27
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
package/dist/react.js CHANGED
@@ -3,7 +3,7 @@ import { ReadAloudController as x, isTranscriptionSupported as L, LiveTranscribe
3
3
  import { jsx as R } from "react/jsx-runtime";
4
4
  import { createPortal as I } from "react-dom";
5
5
  function F(s = {}) {
6
- const [g, h] = u("idle"), [S, y] = u(null), [v, l] = u(null), d = b(s);
6
+ const [g, h] = u("idle"), [S, y] = u(null), [w, l] = u(null), d = b(s);
7
7
  d.current = s;
8
8
  const o = T(
9
9
  () => new x({
@@ -47,6 +47,8 @@ function F(s = {}) {
47
47
  s.voice,
48
48
  s.endpoint,
49
49
  s.maxChunkLength,
50
+ s.format,
51
+ s.markdown,
50
52
  s.synthesize
51
53
  ]), E(() => () => o.stop(), [o]);
52
54
  const f = C(
@@ -54,7 +56,7 @@ function F(s = {}) {
54
56
  l(null), await o.speak(t);
55
57
  },
56
58
  [o]
57
- ), p = C(() => o.stop(), [o]), w = C(() => o.pause(), [o]), i = C(() => o.resume(), [o]), m = C(
59
+ ), p = C(() => o.stop(), [o]), v = C(() => o.pause(), [o]), i = C(() => o.resume(), [o]), m = C(
58
60
  (t) => {
59
61
  o.isActive() ? o.stop() : f(t);
60
62
  },
@@ -66,16 +68,16 @@ function F(s = {}) {
66
68
  isSpeaking: g === "speaking",
67
69
  isPaused: g === "paused",
68
70
  currentChunk: S,
69
- error: v,
71
+ error: w,
70
72
  speak: f,
71
- pause: w,
73
+ pause: v,
72
74
  resume: i,
73
75
  stop: p,
74
76
  toggle: m
75
77
  };
76
78
  }
77
79
  function H(s = {}) {
78
- const [g, h] = u(!1), [S, y] = u(!1), [v, l] = u(""), [d, o] = u(""), [f, p] = u(0), [w, i] = u(null), m = b(s);
80
+ const [g, h] = u(!1), [S, y] = u(!1), [w, l] = u(""), [d, o] = u(""), [f, p] = u(0), [v, i] = u(null), m = b(s);
79
81
  m.current = s, E(() => y(L()), []);
80
82
  const t = T(
81
83
  () => new A({
@@ -127,10 +129,10 @@ function H(s = {}) {
127
129
  return {
128
130
  isListening: g,
129
131
  isSupported: S,
130
- partial: v,
132
+ partial: w,
131
133
  lastPhrase: d,
132
134
  phraseId: f,
133
- error: w,
135
+ error: v,
134
136
  start: a,
135
137
  stop: c,
136
138
  toggle: P
@@ -143,16 +145,16 @@ function M({
143
145
  durationMs: h = O,
144
146
  visible: S = !0,
145
147
  zIndex: y = 2147483e3,
146
- className: v
148
+ className: w
147
149
  }) {
148
- const [l, d] = u(!1), [o, f] = u(!1), [p, w] = u(""), i = b(null);
150
+ const [l, d] = u(!1), [o, f] = u(!1), [p, v] = u(""), i = b(null);
149
151
  return E(() => d(!0), []), E(() => {
150
152
  const a = s.trim();
151
153
  if (!S || !a) {
152
154
  S || f(!1);
153
155
  return;
154
156
  }
155
- return w(a), f(!0), i.current && clearTimeout(i.current), i.current = setTimeout(() => f(!1), h), () => {
157
+ return v(a), f(!0), i.current && clearTimeout(i.current), i.current = setTimeout(() => f(!1), h), () => {
156
158
  i.current && clearTimeout(i.current);
157
159
  };
158
160
  }, [s, g, S, h]), E(() => () => {
@@ -162,7 +164,7 @@ function M({
162
164
  "div",
163
165
  {
164
166
  "aria-hidden": "true",
165
- className: v,
167
+ className: w,
166
168
  "data-spoken-phrase-overlay": "",
167
169
  style: {
168
170
  position: "fixed",
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sources":["../speech/react/useReadAloud.ts","../speech/react/useLiveTranscription.ts","../speech/react/SpokenPhraseOverlay.tsx"],"sourcesContent":["/**\n * @fileoverview React binding for `ReadAloudController`.\n *\n * Keeps one controller alive for the lifetime of the component, mirrors its state\n * into React state, and stops playback on unmount so navigating away never leaves\n * audio running.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n ReadAloudController,\n type ReadAloudChunk,\n type ReadAloudOptions,\n type ReadAloudState,\n} from \"../client/read-aloud\";\n\nexport interface UseReadAloudOptions\n extends Omit<ReadAloudOptions, \"onStateChange\" | \"onChunk\"> {\n /** Called as each chunk starts playing (e.g. to highlight it in the document). */\n onChunk?: (chunk: ReadAloudChunk) => void;\n}\n\nexport interface UseReadAloudReturn {\n state: ReadAloudState;\n /** True while loading, speaking, or paused. */\n isActive: boolean;\n isSpeaking: boolean;\n isPaused: boolean;\n /** The chunk currently being spoken, or `null` between utterances. */\n currentChunk: ReadAloudChunk | null;\n error: Error | null;\n speak: (text: string) => Promise<void>;\n pause: () => void;\n resume: () => void;\n stop: () => void;\n /** Speak `text`, or stop if something is already playing. */\n toggle: (text: string) => void;\n}\n\nexport function useReadAloud(options: UseReadAloudOptions = {}): UseReadAloudReturn {\n const [state, setState] = useState<ReadAloudState>(\"idle\");\n const [currentChunk, setCurrentChunk] = useState<ReadAloudChunk | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Callers routinely pass inline callbacks; hold them in a ref so the controller\n // is never rebuilt mid-playback.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const controller = useMemo(\n () =>\n new ReadAloudController({\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Re-apply the caller's settings (voice, endpoint, …) whenever they change,\n // preserving the callback wiring above.\n useEffect(() => {\n const { onChunk, onEnd, onError, ...rest } = options;\n controller.setOptions({\n ...rest,\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [\n controller,\n options.provider,\n options.voice,\n options.endpoint,\n options.maxChunkLength,\n options.synthesize,\n ]);\n\n useEffect(() => () => controller.stop(), [controller]);\n\n const speak = useCallback(\n async (text: string) => {\n setError(null);\n await controller.speak(text);\n },\n [controller]\n );\n\n const stop = useCallback(() => controller.stop(), [controller]);\n const pause = useCallback(() => controller.pause(), [controller]);\n const resume = useCallback(() => controller.resume(), [controller]);\n\n const toggle = useCallback(\n (text: string) => {\n if (controller.isActive()) {\n controller.stop();\n } else {\n void speak(text);\n }\n },\n [controller, speak]\n );\n\n return {\n state,\n isActive: state !== \"idle\",\n isSpeaking: state === \"speaking\",\n isPaused: state === \"paused\",\n currentChunk,\n error,\n speak,\n pause,\n resume,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview React binding for `LiveTranscriber`.\n *\n * Exposes the two streams a dictation UI needs: `partial`, which changes on every\n * word while a phrase is in progress, and the committed phrases delivered through\n * `onCommit`. `lastPhrase` carries whatever was most recently heard — partial or\n * committed — along with a monotonically increasing `phraseId` so a display can\n * re-trigger its own timeout even when the same words are said twice in a row.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n LiveTranscriber,\n isTranscriptionSupported,\n type LiveTranscriberOptions,\n} from \"../client/live-transcriber\";\n\nexport interface UseLiveTranscriptionOptions\n extends Omit<LiveTranscriberOptions, \"onStateChange\"> {}\n\nexport interface UseLiveTranscriptionReturn {\n isListening: boolean;\n /** True when this browser can dictate at all. */\n isSupported: boolean;\n /** The in-progress phrase, updating as it is spoken. Empty between phrases. */\n partial: string;\n /** The most recent thing heard, partial or committed. */\n lastPhrase: string;\n /** Increments on every `lastPhrase` update, including repeats of the same words. */\n phraseId: number;\n error: Error | null;\n start: () => Promise<void>;\n stop: () => Promise<void>;\n toggle: () => Promise<void>;\n}\n\nexport function useLiveTranscription(\n options: UseLiveTranscriptionOptions = {}\n): UseLiveTranscriptionReturn {\n const [isListening, setIsListening] = useState(false);\n const [isSupported, setIsSupported] = useState(false);\n const [partial, setPartial] = useState(\"\");\n const [lastPhrase, setLastPhrase] = useState(\"\");\n const [phraseId, setPhraseId] = useState(0);\n const [error, setError] = useState<Error | null>(null);\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Support depends on `window`, so it can only be settled after hydration.\n useEffect(() => setIsSupported(isTranscriptionSupported()), []);\n\n const transcriber = useMemo(\n () =>\n new LiveTranscriber({\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Engine/language changes take effect on the next `start()`.\n useEffect(() => {\n transcriber.setOptions({\n engine: options.engine,\n language: options.language,\n model: options.model,\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [transcriber, options.engine, options.language, options.model]);\n\n useEffect(() => {\n return () => {\n void transcriber.stop();\n };\n }, [transcriber]);\n\n const start = useCallback(async () => {\n setError(null);\n await transcriber.start();\n }, [transcriber]);\n\n const stop = useCallback(async () => {\n await transcriber.stop();\n setPartial(\"\");\n }, [transcriber]);\n\n const toggle = useCallback(async () => {\n if (transcriber.isListening()) {\n await stop();\n } else {\n await start();\n }\n }, [transcriber, start, stop]);\n\n return {\n isListening,\n isSupported,\n partial,\n lastPhrase,\n phraseId,\n error,\n start,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview Full-screen display of the phrase that was just spoken.\n *\n * While dictating, the words land in whatever input has focus — which is easy to\n * lose track of when you are looking away from the screen. This overlay echoes the\n * latest phrase in large type at the centre of the viewport and fades out two\n * seconds after the last update, so a glance confirms you were heard correctly.\n *\n * It renders into `document.body` through a portal with pointer events disabled, so\n * it never intercepts clicks or gets clipped by the host app's layout, and it is\n * styled inline so it looks the same in every app that mounts it regardless of the\n * CSS framework in play.\n */\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport interface SpokenPhraseOverlayProps {\n /** The words to display. An empty string hides the overlay. */\n phrase: string;\n /**\n * Bump this whenever `phrase` is refreshed to restart the hide timer, including\n * when the same words are repeated. `useLiveTranscription` supplies it.\n */\n phraseId?: number;\n /** How long the phrase stays up after the last update. Default 2000ms. */\n durationMs?: number;\n /** Set false to suppress the overlay entirely (e.g. once dictation stops). */\n visible?: boolean;\n /** Stacking order. Default 2147483000, above typical app chrome and modals. */\n zIndex?: number;\n className?: string;\n}\n\nconst HIDE_AFTER_MS = 2000;\n\nexport function SpokenPhraseOverlay({\n phrase,\n phraseId,\n durationMs = HIDE_AFTER_MS,\n visible = true,\n zIndex = 2147483000,\n className,\n}: SpokenPhraseOverlayProps) {\n const [mounted, setMounted] = useState(false);\n const [shown, setShown] = useState(false);\n // Held separately from `phrase` so the text stays put while fading out rather\n // than blanking the moment the transcript clears.\n const [displayed, setDisplayed] = useState(\"\");\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n const text = phrase.trim();\n if (!visible || !text) {\n if (!visible) setShown(false);\n return;\n }\n\n setDisplayed(text);\n setShown(true);\n\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => setShown(false), durationMs);\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, [phrase, phraseId, visible, durationMs]);\n\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n if (!mounted || !displayed) return null;\n\n const containerStyle: CSSProperties = {\n position: \"fixed\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n pointerEvents: \"none\",\n padding: \"6vw\",\n zIndex,\n opacity: shown ? 1 : 0,\n transition: \"opacity 320ms ease-out\",\n };\n\n const textStyle: CSSProperties = {\n maxWidth: \"min(1100px, 90vw)\",\n textAlign: \"center\",\n // Scales with the viewport so a short phrase fills the screen on a laptop\n // and still fits on a phone.\n fontSize: \"clamp(2rem, 7vw, 6rem)\",\n fontWeight: 700,\n lineHeight: 1.1,\n letterSpacing: \"-0.02em\",\n color: \"#ffffff\",\n // A dark pill keeps the words readable over light and dark pages alike.\n background: \"rgba(15, 23, 42, 0.82)\",\n borderRadius: \"1.5rem\",\n padding: \"0.6em 0.8em\",\n boxShadow: \"0 25px 80px rgba(0, 0, 0, 0.45)\",\n backdropFilter: \"blur(8px)\",\n transform: shown ? \"scale(1)\" : \"scale(0.96)\",\n transition: \"transform 320ms ease-out\",\n overflowWrap: \"anywhere\",\n };\n\n return createPortal(\n <div\n aria-hidden=\"true\"\n className={className}\n data-spoken-phrase-overlay=\"\"\n style={containerStyle}\n >\n <div style={textStyle}>{displayed}</div>\n </div>,\n document.body\n );\n}\n\nexport default SpokenPhraseOverlay;\n"],"names":["useReadAloud","options","state","setState","useState","currentChunk","setCurrentChunk","error","setError","optionsRef","useRef","controller","useMemo","ReadAloudController","chunk","_b","_a","reason","err","useEffect","onChunk","onEnd","onError","rest","speak","useCallback","text","stop","pause","resume","toggle","useLiveTranscription","isListening","setIsListening","isSupported","setIsSupported","partial","setPartial","lastPhrase","setLastPhrase","phraseId","setPhraseId","isTranscriptionSupported","transcriber","LiveTranscriber","id","start","HIDE_AFTER_MS","SpokenPhraseOverlay","phrase","durationMs","visible","zIndex","className","mounted","setMounted","shown","setShown","displayed","setDisplayed","timerRef","createPortal","jsx"],"mappings":"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"}
1
+ {"version":3,"file":"react.js","sources":["../speech/react/useReadAloud.ts","../speech/react/useLiveTranscription.ts","../speech/react/SpokenPhraseOverlay.tsx"],"sourcesContent":["/**\n * @fileoverview React binding for `ReadAloudController`.\n *\n * Keeps one controller alive for the lifetime of the component, mirrors its state\n * into React state, and stops playback on unmount so navigating away never leaves\n * audio running.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n ReadAloudController,\n type ReadAloudChunk,\n type ReadAloudOptions,\n type ReadAloudState,\n} from \"../client/read-aloud\";\n\nexport interface UseReadAloudOptions\n extends Omit<ReadAloudOptions, \"onStateChange\" | \"onChunk\"> {\n /** Called as each chunk starts playing (e.g. to highlight it in the document). */\n onChunk?: (chunk: ReadAloudChunk) => void;\n}\n\nexport interface UseReadAloudReturn {\n state: ReadAloudState;\n /** True while loading, speaking, or paused. */\n isActive: boolean;\n isSpeaking: boolean;\n isPaused: boolean;\n /** The chunk currently being spoken, or `null` between utterances. */\n currentChunk: ReadAloudChunk | null;\n error: Error | null;\n speak: (text: string) => Promise<void>;\n pause: () => void;\n resume: () => void;\n stop: () => void;\n /** Speak `text`, or stop if something is already playing. */\n toggle: (text: string) => void;\n}\n\nexport function useReadAloud(options: UseReadAloudOptions = {}): UseReadAloudReturn {\n const [state, setState] = useState<ReadAloudState>(\"idle\");\n const [currentChunk, setCurrentChunk] = useState<ReadAloudChunk | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Callers routinely pass inline callbacks; hold them in a ref so the controller\n // is never rebuilt mid-playback.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const controller = useMemo(\n () =>\n new ReadAloudController({\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Re-apply the caller's settings (voice, endpoint, …) whenever they change,\n // preserving the callback wiring above.\n useEffect(() => {\n const { onChunk, onEnd, onError, ...rest } = options;\n controller.setOptions({\n ...rest,\n onStateChange: setState,\n onChunk: (chunk) => {\n setCurrentChunk(chunk);\n optionsRef.current.onChunk?.(chunk);\n },\n onEnd: (reason) => {\n setCurrentChunk(null);\n optionsRef.current.onEnd?.(reason);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [\n controller,\n options.provider,\n options.voice,\n options.endpoint,\n options.maxChunkLength,\n options.format,\n options.markdown,\n options.synthesize,\n ]);\n\n useEffect(() => () => controller.stop(), [controller]);\n\n const speak = useCallback(\n async (text: string) => {\n setError(null);\n await controller.speak(text);\n },\n [controller]\n );\n\n const stop = useCallback(() => controller.stop(), [controller]);\n const pause = useCallback(() => controller.pause(), [controller]);\n const resume = useCallback(() => controller.resume(), [controller]);\n\n const toggle = useCallback(\n (text: string) => {\n if (controller.isActive()) {\n controller.stop();\n } else {\n void speak(text);\n }\n },\n [controller, speak]\n );\n\n return {\n state,\n isActive: state !== \"idle\",\n isSpeaking: state === \"speaking\",\n isPaused: state === \"paused\",\n currentChunk,\n error,\n speak,\n pause,\n resume,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview React binding for `LiveTranscriber`.\n *\n * Exposes the two streams a dictation UI needs: `partial`, which changes on every\n * word while a phrase is in progress, and the committed phrases delivered through\n * `onCommit`. `lastPhrase` carries whatever was most recently heard — partial or\n * committed — along with a monotonically increasing `phraseId` so a display can\n * re-trigger its own timeout even when the same words are said twice in a row.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n LiveTranscriber,\n isTranscriptionSupported,\n type LiveTranscriberOptions,\n} from \"../client/live-transcriber\";\n\nexport interface UseLiveTranscriptionOptions\n extends Omit<LiveTranscriberOptions, \"onStateChange\"> {}\n\nexport interface UseLiveTranscriptionReturn {\n isListening: boolean;\n /** True when this browser can dictate at all. */\n isSupported: boolean;\n /** The in-progress phrase, updating as it is spoken. Empty between phrases. */\n partial: string;\n /** The most recent thing heard, partial or committed. */\n lastPhrase: string;\n /** Increments on every `lastPhrase` update, including repeats of the same words. */\n phraseId: number;\n error: Error | null;\n start: () => Promise<void>;\n stop: () => Promise<void>;\n toggle: () => Promise<void>;\n}\n\nexport function useLiveTranscription(\n options: UseLiveTranscriptionOptions = {}\n): UseLiveTranscriptionReturn {\n const [isListening, setIsListening] = useState(false);\n const [isSupported, setIsSupported] = useState(false);\n const [partial, setPartial] = useState(\"\");\n const [lastPhrase, setLastPhrase] = useState(\"\");\n const [phraseId, setPhraseId] = useState(0);\n const [error, setError] = useState<Error | null>(null);\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // Support depends on `window`, so it can only be settled after hydration.\n useEffect(() => setIsSupported(isTranscriptionSupported()), []);\n\n const transcriber = useMemo(\n () =>\n new LiveTranscriber({\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n }),\n []\n );\n\n // Engine/language changes take effect on the next `start()`.\n useEffect(() => {\n transcriber.setOptions({\n engine: options.engine,\n language: options.language,\n model: options.model,\n onStateChange: setIsListening,\n onPartial: (text) => {\n setPartial(text);\n if (text) {\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n }\n optionsRef.current.onPartial?.(text);\n },\n onCommit: (text) => {\n setPartial(\"\");\n setLastPhrase(text);\n setPhraseId((id) => id + 1);\n optionsRef.current.onCommit?.(text);\n },\n onError: (err) => {\n setError(err);\n optionsRef.current.onError?.(err);\n },\n });\n }, [transcriber, options.engine, options.language, options.model]);\n\n useEffect(() => {\n return () => {\n void transcriber.stop();\n };\n }, [transcriber]);\n\n const start = useCallback(async () => {\n setError(null);\n await transcriber.start();\n }, [transcriber]);\n\n const stop = useCallback(async () => {\n await transcriber.stop();\n setPartial(\"\");\n }, [transcriber]);\n\n const toggle = useCallback(async () => {\n if (transcriber.isListening()) {\n await stop();\n } else {\n await start();\n }\n }, [transcriber, start, stop]);\n\n return {\n isListening,\n isSupported,\n partial,\n lastPhrase,\n phraseId,\n error,\n start,\n stop,\n toggle,\n };\n}\n","/**\n * @fileoverview Full-screen display of the phrase that was just spoken.\n *\n * While dictating, the words land in whatever input has focus — which is easy to\n * lose track of when you are looking away from the screen. This overlay echoes the\n * latest phrase in large type at the centre of the viewport and fades out two\n * seconds after the last update, so a glance confirms you were heard correctly.\n *\n * It renders into `document.body` through a portal with pointer events disabled, so\n * it never intercepts clicks or gets clipped by the host app's layout, and it is\n * styled inline so it looks the same in every app that mounts it regardless of the\n * CSS framework in play.\n */\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport interface SpokenPhraseOverlayProps {\n /** The words to display. An empty string hides the overlay. */\n phrase: string;\n /**\n * Bump this whenever `phrase` is refreshed to restart the hide timer, including\n * when the same words are repeated. `useLiveTranscription` supplies it.\n */\n phraseId?: number;\n /** How long the phrase stays up after the last update. Default 2000ms. */\n durationMs?: number;\n /** Set false to suppress the overlay entirely (e.g. once dictation stops). */\n visible?: boolean;\n /** Stacking order. Default 2147483000, above typical app chrome and modals. */\n zIndex?: number;\n className?: string;\n}\n\nconst HIDE_AFTER_MS = 2000;\n\nexport function SpokenPhraseOverlay({\n phrase,\n phraseId,\n durationMs = HIDE_AFTER_MS,\n visible = true,\n zIndex = 2147483000,\n className,\n}: SpokenPhraseOverlayProps) {\n const [mounted, setMounted] = useState(false);\n const [shown, setShown] = useState(false);\n // Held separately from `phrase` so the text stays put while fading out rather\n // than blanking the moment the transcript clears.\n const [displayed, setDisplayed] = useState(\"\");\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n const text = phrase.trim();\n if (!visible || !text) {\n if (!visible) setShown(false);\n return;\n }\n\n setDisplayed(text);\n setShown(true);\n\n if (timerRef.current) clearTimeout(timerRef.current);\n timerRef.current = setTimeout(() => setShown(false), durationMs);\n\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, [phrase, phraseId, visible, durationMs]);\n\n useEffect(() => {\n return () => {\n if (timerRef.current) clearTimeout(timerRef.current);\n };\n }, []);\n\n if (!mounted || !displayed) return null;\n\n const containerStyle: CSSProperties = {\n position: \"fixed\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n pointerEvents: \"none\",\n padding: \"6vw\",\n zIndex,\n opacity: shown ? 1 : 0,\n transition: \"opacity 320ms ease-out\",\n };\n\n const textStyle: CSSProperties = {\n maxWidth: \"min(1100px, 90vw)\",\n textAlign: \"center\",\n // Scales with the viewport so a short phrase fills the screen on a laptop\n // and still fits on a phone.\n fontSize: \"clamp(2rem, 7vw, 6rem)\",\n fontWeight: 700,\n lineHeight: 1.1,\n letterSpacing: \"-0.02em\",\n color: \"#ffffff\",\n // A dark pill keeps the words readable over light and dark pages alike.\n background: \"rgba(15, 23, 42, 0.82)\",\n borderRadius: \"1.5rem\",\n padding: \"0.6em 0.8em\",\n boxShadow: \"0 25px 80px rgba(0, 0, 0, 0.45)\",\n backdropFilter: \"blur(8px)\",\n transform: shown ? \"scale(1)\" : \"scale(0.96)\",\n transition: \"transform 320ms ease-out\",\n overflowWrap: \"anywhere\",\n };\n\n return createPortal(\n <div\n aria-hidden=\"true\"\n className={className}\n data-spoken-phrase-overlay=\"\"\n style={containerStyle}\n >\n <div style={textStyle}>{displayed}</div>\n </div>,\n document.body\n );\n}\n\nexport default SpokenPhraseOverlay;\n"],"names":["useReadAloud","options","state","setState","useState","currentChunk","setCurrentChunk","error","setError","optionsRef","useRef","controller","useMemo","ReadAloudController","chunk","_b","_a","reason","err","useEffect","onChunk","onEnd","onError","rest","speak","useCallback","text","stop","pause","resume","toggle","useLiveTranscription","isListening","setIsListening","isSupported","setIsSupported","partial","setPartial","lastPhrase","setLastPhrase","phraseId","setPhraseId","isTranscriptionSupported","transcriber","LiveTranscriber","id","start","HIDE_AFTER_MS","SpokenPhraseOverlay","phrase","durationMs","visible","zIndex","className","mounted","setMounted","shown","setShown","displayed","setDisplayed","timerRef","createPortal","jsx"],"mappings":"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"}