use-voice-control 0.1.37 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +77 -3
  2. package/dist/client/index.d.ts +8 -0
  3. package/dist/client/live-transcriber.d.ts +50 -0
  4. package/dist/client/read-aloud.d.ts +65 -0
  5. package/dist/client.js +301 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/react/SpokenPhraseOverlay.d.ts +18 -0
  8. package/dist/react/index.d.ts +8 -0
  9. package/dist/react/useLiveTranscription.d.ts +19 -0
  10. package/dist/react/useReadAloud.d.ts +22 -0
  11. package/dist/react.js +212 -0
  12. package/dist/react.js.map +1 -0
  13. package/package.json +24 -10
  14. package/speech/api-client.ts +7 -0
  15. package/speech/client/index.ts +21 -0
  16. package/speech/client/live-transcriber.ts +216 -0
  17. package/speech/client/read-aloud.ts +305 -0
  18. package/speech/core/KokoroTTS.js +7 -0
  19. package/speech/core/kokoro.js +8 -0
  20. package/speech/legacy/conversation.js +7 -0
  21. package/speech/legacy/stt.js +7 -0
  22. package/speech/legacy/tts.js +7 -0
  23. package/speech/legacy/worker.js +7 -0
  24. package/speech/react/SpokenPhraseOverlay.tsx +126 -0
  25. package/speech/react/index.ts +27 -0
  26. package/speech/react/useLiveTranscription.ts +142 -0
  27. package/speech/react/useReadAloud.ts +137 -0
  28. package/speech/ui/AudioPlayer.js +6 -0
  29. package/speech/ui/ui.js +7 -0
  30. package/speech/ui/voice-selector.js +3 -0
  31. package/speech/ui/voices.js +7 -0
  32. package/speech/utils/audio-utils.js +7 -0
  33. package/speech/utils/phonemize.js +7 -0
  34. package/speech/utils/semantic-split.d.ts +10 -0
  35. package/speech/utils/semantic-split.js +7 -0
  36. package/speech/utils/sentence-detector.d.ts +13 -0
  37. package/speech/utils/sentence-detector.js +5 -1
