Skip to content

Commit

Permalink
Merge pull request #54 from JamesBrill/reset-transcript-in-command-ca…
Browse files Browse the repository at this point in the history
…llback

Reset transcript in command callback
  • Loading branch information
JamesBrill authored Aug 14, 2020
2 parents 2568bb6 + e286b4e commit af3d083
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 31 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This version requires React 16.8 so that React hooks can be used. If you're used
* [Supported browsers](#supported-browsers)
* [API docs](docs/API.md)
* [Version 3 migration guide](docs/V3-MIGRATION.md)
* [TypeScript declaration file in DefinitelyTyped](https://github.com/OleksandrYehorov/DefinitelyTyped/blob/master/types/react-speech-recognition/index.d.ts)

## Installation

Expand Down Expand Up @@ -132,12 +133,14 @@ const { resetTranscript } = useSpeechRecognition()

To respond when the user says a particular phrase, you can pass in a list of commands to the `useSpeechRecognition` hook. Each command is an object with the following properties:
- `command`: This is a string or `RegExp` representing the phrase you want to listen for
- `callback`: The function that is executed when the command is spoken
- `callback`: The function that is executed when the command is spoken. The last argument that this function receives will always be an object containing the following properties:
- `resetTranscript`: A function that sets the transcript to an empty string
- `matchInterim`: Boolean that determines whether "interim" results should be matched against the command. This will make your component respond faster to commands, but also makes false positives more likely - i.e. the command may be detected when it is not spoken. This is `false` by default and should only be set for simple commands.
- `isFuzzyMatch`: Boolean that determines whether the comparison between speech and `command` is based on similarity rather than an exact match. Fuzzy matching is useful for commands that are easy to mispronounce or be misinterpreted by the Speech Recognition engine (e.g. names of places, sports teams, restaurant menu items). It is intended for commands that are string literals without special characters. If `command` is a string with special characters or a `RegExp`, it will be converted to a string without special characters when fuzzy matching. The similarity that is needed to match the command can be configured with `fuzzyMatchingThreshold`. `isFuzzyMatch` is `false` by default. When it is set to `true`, it will pass three arguments to `callback`:
- `isFuzzyMatch`: Boolean that determines whether the comparison between speech and `command` is based on similarity rather than an exact match. Fuzzy matching is useful for commands that are easy to mispronounce or be misinterpreted by the Speech Recognition engine (e.g. names of places, sports teams, restaurant menu items). It is intended for commands that are string literals without special characters. If `command` is a string with special characters or a `RegExp`, it will be converted to a string without special characters when fuzzy matching. The similarity that is needed to match the command can be configured with `fuzzyMatchingThreshold`. `isFuzzyMatch` is `false` by default. When it is set to `true`, it will pass four arguments to `callback`:
- The value of `command`
- The speech that matched `command`
- The similarity between `command` and the speech
- The object mentioned in the `callback` description above
- `fuzzyMatchingThreshold`: If the similarity of speech to `command` is higher than this value when `isFuzzyMatch` is turned on, the `callback` will be invoked. You should set this only if `isFuzzyMatch` is `true`. It takes values between `0` (will match anything) and `1` (needs an exact match). The default value is `0.8`.

### Command symbols
Expand Down Expand Up @@ -189,6 +192,10 @@ const Dictaphone = () => {
// If the spokenPhrase is "Benji", the message would be "Beijing and Benji are 40% similar"
isFuzzyMatch: true,
fuzzyMatchingThreshold: 0.2
},
{
command: 'clear',
callback: ({ resetTranscript }) => resetTranscript()
}
]
Expand Down
7 changes: 6 additions & 1 deletion example/src/Dictaphone/DictaphoneWidgetA.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ const DictaphoneWidgetA = () => {
// If the spokenPhrase is "Benji", the message would be "Beijing and Benji are 40% similar"
isFuzzyMatch: true,
fuzzyMatchingThreshold: 0.2
}
},
{
command: 'clear',
callback: ({ resetTranscript }) => resetTranscript(),
matchInterim: true
},
]

