@untemps/react-vocal
Advanced tools
Comparing version 1.6.8 to 1.7.0
@@ -0,1 +1,8 @@ | ||
# [1.7.0](https://github.com/untemps/react-vocal/compare/v1.6.8...v1.7.0) (2021-07-16) | ||
### Features | ||
* Add commands support ([#58](https://github.com/untemps/react-vocal/issues/58)) ([12e086d](https://github.com/untemps/react-vocal/commit/12e086d2308fff738de8a0b370108432f0df3265)), closes [#60](https://github.com/untemps/react-vocal/issues/60) | ||
## [1.6.8](https://github.com/untemps/react-vocal/compare/v1.6.7...v1.6.8) (2021-07-03) | ||
@@ -2,0 +9,0 @@ |
@@ -15,3 +15,3 @@ import babel from '@rollup/plugin-babel' | ||
replace({ | ||
'process.env.NODE_ENV': JSON.stringify('production'), | ||
'process.env.NODE_ENV': JSON.stringify('development'), | ||
}), | ||
@@ -18,0 +18,0 @@ babel({ |
@@ -9,2 +9,3 @@ import 'regenerator-runtime/runtime' | ||
const [logs, setLogs] = useState('') | ||
const [borderColor, setBorderColor] = useState() | ||
@@ -31,4 +32,13 @@ const _log = (value) => setLogs((logs) => `${logs}${logs.length > 0 ? '\n' : ''} ----- ${value}`) | ||
<> | ||
<Vocal onStart={_onVocalStart} onEnd={_onVocalEnd} onResult={_onVocalResult} onError={_onVocalError} /> | ||
<textarea value={logs} rows={30} disabled style={{ width: '100%', marginTop: 16 }} /> | ||
<Vocal | ||
lang="fr" | ||
commands={{ | ||
'Change la bordure en rouge': () => setBorderColor('red'), | ||
}} | ||
onStart={_onVocalStart} | ||
onEnd={_onVocalEnd} | ||
onResult={_onVocalResult} | ||
onError={_onVocalError} | ||
/> | ||
<textarea value={logs} rows={30} disabled style={{ width: '100%', marginTop: 16, borderColor }} /> | ||
</> | ||
@@ -35,0 +45,0 @@ ) |
{ | ||
"name": "@untemps/react-vocal", | ||
"version": "1.6.8", | ||
"version": "1.7.0", | ||
"author": "Vincent Le Badezet <v.lebadezet@untemps.net>", | ||
@@ -65,3 +65,4 @@ "repository": "git@github.com:untemps/react-vocal.git", | ||
"dependencies": { | ||
"@untemps/vocal": "^1.3.0" | ||
"@untemps/vocal": "^1.3.0", | ||
"fuse.js": "^6.4.6" | ||
}, | ||
@@ -68,0 +69,0 @@ "jest": { |
350
README.md
@@ -54,27 +54,29 @@ <p align="center"> | ||
const App = () => { | ||
const [result, setResult] = useState('') | ||
const [result, setResult] = useState('') | ||
const _onVocalStart = () => { | ||
setResult('') | ||
} | ||
const _onVocalStart = () => { | ||
setResult('') | ||
} | ||
const _onVocalResult = (result) => { | ||
setResult(result) | ||
} | ||
const _onVocalResult = (result) => { | ||
setResult(result) | ||
} | ||
return ( | ||
<div className="App"> | ||
<span style={{position: 'relative'}}> | ||
<Vocal | ||
onStart={_onVocalStart} | ||
onResult={_onVocalResult} | ||
style={{width: 16, position: 'absolute', right: 10, top: -2}} | ||
/> | ||
<input defaultValue={result} style={{width: 300, height: 40}}/> | ||
</span> | ||
</div> | ||
) | ||
return ( | ||
<div className="App"> | ||
<span style={{ position: 'relative' }}> | ||
<Vocal | ||
onStart={_onVocalStart} | ||
onResult={_onVocalResult} | ||
style={{ width: 16, position: 'absolute', right: 10, top: -2 }} | ||
/> | ||
<input defaultValue={result} style={{ width: 300, height: 40 }} /> | ||
</span> | ||
</div> | ||
) | ||
} | ||
``` | ||
--- | ||
#### Custom component | ||
@@ -84,10 +86,10 @@ | ||
- Idle | ||
![Idle state](assets/icon-idle.png) | ||
- Listening | ||
![Listening state](assets/icon-listening.png) | ||
- Idle | ||
![Idle state](assets/icon-idle.png) | ||
- Listening | ||
![Listening state](assets/icon-listening.png) | ||
But you can provide your own component. | ||
- With a simple React element: | ||
- With a simple React element: | ||
@@ -98,7 +100,7 @@ ```javascript | ||
const App = () => { | ||
return ( | ||
<Vocal> | ||
<button>Start</button> | ||
</Vocal> | ||
) | ||
return ( | ||
<Vocal> | ||
<button>Start</button> | ||
</Vocal> | ||
) | ||
} | ||
@@ -111,3 +113,3 @@ ``` | ||
- With a function that returns a React element: | ||
- With a function that returns a React element: | ||
@@ -117,25 +119,35 @@ ```javascript | ||
const Play = () => <div style={{ | ||
width: 0, | ||
height: 0, | ||
marginLeft: 1, | ||
borderStyle: 'solid', | ||
borderWidth: '4px 0 4px 8px', | ||
borderColor: 'transparent transparent transparent black' | ||
}}/> | ||
const Play = () => ( | ||
<div | ||
style={{ | ||
width: 0, | ||
height: 0, | ||
marginLeft: 1, | ||
borderStyle: 'solid', | ||
borderWidth: '4px 0 4px 8px', | ||
borderColor: 'transparent transparent transparent black', | ||
}} | ||
/> | ||
) | ||
const Stop = () => <div style={{ | ||
width: 8, | ||
height: 8, | ||
backgroundColor: 'black' | ||
}}/> | ||
const Stop = () => ( | ||
<div | ||
style={{ | ||
width: 8, | ||
height: 8, | ||
backgroundColor: 'black', | ||
}} | ||
/> | ||
) | ||
const App = () => { | ||
return ( | ||
<Vocal>{(start, stop, isStarted) => ( | ||
<button style={{padding: 5}} onClick={isStarted ? stop : start}> | ||
{isStarted ? <Stop/> : <Play/>} | ||
</button> | ||
)}</Vocal> | ||
) | ||
return ( | ||
<Vocal> | ||
{(start, stop, isStarted) => ( | ||
<button style={{ padding: 5 }} onClick={isStarted ? stop : start}> | ||
{isStarted ? <Stop /> : <Play />} | ||
</button> | ||
)} | ||
</Vocal> | ||
) | ||
} | ||
@@ -146,24 +158,68 @@ ``` | ||
| Arguments | Type | Description | | ||
| ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------ | | ||
| start | func | The function used to start the recognition | | ||
| stop | func | The function used to stop the recognition | | ||
| isStarted | bool | A flag that indicates whether the recognition is started or not | | ||
| Arguments | Type | Description | | ||
| --------- | ---- | --------------------------------------------------------------- | | ||
| start | func | The function used to start the recognition | | ||
| stop | func | The function used to stop the recognition | | ||
| isStarted | bool | A flag that indicates whether the recognition is started or not | | ||
#### API | ||
--- | ||
| Props | Type | Default | Description | | ||
| ------------- | ----------------- | ------- | -------------------------------------------------------------------------------------------------- | | ||
| lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) | | ||
| grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) | | ||
| timeout | number | 3000 | Time in ms to wait before discarding the recognition | | ||
| style | object | null | Styles of the root element if className is not specified | | ||
#### Commands | ||
The `Vocal` component accepts a `commands` prop to map special recognition results to callbacks. | ||
That means you can define vocal commands to trigger specific functions. | ||
```javascript | ||
const App = () => { | ||
return ( | ||
<Vocal commands={{ | ||
'switch border color': () => setBorderColor('red'), | ||
}}/> | ||
) | ||
} | ||
``` | ||
`commands` object is a key/pair model where the `key` is the command to be caught by the recognition and the `value` is the callback triggered when the command is detected. | ||
`key` is not case sensitive. | ||
```javascript | ||
const commands = { | ||
submit: () => submitForm(), | ||
'Change the background color': () => setBackgroundColor('red'), | ||
'PLAY MUSIC': play | ||
} | ||
``` | ||
The component utilizes a special hook called `useCommands` to respond to the commands. | ||
The hook performs a fuzzy search to match approximate commands if needed. This allows to fix accidental typos or approximate recognition results. | ||
To do so the hook uses [fuse.js](https://fusejs.io/) which implements an algorithm to find strings that are approximately equal to a given input. The score precision that distinguishes acceptable command-to-callback mapping from negative matching can be customized in the hook instantiantion. | ||
```javascript | ||
useCommands(commands, threshold) // threshold is the limit not to exceed to be considered a match | ||
``` | ||
See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for more details. | ||
> :warning: **The `Vocal` component doesn't expose that score yet.** For now on you have to deal with the default value (*0.4*) | ||
--- | ||
#### `Vocal` component API | ||
| Props | Type | Default | Description | | ||
| ------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------- | | ||
| commands | object | null | Callbacks to be triggered when specified commands are detected by the recognition | | ||
| lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) | | ||
| grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) | | ||
| timeout | number | 3000 | Time in ms to wait before discarding the recognition | | ||
| style | object | null | Styles of the root element if className is not specified | | ||
| className | string | null | Class of the root element | | ||
| onStart | func | null | Handler called when the recognition starts | | ||
| onEnd | func | null | Handler called when the recognition ends | | ||
| onSpeechStart | func | null | Handler called when the speech starts | | ||
| onSpeechEnd | func | null | Handler called when the speech ends | | ||
| onResult | func | null | Handler called when a result is recognized | | ||
| onError | func | null | Handler called when an error occurs | | ||
| onNoMatch | func | null | Handler called when no result can be recognized | | ||
| onStart | func | null | Handler called when the recognition starts | | ||
| onEnd | func | null | Handler called when the recognition ends | | ||
| onSpeechStart | func | null | Handler called when the speech starts | | ||
| onSpeechEnd | func | null | Handler called when the speech ends | | ||
| onResult | func | null | Handler called when a result is recognized | | ||
| onError | func | null | Handler called when an error occurs | | ||
| onNoMatch | func | null | Handler called when no result can be recognized | | ||
@@ -175,54 +231,56 @@ ### `useVocal` hook | ||
```javascript | ||
import React, {useState} from 'react' | ||
import {useVocal} from '@untemps/react-vocal' | ||
import React, { useState } from 'react' | ||
import { useVocal } from '@untemps/react-vocal' | ||
import Icon from './Icon' | ||
const App = () => { | ||
const [isListening, setIsListening] = useState(false) | ||
const [result, setResult] = useState('') | ||
const [isListening, setIsListening] = useState(false) | ||
const [result, setResult] = useState('') | ||
const [, {start, subscribe}] = useVocal('fr_FR') | ||
const [, { start, subscribe }] = useVocal('fr_FR') | ||
const _onButtonClick = () => { | ||
setIsListening(true) | ||
const _onButtonClick = () => { | ||
setIsListening(true) | ||
subscribe('speechstart', _onVocalStart) | ||
subscribe('result', _onVocalResult) | ||
subscribe('error', _onVocalError) | ||
start() | ||
} | ||
subscribe('speechstart', _onVocalStart) | ||
subscribe('result', _onVocalResult) | ||
subscribe('error', _onVocalError) | ||
start() | ||
} | ||
const _onVocalStart = () => { | ||
setResult('') | ||
} | ||
const _onVocalStart = () => { | ||
setResult('') | ||
} | ||
const _onVocalResult = (result) => { | ||
setIsListening(false) | ||
const _onVocalResult = (result) => { | ||
setIsListening(false) | ||
setResult(result) | ||
} | ||
setResult(result) | ||
} | ||
const _onVocalError = (e) => { | ||
console.error(e) | ||
} | ||
const _onVocalError = (e) => { | ||
console.error(e) | ||
} | ||
return ( | ||
<div> | ||
<span style={{position: 'relative'}}> | ||
<div | ||
role="button" | ||
aria-label="Vocal" | ||
tabIndex={0} | ||
style={{width: 16, position: 'absolute', right: 10, top: 2}} | ||
onClick={_onButtonClick} | ||
> | ||
<Icon color={isListening ? 'red' : 'blue'}/> | ||
</div> | ||
<input defaultValue={result} style={{width: 300, height: 40}}/> | ||
</span> | ||
</div> | ||
) | ||
return ( | ||
<div> | ||
<span style={{ position: 'relative' }}> | ||
<div | ||
role="button" | ||
aria-label="Vocal" | ||
tabIndex={0} | ||
style={{ width: 16, position: 'absolute', right: 10, top: 2 }} | ||
onClick={_onButtonClick} | ||
> | ||
<Icon color={isListening ? 'red' : 'blue'} /> | ||
</div> | ||
<input defaultValue={result} style={{ width: 300, height: 40 }} /> | ||
</span> | ||
</div> | ||
) | ||
} | ||
``` | ||
--- | ||
#### Signature | ||
@@ -234,7 +292,9 @@ | ||
| Args | Type | Default | Description | | ||
| ------------- | ----------------- | ------- | ---------------------------------------------------------------------------------------------------- | | ||
| lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) | | ||
| grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) | | ||
| Args | Type | Default | Description | | ||
| -------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------- | | ||
| lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) | | ||
| grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) | | ||
--- | ||
#### Return value | ||
@@ -246,11 +306,11 @@ | ||
| Args | Type | Description | | ||
| ------------- | ----------------- | ----------------------------------------------------------- | | ||
| ref | Ref | React ref to the SpeechRecognitionWrapper instance | | ||
| start | func | Function to start the recognition | | ||
| stop | func | Function to stop the recognition | | ||
| abort | func | Function to abort the recognition | | ||
| subscribe | func | Function to subscribe to recognition events | | ||
| unsubscribe | func | Function to unsubscribe to recognition events | | ||
| clean | func | Function to clean subscription to recognition events | | ||
| Args | Type | Description | | ||
| ----------- | ---- | ---------------------------------------------------- | | ||
| ref | Ref | React ref to the SpeechRecognitionWrapper instance | | ||
| start | func | Function to start the recognition | | ||
| stop | func | Function to stop the recognition | | ||
| abort | func | Function to abort the recognition | | ||
| subscribe | func | Function to subscribe to recognition events | | ||
| unsubscribe | func | Function to unsubscribe to recognition events | | ||
| clean | func | Function to clean subscription to recognition events | | ||
@@ -262,10 +322,6 @@ ### Browser support flag | ||
```javascript | ||
import Vocal, {isSupported} from '@untemps/react-vocal' | ||
import Vocal, { isSupported } from '@untemps/react-vocal' | ||
const App = () => { | ||
return isSupported ? ( | ||
<Vocal/> | ||
) : ( | ||
<p>Your browser does not support Web Speech API</p> | ||
) | ||
return isSupported ? <Vocal /> : <p>Your browser does not support Web Speech API</p> | ||
} | ||
@@ -276,15 +332,15 @@ ``` | ||
| Events | Description | | ||
| ------------- | --------------------------------------------------------------------------------------------------- | | ||
| audioend | Fired when the user agent has finished capturing audio for recognition | | ||
| audiostart | Fired when the user agent has started to capture audio for recognition | | ||
| end | Fired when the recognition service has disconnected | | ||
| error | Fired when a recognition error occurs | | ||
| nomatch | Fired when the recognition service returns a final result with no significant recognition | | ||
| result | Fired when the recognition service returns a result | | ||
| soundend | Fired when any sound — recognisable or not — has stopped being detected | | ||
| soundstart | Fired when any sound — recognisable or not — has been detected | | ||
| speechend | Fired when speech recognized by the recognition service has stopped being detected | | ||
| speechstart | Fired when sound recognized by the recognition service as speech has been detected | | ||
| start | fired when the recognition service has begun listening to incoming audio | | ||
| Events | Description | | ||
| ----------- | ----------------------------------------------------------------------------------------- | | ||
| audioend | Fired when the user agent has finished capturing audio for recognition | | ||
| audiostart | Fired when the user agent has started to capture audio for recognition | | ||
| end | Fired when the recognition service has disconnected | | ||
| error | Fired when a recognition error occurs | | ||
| nomatch | Fired when the recognition service returns a final result with no significant recognition | | ||
| result | Fired when the recognition service returns a result | | ||
| soundend | Fired when any sound — recognisable or not — has stopped being detected | | ||
| soundstart | Fired when any sound — recognisable or not — has been detected | | ||
| speechend | Fired when speech recognized by the recognition service has stopped being detected | | ||
| speechstart | Fired when sound recognized by the recognition service as speech has been detected | | ||
| start | fired when the recognition service has begun listening to incoming audio | | ||
@@ -308,12 +364,12 @@ ### Notes | ||
- Fork the repository | ||
- Create a feature branch (preferred name convention: `[feature type]_[imperative verb]-[description of the feature]`) | ||
- Develop the feature AND write the tests (or write the tests AND develop the feature) | ||
- Commit your changes | ||
using [Angular Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) | ||
- Submit a Pull Request | ||
- Fork the repository | ||
- Create a feature branch (preferred name convention: `[feature type]_[imperative verb]-[description of the feature]`) | ||
- Develop the feature AND write the tests (or write the tests AND develop the feature) | ||
- Commit your changes | ||
using [Angular Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) | ||
- Submit a Pull Request | ||
## Roadmap | ||
- Add a connector management to plug external speech-to-text services in | ||
- Support continuous speech | ||
- Add a connector management to plug external speech-to-text services in | ||
- Support continuous speech |
@@ -130,2 +130,23 @@ import React from 'react' | ||
it('responds to command', async () => { | ||
const callback = jest.fn() | ||
const recognition = new SpeechRecognitionWrapper() | ||
const commands = { foo: callback } | ||
const { getByTestId } = render(getInstance({ __rsInstance: recognition, commands })) | ||
let flag = false | ||
recognition.addEventListener('start', async () => { | ||
flag = true | ||
}) | ||
await act(async () => { | ||
fireEvent.click(getByTestId('__vocal-root__')) | ||
await waitFor(() => flag) | ||
recognition.instance.say('Foo') | ||
await waitFor(() => expect(callback).toHaveBeenCalledWith('Foo')) | ||
}) | ||
}) | ||
it('triggers onStart handler', async () => { | ||
@@ -132,0 +153,0 @@ const onStart = jest.fn() |
@@ -9,2 +9,3 @@ import React, { cloneElement, isValidElement, useRef, useState } from 'react' | ||
import useTimeout from '../hooks/useTimeout' | ||
import useCommands from '../hooks/useCommands' | ||
@@ -15,2 +16,3 @@ import Icon from './Icon' | ||
children, | ||
commands, | ||
lang, | ||
@@ -36,2 +38,3 @@ grammars, | ||
const [, { start, stop, subscribe, unsubscribe }] = useVocal(lang, grammars, __rsInstance) | ||
const triggerCommand = useCommands(commands) | ||
@@ -121,2 +124,4 @@ const _onEnd = (e) => { | ||
triggerCommand(result) | ||
!!onResult && onResult(result, event) | ||
@@ -185,2 +190,4 @@ } | ||
Vocal.propTypes = { | ||
/** Defines callbacks to be triggered when keys are detected by the recognition */ | ||
commands: PropTypes.objectOf(PropTypes.func), | ||
/** Defines the language understood by the recognition (https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/lang) */ | ||
@@ -217,2 +224,3 @@ lang: PropTypes.string, | ||
Vocal.defaultProps = { | ||
commands: null, | ||
lang: 'en-US', | ||
@@ -219,0 +227,0 @@ grammars: null, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1008251
40
4680
364
4
+ Addedfuse.js@^6.4.6
+ Addedfuse.js@6.6.2(transitive)