@@ -0,0 +1,22 @@
1
+ import { ReadAloudChunk, ReadAloudOptions, ReadAloudState } from '../client/read-aloud';
2
+ export interface UseReadAloudOptions extends Omit<ReadAloudOptions, "onStateChange" | "onChunk"> {
3
+ /** Called as each chunk starts playing (e.g. to highlight it in the document). */
4
+ onChunk?: (chunk: ReadAloudChunk) => void;
5
+ }
6
+ export interface UseReadAloudReturn {
7
+ state: ReadAloudState;
8
+ /** True while loading, speaking, or paused. */
9
+ isActive: boolean;
10
+ isSpeaking: boolean;
11
+ isPaused: boolean;
12
+ /** The chunk currently being spoken, or `null` between utterances. */
13
+ currentChunk: ReadAloudChunk | null;
14
+ error: Error | null;
15
+ speak: (text: string) => Promise<void>;
16
+ pause: () => void;
17
+ resume: () => void;
18
+ stop: () => void;
19
+ /** Speak `text`, or stop if something is already playing. */
20
+ toggle: (text: string) => void;
21
+ }
22
+ export declare function useReadAloud(options?: UseReadAloudOptions): UseReadAloudReturn;
package/dist/react.js ADDED
@@ -0,0 +1,212 @@
1
+ import { useState as u, useRef as b, useMemo as T, useEffect as E, useCallback as C } from "react";
2
+ import { ReadAloudController as x, isTranscriptionSupported as L, LiveTranscriber as A } from "./client.js";
3
+ import { jsx as R } from "react/jsx-runtime";
4
+ import { createPortal as I } from "react-dom";
5
+ function F(s = {}) {
6
+ const [g, h] = u("idle"), [S, y] = u(null), [v, l] = u(null), d = b(s);
7
+ d.current = s;
8
+ const o = T(
9
+ () => new x({
10
+ onStateChange: h,
11
+ onChunk: (t) => {
12
+ var a, c;
13
+ y(t), (c = (a = d.current).onChunk) == null || c.call(a, t);
14
+ },
15
+ onEnd: (t) => {
16
+ var a, c;
17
+ y(null), (c = (a = d.current).onEnd) == null || c.call(a, t);
18
+ },
19
+ onError: (t) => {
20
+ var a, c;
21
+ l(t), (c = (a = d.current).onError) == null || c.call(a, t);
22
+ }
23
+ }),
24
+ []
25
+ );
26
+ E(() => {
27
+ const { onChunk: t, onEnd: a, onError: c, ...P } = s;
28
+ o.setOptions({
29
+ ...P,
30
+ onStateChange: h,
31
+ onChunk: (e) => {
32
+ var n, r;
33
+ y(e), (r = (n = d.current).onChunk) == null || r.call(n, e);
34
+ },
35
+ onEnd: (e) => {
36
+ var n, r;
37
+ y(null), (r = (n = d.current).onEnd) == null || r.call(n, e);
38
+ },
39
+ onError: (e) => {
40
+ var n, r;
41
+ l(e), (r = (n = d.current).onError) == null || r.call(n, e);
42
+ }
43
+ });
44
+ }, [
45
+ o,
46
+ s.provider,
47
+ s.voice,
48
+ s.endpoint,
49
+ s.maxChunkLength,
50
+ s.synthesize
51
+ ]), E(() => () => o.stop(), [o]);
52
+ const f = C(
53
+ async (t) => {
54
+ l(null), await o.speak(t);
55
+ },
56
+ [o]
57
+ ), p = C(() => o.stop(), [o]), w = C(() => o.pause(), [o]), i = C(() => o.resume(), [o]), m = C(
58
+ (t) => {
59
+ o.isActive() ? o.stop() : f(t);
60
+ },
61
+ [o, f]
62
+ );
63
+ return {
64
+ state: g,
65
+ isActive: g !== "idle",
66
+ isSpeaking: g === "speaking",
67
+ isPaused: g === "paused",
68
+ currentChunk: S,
69
+ error: v,
70
+ speak: f,
71
+ pause: w,
72
+ resume: i,
73
+ stop: p,
74
+ toggle: m
75
+ };
76
+ }
77
+ 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);
79
+ m.current = s, E(() => y(L()), []);
80
+ const t = T(
81
+ () => new A({
82
+ onStateChange: h,
83
+ onPartial: (e) => {
84
+ var n, r;
85
+ l(e), e && (o(e), p((k) => k + 1)), (r = (n = m.current).onPartial) == null || r.call(n, e);
86
+ },
87
+ onCommit: (e) => {
88
+ var n, r;
89
+ l(""), o(e), p((k) => k + 1), (r = (n = m.current).onCommit) == null || r.call(n, e);
90
+ },
91
+ onError: (e) => {
92
+ var n, r;
93
+ i(e), (r = (n = m.current).onError) == null || r.call(n, e);
94
+ }
95
+ }),
96
+ []
97
+ );
98
+ E(() => {
99
+ t.setOptions({
100
+ engine: s.engine,
101
+ language: s.language,
102
+ model: s.model,
103
+ onStateChange: h,
104
+ onPartial: (e) => {
105
+ var n, r;
106
+ l(e), e && (o(e), p((k) => k + 1)), (r = (n = m.current).onPartial) == null || r.call(n, e);
107
+ },
108
+ onCommit: (e) => {
109
+ var n, r;
110
+ l(""), o(e), p((k) => k + 1), (r = (n = m.current).onCommit) == null || r.call(n, e);
111
+ },
112
+ onError: (e) => {
113
+ var n, r;
114
+ i(e), (r = (n = m.current).onError) == null || r.call(n, e);
115
+ }
116
+ });
117
+ }, [t, s.engine, s.language, s.model]), E(() => () => {
118
+ t.stop();
119
+ }, [t]);
120
+ const a = C(async () => {
121
+ i(null), await t.start();
122
+ }, [t]), c = C(async () => {
123
+ await t.stop(), l("");
124
+ }, [t]), P = C(async () => {
125
+ t.isListening() ? await c() : await a();
126
+ }, [t, a, c]);
127
+ return {
128
+ isListening: g,
129
+ isSupported: S,
130
+ partial: v,
131
+ lastPhrase: d,
132
+ phraseId: f,
133
+ error: w,
134
+ start: a,
135
+ stop: c,
136
+ toggle: P
137
+ };
138
+ }
139
+ const O = 2e3;
140
+ function M({
141
+ phrase: s,
142
+ phraseId: g,
143
+ durationMs: h = O,
144
+ visible: S = !0,
145
+ zIndex: y = 2147483e3,
146
+ className: v
147
+ }) {
148
+ const [l, d] = u(!1), [o, f] = u(!1), [p, w] = u(""), i = b(null);
149
+ return E(() => d(!0), []), E(() => {
150
+ const a = s.trim();
151
+ if (!S || !a) {
152
+ S || f(!1);
153
+ return;
154
+ }
155
+ return w(a), f(!0), i.current && clearTimeout(i.current), i.current = setTimeout(() => f(!1), h), () => {
156
+ i.current && clearTimeout(i.current);
157
+ };
158
+ }, [s, g, S, h]), E(() => () => {
159
+ i.current && clearTimeout(i.current);
160
+ }, []), !l || !p ? null : I(
161
+ /* @__PURE__ */ R(
162
+ "div",
163
+ {
164
+ "aria-hidden": "true",
165
+ className: v,
166
+ "data-spoken-phrase-overlay": "",
167
+ style: {
168
+ position: "fixed",
169
+ inset: 0,
170
+ display: "flex",
171
+ alignItems: "center",
172
+ justifyContent: "center",
173
+ pointerEvents: "none",
174
+ padding: "6vw",
175
+ zIndex: y,
176
+ opacity: o ? 1 : 0,
177
+ transition: "opacity 320ms ease-out"
178
+ },
179
+ children: /* @__PURE__ */ R("div", { style: {
180
+ maxWidth: "min(1100px, 90vw)",
181
+ textAlign: "center",
182
+ // Scales with the viewport so a short phrase fills the screen on a laptop
183
+ // and still fits on a phone.
184
+ fontSize: "clamp(2rem, 7vw, 6rem)",
185
+ fontWeight: 700,
186
+ lineHeight: 1.1,
187
+ letterSpacing: "-0.02em",
188
+ color: "#ffffff",
189
+ // A dark pill keeps the words readable over light and dark pages alike.
190
+ background: "rgba(15, 23, 42, 0.82)",
191
+ borderRadius: "1.5rem",
192
+ padding: "0.6em 0.8em",
193
+ boxShadow: "0 25px 80px rgba(0, 0, 0, 0.45)",
194
+ backdropFilter: "blur(8px)",
195
+ transform: o ? "scale(1)" : "scale(0.96)",
196
+ transition: "transform 320ms ease-out",
197
+ overflowWrap: "anywhere"
198
+ }, children: p })
199
+ }
200
+ ),
201
+ document.body
202
+ );
203
+ }
204
+ export {
205
+ A as LiveTranscriber,
206
+ x as ReadAloudController,
207
+ M as SpokenPhraseOverlay,
208
+ L as isTranscriptionSupported,
209
+ H as useLiveTranscription,
210
+ F as useReadAloud
211
+ };
212
+ //# sourceMappingURL=react.js.map
@@ -0,0 +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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-voice-control",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "React voice control with speech transcription, vocalization, and interruption (STT/TTS/VAD) support.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,14 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js"
17
17
  },
