• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JamesBrill / react-speech-recognition / 15266137132

27 May 2025 03:35AM UTC coverage: 93.207% (-1.1%) from 94.347%
15266137132

Pull #251

github

smorimoto
_

Signed-off-by: Sora Morimoto <sora@morimoto.io>
Pull Request #251: Unvendor corti

122 of 142 branches covered (85.92%)

Branch coverage included in aggregate %.

564 of 594 relevant lines covered (94.95%)

60.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.31
/src/RecognitionManager.js
1
import debounce from "lodash.debounce";
1✔
2
import { isNative } from "./NativeSpeechRecognition";
1✔
3
import isAndroid from "./isAndroid";
1✔
4
import { browserSupportsPolyfills, concatTranscripts } from "./utils";
1✔
5

6
export default class RecognitionManager {
1✔
7
  constructor(SpeechRecognition) {
1✔
8
    this.recognition = null;
39✔
9
    this.pauseAfterDisconnect = false;
39✔
10
    this.interimTranscript = "";
39✔
11
    this.finalTranscript = "";
39✔
12
    this.listening = false;
39✔
13
    this.isMicrophoneAvailable = true;
39✔
14
    this.subscribers = {};
39✔
15
    this.onStopListening = () => {};
39✔
16
    this.previousResultWasFinalOnly = false;
39✔
17

18
    this.resetTranscript = this.resetTranscript.bind(this);
39✔
19
    this.startListening = this.startListening.bind(this);
39✔
20
    this.stopListening = this.stopListening.bind(this);
39✔
21
    this.abortListening = this.abortListening.bind(this);
39✔
22
    this.setSpeechRecognition = this.setSpeechRecognition.bind(this);
39✔
23
    this.disableRecognition = this.disableRecognition.bind(this);
39✔
24

25
    this.setSpeechRecognition(SpeechRecognition);
39✔
26

27
    if (isAndroid()) {
39✔
28
      this.updateFinalTranscript = debounce(this.updateFinalTranscript, 250, {
1✔
29
        leading: true,
1✔
30
      });
1✔
31
    }
1✔
32
  }
39✔
33

34
  setSpeechRecognition(SpeechRecognition) {
1✔
35
    const browserSupportsRecogniser =
97✔
36
      !!SpeechRecognition &&
97✔
37
      (isNative(SpeechRecognition) || browserSupportsPolyfills());
97✔
38
    if (browserSupportsRecogniser) {
97✔
39
      this.disableRecognition();
89✔
40
      this.recognition = new SpeechRecognition();
89✔
41
      this.recognition.continuous = false;
89✔
42
      this.recognition.interimResults = true;
89✔
43
      this.recognition.onresult = this.updateTranscript.bind(this);
89✔
44
      this.recognition.onend = this.onRecognitionDisconnect.bind(this);
89✔
45
      this.recognition.onerror = this.onError.bind(this);
89✔
46
    }
89✔
47
    this.emitBrowserSupportsSpeechRecognitionChange(browserSupportsRecogniser);
97✔
48
  }
97✔
49

50
  subscribe(id, callbacks) {
1✔
51
    this.subscribers[id] = callbacks;
47✔
52
  }
47✔
53

54
  unsubscribe(id) {
1✔
55
    delete this.subscribers[id];
×
56
  }
×
57

58
  emitListeningChange(listening) {
1✔
59
    this.listening = listening;
72✔
60
    Object.keys(this.subscribers).forEach((id) => {
72✔
61
      const { onListeningChange } = this.subscribers[id];
76✔
62
      onListeningChange(listening);
76✔
63
    });
72✔
64
  }
72✔
65

66
  emitMicrophoneAvailabilityChange(isMicrophoneAvailable) {
1✔
67
    this.isMicrophoneAvailable = isMicrophoneAvailable;
2✔
68
    Object.keys(this.subscribers).forEach((id) => {
2✔
69
      const { onMicrophoneAvailabilityChange } = this.subscribers[id];
2✔
70
      onMicrophoneAvailabilityChange(isMicrophoneAvailable);
2✔
71
    });
2✔
72
  }
2✔
73

74
  emitTranscriptChange(interimTranscript, finalTranscript) {
1✔
75
    Object.keys(this.subscribers).forEach((id) => {
28✔
76
      const { onTranscriptChange } = this.subscribers[id];
29✔
77
      onTranscriptChange(interimTranscript, finalTranscript);
29✔
78
    });
28✔
79
  }
28✔
80

81
  emitClearTranscript() {
1✔
82
    Object.keys(this.subscribers).forEach((id) => {
36✔
83
      const { onClearTranscript } = this.subscribers[id];
37✔
84
      onClearTranscript();
37✔
85
    });
36✔
86
  }
36✔
87

88
  emitBrowserSupportsSpeechRecognitionChange(
1✔
89
    browserSupportsSpeechRecognitionChange,
97✔
90
  ) {
97✔
91
    Object.keys(this.subscribers).forEach((id) => {
97✔
92
      const {
323✔
93
        onBrowserSupportsSpeechRecognitionChange,
323✔
94
        onBrowserSupportsContinuousListeningChange,
323✔
95
      } = this.subscribers[id];
323✔
96
      onBrowserSupportsSpeechRecognitionChange(
323✔
97
        browserSupportsSpeechRecognitionChange,
323✔
98
      );
323✔
99
      onBrowserSupportsContinuousListeningChange(
323✔
100
        browserSupportsSpeechRecognitionChange,
323✔
101
      );
323✔
102
    });
97✔
103
  }
97✔
104

105
  disconnect(disconnectType) {
1✔
106
    if (this.recognition && this.listening) {
44✔
107
      switch (disconnectType) {
5✔
108
        case "ABORT":
5✔
109
          this.pauseAfterDisconnect = true;
1✔
110
          this.abort();
1✔
111
          break;
1✔
112
        case "RESET":
5✔
113
          this.pauseAfterDisconnect = false;
1✔
114
          this.abort();
1✔
115
          break;
1✔
116
        case "STOP":
5✔
117
        default:
5✔
118
          this.pauseAfterDisconnect = true;
3✔
119
          this.stop();
3✔
120
      }
5✔
121
    }
5✔
122
  }
44✔
123

124
  disableRecognition() {
1✔
125
    if (this.recognition) {
90✔
126
      this.recognition.onresult = () => {};
51✔
127
      this.recognition.onend = () => {};
51✔
128
      this.recognition.onerror = () => {};
51✔
129
      if (this.listening) {
51✔
130
        this.stopListening();
2✔
131
      }
2✔
132
    }
51✔
133
  }
90✔
134

135
  onError(event) {
1✔
136
    if (event && event.error && event.error === "not-allowed") {
1✔
137
      this.emitMicrophoneAvailabilityChange(false);
1✔
138
      this.disableRecognition();
1✔
139
    }
1✔
140
  }
1✔
141

142
  onRecognitionDisconnect() {
1✔
143
    this.onStopListening();
30✔
144
    this.listening = false;
30✔
145
    if (this.pauseAfterDisconnect) {
30✔
146
      this.emitListeningChange(false);
2✔
147
    } else if (this.recognition) {
30✔
148
      if (this.recognition.continuous) {
28✔
149
        this.startListening({ continuous: this.recognition.continuous });
1✔
150
      } else {
28✔
151
        this.emitListeningChange(false);
27✔
152
      }
27✔
153
    }
28✔
154
    this.pauseAfterDisconnect = false;
30✔
155
  }
30✔
156

157
  updateTranscript({ results, resultIndex }) {
1✔
158
    const currentIndex =
28✔
159
      resultIndex === undefined ? results.length - 1 : resultIndex;
28!
160
    this.interimTranscript = "";
28✔
161
    this.finalTranscript = "";
28✔
162
    for (let i = currentIndex; i < results.length; ++i) {
28✔
163
      if (
28✔
164
        results[i].isFinal &&
28✔
165
        (!isAndroid() || results[i][0].confidence > 0)
28!
166
      ) {
28✔
167
        this.updateFinalTranscript(results[i][0].transcript);
28✔
168
      } else {
28!
169
        this.interimTranscript = concatTranscripts(
×
170
          this.interimTranscript,
×
171
          results[i][0].transcript,
×
172
        );
×
173
      }
×
174
    }
28✔
175
    let isDuplicateResult = false;
28✔
176
    if (this.interimTranscript === "" && this.finalTranscript !== "") {
28✔
177
      if (this.previousResultWasFinalOnly) {
28!
178
        isDuplicateResult = true;
×
179
      }
×
180
      this.previousResultWasFinalOnly = true;
28✔
181
    } else {
28!
182
      this.previousResultWasFinalOnly = false;
×
183
    }
×
184
    if (!isDuplicateResult) {
28✔
185
      this.emitTranscriptChange(this.interimTranscript, this.finalTranscript);
28✔
186
    }
28✔
187
  }
28✔
188

189
  updateFinalTranscript(newFinalTranscript) {
1✔
190
    this.finalTranscript = concatTranscripts(
28✔
191
      this.finalTranscript,
28✔
192
      newFinalTranscript,
28✔
193
    );
28✔
194
  }
28✔
195

196
  resetTranscript() {
1✔
197
    this.disconnect("RESET");
38✔
198
  }
38✔
199

200
  async startListening({ continuous = false, language } = {}) {
1✔
201
    if (!this.recognition) {
38!
202
      return;
×
203
    }
×
204

205
    const isContinuousChanged = continuous !== this.recognition.continuous;
38✔
206
    const isLanguageChanged = language && language !== this.recognition.lang;
38✔
207
    if (isContinuousChanged || isLanguageChanged) {
38✔
208
      if (this.listening) {
2!
209
        await this.stopListening();
×
210
      }
×
211
      this.recognition.continuous = isContinuousChanged
2✔
212
        ? continuous
2✔
213
        : this.recognition.continuous;
2✔
214
      this.recognition.lang = isLanguageChanged
2✔
215
        ? language
2✔
216
        : this.recognition.lang;
2✔
217
    }
2✔
218
    if (!this.listening) {
38✔
219
      if (!this.recognition.continuous) {
38✔
220
        this.resetTranscript();
36✔
221
        this.emitClearTranscript();
36✔
222
      }
36✔
223
      try {
38✔
224
        await this.start();
38✔
225
        this.emitListeningChange(true);
37✔
226
      } catch (e) {
38✔
227
        // DOMExceptions indicate a redundant microphone start - safe to swallow
1✔
228
        if (!(e instanceof DOMException)) {
1✔
229
          this.emitMicrophoneAvailabilityChange(false);
1✔
230
        }
1✔
231
      }
1✔
232
    }
38✔
233
  }
38✔
234

235
  async abortListening() {
1✔
236
    this.disconnect("ABORT");
1✔
237
    this.emitListeningChange(false);
1✔
238
    await new Promise((resolve) => {
1✔
239
      this.onStopListening = resolve;
1✔
240
    });
1!
241
  }
1✔
242

243
  async stopListening() {
1✔
244
    this.disconnect("STOP");
5✔
245
    this.emitListeningChange(false);
5✔
246
    await new Promise((resolve) => {
5✔
247
      this.onStopListening = resolve;
5✔
248
    });
5!
249
  }
5✔
250

251
  getRecognition() {
1✔
252
    return this.recognition;
38✔
253
  }
38✔
254

255
  async start() {
1✔
256
    if (this.recognition && !this.listening) {
38✔
257
      await this.recognition.start();
38✔
258
      this.listening = true;
37✔
259
    }
37✔
260
  }
38✔
261

262
  stop() {
1✔
263
    if (this.recognition && this.listening) {
3✔
264
      this.recognition.stop();
3✔
265
      this.listening = false;
3✔
266
    }
3✔
267
  }
3✔
268

269
  abort() {
1✔
270
    if (this.recognition && this.listening) {
2✔
271
      this.recognition.abort();
2✔
272
      this.listening = false;
2✔
273
    }
2✔
274
  }
2✔
275
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc