Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/typedoc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Deploy typedoc to Pages

on:
push:
branches:
- master
workflow_dispatch:

permissions: read-all

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
contents: read
pages: write
id-token: write
runs-on: ubuntu-latest
steps:
- name: Checkout tree
uses: actions/checkout@v4

- name: Set-up Node.js
uses: actions/setup-node@v4
with:
check-latest: true
node-version-file: .nvmrc

- run: corepack enable

- run: yarn install --immutable

- run: yarn typedoc

- name: Set-up Pages
uses: actions/configure-pages@v5

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site

- name: Deploy odoc to GitHub Pages
uses: actions/deploy-pages@v4
id: deployment
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/_site/
/.yarn/
/coverage/
/dist/
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# react-speech-recognition
A React hook that converts speech from the microphone to text and makes it available to your React components.

[![npm version](https://img.shields.io/npm/v/react-speech-recognition.svg)](https://www.npmjs.com/package/react-speech-recognition)
Expand Down Expand Up @@ -88,7 +87,7 @@ By default, speech recognition is not supported in all browsers, with the best n
* ❌ Without a polyfill, your users' voice data will be sent to big tech companies like Google or Apple to be transcribed
* ✅ With a polyfill, `react-speech-recognition` will be suitable for use in commercial applications
* ❌ Without a polyfill, `react-speech-recognition` will still be fine for personal projects or use cases where cross-browser support is not needed

`react-speech-recognition` currently supports polyfills for the following cloud providers:

<div>
Expand Down
19 changes: 15 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.cts",
"files": [
"dist"
],
Expand All @@ -33,19 +36,27 @@
"fix": "biome check --fix",
"lint": "biome check",
"prepack": "bunchee",
"test": "vitest"
"test": "vitest",
"typedoc": "typedoc"
},
"dependencies": {
"lodash.debounce": "^4.0.8"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@testing-library/react-hooks": "3.7.0",
"@vitest/coverage-v8": "3.0.8",
"bunchee": "6.4.0",
"@tsconfig/strictest": "2.0.5",
"@types/lodash.debounce": "4.0.9",
"@vitest/coverage-v8": "3.1.1",
"bunchee": "6.5.0",
"corti": "1.0.0",
"jsdom": "26.0.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-test-renderer": "16.14.0",
"typedoc": "0.28.1",
"typescript": "5.8.2",
"vitest": "3.0.8"
"vitest": "3.1.1"
},
"peerDependencies": {
"react": ">=16.8.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const NativeSpeechRecognition =
window.msSpeechRecognition ||
window.oSpeechRecognition);

export const isNative = (SpeechRecognition) =>
export const isNative = (SpeechRecognition: unknown) =>
SpeechRecognition === NativeSpeechRecognition;

export default NativeSpeechRecognition;
90 changes: 52 additions & 38 deletions src/RecognitionManager.js → src/RecognitionManager.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { isNative } from "./NativeSpeechRecognition";
import isAndroid from "./isAndroid";
import { browserSupportsPolyfills, concatTranscripts, debounce } from "./utils";
import debounce from "lodash.debounce";
import { isNative } from "./NativeSpeechRecognition.js";
import isAndroid from "./isAndroid.js";
import {
Disconnect,
type ListeningOptions,
type SpeechRecognition,
type SpeechRecognitionErrorEvent,
type SpeechRecognitionEvent,
type SubscriberCallbacks,
type SubscriberId,
type SubscriberMap,
type Transcript,
} from "./types.js";
import { browserSupportsPolyfills, concatTranscripts } from "./utils.js";

export default class RecognitionManager {
constructor(SpeechRecognition) {
this.recognition = null;
this.pauseAfterDisconnect = false;
this.interimTranscript = "";
this.finalTranscript = "";
this.listening = false;
this.isMicrophoneAvailable = true;
this.subscribers = {};
this.onStopListening = () => {};
this.previousResultWasFinalOnly = false;
private recognition: SpeechRecognition | null = null;
private pauseAfterDisconnect = false;
public interimTranscript = "";
private finalTranscript = "";
public listening = false;
public isMicrophoneAvailable = true;
private subscribers: SubscriberMap = {};
private onStopListening = () => {};
private previousResultWasFinalOnly = false;

constructor(SpeechRecognition: SpeechRecognition) {
this.resetTranscript = this.resetTranscript.bind(this);
this.startListening = this.startListening.bind(this);
this.stopListening = this.stopListening.bind(this);
Expand All @@ -24,15 +36,11 @@ export default class RecognitionManager {
this.setSpeechRecognition(SpeechRecognition);

if (isAndroid()) {
this.updateFinalTranscript = debounce(
this.updateFinalTranscript,
250,
true,
);
this.updateFinalTranscript = debounce(this.updateFinalTranscript, 250);
}
}

setSpeechRecognition(SpeechRecognition) {
setSpeechRecognition(SpeechRecognition: SpeechRecognition) {
const browserSupportsRecogniser =
!!SpeechRecognition &&
(isNative(SpeechRecognition) || browserSupportsPolyfills());
Expand All @@ -48,31 +56,34 @@ export default class RecognitionManager {
this.emitBrowserSupportsSpeechRecognitionChange(browserSupportsRecogniser);
}

subscribe(id, callbacks) {
subscribe(id: SubscriberId, callbacks: SubscriberCallbacks) {
this.subscribers[id] = callbacks;
}

unsubscribe(id) {
unsubscribe(id: SubscriberId) {
delete this.subscribers[id];
}

emitListeningChange(listening) {
emitListeningChange(listening: boolean) {
this.listening = listening;
Object.keys(this.subscribers).forEach((id) => {
const { onListeningChange } = this.subscribers[id];
onListeningChange(listening);
});
}

emitMicrophoneAvailabilityChange(isMicrophoneAvailable) {
emitMicrophoneAvailabilityChange(isMicrophoneAvailable: boolean) {
this.isMicrophoneAvailable = isMicrophoneAvailable;
Object.keys(this.subscribers).forEach((id) => {
const { onMicrophoneAvailabilityChange } = this.subscribers[id];
onMicrophoneAvailabilityChange(isMicrophoneAvailable);
});
}

emitTranscriptChange(interimTranscript, finalTranscript) {
emitTranscriptChange(
interimTranscript: Transcript,
finalTranscript: Transcript,
) {
Object.keys(this.subscribers).forEach((id) => {
const { onTranscriptChange } = this.subscribers[id];
onTranscriptChange(interimTranscript, finalTranscript);
Expand All @@ -87,7 +98,7 @@ export default class RecognitionManager {
}

emitBrowserSupportsSpeechRecognitionChange(
browserSupportsSpeechRecognitionChange,
browserSupportsSpeechRecognitionChange: boolean,
) {
Object.keys(this.subscribers).forEach((id) => {
const {
Expand All @@ -103,18 +114,18 @@ export default class RecognitionManager {
});
}

disconnect(disconnectType) {
disconnect(disconnectType: Disconnect) {
if (this.recognition && this.listening) {
switch (disconnectType) {
case "ABORT":
case Disconnect.Abort:
this.pauseAfterDisconnect = true;
this.abort();
break;
case "RESET":
case Disconnect.Reset:
this.pauseAfterDisconnect = false;
this.abort();
break;
case "STOP":
case Disconnect.Stop:
default:
this.pauseAfterDisconnect = true;
this.stop();
Expand All @@ -133,7 +144,7 @@ export default class RecognitionManager {
}
}

onError(event) {
onError(event: SpeechRecognitionErrorEvent) {
if (event && event.error && event.error === "not-allowed") {
this.emitMicrophoneAvailabilityChange(false);
this.disableRecognition();
Expand All @@ -155,7 +166,7 @@ export default class RecognitionManager {
this.pauseAfterDisconnect = false;
}

updateTranscript({ results, resultIndex }) {
updateTranscript({ results, resultIndex }: SpeechRecognitionEvent) {
const currentIndex =
resultIndex === undefined ? results.length - 1 : resultIndex;
this.interimTranscript = "";
Expand Down Expand Up @@ -187,18 +198,21 @@ export default class RecognitionManager {
}
}

updateFinalTranscript(newFinalTranscript) {
updateFinalTranscript(newFinalTranscript: Transcript) {
this.finalTranscript = concatTranscripts(
this.finalTranscript,
newFinalTranscript,
);
}

resetTranscript() {
this.disconnect("RESET");
this.disconnect(Disconnect.Reset);
}

async startListening({ continuous = false, language } = {}) {
async startListening({
continuous = false,
language,
}: ListeningOptions = {}) {
if (!this.recognition) {
return;
}
Expand Down Expand Up @@ -234,17 +248,17 @@ export default class RecognitionManager {
}

async abortListening() {
this.disconnect("ABORT");
this.disconnect(Disconnect.Abort);
this.emitListeningChange(false);
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
this.onStopListening = resolve;
});
}

async stopListening() {
this.disconnect("STOP");
this.disconnect(Disconnect.Stop);
this.emitListeningChange(false);
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
this.onStopListening = resolve;
});
}
Expand Down
Loading