18
+ "./client": {
19
+ "types": "./dist/client/index.d.ts",
20
+ "import": "./dist/client.js"
21
+ },
22
+ "./react": {
23
+ "types": "./dist/react/index.d.ts",
24
+ "import": "./dist/react.js"
25
+ },
18
26
  "./api-client": {
19
27
  "types": "./speech/api-client.ts",
20
28
  "import": "./speech/api-client.ts"
@@ -27,26 +35,32 @@
27
35
  ],
28
36
  "scripts": {
29
37
  "build": "tsc && vite build",
38
+ "prepare": "tsc && vite build",
30
39
  "dev": "vite build --watch",
31
- "type-check": "tsc --noEmit"
40
+ "type-check": "tsc --noEmit",
41
+ "test": "vitest run",
42
+ "test:coverage": "vitest run --coverage"
32
43
  },
33
44
  "dependencies": {
34
45
  "@huggingface/transformers": "^3.8.1",
35
46
  "lucide-react": "^0.344.0",
36
- "@moonshine-ai/moonshine-js": "^0.1.29",
37
- "react": "^18.0.0",
38
- "react-dom": "^18.0.0"
47
+ "@moonshine-ai/moonshine-js": "^0.1.29"
39
48
  },
40
49
  "devDependencies": {
41
- "@types/react": "^18.0.0",
42
- "@types/react-dom": "^18.0.0",
50
+ "@types/react": "^19.2.17",
51
+ "@vitejs/plugin-react": "^4.3.4",
52
+ "@types/react-dom": "^19.2.3",
53
+ "react": "^19.2.7",
54
+ "react-dom": "^19.2.7",
55
+ "@vitest/coverage-v8": "^4.0.18",
43
56
  "typescript": "^5.3.3",
44
57
  "vite": "^5.0.0",
45
- "vite-plugin-dts": "^5.0.0"
58
+ "vite-plugin-dts": "^5.0.0",
59
+ "vitest": "^4.0.18"
46
60
  },