return (
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-speech-recognition",
"version": "3.1.0",
"version": "3.2.0",
"description": "💬Speech recognition for your React app",
"main": "lib/index.js",
"scripts": {
Expand Down
18 changes: 9 additions & 9 deletions src/SpeechRecognition.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const useSpeechRecognition = ({
dispatch(clearTrancript())
}

const resetTranscript = useCallback(() => {
recognitionManager.resetTranscript()
clearTranscript()
}, [recognitionManager])

const matchCommands = useCallback(
(newInterimTranscript, newFinalTranscript) => {
commands.forEach(({ command, callback, matchInterim = false, isFuzzyMatch = false, fuzzyMatchingThreshold = 0.8 }) => {
Expand All @@ -34,26 +39,26 @@ const useSpeechRecognition = ({
.trim()
const howSimilar = compareTwoStringsUsingDiceCoefficient(commandWithoutSpecials, input)
if (howSimilar >= fuzzyMatchingThreshold) {
callback(commandWithoutSpecials, input, howSimilar)
callback(commandWithoutSpecials, input, howSimilar, { resetTranscript })
}
} else {
const pattern = commandToRegExp(command)
const result = pattern.exec(input)
if (result) {
const parameters = result.slice(1)
callback(...parameters)
callback(...parameters, { resetTranscript })
}
}
})
}, [commands]
}, [commands, resetTranscript]
)

const handleTranscriptChange = useCallback(
(newInterimTranscript, newFinalTranscript) => {
matchCommands(newInterimTranscript, newFinalTranscript)
if (transcribing) {
dispatch(appendTrancript(newInterimTranscript, newFinalTranscript))
}
matchCommands(newInterimTranscript, newFinalTranscript)
}, [matchCommands, transcribing]
)

Expand All @@ -65,11 +70,6 @@ const useSpeechRecognition = ({
}, [clearTranscriptOnListen]
)

const resetTranscript = () => {
recognitionManager.resetTranscript()
clearTranscript()
}

useEffect(() => {
const id = SpeechRecognition.counter
SpeechRecognition.counter += 1
Expand Down
71 changes: 54 additions & 17 deletions src/SpeechRecognition.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,35 @@ describe('SpeechRecognition', () => {
expect(finalTranscript).toEqual(expectedTranscript)
})

test('can reset transcript from command callback', async () => {
mockRecognitionManager()
const commands = [
{
command: 'clear',
callback: ({ resetTranscript }) => resetTranscript()
}
]
const { result } = renderHook(() => useSpeechRecognition({ commands }))

await act(async () => {
await SpeechRecognition.startListening({ continuous: true })
})
act(() => {
SpeechRecognition.getRecognition().say('test')
})

expect(result.current.transcript).toBe('test')

act(() => {
SpeechRecognition.getRecognition().say('clear')
})

const { transcript, interimTranscript, finalTranscript } = result.current
expect(transcript).toEqual('')
expect(interimTranscript).toEqual('')
expect(finalTranscript).toEqual('')
})

test('can set language', async () => {
mockRecognitionManager()
renderHook(() => useSpeechRecognition())
Expand Down Expand Up @@ -421,7 +450,8 @@ describe('SpeechRecognition', () => {
callback: mockCommandCallback
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I want to eat pizza and fries'

await act(async () => {
Expand All @@ -432,7 +462,7 @@ describe('SpeechRecognition', () => {
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('pizza')
expect(mockCommandCallback).toBeCalledWith('pizza', { resetTranscript })
})

test('matches one splat at the end of the sentence', async () => {
Expand All @@ -444,7 +474,8 @@ describe('SpeechRecognition', () => {
callback: mockCommandCallback
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I want to eat pizza and fries'

await act(async () => {
Expand All @@ -455,7 +486,7 @@ describe('SpeechRecognition', () => {
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('pizza and fries')
expect(mockCommandCallback).toBeCalledWith('pizza and fries', { resetTranscript })
})

test('matches two splats', async () => {
Expand All @@ -467,7 +498,8 @@ describe('SpeechRecognition', () => {
callback: mockCommandCallback
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I want to eat pizza and fries'

await act(async () => {
Expand All @@ -478,7 +510,7 @@ describe('SpeechRecognition', () => {
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('pizza', 'fries')
expect(mockCommandCallback).toBeCalledWith('pizza', 'fries', { resetTranscript })
})

test('matches optional words when optional word spoken', async () => {
Expand Down Expand Up @@ -534,7 +566,8 @@ describe('SpeechRecognition', () => {
callback: mockCommandCallback
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I spy with my little eye'

await act(async () => {
Expand All @@ -545,7 +578,7 @@ describe('SpeechRecognition', () => {
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('spy')
expect(mockCommandCallback).toBeCalledWith('spy', { resetTranscript })
})

test('matches regex', async () => {
Expand Down Expand Up @@ -611,7 +644,8 @@ describe('SpeechRecognition', () => {
callback: mockCommandCallback3
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I want to eat pizza and fries are great'

await act(async () => {
Expand All @@ -622,9 +656,9 @@ describe('SpeechRecognition', () => {
})

expect(mockCommandCallback1.mock.calls.length).toBe(1)
expect(mockCommandCallback1).toBeCalledWith('pizza', 'fries are great')
expect(mockCommandCallback1).toBeCalledWith('pizza', 'fries are great', { resetTranscript })
expect(mockCommandCallback2.mock.calls.length).toBe(1)
expect(mockCommandCallback2).toBeCalledWith('I want to eat pizza')
expect(mockCommandCallback2).toBeCalledWith('I want to eat pizza', { resetTranscript })
expect(mockCommandCallback3.mock.calls.length).toBe(0)
})

Expand Down Expand Up @@ -779,7 +813,8 @@ test('callback is called with command, transcript and similarity ratio between t
fuzzyMatchingThreshold: 0.5
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I want to drink'

await act(async () => {
Expand All @@ -790,7 +825,7 @@ test('callback is called with command, transcript and similarity ratio between t
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('I want to eat', 'I want to drink', 0.6)
expect(mockCommandCallback).toBeCalledWith('I want to eat', 'I want to drink', 0.6, { resetTranscript })
})

test('different callbacks can be called for the same speech and with fuzzyMatchingThreshold', async () => {
Expand Down Expand Up @@ -835,7 +870,8 @@ test('when command is regex with fuzzy match true runs similarity check with reg
isFuzzyMatch: true
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'This is a test'

await act(async () => {
Expand All @@ -846,7 +882,7 @@ test('when command is regex with fuzzy match true runs similarity check with reg
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('This is a s test', 'This is a test', 0.8571428571428571)
expect(mockCommandCallback).toBeCalledWith('This is a s test', 'This is a test', 0.8571428571428571, { resetTranscript })
})

test('when command is string special characters with fuzzy match true, special characters are removed from string and then we test similarity', async () => {
Expand All @@ -859,7 +895,8 @@ test('when command is string special characters with fuzzy match true, special c
isFuzzyMatch: true
}
]
renderHook(() => useSpeechRecognition({ commands }))
const { result } = renderHook(() => useSpeechRecognition({ commands }))
const { resetTranscript } = result.current
const speech = 'I would like a pizza'

await act(async () => {
Expand All @@ -870,5 +907,5 @@ test('when command is string special characters with fuzzy match true, special c
})

expect(mockCommandCallback.mock.calls.length).toBe(1)
expect(mockCommandCallback).toBeCalledWith('I would like a pizza', 'I would like a pizza', 1)
expect(mockCommandCallback).toBeCalledWith('I would like a pizza', 'I would like a pizza', 1, { resetTranscript })
})

0 comments on commit af3d083

Please sign in to comment.