Skip to content

Commit c4ec519

Browse files
committed
[IMPL] useSpeechSynthesis hook
1 parent f847a27 commit c4ec519

12 files changed

Lines changed: 552 additions & 30 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { useState } from "react";
2+
import { LanguageBCP47Tags, useSpeechSynthesis } from "../../../../../../packages/react-tools/src"
3+
4+
/**
5+
The component use _useSpeechSynthesis_ hook to read a text from an input text. Other fields are renders to setting properties as _voice_ _rate_ and _pitch_ of SpeechRecognition.
6+
*/
7+
export const UseSpeechSynthesis = () => {
8+
const { state, speak, pause, resume, cancel } = useSpeechSynthesis({
9+
onError(ev) {
10+
console.log(ev)
11+
},
12+
});
13+
const [form, setForm] = useState<{ text: string, voice: string, lang: string, rate: string, pitch: string }>({ text: "", voice: "", lang: "", rate: "1", pitch: "1" });
14+
15+
if (!state.isSupported) {
16+
return <div style={{ display: "flex", justifyContent: "center" }}>
17+
<p>Speech Synthesis not supported</p>
18+
</div>
19+
}
20+
21+
return <div style={{ display: "flex", flexDirection: "column", gap: 10, justifyContent: "center", width: 'fit-content' }}>
22+
<div>
23+
<label htmlFor="text">Text to speak</label>
24+
<input type="text" value={form.text} onChange={e => setForm(f => ({...f, text: e.target.value}))} id="text" name="text"/>
25+
</div>
26+
<div>
27+
<label htmlFor="text">Voices</label>
28+
<select value={form.voice} onChange={e => setForm(f => ({ ...f, voice: e.target.value }))} disabled={state.status === "speaking"}>
29+
<>
30+
{
31+
(state.voices || []).map(el => (
32+
<option key={el.name} value={el.name}>{ `${el.name} - ${el.lang}${el.default ? ' - DEFAULT' : ''}` }</option>
33+
))
34+
}
35+
</>
36+
</select>
37+
</div>
38+
<div>
39+
<label htmlFor="text">Voices</label>
40+
<select value={form.lang} onChange={e => setForm(f => ({ ...f, lang: e.target.value }))} disabled={state.status === "speaking"}>
41+
<>
42+
{
43+
["it-IT", "en-US", "en-GB", "de-DE", "es-ES", "fr-FR"].map(el => (
44+
<option key={el} value={el}>{`${el}`}</option>
45+
))
46+
}
47+
</>
48+
</select>
49+
</div>
50+
<div>
51+
<label htmlFor="range">Rate</label>
52+
<input type="range" id="range" name="range" min="0.1" max="10" step="0.1" value={form.rate} onChange={e => setForm(f => ({...f, rate: e.target.value}))} disabled={state.status === "speaking"}/>
53+
</div>
54+
<div>
55+
<label htmlFor="pitch">Pitch</label>
56+
<input type="range" id="pitch" name="pitch" value={form.pitch} min="0" max="2" step="0.1" onChange={e => setForm(f => ({ ...f, pitch: e.target.value }))} disabled={state.status === "speaking"}/>
57+
</div>
58+
<div style={{ display: "flex", gap: 10 }}>
59+
<button
60+
type="button"
61+
onClick={() => {
62+
speak({
63+
text: form.text,
64+
lang: form.lang as LanguageBCP47Tags,
65+
voice: (state.voices || []).filter(v => v.name === form.voice)[0],
66+
rate: Number(form.rate),
67+
pitch: Number(form.pitch),
68+
})
69+
}}
70+
>
71+
Speak
72+
</button>
73+
<button
74+
type="button"
75+
onClick={() => {
76+
state.status === "paused" ? resume() : pause();
77+
}}
78+
disabled={!["speaking", "paused"].includes(state.status)}
79+
>
80+
{state.status === "paused" ? "Resume" : "Pause"}
81+
</button>
82+
<button
83+
type="button"
84+
onClick={cancel}
85+
disabled={state.status === "end"}
86+
>
87+
Cancel
88+
</button>
89+
</div>
90+
</div>
91+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ export const COMPONENTS = [
9898
"useVibrate",
9999
"useBluetooth",
100100
"useScreenWakeLock",
101-
"useSpeechRecognition"
101+
"useSpeechRecognition",
102+
"useSpeechSynthesis"
102103
]
103104
],
104105
//UTILS