47
61
  "peerDependencies": {
48
- "react": "^18.0.0",
49
- "react-dom": "^18.0.0"
62
+ "react": "^18.0.0 || ^19.0.0",
63
+ "react-dom": "^18.0.0 || ^19.0.0"
50
64
  },
51
65
  "optionalDependencies": {
52
66
  "@huggingface/transformers": "^3.0.0"
@@ -1,3 +1,10 @@
1
+ /**
2
+ * @fileoverview Browser-side helper functions for calling the package's TTS/STT HTTP API routes.
3
+ *
4
+ * Provides `generateSpeechFromText`/`createAudioURL`/`speakText` for turning text into
5
+ * playable audio via `POST /api/speech/tts`, and `checkSTTAPI` to probe availability of
6
+ * `/api/speech/stt`.
7
+ */
1
8
  import type { TTSOptions } from "./types/types";
2
9
 
3
10
  export async function generateSpeechFromText(
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @fileoverview Public entry point for the framework-agnostic browser engines:
3
+ * read-aloud playback and live dictation. React callers should prefer the hooks in
4
+ * `use-voice-control/react`, which wrap these.
5
+ */
6
+ export {
7
+ ReadAloudController,
8
+ type ReadAloudChunk,
9
+ type ReadAloudOptions,
10
+ type ReadAloudState,
11
+ type SynthesizeFn,
12
+ } from "./read-aloud";
13
+
14
+ export {
15
+ LiveTranscriber,
16
+ isTranscriptionSupported,
17
+ type LiveTranscriberOptions,
18
+ type TranscriberEngine,
19
+ } from "./live-transcriber";
20
+
21
+ export type { TTSProvider, KokoroVoice, DeepgramSpeaker } from "../types/types";
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @fileoverview Browser-side live dictation engine.
3
+ *
4
+ * `LiveTranscriber` streams microphone audio to a recognizer and reports two kinds
5
+ * of text: `onPartial`, the in-progress guess that keeps changing while a phrase is
6
+ * being spoken, and `onCommit`, a phrase the recognizer has settled on. Callers use
7
+ * the first to type text into an input as it is being said, and the second to
8
+ * finalize it.
9
+ *
10
+ * Two engines are supported. The browser's own `SpeechRecognition` is preferred when
11
+ * present because it needs no model download; otherwise the package's bundled
12
+ * Moonshine model runs the recognition entirely on-device. Chromium's recognizer
13
+ * stops itself after a pause, so a session that is still meant to be listening is
14
+ * restarted automatically.
15
+ */
16
+
17
+ export type TranscriberEngine = "auto" | "webspeech" | "moonshine";
18
+
19
+ export interface LiveTranscriberOptions {
20
+ /** Which recognizer to use. Default `auto` (browser first, Moonshine as fallback). */
21
+ engine?: TranscriberEngine;
22
+ /** BCP-47 language tag for the browser recognizer. Default `en-US`. */
23
+ language?: string;
24
+ /** Moonshine model name. Default `model/small`. */
25
+ model?: string;
26
+ /** Fires continuously with the current in-progress phrase. */
27
+ onPartial?: (text: string) => void;
28
+ /** Fires once per phrase the recognizer has settled on. */
29
+ onCommit?: (text: string) => void;
30
+ /** Fires whenever the microphone starts or stops. */
31
+ onStateChange?: (listening: boolean) => void;
32
+ onError?: (error: Error) => void;
33
+ }
34
+
35
+ type SpeechRecognitionCtor = new () => any;
36
+
37
+ function getSpeechRecognition(): SpeechRecognitionCtor | null {
38
+ if (typeof window === "undefined") return null;
39
+ return (
40
+ (window as any).SpeechRecognition ||
41
+ (window as any).webkitSpeechRecognition ||
42
+ null
43
+ );
44
+ }
45
+
46
+ /** True when this browser can dictate at all (either engine). */
47
+ export function isTranscriptionSupported(): boolean {
48
+ if (typeof window === "undefined") return false;
49
+ if (getSpeechRecognition()) return true;
50
+ return !!navigator.mediaDevices?.getUserMedia;
51
+ }
52
+
53
+ export class LiveTranscriber {
54
+ private options: LiveTranscriberOptions;
55
+ private listening = false;
56
+ /** Distinguishes a deliberate `stop()` from Chromium's idle auto-stop. */
57
+ private stopRequested = false;
58
+ private recognition: any = null;
59
+ private moonshine: any = null;
60
+
61
+ constructor(options: LiveTranscriberOptions = {}) {
62
+ this.options = options;
63
+ }
64
+
65
+ setOptions(options: LiveTranscriberOptions): void {
66
+ this.options = options;
67
+ }
68
+
69
+ isListening(): boolean {
70
+ return this.listening;
71
+ }
72
+
73
+ async start(): Promise<void> {
74
+ if (this.listening) return;
75
+ this.stopRequested = false;
76
+
77
+ const engine = this.options.engine ?? "auto";
78
+ const Recognition = getSpeechRecognition();
79
+
80
+ try {
81
+ if (engine !== "moonshine" && Recognition) {
82
+ this.startWebSpeech(Recognition);
83
+ return;
84
+ }
85
+ if (engine === "webspeech") {
86
+ throw new Error("Speech recognition is not available in this browser");
87
+ }
88
+ await this.startMoonshine();
89
+ } catch (error) {
90
+ this.setListening(false);
91
+ this.options.onError?.(
92
+ error instanceof Error ? error : new Error(String(error))
93
+ );
94
+ }
95
+ }
96
+
97
+ async stop(): Promise<void> {
98
+ this.stopRequested = true;
99
+
100
+ if (this.recognition) {
101
+ try {
102
+ this.recognition.stop();
103
+ } catch {
104
+ /* already stopped */
105
+ }
106
+ this.recognition = null;
107
+ }
108
+
109
+ if (this.moonshine) {
110
+ try {
111
+ await this.moonshine.stop?.();
112
+ } catch {
113
+ /* already stopped */
114
+ }
115
+ this.moonshine = null;
116
+ }
117
+
118
+ this.setListening(false);
119
+ }
120
+
121
+ async toggle(): Promise<void> {
122
+ if (this.listening) {
123
+ await this.stop();
124
+ } else {
125
+ await this.start();
126
+ }
127
+ }
128
+
129
+ private setListening(listening: boolean): void {
130
+ if (this.listening === listening) return;
131
+ this.listening = listening;
132
+ this.options.onStateChange?.(listening);
133
+ }
134
+
135
+ private startWebSpeech(Recognition: SpeechRecognitionCtor): void {
136
+ const recognition = new Recognition();
137
+ recognition.continuous = true;
138
+ recognition.interimResults = true;
139
+ recognition.lang = this.options.language ?? "en-US";
140
+
141
+ recognition.onstart = () => this.setListening(true);
142
+
143
+ recognition.onresult = (event: any) => {
144
+ let interim = "";
145
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
146
+ const result = event.results[i];
147
+ const text = String(result[0]?.transcript ?? "").trim();
148
+ if (!text) continue;
149
+ if (result.isFinal) {
150
+ // No `onPartial("")` first: a commit supersedes the interim phrase, and
151
+ // clearing it separately would make consumers that write the interim
152
+ // into a document erase and re-insert the same words.
153
+ this.options.onCommit?.(text);
154
+ } else {
155
+ interim += `${interim ? " " : ""}${text}`;
156
+ }
157
+ }
158
+ if (interim) this.options.onPartial?.(interim);
159
+ };
160
+
161
+ recognition.onerror = (event: any) => {
162
+ const code = event?.error;
163
+ // `no-speech` and `aborted` are routine during a long dictation session;
164
+ // `onend` restarts the recognizer for us.
165
+ if (code === "no-speech" || code === "aborted") return;
166
+ this.stopRequested = true;
167
+ this.options.onError?.(new Error(`Speech recognition error: ${code}`));
168
+ };
169
+
170
+ recognition.onend = () => {
171
+ if (this.stopRequested || this.recognition !== recognition) {
172
+ this.recognition = null;
173
+ this.setListening(false);
174
+ return;
175
+ }
176
+ // Chromium ends the session after a pause — start a fresh one so the user
177
+ // can keep dictating without touching the button again.
178
+ try {
179
+ recognition.start();
180
+ } catch {
181
+ this.recognition = null;
182
+ this.setListening(false);
183
+ }
184
+ };
185
+
186
+ this.recognition = recognition;
187
+ recognition.start();
188
+ }
189
+
190
+ private async startMoonshine(): Promise<void> {
191
+ const Moonshine = await import("@moonshine-ai/moonshine-js");
192
+
193
+ const transcriber = new Moonshine.MicrophoneTranscriber(
194
+ this.options.model ?? "model/small",
195
+ {
196
+ onTranscriptionUpdated: (text: string) => {
197
+ this.options.onPartial?.(String(text ?? "").trim());
198
+ },
199
+ onTranscriptionCommitted: (text: string) => {
200
+ const committed = String(text ?? "").trim();
201
+ if (committed) this.options.onCommit?.(committed);
202
+ else this.options.onPartial?.("");
203
+ },
204
+ },
205
+ false // streaming mode
206
+ );
207
+
208
+ this.moonshine = transcriber;
209
+ await transcriber.start();
210
+ if (this.stopRequested) {
211
+ await this.stop();
212
+ return;
213
+ }
214
+ this.setListening(true);
215
+ }
216
+ }