Skip to content

Commit

Permalink
feat(hooks): Introduces useSpeechRecognition (#409)
Browse files Browse the repository at this point in the history
* feat(hooks): Introduces useSpeechRecognition

* feat(hooks): updates changelog
  • Loading branch information
antonioru committed Mar 19, 2023
1 parent ebd509f commit 8e14a1a
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1036,3 +1036,9 @@ Errored release
### Fixes

- package.json specifiers (exports)

## [4.3.0] - 2023-03-19

### Adds

- `useSpeechRecognition` hook
40 changes: 40 additions & 0 deletions docs/useSpeechRecognition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# useSpeechSynthesis

A hook that provides an interface for using the [Web_Speech_API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API) to
recognize and transcribe speech in a user's browser.

### Why? 馃挕

- Abstracts the implementation details of the Web Speech API into a single reusable function.

### Basic Usage:

```jsx harmony
import { Button, Space, Tag, Typography, Input } from 'antd';
import useSpeechRecognition from 'beautiful-react-hooks/useSpeechRecognition';

const SpeechSynthesisDemo = () => {
const [name, setName] = React.useState('Antonio');
const { startRecording, transcript, stopRecording, isRecording, isSupported } = useSpeechRecognition();

return (
<DisplayDemo title="useSpeechSynthesis">
<Space direction="vertical">
<Typography.Paragraph>
Supported: <Tag color={isSupported ? 'green' : 'red'}>{isSupported ? 'Yes' : 'No'}</Tag>
</Typography.Paragraph>
<Button onClick={!isRecording ? startRecording : stopRecording} type="primary">
{isRecording ? 'Stop' : 'Start'} recording
</Button>
<Typography.Paragraph>
{transcript}
</Typography.Paragraph>
</Space>
</DisplayDemo>
);
};

<SpeechSynthesisDemo />
```

<!-- Types -->
85 changes: 85 additions & 0 deletions src/useSpeechRecognition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useCallback, useEffect, useMemo, useState } from 'react'

declare global {
interface SpeechRecognitionEvent extends Event {
results: SpeechRecognitionResultList
}

interface SpeechRecognitionPolyfill {
start: () => void
stop: () => void
abort: () => void
addEventListener: (event: string, callback: (event: SpeechRecognitionEvent) => void) => void
removeEventListener: (event: string, callback: (event: SpeechRecognitionEvent) => void) => void

// eslint-disable-next-line @typescript-eslint/no-misused-new
new(): SpeechRecognitionPolyfill
}

interface Window {
SpeechRecognition?: SpeechRecognitionPolyfill
webkitSpeechRecognition?: SpeechRecognitionPolyfill
}
}

const SpeechRecognition = window.SpeechRecognition ?? window.webkitSpeechRecognition

/**
* A hook that provides an interface for using the Web Speech API to recognize and transcribe speech in a user's browser.
*/
const useSpeechRecognition = () => {
const spInstance = useMemo(() => SpeechRecognition ? new SpeechRecognition() : null, [])
const [isRecording, setIsRecording] = useState(false)
const [transcript, setTranscript] = useState('')
const isSupported = !!spInstance

useEffect(() => {
const getResults = (event: SpeechRecognitionEvent) => {
const nextTranscript = event.results[0][0].transcript
setTranscript(nextTranscript)
}

if (spInstance && isSupported) {
spInstance.addEventListener('result', getResults)
}
return () => {
if (spInstance && isSupported) {
spInstance.stop()
spInstance.abort()
spInstance.removeEventListener('result', getResults)
}
}
}, [spInstance])

const startRecording = useCallback(() => {
if (spInstance && isSupported) {
spInstance.start()
setIsRecording(true)
}
}, [spInstance])

const stopRecording = useCallback(() => {
if (spInstance && isSupported) {
spInstance.stop()
setIsRecording(false)
}
}, [spInstance])

return Object.freeze<UseSpeechRecognitionResult>({
isSupported,
transcript,
isRecording,
startRecording,
stopRecording
})
}

export interface UseSpeechRecognitionResult {
isSupported: boolean
transcript: string
isRecording: boolean
startRecording: () => void
stopRecording: () => void
}

export default useSpeechRecognition
35 changes: 35 additions & 0 deletions test/useSpeechRecognition.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { cleanup, renderHook } from '@testing-library/react-hooks'
import useSpeechRecognition from '../dist/useSpeechRecognition'
import SpeechSynthesisUtteranceMock from './mocks/SpeechSynthesisUtterance.mock'
import SpeechSynthesisMock from './mocks/SpeechSynthesis.mock'
import assertHook from './utils/assertHook'

describe('useSpeechRecognition', () => {
const originalSpeechSynth = global.speechSynthesis
const originalSpeechSynthesisUtterance = global.SpeechSynthesisUtterance

before(() => {
global.speechSynthesis = SpeechSynthesisMock
global.SpeechSynthesisUtterance = SpeechSynthesisUtteranceMock
})

beforeEach(() => cleanup())

after(() => {
global.SpeechSynthesisUtterance = originalSpeechSynthesisUtterance
global.speechSynthesis = originalSpeechSynth
})

assertHook(useSpeechRecognition)

it('should return an object containing the speak function and the utter', () => {
const { result } = renderHook(() => useSpeechRecognition())

expect(result.current).to.be.an('object')
expect(result.current.startRecording).to.be.a('function')
expect(result.current.stopRecording).to.be.a('function')
expect(result.current.transcript).to.be.a('string')
expect(result.current.isRecording).to.be.a('boolean')
expect(result.current.isSupported).to.be.a('boolean')
})
})

0 comments on commit 8e14a1a

Please sign in to comment.