Skip to content

Commit ca0b7d0

Browse files
committed
[WIP] useSpeechRecognition hook
1 parent 77f91be commit ca0b7d0

16 files changed

Lines changed: 568 additions & 18 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useCallback, useMemo, useRef, useState } from "react";
2+
import { SpeechGrammarList, SpeechRecognitionErrorEvent, usePerformAction, useSpeechRecognition } from "../../../../../../packages/react-tools/src"
3+
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
const SpeechGrammarListP = ((window as any).SpeechGrammarList || (window as any).webkitSpeechGrammarList);
6+
7+
/**
8+
This component use _useSpeechRecognition_ hook to simulate a Speech color change app. When button _start_ is clicked, you can say an HTML color keyword and the bordered div color will change to that color.
9+
*/
10+
export const UseSpeechRecognition = () => {
11+
const colors = ['aqua', 'azure', 'beige', 'bisque', 'black', 'blue', 'brown', 'chocolate', 'coral', 'crimson', 'cyan', 'fuchsia', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'indigo', 'ivory', 'khaki', 'lavender', 'lime', 'linen', 'magenta', 'maroon', 'moccasin', 'navy', 'olive', 'orange', 'orchid', 'peru', 'pink', 'plum', 'purple', 'red', 'salmon', 'sienna', 'silver', 'snow', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'white', 'yellow', 'transparent']
12+
const grammar = `#JSGF V1.0; grammar colors; public <color> = ${colors.join(' | ')} ;`
13+
14+
const btnRef = useRef<HTMLButtonElement>(null);
15+
const perform = usePerformAction(() => btnRef.current?.focus());
16+
17+
const [message, setMessage] = useState("Ready");
18+
19+
const [state, start, stop] = useSpeechRecognition({
20+
onStart: useCallback(() => {
21+
console.log("onStart");
22+
setMessage("Listening...")
23+
}, []),
24+
onSpeechEnd: () => {
25+
console.log("onSpeechEnd");
26+
stop();
27+
setMessage("Finish");
28+
perform();
29+
},
30+
onNoMatch: useCallback(() => {
31+
console.log("onNoMatch");
32+
setMessage("Color not recognized.")
33+
}, []),
34+
onError: useCallback((ev: SpeechRecognitionErrorEvent) => {
35+
console.log("onError");
36+
setMessage(`Error occurred in recognition: ${ev.message ? ev.message : ev.error}`);
37+
}, []),
38+
});
39+
40+
const onStart = () => {
41+
const grammars = new SpeechGrammarListP() as SpeechGrammarList;
42+
grammars.addFromString(grammar, 1);
43+
start({
44+
lang: "en-US",
45+
continuous: false,
46+
interimResults: false,
47+
maxAlternatives: 1,
48+
grammars
49+
});
50+
}
51+
52+
const color = useMemo(() => {
53+
let colr = "transparent";
54+
if (state.result.results) {
55+
const color = state.result.results[0][0].transcript;
56+
if (colors.includes(color)) {
57+
colr = color;
58+
}
59+
}
60+
return colr;
61+
}, [state.result.results, colors]);
62+
63+
return <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", gap: 10 }}>
64+
{
65+
state.isSupported
66+
? <>
67+
<div style={{ display: 'flex', flexDirection: 'column' }}>
68+
<p>Click on start to say a color to change backgroundColor of bordered div. Try</p>
69+
<div style={{ display: 'flex', flexWrap: "wrap", gap: 10 }}>
70+
{
71+
colors.map(el => <span key={el} style={{ color: el }}>{el}</span>)
72+
}
73+
</div>
74+
</div>
75+
<p>{message}</p>
76+
<div style={{ border: "1px solid lightgray", width: 300, height: 150, backgroundColor: color, margin: '0 auto' }}>
77+
{
78+
state.result && <p>Color is: {color}</p>
79+
}
80+
</div>
81+
<div style={{ display: 'flex', justifyContent: "center", gap: 10 }}>
82+
<button ref={btnRef} onClick={onStart} disabled={state.isListening}>Start</button>
83+
</div>
84+
</>
85+
: <p>Speech Recognition not supported</p>
86+
}
87+
</div>
88+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ export const COMPONENTS = [
9797
"useDeviceOrientation",
9898
"useVibrate",
9999
"useBluetooth",
100-
"useScreenWakeLock"
100+
"useScreenWakeLock",
101+
"useSpeechRecognition"
101102
]
102103
],
103104
//UTILS

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function that will be executed on release event.
5050
- _()=>Promise<void>_
5151
> - 1. __info__: object with these properties:
5252
> - _isSupported_: returns a boolean to know if API is available.
53-
> - _type_: return a string representation of the currently acquired WakeLock type.
53+
> - _type_: returns a string representation of the currently acquired WakeLock type.
5454
> - _isActive_: returns a boolean indicating whether the WakeLockSentinel has been activated.
5555
> - 2. __acquire__: function to request a WakeLock.
5656
> - 3. __release__: function to release a WakeLock.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# useSpeechRecognition
2+
Hook to use _SpeechRecognition API_. Refer to [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseSpeechRecognition = () => {
8+
const colors = ['aqua', 'azure', 'beige', 'bisque', 'black', 'blue', 'brown', 'chocolate', 'coral', 'crimson', 'cyan', 'fuchsia', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'indigo', 'ivory', 'khaki', 'lavender', 'lime', 'linen', 'magenta', 'maroon', 'moccasin', 'navy', 'olive', 'orange', 'orchid', 'peru', 'pink', 'plum', 'purple', 'red', 'salmon', 'sienna', 'silver', 'snow', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'white', 'yellow', 'transparent']
9+
const grammar = `#JSGF V1.0; grammar colors; public <color> = ${colors.join(' | ')} ;`
10+
11+
const btnRef = useRef<HTMLButtonElement>();
12+
const perform = usePerformAction(() => btnRef.current?.focus());
13+
14+
const [message, setMessage] = useState("Ready");
15+
16+
const [state, start, stop] = useSpeechRecognition({
17+
onStart: useCallback(() => {
18+
console.log("onStart");
19+
setMessage("Listening...")
20+
}, []),
21+
onEnd: useCallback(() => {
22+
console.log("onEnd");
23+
setMessage("Finish");
24+
}, []),
25+
onAudioEnd: () => {
26+
console.log("onAudioEnd");
27+
},
28+
onAudioStart: () => {
29+
console.log("onAudioStart");
30+
},
31+
onSoundStart: () => {
32+
console.log("onSoundStart");
33+
},
34+
onSoundEnd: () => {
35+
console.log("onSoundEnd");
36+
},
37+
onSpeechStart: () => {
38+
console.log("onSpeechStart");
39+
},
40+
onSpeechEnd: () => {
41+
console.log("onSpeechEnd");
42+
},
43+
onNoMatch: useCallback(() => {
44+
console.log("onNoMatch");
45+
setMessage("Color not recognized.")
46+
}, []),
47+
onResult: useCallback(() => {
48+
console.log("onResult");
49+
stop();
50+
setMessage("Ready");
51+
perform();
52+
}, []),
53+
onError: useCallback((ev: SpeechRecognitionErrorEvent) => {
54+
console.log("onError");
55+
setMessage(`Error occurred in recognition: ${ev.message ? ev.message : ev.error}`);
56+
}, []),
57+
});
58+
59+
const onStart = () => {
60+
const grammars = new SpeechGrammarListP() as SpeechGrammarList;
61+
grammars.addFromString(grammar, 1);
62+
start({
63+
lang: "en-US",
64+
continuous: true,
65+
interimResults: false,
66+
maxAlternatives: 1,
67+
grammars
68+
});
69+
}
70+
71+
const color = useMemo(() => {
72+
let colr = "transparent";
73+
if (state.result.results) {
74+
const color = state.result.results[0][0].transcript;
75+
if (colors.includes(color)) {
76+
colr = color;
77+
}
78+
}
79+
return colr;
80+
}, [state.result, colors]);
81+
82+
return <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", gap: 10 }}>
83+
{
84+
state.isSupported
85+
? <>
86+
<div style={{ display: 'flex', flexDirection: 'column' }}>
87+
<p>Click on start to say a color to change backgroundColor of bordered div. Try</p>
88+
<div style={{ display: 'flex', flexWrap: "wrap", gap: 10 }}>
89+
{
90+
colors.map(el => <span key={el} style={{ color: el }}>{el}</span>)
91+
}
92+
</div>
93+
</div>
94+
<p>{message}</p>
95+
<div style={{ border: "1px solid lightgray", width: 300, height: 150, backgroundColor: color, margin: '0 auto' }}>
96+
{
97+
state.result && <p>Color is: {color}</p>
98+
}
99+
</div>
100+
<div style={{ display: 'flex', justifyContent: "center", gap: 10 }}>
101+
<button ref={btnRef} onClick={onStart} disabled={state.isListening}>Start</button>
102+
</div>
103+
</>
104+
: <p>Speech Recognition not supported</p>
105+
}
106+
</div>
107+
}
108+
```
109+
110+
> This component use _useSpeechRecognition_ hook to simulate a Speech color change app. When button _start_ is clicked, you can say an HTML color keyword and the bordered div color will change to that color.
111+
112+
113+
## API
114+
115+
```tsx
116+
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"] }): [{isSupported: boolean, isListening: boolean, result: {results: SpeechRecognitionEvent["results"]|null, resultIndex: SpeechRecognitionEvent["resultIndex"]|null}}, (config?: SpeechRecognitionConfig)=>void, ()=>void]
117+
```
118+
119+
> ### Params
120+
>
121+
> - __opts__: _Object_
122+
options.
123+
> - __opts.defaultConfig?__: _Object_
124+
config parameters for current SpeechRecognition.
125+
> - __opts.defaultConfig.grammars?__: _SpeechGrammarList_
126+
a _SpeechGrammarList_ containing the SpeechGrammar objects that represent your grammar for your app.
127+
> - __opts.defaultConfig.lang?__: _LanguageBCP47Tags_
128+
a string representing the BCP 47 language tag for the current SpeechRecognition.
129+
> - __opts.defaultConfig.continuous?__: _boolean_
130+
a boolean value representing the current SpeechRecognition's continuous status. true means continuous, and false means not continuous (single result each time.).
131+
> - __opts.defaultConfig.interimResults?__: _boolean_
132+
a boolean value representing the state of the current SpeechRecognition's interim results. true means interim results are returned, and false means they aren't.
133+
> - __opts.defaultConfig.maxAlternatives?__: _number_
134+
a number representing the maximum returned alternatives for each result.
135+
> - __opts.onAudioStart?__: _((this: SpeechRecognition, ev: Event) => void) | null_
136+
function that will be executed when _audiostart_ event is dispatched.
137+
> - __opts.onAudioEnd?__: _((this: SpeechRecognition, ev: Event) => void) | null_
138+
function that will be executed when _audioend_ event is dispatched.
139+
> - __opts.onEnd?__: _((this: SpeechRecognition, ev: Event) => void) | null_
140+
function that will be executed when _end_ event is dispatched.
141+
> - __opts.onError?__: _((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null_
142+
function that will be executed when _error_ event is dispatched.
143+
> - __opts.onNoMatch?__: _((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null_
144+
function that will be executed when _nomatch_ event is dispatched.
145+
> - __opts.onResult?__: _((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null_
146+
function that will be executed when _result_ event is dispatched.
147+
> - __opts.onSoundStart?__: _((this: SpeechRecognition, ev: Event) => void) | null_
148+
function that will be executed when _soundstart_ event is dispatched.
149+
> - __opts.onSoundEnd?__: _((this: SpeechRecognition, ev: Event) => void) | null_
150+
function that will be executed when _soundend_ event is dispatched.
151+
> - __opts.onSpeechStart?__: _((this: SpeechRecognition, ev: Event) => void) | null_
152+
function that will be executed when _speechstart_ event is dispatched.
153+
> - __opts.onSpeechEnd?__: _((this: SpeechRecognition, ev: Event) => void) | null_
154+
function that will be executed when _speechend_ event is dispatched.
155+
> - __opts.onStart?__: _((this: SpeechRecognition, ev: Event) => void) | null_
156+
function that will be executed when _start_ event is dispatched.
157+
>
158+
159+
> ### Returns
160+
>
161+
> __result__: __Array__:
162+
- __Object__:
163+
- __isSupported__ : _boolean_
164+
- __isListening__ : _boolean_
165+
- _result:{results: SpeechRecognitionEvent["results"]|null, resultIndex:SpeechRecognitionEvent["resultIndex"]|null}_
166+
- _(config?: SpeechRecognitionConfig)=>void_
167+
- _()=>void_
168+
> - 1. __state__: object with these properties:
169+
> - _isSupported_: returns a boolean to know if API is available.
170+
> - _isListening_: returns a boolean indicating current SpeechRecognition execution or not.
171+
> - _result_: returns result of SpeechRecognition execution.
172+
> - 2. __start__: function to start SpeechRecognition.
173+
> - 3. __stop__: function to stop SpeechRecognition.
174+
>

packages/react-tools/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@
9999
- [x] useVibrate (??? mobile)
100100
- [x] useBluetooth
101101
- [x] useScreenWakeLock
102-
- [ ] useSpeech (https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance#examples)
102+
- [x] useSpeechRecognition
103+
- [ ] useSpeechSynthesis (https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance#examples)
103104
- [ ] useDevicesList (https://vueuse.org/core/useDevicesList/)
104105
- [ ] useFPS (https://vueuse.org/core/useFps/)
105106
- [ ] 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
@@ -77,4 +77,5 @@ export { useDeviceOrientation } from './useDeviceOrientation';
7777
export { useVibrate } from './useVibrate';
7878
export { useDerivedState } from './useDerivedState';
7979
export { useBluetooth } from './useBluetooth';
80-
export { useScreenWakeLock } from './useScreenWakeLock';
80+
export { useScreenWakeLock } from './useScreenWakeLock';
81+
export { useSpeechRecognition } from './useSpeechRecognition';

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const handler = (evt: DeviceMotionEvent) => listeners.forEach(l => l(evt));
1111
*/
1212
export const useDeviceMotion = (): DeviceMotionProps => {
1313
const prev = useRef<DeviceMotionProps>({
14-
isSupported: "DeviceMotionEvent" in window,
14+
isSupported: !!window && "DeviceMotionEvent" in window,
1515
acceleration: null,
1616
accelerationIncludingGravity: null,
1717
interval: null,
@@ -33,12 +33,12 @@ export const useDeviceMotion = (): DeviceMotionProps => {
3333
};
3434
notif();
3535
};
36-
if ("DeviceMotionEvent" in window) {
36+
if (!!window && "DeviceMotionEvent" in window) {
3737
listeners.add(listener);
3838
listeners.size === 1 && addEventListener("devicemotion", handler);
3939
}
4040
return () => {
41-
if ("DeviceMotionEvent" in window) {
41+
if (!!window && "DeviceMotionEvent" in window) {
4242
listeners.delete(listener);
4343
listeners.size === 0 && window.removeEventListener("devicemotion", handler)
4444
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const handler = (evt: DeviceOrientationEvent) => listeners.forEach(l => l(evt));
1111
*/
1212
export const useDeviceOrientation = (): DeviceOrientationProps => {
1313
const prev = useRef<DeviceOrientationProps>({
14-
isSupported: "DeviceOrientationEvent" in window,
14+
isSupported: !!window && "DeviceOrientationEvent" in window,
1515
absolute: null,
1616
alpha: null,
1717
beta: null,
@@ -33,12 +33,12 @@ export const useDeviceOrientation = (): DeviceOrientationProps => {
3333
};
3434
notif();
3535
};
36-
if ("DeviceOrientationEvent" in window) {
36+
if (!!window && "DeviceOrientationEvent" in window) {
3737
listeners.add(listener);
3838
listeners.size === 1 && addEventListener("deviceorientation", handler);
3939
}
4040
return () => {
41-
if ("DeviceOrientationEvent" in window) {
41+
if (!!window && "DeviceOrientationEvent" in window) {
4242
listeners.delete(listener);
4343
listeners.size === 0 && window.removeEventListener("deviceorientation", handler)
4444
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { useCallback } from "react";
88
* @returns {{isSupported: boolean, open: (signal?: AbortSignal) => Promise<`#${string}`>|Promise<void>}} result - __isSupported__ to known if EyeDropper API is supported and __share__ function to use EyeDropper API.
99
*/
1010
export const useEyeDropper = ({ onStart, onFinish }: { onStart?: () => void, onFinish?: (result: `#${string}`) => void } = {}) => {
11-
const isSupported = window !== undefined && "EyeDropper" in window;
11+
const isSupported = !!window && "EyeDropper" in window;
1212

1313
const open = useCallback((signal?: AbortSignal) => {
14-
if ("EyeDropper" in window) {
14+
if (!!window && "EyeDropper" in window) {
1515
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1616
const eyeDropper = new (window as any).EyeDropper();
1717
return Promise.resolve(onStart && onStart())

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const useScreen = (allScreen?:boolean): [ScreenDetails, (orientation: Ori
4848
let pending = false;
4949
if (listeners.size === 1) {
5050
screen.orientation.onchange = listener;
51-
if ("getScreenDetails" in window) {
51+
if (!!window && "getScreenDetails" in window) {
5252
pending = true;
5353
(window.getScreenDetails as () => Promise<ScreenDetailsEvt>)()
5454
.then(screensDetails => {

0 commit comments

Comments
 (0)