Skip to content

Commit 4db9769

Browse files
authored
feat: implement isRecognitionAvailable() (#28)
1 parent 9f32539 commit 4db9769

10 files changed

+77
-28
lines changed

README.md

+29-13
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ expo-speech-recognition implements the iOS [`SFSpeechRecognizer`](https://develo
3838
- [getSpeechRecognitionServices()](#getspeechrecognitionservices-string-android-only)
3939
- [getDefaultRecognitionService()](#getdefaultrecognitionservice--packagename-string--android-only)
4040
- [getAssistantService()](#getassistantservice--packagename-string--android-only)
41+
- [isRecognitionAvailable()](#isrecognitionavailable-boolean)
4142
- [supportsOnDeviceRecognition()](#supportsondevicerecognition-boolean)
42-
- [supportsRecording()](#supportsrecording-boolean-android-only)
43+
- [supportsRecording()](#supportsrecording-boolean)
4344
- [androidTriggerOfflineModelDownload()](#androidtriggerofflinemodeldownload-locale-string--promise-status-opened_dialog--download_success--download_canceled-message-string-)
4445
- [setCategoryIOS()](#setcategoryios-void-ios-only)
4546
- [getAudioSessionCategoryAndOptionsIOS()](#getaudiosessioncategoryandoptionsios-ios-only)
@@ -307,7 +308,7 @@ Events are largely based on the [Web Speech API](https://developer.mozilla.org/e
307308
| ------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
308309
| `audiostart` | Audio capturing has started | Includes the `uri` if `recordingOptions.persist` is enabled. |
309310
| `audioend` | Audio capturing has ended | Includes the `uri` if `recordingOptions.persist` is enabled. |
310-
| `end` | Speech recognition service has disconnected. | This should be the last event dispatched. |
311+
| `end` | Speech recognition service has disconnected. | This should always be the last event dispatched, including after errors. |
311312
| `error` | Fired when a speech recognition error occurs. | You'll also receive an `error` event (with code "aborted") when calling `.abort()` |
312313
| `nomatch` | Speech recognition service returns a final result with no significant recognition. | You may have non-final results recognized. This may get emitted after cancellation. |
313314
| `result` | Speech recognition service returns a word or phrase has been positively recognized. | On Android, continous mode runs as a segmented session, meaning when a final result is reached, additional partial and final results will cover a new segment separate from the previous final result. On iOS, you should expect one final result before speech recognition has stopped. |
@@ -361,7 +362,7 @@ The error code is based on the [Web Speech API error codes](https://developer.mo
361362
If you would like to persist the recognized audio for later use, you can enable the `recordingOptions.persist` option when calling `start()`. Enabling this setting will emit an `{ uri: string }` event object in the `audiostart` and `audioend` events with the local file path.
362363

363364
> [!IMPORTANT]
364-
> This feature is available on Android 13+ and iOS. Call `supportsRecording()` to see if it's available before using this feature.
365+
> This feature is available on Android 13+ and iOS. Call [`supportsRecording()`](#supportsrecording-boolean) to see if it's available before using this feature.
365366
366367
Default audio output formats:
367368

@@ -838,9 +839,9 @@ Get list of speech recognition services available on the device.
838839
> This only includes services that are listed under `androidSpeechServicePackages` in your app.json as well as the core services listed under `forceQueryable` when running the command: `adb shell dumpsys package queries`
839840
840841
```ts
841-
import { ExpoSpeechRecognitionModule } from "expo-speech-recognition";
842+
import { getSpeechRecognitionServices } from "expo-speech-recognition";
842843

843-
const packages = ExpoSpeechRecognitionModule.getSpeechRecognitionServices();
844+
const packages = getSpeechRecognitionServices();
844845
console.log("Speech recognition services:", packages.join(", "));
845846
// e.g. ["com.google.android.as", "com.google.android.tts", "com.samsung.android.bixby.agent"]
846847
```
@@ -850,9 +851,9 @@ console.log("Speech recognition services:", packages.join(", "));
850851
Returns the default voice recognition service on the device.
851852

852853
```ts
853-
import { ExpoSpeechRecognitionModule } from "expo-speech-recognition";
854+
import { getDefaultRecognitionService } from "expo-speech-recognition";
854855

855-
const service = ExpoSpeechRecognitionModule.getDefaultRecognitionService();
856+
const service = getDefaultRecognitionService();
856857
console.log("Default recognition service:", service.packageName);
857858
// Usually this is "com.google.android.tts" on Android 13+ and "com.google.android.googlequicksearchbox" on Android <=12.
858859
// For on-device recognition, "com.google.android.as" will likely be used.
@@ -863,17 +864,32 @@ console.log("Default recognition service:", service.packageName);
863864
Returns the default voice assistant service on the device.
864865

865866
```ts
866-
import { ExpoSpeechRecognitionModule } from "expo-speech-recognition";
867+
import { getAssistantService } from "expo-speech-recognition";
867868

868-
const service = ExpoSpeechRecognitionModule.getAssistantService();
869+
const service = getAssistantService();
869870
console.log("Default assistant service:", service.packageName);
870871
// Usually "com.google.android.googlequicksearchbox" for Google
871872
// or "com.samsung.android.bixby.agent" for Samsung
872873
```
873874

875+
### `isRecognitionAvailable(): boolean`
876+
877+
Whether speech recognition is currently available on the device.
878+
879+
If this method returns false, calling `start()` will fail and emit an error event with the code `service-not-allowed` or `language-not-supported`. You should also ask the user to enable speech recognition in the system settings (i.e, for iOS to enable Siri & Dictation). On Android, you should ask the user to install and enable `com.google.android.tts` (Android 13+) or `com.google.android.googlequicksearchbox` (Android <= 12) as a default voice recognition service.
880+
881+
For Web, this method only checks if the browser has the Web SpeechRecognition API available, however keep in mind that browsers (like Brave) may still have the APIs but not have it implemented yet. Refer to [Platform Compatibility Table](#platform-compatibility-table) for more information. You may want to use a user agent parser to fill in the gaps.
882+
883+
```ts
884+
import { isRecognitionAvailable } from "expo-speech-recognition";
885+
886+
const available = isRecognitionAvailable();
887+
console.log("Speech recognition available:", available);
888+
```
889+
874890
### `supportsOnDeviceRecognition(): boolean`
875891

876-
Whether on-device speech recognition is available on the device.
892+
Whether the device supports on-device speech recognition.
877893

878894
```ts
879895
import { supportsOnDeviceRecognition } from "expo-speech-recognition";
@@ -882,7 +898,7 @@ const available = supportsOnDeviceRecognition();
882898
console.log("OnDevice recognition available:", available);
883899
```
884900

885-
### `supportsRecording(): boolean` (Android only)
901+
### `supportsRecording(): boolean`
886902

887903
Whether audio recording is supported during speech recognition. This mostly applies to Android devices, to check if it's at least Android 13.
888904

@@ -902,10 +918,10 @@ You can see which locales are supported and installed on your device by running
902918
To download the offline model for a specific locale, use the `androidTriggerOfflineModelDownload` function.
903919

904920
```ts
905-
import { ExpoSpeechRecognitionModule } from "expo-speech-recognition";
921+
import { androidTriggerOfflineModelDownload } from "expo-speech-recognition";
906922

907923
// Download the offline model for the specified locale
908-
ExpoSpeechRecognitionModule.androidTriggerOfflineModelDownload({
924+
androidTriggerOfflineModelDownload({
909925
locale: "en-US",
910926
})
911927
.then((result) => {

android/src/main/java/expo/modules/speechrecognition/ExpoSpeechRecognitionModule.kt

+4
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ class ExpoSpeechRecognitionModule : Module() {
183183
}
184184
}
185185

186+
Function("isRecognitionAvailable") {
187+
SpeechRecognizer.isRecognitionAvailable(appContext.reactContext!!)
188+
}
189+
186190
Function("supportsRecording") {
187191
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
188192
}

example/App.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,15 @@ function OtherSettings(props: {
786786
});
787787
}}
788788
/>
789+
<BigButton
790+
title="Call isRecognitionAvailable()"
791+
color="#7C90DB"
792+
onPress={() => {
793+
const isAvailable =
794+
ExpoSpeechRecognitionModule.isRecognitionAvailable();
795+
Alert.alert("isRecognitionAvailable()", isAvailable.toString());
796+
}}
797+
/>
789798
{Platform.OS === "ios" && (
790799
<BigButton
791800
title="Set audio session active state"

ios/ExpoSpeechRecognitionModule.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,11 @@ public class ExpoSpeechRecognitionModule: Module {
279279
}
280280

281281
Function("supportsRecording") { () -> Bool in
282-
let recognizer: SFSpeechRecognizer? = SFSpeechRecognizer()
282+
return true
283+
}
284+
285+
Function("isRecognitionAvailable") { () -> Bool in
286+
let recognizer = SFSpeechRecognizer()
283287
return recognizer?.isAvailable ?? false
284288
}
285289

ios/ExpoSpeechRecognizer.swift

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ enum RecognizerError: Error {
2323
}
2424
}
2525

26-
/// A helper for transcribing speech to text using SFSpeechRecognizer and AVAudioEngine.
2726
actor ExpoSpeechRecognizer: ObservableObject {
2827

2928
private var options: SpeechRecognitionOptions?

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "expo-speech-recognition",
3-
"version": "0.2.20",
3+
"version": "0.2.21",
44
"description": "Speech Recognition for React Native Expo projects",
55
"main": "build/index.js",
66
"types": "build/index.d.ts",

src/ExpoSpeechRecognitionModule.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const ExpoSpeechRecognitionModule: ExpoSpeechRecognitionModuleType = {
3131
ExpoSpeechRecognitionNativeModule.supportsOnDeviceRecognition(),
3232
supportsRecording: () =>
3333
ExpoSpeechRecognitionNativeModule.supportsRecording(),
34+
isRecognitionAvailable: () =>
35+
ExpoSpeechRecognitionNativeModule.isRecognitionAvailable(),
3436
};
3537

3638
export const ExpoSpeechRecognitionModuleEmitter = new EventEmitter(

src/ExpoSpeechRecognitionModule.types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,13 @@ export interface ExpoSpeechRecognitionModuleType extends NativeModule {
570570
* This mostly applies to Android devices, to check if it's greater than Android 13.
571571
*/
572572
supportsRecording(): boolean;
573+
/**
574+
* Whether on-device speech recognition is available.
575+
*
576+
* If this method returns false, `start()` will fail and emit an error event with the code `service-not-allowed` or `language-not-supported`.
577+
*/
578+
isRecognitionAvailable(): boolean;
579+
573580
/**
574581
* Downloads the offline model for the specified locale.
575582
* Note: this is only supported on Android 13 and above.

src/ExpoSpeechRecognitionModule.web.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -82,31 +82,19 @@ export const ExpoSpeechRecognitionModule: ExpoSpeechRecognitionModuleType = {
8282
);
8383
},
8484
getSpeechRecognitionServices: () => {
85-
console.warn(
86-
"getSpeechRecognitionServices is not supported on web. Returning an empty array.",
87-
);
8885
return [] as string[];
8986
},
9087
getDefaultRecognitionService: () => {
91-
console.warn(
92-
"getDefaultRecognitionService is not supported on web. Returning an empty object.",
93-
);
9488
return {
9589
packageName: "",
9690
};
9791
},
9892
getAssistantService: () => {
99-
console.warn(
100-
"getAssistantService is not supported on web. Returning an empty object.",
101-
);
10293
return {
10394
packageName: "",
10495
};
10596
},
10697
supportsOnDeviceRecognition: () => {
107-
console.warn(
108-
"supportsOnDeviceRecognition is not supported on web. Returning false.",
109-
);
11098
return false;
11199
},
112100
supportsRecording: () => {
@@ -137,6 +125,13 @@ export const ExpoSpeechRecognitionModule: ExpoSpeechRecognitionModuleType = {
137125
setAudioSessionActiveIOS: () => {
138126
console.warn("setAudioSessionActiveIOS is not supported on web.");
139127
},
128+
isRecognitionAvailable: () => {
129+
const hasSpeechRecognitionAPI =
130+
typeof webkitSpeechRecognition !== "undefined" ||
131+
typeof SpeechRecognition !== "undefined";
132+
133+
return hasSpeechRecognitionAPI;
134+
},
140135
};
141136

142137
/**
@@ -176,6 +171,7 @@ const webToNativeEventMap: {
176171
start: (ev) => null,
177172
soundend: (ev) => null,
178173
};
174+
179175
export const ExpoSpeechRecognitionModuleEmitter = {
180176
_nativeListeners: new Map() as Map<string, Set<(event: any) => void>>,
181177
_clientListeners: new Map() as Map<

src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ export const getAudioSessionCategoryAndOptionsIOS =
5151
export const setAudioSessionActiveIOS =
5252
ExpoSpeechRecognitionModule.setAudioSessionActiveIOS;
5353

54+
export const androidTriggerOfflineModelDownload =
55+
ExpoSpeechRecognitionModule.androidTriggerOfflineModelDownload;
56+
57+
export const isRecognitionAvailable =
58+
ExpoSpeechRecognitionModule.isRecognitionAvailable;
59+
60+
export const getDefaultRecognitionService =
61+
ExpoSpeechRecognitionModule.getDefaultRecognitionService;
62+
63+
export const getAssistantService =
64+
ExpoSpeechRecognitionModule.getAssistantService;
65+
5466
export const addSpeechRecognitionListener = <
5567
T extends keyof ExpoSpeechRecognitionNativeEventMap,
5668
>(

0 commit comments

Comments
 (0)