apps/react-tools-demo/src/markdown/useSpeechRecognition.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,20 @@ export const UseSpeechRecognition = () => {
1212
const perform = usePerformAction(() => btnRef.current?.focus());
1313

1414
const [message, setMessage] = useState("Ready");
15-
const [count, setCount] = useState(0);
1615

1716
const [state, start, stop] = useSpeechRecognition({
1817
onStart: useCallback(() => {
19-
console.log("onStart");
20-
setMessage("Listening... " + count)
21-
}, [count]),
22-
onSpeechEnd: () => {
23-
console.log("onSpeechEnd");
18+
setMessage("Listening...")
19+
}, []),
20+
onEnd: useCallback(() => {
2421
stop();
2522
setMessage("Finish");
2623
perform();
27-
},
24+
}, [perform]),
2825
onNoMatch: useCallback(() => {
29-
console.log("onNoMatch");
3026
setMessage("Color not recognized.")
3127
}, []),
3228
onError: useCallback((ev: SpeechRecognitionErrorEvent) => {
33-
console.log("onError");
3429
setMessage(`Error occurred in recognition: ${ev.message ? ev.message : ev.error}`);
3530
}, []),
3631
});
@@ -78,7 +73,6 @@ export const UseSpeechRecognition = () => {
7873
</div>
7974
<div style={{ display: 'flex', justifyContent: "center", gap: 10 }}>
8075
<button ref={btnRef} onClick={onStart} disabled={state.isListening}>Start</button>
81-
<button onClick={()=>setCount(c=>c+1)}>Increment</button>
8276
</div>
8377
</>
8478
: <p>Speech Recognition not supported</p>
@@ -93,13 +87,15 @@ export const UseSpeechRecognition = () => {
9387
## API
9488

9589
```tsx
96-
useSpeechRecognition({ defaultConfig, onAudioStart, onAudioEnd, onEnd, onError, onNoMatch, onResult, onSoundStart, onSoundEnd, onSpeechStart, onSpeechEnd, onStart }: { defaultConfig?: SpeechRecognitionConfig, onAudioStart?: SpeechRecognition["onaudiostart"], onAudioEnd?: SpeechRecognition["onaudioend"], onEnd?: SpeechRecognition["onend"], onError?: SpeechRecognition["onerror"], onNoMatch?: SpeechRecognition["onnomatch"], onResult?: SpeechRecognition["onresult"], onSoundStart?: SpeechRecognition["onsoundstart"], onSoundEnd?: SpeechRecognition["onsoundend"], onSpeechStart?: SpeechRecognition["onspeechstart"], onSpeechEnd?: SpeechRecognition["onspeechend"], onStart?: SpeechRecognition["onstart"] }): [SpeechRecognitionState, (config?: SpeechRecognitionConfig) => void, () => void, (resultAlso?: boolean) => void]
90+
useSpeechRecognition({ alreadyStarted, defaultConfig, onAudioStart, onAudioEnd, onEnd, onError, onNoMatch, onResult, onSoundStart, onSoundEnd, onSpeechStart, onSpeechEnd, onStart }: UseSpeechRecognitionProps): [SpeechRecognitionState, (config?: SpeechRecognitionConfig) => void, () => void, (resultAlso?: boolean) => void]
9791
```
9892
9993
> ### Params
10094
>
101-
> - __opts__: _Object_
95+
> - __opts__: _UseSpeechRecognitionProps_
10296
options.
97+
> - __opts.alreadyStarted=false?__: _boolean_
98+
istant start SpeechRecognition if it is available.
10399
> - __opts.defaultConfig?__: _Object_
104100
config parameters for current SpeechRecognition.
105101
> - __opts.defaultConfig.grammars?__: _SpeechGrammarList_
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# useSpeechSynthesis
2+
Hook to use _SpeechSynthesis API_. Refer to [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseSpeechSynthesis = () => {
8+
const { state, speak, pause, resume, cancel } = useSpeechSynthesis({
9+
onError(ev) {
10+
console.log(ev)
11+
},
12+
});
13+
const [form, setForm] = useState<{ text: string, voice: string, lang: string, rate: string, pitch: string }>({ text: "", voice: "", lang: "", rate: "1", pitch: "1" });
14+
15+
if (!state.isSupported) {
16+
return <div style={{ display: "flex", justifyContent: "center" }}>
17+
<p>Speech Synthesis not supported</p>
18+
</div>
19+
}
20+
21+
return <div style={{ display: "flex", flexDirection: "column", gap: 10, justifyContent: "center", width: 'fit-content' }}>
22+
<div>
23+
<label htmlFor="text">Text to speak</label>
24+
<input type="text" value={form.text} onChange={e => setForm(f => ({...f, text: e.target.value}))} id="text" name="text"/>
25+
</div>
26+
<div>
27+
<label htmlFor="text">Voices</label>
28+
<select value={form.voice} onChange={e => setForm(f => ({ ...f, voice: e.target.value }))} disabled={state.status === "speaking"}>
29+
<>
30+
{
31+
(state.voices || []).map(el => (
32+
<option key={el.name} value={el.name}>{ `${el.name} - ${el.lang}${el.default ? ' - DEFAULT' : ''}` }</option>
33+
))
34+
}
35+
</>
36+
</select>
37+
</div>
38+
<div>
39+
<label htmlFor="text">Voices</label>
40+
<select value={form.lang} onChange={e => setForm(f => ({ ...f, lang: e.target.value }))} disabled={state.status === "speaking"}>
41+
<>
42+
{
43+
["it-IT", "en-US", "en-GB", "de-DE", "es-ES", "fr-FR"].map(el => (
44+
<option key={el} value={el}>{`${el}`}</option>
45+
))
46+
}
47+
</>
48+
</select>
49+
</div>
50+
<div>
51+
<label htmlFor="range">Rate</label>
52+
<input type="range" id="range" name="range" min="0.1" max="10" step="0.1" value={form.rate} onChange={e => setForm(f => ({...f, rate: e.target.value}))} disabled={state.status === "speaking"}/>
53+
</div>
54+
<div>
55+
<label htmlFor="pitch">Pitch</label>
56+
<input type="range" id="pitch" name="pitch" value={form.pitch} min="0" max="2" step="0.1" onChange={e => setForm(f => ({ ...f, pitch: e.target.value }))} disabled={state.status === "speaking"}/>
57+
</div>
58+
<div style={{ display: "flex", gap: 10 }}>
59+
<button
60+
type="button"
61+
onClick={() => {
62+
speak({
63+
text: form.text,
64+
lang: form.lang as LanguageBCP47Tags,
65+
voice: (state.voices || []).filter(v => v.name === form.voice)[0],
66+
rate: Number(form.rate),
67+
pitch: Number(form.pitch),
68+
})
69+
}}
70+
>
71+
Speak
72+
</button>
73+
<button
74+
type="button"
75+
onClick={() => {
76+
state.status === "paused" ? resume() : pause();
77+
}}
78+
disabled={!["speaking", "paused"].includes(state.status)}
79+
>
80+
{state.status === "paused" ? "Resume" : "Pause"}
81+
</button>
82+
<button
83+
type="button"
84+
onClick={cancel}
85+
disabled={state.status === "end"}
86+
>
87+
Cancel
88+
</button>
89+
</div>
90+
</div>
91+
}
92+
```
93+
94+
> The component use _useSpeechSynthesis_ hook to read a text from an input text. Other fields are renders to setting properties as _voice_ _rate_ and _pitch_ of SpeechRecognition.
95+
96+
97+
## API
98+
99+
```tsx
100+
useSpeechSynthesis(opts?: UseSpeechSynthesisProps): ReturnType<UseSpeechSynthesis>
101+
```
102+
103+
> ### Params
104+
>
105+
> - __opts?__: _UseSpeechSynthesisProps_
106+
options.
107+
> - __opts.onSpeak?__: _() => void_
108+
function that will be executed when _speak_ event is fired.
109+
> - __opts.onStart?__: _SpeechSynthesisUtterance["onstart"]_
110+
function that will be executed when _start_ event is fired.
111+
> - __opts.onPause?__: _SpeechSynthesisUtterance["onpause"]_
112+
function that will be executed when _pause_ event is fired.
113+
> - __opts.onResume?__: _SpeechSynthesisUtterance["onresume"]_
114+
function that will be executed when _resume_ event is fired.
115+
> - __opts.onBoundary?__: _SpeechSynthesisUtterance["onboundary"]_
116+
function that will be executed when _boundary_ event is fired.
117+
> - __opts.onMark?__: _SpeechSynthesisUtterance["onmark"]_
118+
function that will be executed when _mark_ event is fired.
119+
> - __opts.onError?__: _SpeechSynthesisUtterance["onerror"]_
120+
function that will be executed when _error_ event is fired.
121+
> - __opts.onEnd?__: _SpeechSynthesisUtterance["onend"]_
122+
function that will be executed when _end_ event is fired.
123+
> - __opts.onCancel?__: _SpeechSynthesisonCancel_
124+
function that will be executed when _cancel_ event is fired.
125+
> - __opts.lang?__: _LanguageBCP47Tags_
126+
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/lang.
127+
> - __opts.pitch?__: _SpeechSynthesisUtterance["pitch"]_
128+
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/pitch).
129+
> - __opts.rate?__: _SpeechSynthesisUtterance["rate"]_
130+
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/rate).
131+
> - __opts.voice?__: _SpeechSynthesisUtterance["voice"]_
132+
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/voice).
133+
> - __opts.volume?__: _SpeechSynthesisUtterance["volume"]_
134+
[MDN Reference](https://developer.mozilla.org/docs/Web/API/SpeechSynthesisUtterance/volume).
135+
>
136+
137+
> ### Returns
138+
>
139+
> __return__: _ReturnType<UseSpeechSynthesis>_
140+
> - __state__: object with these properties:
141+
> - _isSupported_: Returns a boolean value indicating SpeechSynthesis availability.
142+
> - _status_: Returns the current status of SpeechSynthesis.
143+
> - _voices_: Returns the list of available voices.
144+
> - __speak__: Function to start speaking.
145+
> - __pause__: Function to keep in pause speaking.
146+
> - __resume__: Function to resume speaking.
147+
> - __cancel__: Function to cancel speaking.
148+
>

packages/react-tools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
- [x] useBluetooth
101101
- [x] useScreenWakeLock
102102
- [x] useSpeechRecognition
103-
- [ ] useSpeechSynthesis (https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance#examples)
103+
- [x] useSpeechSynthesis
104104
- [ ] useDevicesList (https://vueuse.org/core/useDevicesList/)
105105
- [ ] useFPS (https://vueuse.org/core/useFps/)
106106
- [ ] useParallax (https://vueuse.org/core/useParallax/)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,5 @@ export { useVibrate } from './useVibrate';
7878
export { useDerivedState } from './useDerivedState';
7979
export { useBluetooth } from './useBluetooth';
8080
export { useScreenWakeLock } from './useScreenWakeLock';
81-
export { useSpeechRecognition } from './useSpeechRecognition';
81+
export { useSpeechRecognition } from './useSpeechRecognition';
82+
export { useSpeechSynthesis } from './useSpeechSynthesis';

0 commit comments

Comments
 (0)