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.
- package/README.md +77 -3
- package/dist/client/index.d.ts +8 -0
- package/dist/client/live-transcriber.d.ts +50 -0
- package/dist/client/read-aloud.d.ts +65 -0
- package/dist/client.js +301 -0
- package/dist/client.js.map +1 -0
- package/dist/react/SpokenPhraseOverlay.d.ts +18 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/useLiveTranscription.d.ts +19 -0
- package/dist/react/useReadAloud.d.ts +22 -0
- package/dist/react.js +212 -0
- package/dist/react.js.map +1 -0
- package/package.json +24 -10
- package/speech/api-client.ts +7 -0
- package/speech/client/index.ts +21 -0
- package/speech/client/live-transcriber.ts +216 -0
- package/speech/client/read-aloud.ts +305 -0
- package/speech/core/KokoroTTS.js +7 -0
- package/speech/core/kokoro.js +8 -0
- package/speech/legacy/conversation.js +7 -0
- package/speech/legacy/stt.js +7 -0
- package/speech/legacy/tts.js +7 -0
- package/speech/legacy/worker.js +7 -0
- package/speech/react/SpokenPhraseOverlay.tsx +126 -0
- package/speech/react/index.ts +27 -0
- package/speech/react/useLiveTranscription.ts +142 -0
- package/speech/react/useReadAloud.ts +137 -0
- package/speech/ui/AudioPlayer.js +6 -0
- package/speech/ui/ui.js +7 -0
- package/speech/ui/voice-selector.js +3 -0
- package/speech/ui/voices.js +7 -0
- package/speech/utils/audio-utils.js +7 -0
- package/speech/utils/phonemize.js +7 -0
- package/speech/utils/semantic-split.d.ts +10 -0
- package/speech/utils/semantic-split.js +7 -0
- package/speech/utils/sentence-detector.d.ts +13 -0
- package/speech/utils/sentence-detector.js +5 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview React binding for `ReadAloudController`.
|
|
3
|
+
*
|
|
4
|
+
* Keeps one controller alive for the lifetime of the component, mirrors its state
|
|
5
|
+
* into React state, and stops playback on unmount so navigating away never leaves
|
|
6
|
+
* audio running.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ReadAloudController,
|
|
12
|
+
type ReadAloudChunk,
|
|
13
|
+
type ReadAloudOptions,
|
|
14
|
+
type ReadAloudState,
|
|
15
|
+
} from "../client/read-aloud";
|
|
16
|
+
|
|
17
|
+
export interface UseReadAloudOptions
|
|
18
|
+
extends Omit<ReadAloudOptions, "onStateChange" | "onChunk"> {
|
|
19
|
+
/** Called as each chunk starts playing (e.g. to highlight it in the document). */
|
|
20
|
+
onChunk?: (chunk: ReadAloudChunk) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseReadAloudReturn {
|
|
24
|
+
state: ReadAloudState;
|
|
25
|
+
/** True while loading, speaking, or paused. */
|
|
26
|
+
isActive: boolean;
|
|
27
|
+
isSpeaking: boolean;
|
|
28
|
+
isPaused: boolean;
|
|
29
|
+
/** The chunk currently being spoken, or `null` between utterances. */
|
|
30
|
+
currentChunk: ReadAloudChunk | null;
|
|
31
|
+
error: Error | null;
|
|
32
|
+
speak: (text: string) => Promise<void>;
|
|
33
|
+
pause: () => void;
|
|
34
|
+
resume: () => void;
|
|
35
|
+
stop: () => void;
|
|
36
|
+
/** Speak `text`, or stop if something is already playing. */
|
|
37
|
+
toggle: (text: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useReadAloud(options: UseReadAloudOptions = {}): UseReadAloudReturn {
|
|
41
|
+
const [state, setState] = useState<ReadAloudState>("idle");
|
|
42
|
+
const [currentChunk, setCurrentChunk] = useState<ReadAloudChunk | null>(null);
|
|
43
|
+
const [error, setError] = useState<Error | null>(null);
|
|
44
|
+
|
|
45
|
+
// Callers routinely pass inline callbacks; hold them in a ref so the controller
|
|
46
|
+
// is never rebuilt mid-playback.
|
|
47
|
+
const optionsRef = useRef(options);
|
|
48
|
+
optionsRef.current = options;
|
|
49
|
+
|
|
50
|
+
const controller = useMemo(
|
|
51
|
+
() =>
|
|
52
|
+
new ReadAloudController({
|
|
53
|
+
onStateChange: setState,
|
|
54
|
+
onChunk: (chunk) => {
|
|
55
|
+
setCurrentChunk(chunk);
|
|
56
|
+
optionsRef.current.onChunk?.(chunk);
|
|
57
|
+
},
|
|
58
|
+
onEnd: (reason) => {
|
|
59
|
+
setCurrentChunk(null);
|
|
60
|
+
optionsRef.current.onEnd?.(reason);
|
|
61
|
+
},
|
|
62
|
+
onError: (err) => {
|
|
63
|
+
setError(err);
|
|
64
|
+
optionsRef.current.onError?.(err);
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
[]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Re-apply the caller's settings (voice, endpoint, …) whenever they change,
|
|
71
|
+
// preserving the callback wiring above.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const { onChunk, onEnd, onError, ...rest } = options;
|
|
74
|
+
controller.setOptions({
|
|
75
|
+
...rest,
|
|
76
|
+
onStateChange: setState,
|
|
77
|
+
onChunk: (chunk) => {
|
|
78
|
+
setCurrentChunk(chunk);
|
|
79
|
+
optionsRef.current.onChunk?.(chunk);
|
|
80
|
+
},
|
|
81
|
+
onEnd: (reason) => {
|
|
82
|
+
setCurrentChunk(null);
|
|
83
|
+
optionsRef.current.onEnd?.(reason);
|
|
84
|
+
},
|
|
85
|
+
onError: (err) => {
|
|
86
|
+
setError(err);
|
|
87
|
+
optionsRef.current.onError?.(err);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}, [
|
|
91
|
+
controller,
|
|
92
|
+
options.provider,
|
|
93
|
+
options.voice,
|
|
94
|
+
options.endpoint,
|
|
95
|
+
options.maxChunkLength,
|
|
96
|
+
options.synthesize,
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => () => controller.stop(), [controller]);
|
|
100
|
+
|
|
101
|
+
const speak = useCallback(
|
|
102
|
+
async (text: string) => {
|
|
103
|
+
setError(null);
|
|
104
|
+
await controller.speak(text);
|
|
105
|
+
},
|
|
106
|
+
[controller]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const stop = useCallback(() => controller.stop(), [controller]);
|
|
110
|
+
const pause = useCallback(() => controller.pause(), [controller]);
|
|
111
|
+
const resume = useCallback(() => controller.resume(), [controller]);
|
|
112
|
+
|
|
113
|
+
const toggle = useCallback(
|
|
114
|
+
(text: string) => {
|
|
115
|
+
if (controller.isActive()) {
|
|
116
|
+
controller.stop();
|
|
117
|
+
} else {
|
|
118
|
+
void speak(text);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[controller, speak]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
state,
|
|
126
|
+
isActive: state !== "idle",
|
|
127
|
+
isSpeaking: state === "speaking",
|
|
128
|
+
isPaused: state === "paused",
|
|
129
|
+
currentChunk,
|
|
130
|
+
error,
|
|
131
|
+
speak,
|
|
132
|
+
pause,
|
|
133
|
+
resume,
|
|
134
|
+
stop,
|
|
135
|
+
toggle,
|
|
136
|
+
};
|
|
137
|
+
}
|
package/speech/ui/AudioPlayer.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `AudioPlayer` class that queues raw Float32 audio chunks and plays them back sequentially through the Web Audio API.
|
|
3
|
+
*
|
|
4
|
+
* Supports queueing additional chunks while playback is in progress, stopping playback and
|
|
5
|
+
* clearing the queue, and closing the underlying `AudioContext`.
|
|
6
|
+
*/
|
|
1
7
|
const SAMPLE_RATE = 24000;
|
|
2
8
|
|
|
3
9
|
export class AudioPlayer {
|
package/speech/ui/ui.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Legacy demo UI glue: renders the conversation transcript, drives the tab navigation, and wires the recording toggle button to a `Conversation` instance.
|
|
3
|
+
*
|
|
4
|
+
* Provides `displayConversation` (renders chat history into the DOM) and a
|
|
5
|
+
* `DOMContentLoaded` handler that sets up tab switching, the voice selector, the
|
|
6
|
+
* record/stop button (with a live timer), and an F8 "continue" keyboard shortcut.
|
|
7
|
+
*/
|
|
1
8
|
import { Conversation } from './conversation.js';
|
|
2
9
|
import { initVoiceSelector } from './voice-selector.js';
|
|
3
10
|
|
package/speech/ui/voices.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Kokoro voice catalog and per-voice style-vector loading/caching used by the legacy TTS demo.
|
|
3
|
+
*
|
|
4
|
+
* Exports the `VOICES` metadata map (name, language, gender, quality grade) plus
|
|
5
|
+
* `getVoiceData`, which fetches each voice's style-vector `.bin` file from the Hugging Face
|
|
6
|
+
* Hub (via the Cache API and an in-memory `VOICE_CACHE`) and returns it as a `Float32Array`.
|
|
7
|
+
*/
|
|
1
8
|
export const VOICES = Object.freeze({
|
|
2
9
|
af_heart: {
|
|
3
10
|
name: "Heart",
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Audio-buffer utility functions for resampling, gain adjustment, and WAV encoding, used by the legacy STT/TTS pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Exports `resampleAudio` (via `OfflineAudioContext`), `applyAudioGain` (volume boost with
|
|
5
|
+
* clamping, used to retry failed transcriptions), and `convertAudioBufferToWav` (manual RIFF/WAV
|
|
6
|
+
* header + PCM encoding) plus its internal `writeString` helper.
|
|
7
|
+
*/
|
|
1
8
|
//see also: https://github.com/colmeye/js-mediarecorder-to-wav/blob/main/wave-worker.js
|
|
2
9
|
|
|
3
10
|
// 16000
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `phonemize` converts input text into phonemes for the Kokoro TTS pipeline, using the `phonemizer` (espeak-ng) library loaded from a CDN.
|
|
3
|
+
*
|
|
4
|
+
* Normalizes text first (quotes/punctuation, abbreviations, numbers, currency,
|
|
5
|
+
* possessives, hyphenation) via `normalize_text`, splits on punctuation to preserve it, runs
|
|
6
|
+
* each section through espeak-ng, then applies Kokoro-specific phoneme post-processing.
|
|
7
|
+
*/
|
|
1
8
|
import { phonemize as espeakng } from "https://cdn.jsdelivr.net/npm/phonemizer@1.2.1/dist/phonemizer.min.js";
|
|
2
9
|
//import { phonemize as espeakng } from "phonemizer";
|
|
3
10
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type declarations for the JavaScript chunker in `semantic-split.js`,
|
|
3
|
+
* so TypeScript callers under `speech/client` can import it directly.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Split text into TTS-sized chunks along paragraph, sentence, comma, and word boundaries. */
|
|
7
|
+
export function splitTextSmart(text: string, maxChunkLength?: number): string[];
|
|
8
|
+
|
|
9
|
+
/** Break a single oversized sentence down along commas, then words. */
|
|
10
|
+
export function splitLongSentence(sentence: string, maxLen: number): string[];
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Splits long text into TTS-sized chunks along paragraph, sentence, comma, and word boundaries so speech synthesis can process it incrementally.
|
|
3
|
+
*
|
|
4
|
+
* Exports `splitTextSmart` (primary chunker, falling back to `splitLongSentence` for
|
|
5
|
+
* oversized sentences) and `splitLongSentence`. Also retains an older, unused
|
|
6
|
+
* `splitTextSmartOld` variant that lacks the long-sentence fallback.
|
|
7
|
+
*/
|
|
1
8
|
export function splitTextSmart(text, maxChunkLength = 500) {
|
|
2
9
|
const paragraphChunks = text.split(/\n\s*\n/);
|
|
3
10
|
const finalChunks = [];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type declarations for the JavaScript helpers in `sentence-detector.js`,
|
|
3
|
+
* so TypeScript callers under `speech/client` can import it directly.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** True when the text reads as a finished sentence (or is long enough to speak anyway). */
|
|
7
|
+
export function isCompleteSentence(text: string): boolean;
|
|
8
|
+
|
|
9
|
+
/** Split an accumulator + new chunk into speakable sentences plus the leftover remainder. */
|
|
10
|
+
export function processStreamingText(
|
|
11
|
+
accumulator: string,
|
|
12
|
+
newContent: string
|
|
13
|
+
): { sentences: string[]; remainder: string };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* Utility functions for detecting sentence boundaries in streaming text
|
|
3
|
+
* @fileoverview Utility functions for detecting sentence boundaries in streaming text so speech can be synthesized incrementally as an LLM response streams in.
|
|
4
|
+
*
|
|
5
|
+
* Exports `isCompleteSentence` (checks basic/dialog/list-item/paragraph endings plus a
|
|
6
|
+
* length-based fallback) and `processStreamingText`, which splits an accumulator + new
|
|
7
|
+
* chunk into ready-to-speak sentences and a leftover remainder.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
// These are common sentence ending patterns
|