@capgo/capacitor-speech-recognition 8.1.11 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -21
- package/android/src/main/java/app/capgo/speechrecognition/Constants.java +1 -0
- package/android/src/main/java/app/capgo/speechrecognition/SpeechRecognitionPlugin.java +23 -2
- package/dist/docs.json +61 -5
- package/dist/esm/definitions.d.ts +49 -8
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/SpeechRecognitionPlugin/AudioLevelMetering.swift +40 -0
- package/ios/Sources/SpeechRecognitionPlugin/LegacyOnDeviceRecognitionRequirement.swift +21 -0
- package/ios/Sources/SpeechRecognitionPlugin/OnDeviceRecognitionAvailabilityResolver.swift +39 -0
- package/ios/Sources/SpeechRecognitionPlugin/SpeechAnalyzerRecognitionSession.swift +17 -0
- package/ios/Sources/SpeechRecognitionPlugin/SpeechRecognitionPlugin.swift +85 -7
- package/ios/Tests/SpeechRecognitionPluginTests/SpeechRecognitionPluginTests.swift +150 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -218,6 +218,7 @@ Add the following keys to your app `Info.plist`:
|
|
|
218
218
|
* [`addListener('partialResults', ...)`](#addlistenerpartialresults-)
|
|
219
219
|
* [`addListener('listeningState', ...)`](#addlistenerlisteningstate-)
|
|
220
220
|
* [`addListener('error', ...)`](#addlistenererror-)
|
|
221
|
+
* [`addListener('audioLevel', ...)`](#addlisteneraudiolevel-)
|
|
221
222
|
* [`addListener('readyForNextSession', ...)`](#addlistenerreadyfornextsession-)
|
|
222
223
|
* [`removeAllListeners()`](#removealllisteners)
|
|
223
224
|
* [Interfaces](#interfaces)
|
|
@@ -244,24 +245,29 @@ Checks whether the native speech recognition service is usable on the current de
|
|
|
244
245
|
### isOnDeviceRecognitionAvailable(...)
|
|
245
246
|
|
|
246
247
|
```typescript
|
|
247
|
-
isOnDeviceRecognitionAvailable(options?: Pick<SpeechRecognitionStartOptions, "language"> | undefined) => Promise<SpeechRecognitionAvailability>
|
|
248
|
+
isOnDeviceRecognitionAvailable(options?: Pick<SpeechRecognitionStartOptions, "language" | "preferLegacyRecognizer"> | undefined) => Promise<SpeechRecognitionAvailability>
|
|
248
249
|
```
|
|
249
250
|
|
|
250
|
-
Checks whether
|
|
251
|
+
Checks whether on-device speech recognition is available for the selected locale.
|
|
251
252
|
|
|
252
253
|
This is the capability check you should use before enabling `useOnDeviceRecognition`.
|
|
253
|
-
|
|
254
|
-
on-device path for that platform.
|
|
254
|
+
On iOS, the result depends on which recognizer path `start()` will use:
|
|
255
255
|
|
|
256
|
-
|
|
256
|
+
- When `preferLegacyRecognizer` is `false` (default) on iOS 26+, this checks the modern
|
|
257
|
+
`SpeechAnalyzer` path.
|
|
258
|
+
- On older iOS versions, or when `preferLegacyRecognizer` is `true`, this checks
|
|
259
|
+
`SFSpeechRecognizer.supportsOnDeviceRecognition` for the legacy path.
|
|
260
|
+
|
|
261
|
+
Pass the same `preferLegacyRecognizer` value here and in `start()` so the availability
|
|
262
|
+
check matches the route that recognition will take.
|
|
257
263
|
|
|
258
264
|
Platform SDK docs:
|
|
259
265
|
iOS: [Speech](https://developer.apple.com/documentation/speech)
|
|
260
266
|
Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)
|
|
261
267
|
|
|
262
|
-
| Param | Type
|
|
263
|
-
| ------------- |
|
|
264
|
-
| **`options`** | <code><a href="#pick">Pick</a><<a href="#speechrecognitionstartoptions">SpeechRecognitionStartOptions</a>, 'language'></code> |
|
|
268
|
+
| Param | Type |
|
|
269
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
270
|
+
| **`options`** | <code><a href="#pick">Pick</a><<a href="#speechrecognitionstartoptions">SpeechRecognitionStartOptions</a>, 'language' \| 'preferLegacyRecognizer'></code> |
|
|
265
271
|
|
|
266
272
|
**Returns:** <code>Promise<<a href="#speechrecognitionavailability">SpeechRecognitionAvailability</a>></code>
|
|
267
273
|
|
|
@@ -512,6 +518,29 @@ Listen for recognition errors.
|
|
|
512
518
|
--------------------
|
|
513
519
|
|
|
514
520
|
|
|
521
|
+
### addListener('audioLevel', ...)
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
addListener(eventName: 'audioLevel', listenerFunc: (event: SpeechRecognitionAudioLevelEvent) => void) => Promise<PluginListenerHandle>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Listen for live microphone input level while recognition is active.
|
|
528
|
+
|
|
529
|
+
Emits roughly 10–20 times per second with a normalized `0..1` level.
|
|
530
|
+
No events are emitted when recognition is idle.
|
|
531
|
+
|
|
532
|
+
iOS and Android only. Web accepts listener registration but does not emit events.
|
|
533
|
+
|
|
534
|
+
| Param | Type |
|
|
535
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
536
|
+
| **`eventName`** | <code>'audioLevel'</code> |
|
|
537
|
+
| **`listenerFunc`** | <code>(event: <a href="#speechrecognitionaudiolevelevent">SpeechRecognitionAudioLevelEvent</a>) => void</code> |
|
|
538
|
+
|
|
539
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
540
|
+
|
|
541
|
+
--------------------
|
|
542
|
+
|
|
543
|
+
|
|
515
544
|
### addListener('readyForNextSession', ...)
|
|
516
545
|
|
|
517
546
|
```typescript
|
|
@@ -555,19 +584,20 @@ Removes every registered listener.
|
|
|
555
584
|
|
|
556
585
|
Configure how the recognizer behaves when calling {@link SpeechRecognitionPlugin.start}.
|
|
557
586
|
|
|
558
|
-
| Prop | Type | Description
|
|
559
|
-
| ---------------------------- | --------------------- |
|
|
560
|
-
| **`language`** | <code>string</code> | Locale identifier such as `en-US`. When omitted the device language is used.
|
|
561
|
-
| **`maxResults`** | <code>number</code> | Maximum number of final matches returned by native APIs. Defaults to `5`.
|
|
562
|
-
| **`prompt`** | <code>string</code> | Prompt message shown inside the Android system dialog (ignored on iOS).
|
|
563
|
-
| **`popup`** | <code>boolean</code> | When `true`, Android shows the OS speech dialog instead of running inline recognition. Defaults to `false`.
|
|
564
|
-
| **`partialResults`** | <code>boolean</code> | Emits partial transcription updates through the `partialResults` listener while audio is captured.
|
|
565
|
-
| **`addPunctuation`** | <code>boolean</code> | Enables native punctuation handling where supported (iOS 16+).
|
|
566
|
-
| **`contextualStrings`** | <code>string[]</code> | Words or phrases that should be recognized more accurately by native speech APIs. On iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings` when the plugin uses the legacy `SFSpeechRecognizer` path. That path is the default on all iOS versions, the fallback below iOS 26, and still available on iOS 26+ by leaving `useOnDeviceRecognition` disabled. Ignored by Android and by the iOS 26+ `SpeechAnalyzer` path.
|
|
567
|
-
| **`useOnDeviceRecognition`** | <code>boolean</code> | Opt in to the platform's newer on-device recognition path when available. On iOS 26+, this uses Apple's `SpeechAnalyzer` / `SpeechTranscriber` pipeline. On recent Android versions, this uses the on-device `SpeechRecognizer` path. It is intentionally opt-in so existing apps keep the legacy flow unless they choose to roll out the new behavior. On iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version.
|
|
568
|
-
| **`
|
|
569
|
-
| **`
|
|
570
|
-
| **`
|
|
587
|
+
| Prop | Type | Description | Default | Since |
|
|
588
|
+
| ---------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------ |
|
|
589
|
+
| **`language`** | <code>string</code> | Locale identifier such as `en-US`. When omitted the device language is used. | | |
|
|
590
|
+
| **`maxResults`** | <code>number</code> | Maximum number of final matches returned by native APIs. Defaults to `5`. | | |
|
|
591
|
+
| **`prompt`** | <code>string</code> | Prompt message shown inside the Android system dialog (ignored on iOS). | | |
|
|
592
|
+
| **`popup`** | <code>boolean</code> | When `true`, Android shows the OS speech dialog instead of running inline recognition. Defaults to `false`. | | |
|
|
593
|
+
| **`partialResults`** | <code>boolean</code> | Emits partial transcription updates through the `partialResults` listener while audio is captured. | | |
|
|
594
|
+
| **`addPunctuation`** | <code>boolean</code> | Enables native punctuation handling where supported (iOS 16+). | | |
|
|
595
|
+
| **`contextualStrings`** | <code>string[]</code> | Words or phrases that should be recognized more accurately by native speech APIs. On iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings` when the plugin uses the legacy `SFSpeechRecognizer` path. That path is the default on all iOS versions, the fallback below iOS 26, and still available on iOS 26+ either by leaving `useOnDeviceRecognition` disabled or by setting `preferLegacyRecognizer` — so contextual strings and on-device recognition can be used together. Ignored by Android and by the iOS 26+ `SpeechAnalyzer` path. | | |
|
|
596
|
+
| **`useOnDeviceRecognition`** | <code>boolean</code> | Opt in to the platform's newer on-device recognition path when available. On iOS 26+, this uses Apple's `SpeechAnalyzer` / `SpeechTranscriber` pipeline. On recent Android versions, this uses the on-device `SpeechRecognizer` path. It is intentionally opt-in so existing apps keep the legacy flow unless they choose to roll out the new behavior. On iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version. On the legacy `SFSpeechRecognizer` path, enabling this rejects with `ON_DEVICE_RECOGNITION_UNAVAILABLE` when on-device recognition is not supported for the locale. On iOS 26+, enabling this without `preferLegacyRecognizer` uses the modern `SpeechAnalyzer` path when available; otherwise recognition falls back to the legacy path with the same rejection rule. Use {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable} before enabling it in production. Platform SDK docs: iOS: [Speech](https://developer.apple.com/documentation/speech), [SpeechAnalyzer](https://developer.apple.com/documentation/speech/speechanalyzer), [SpeechTranscriber](https://developer.apple.com/documentation/speech/speechtranscriber) Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer) Defaults to `false`. | | |
|
|
597
|
+
| **`preferLegacyRecognizer`** | <code>boolean</code> | iOS only: skip the modern `SpeechAnalyzer` path even when it is available, so that `useOnDeviceRecognition` applies to `SFSpeechRecognizer` (`requiresOnDeviceRecognition`) instead. Useful on iOS 26 devices where the modern path starts and stops a session without ever emitting `partialResults`. | <code>false</code> | 8.1.11 |
|
|
598
|
+
| **`allowForSilence`** | <code>number</code> | Allow a number of milliseconds of silence before splitting the recognition session into segments. Required to be greater than zero and currently supported on Android only. | | |
|
|
599
|
+
| **`continuousPTT`** | <code>boolean</code> | EXPERIMENTAL: Keep a PTT session alive across silence by restarting recognition while the button stays held. This restart behavior is implemented for Android inline recognition and iOS native recognition. | | |
|
|
600
|
+
| **`muteRecognizerBeep`** | <code>boolean</code> | Suppresses the Android system beep when inline recognition starts or restarts. Uses a best-effort combination of an undocumented recognizer intent extra and temporary notification/system stream volume muting. Some devices ignore the intent extra; the volume fallback is the portable path. Defaults to `true` when `continuousPTT` is enabled. | | |
|
|
571
601
|
|
|
572
602
|
|
|
573
603
|
#### SpeechRecognitionMatches
|
|
@@ -689,6 +719,19 @@ Raised whenever native recognition reports an error.
|
|
|
689
719
|
| **`sessionId`** | <code>number</code> |
|
|
690
720
|
|
|
691
721
|
|
|
722
|
+
#### SpeechRecognitionAudioLevelEvent
|
|
723
|
+
|
|
724
|
+
Live microphone level while recognition is active.
|
|
725
|
+
|
|
726
|
+
`level` is normalized to `0..1` for easy waveform / meter UI.
|
|
727
|
+
|
|
728
|
+
Emitted on iOS and Android only. Web accepts listener registration but does not emit events.
|
|
729
|
+
|
|
730
|
+
| Prop | Type |
|
|
731
|
+
| ----------- | ------------------- |
|
|
732
|
+
| **`level`** | <code>number</code> |
|
|
733
|
+
|
|
734
|
+
|
|
692
735
|
#### SpeechRecognitionReadyEvent
|
|
693
736
|
|
|
694
737
|
Emitted after native resources have been torn down and the plugin is ready for another session.
|
|
@@ -14,6 +14,7 @@ public interface Constants {
|
|
|
14
14
|
String PARTIAL_RESULTS_EVENT = "partialResults";
|
|
15
15
|
String ERROR_EVENT = "error";
|
|
16
16
|
String READY_FOR_NEXT_SESSION_EVENT = "readyForNextSession";
|
|
17
|
+
String AUDIO_LEVEL_EVENT = "audioLevel";
|
|
17
18
|
String RECORD_AUDIO_PERMISSION = Manifest.permission.RECORD_AUDIO;
|
|
18
19
|
String LANGUAGE_ERROR = "Could not get list of languages";
|
|
19
20
|
String EXTRA_DICTATE_BEEP = "android.speech.extra.DICTATE_BEEP";
|
|
@@ -9,6 +9,7 @@ import android.os.Build;
|
|
|
9
9
|
import android.os.Bundle;
|
|
10
10
|
import android.os.Handler;
|
|
11
11
|
import android.os.Looper;
|
|
12
|
+
import android.os.SystemClock;
|
|
12
13
|
import android.speech.ModelDownloadListener;
|
|
13
14
|
import android.speech.RecognitionListener;
|
|
14
15
|
import android.speech.RecognitionSupport;
|
|
@@ -42,10 +43,12 @@ public class SpeechRecognitionPlugin extends Plugin implements Constants {
|
|
|
42
43
|
|
|
43
44
|
public static final String SPEECH_RECOGNITION = "speechRecognition";
|
|
44
45
|
private static final String TAG = "SpeechRecognition";
|
|
45
|
-
private static final String PLUGIN_VERSION = "8.
|
|
46
|
+
private static final String PLUGIN_VERSION = "8.1.11";
|
|
46
47
|
private static final int FORCE_STOP_TIMEOUT_MS = 1500;
|
|
47
48
|
private static final int STOP_FALLBACK_TIMEOUT_MS = 500;
|
|
48
49
|
private static final int CONTINUOUS_RESTART_DELAY_MS = 100;
|
|
50
|
+
/** Target audioLevel emit rate (~15 Hz), matching the public docs. */
|
|
51
|
+
private static final long AUDIO_LEVEL_EMIT_INTERVAL_MS = 67;
|
|
49
52
|
|
|
50
53
|
private enum ListeningState {
|
|
51
54
|
IDLE,
|
|
@@ -60,6 +63,7 @@ public class SpeechRecognitionPlugin extends Plugin implements Constants {
|
|
|
60
63
|
private final ReentrantLock lock = new ReentrantLock();
|
|
61
64
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
62
65
|
private boolean listening = false;
|
|
66
|
+
private long lastAudioLevelEmitElapsedMs = 0;
|
|
63
67
|
private JSONArray previousPartialResults = new JSONArray();
|
|
64
68
|
|
|
65
69
|
private Runnable forceStopRunnable;
|
|
@@ -346,6 +350,8 @@ public class SpeechRecognitionPlugin extends Plugin implements Constants {
|
|
|
346
350
|
try {
|
|
347
351
|
speechRecognizer.stopListening();
|
|
348
352
|
} catch (Exception ignored) {}
|
|
353
|
+
// Stop metering immediately (onRmsChanged gated on listening).
|
|
354
|
+
listening(false);
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
forceStopRunnable = () -> {
|
|
@@ -1106,7 +1112,22 @@ public class SpeechRecognitionPlugin extends Plugin implements Constants {
|
|
|
1106
1112
|
public void onBeginningOfSpeech() {}
|
|
1107
1113
|
|
|
1108
1114
|
@Override
|
|
1109
|
-
public void onRmsChanged(float rmsdB) {
|
|
1115
|
+
public void onRmsChanged(float rmsdB) {
|
|
1116
|
+
// SpeechRecognizer reports approximate speech energy in dB, typically ~-2..10.
|
|
1117
|
+
// Map that range into the documented audioLevel 0..1 scale.
|
|
1118
|
+
if (isStale() || !listening) {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
long now = SystemClock.elapsedRealtime();
|
|
1122
|
+
if (now - lastAudioLevelEmitElapsedMs < AUDIO_LEVEL_EMIT_INTERVAL_MS) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
lastAudioLevelEmitElapsedMs = now;
|
|
1126
|
+
double level = Math.max(0.0, Math.min(1.0, (rmsdB + 2.0) / 12.0));
|
|
1127
|
+
JSObject payload = new JSObject();
|
|
1128
|
+
payload.put("level", level);
|
|
1129
|
+
notifyListeners(AUDIO_LEVEL_EVENT, payload);
|
|
1130
|
+
}
|
|
1110
1131
|
|
|
1111
1132
|
@Override
|
|
1112
1133
|
public void onBufferReceived(byte[] buffer) {}
|
package/dist/docs.json
CHANGED
|
@@ -19,17 +19,17 @@
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"name": "isOnDeviceRecognitionAvailable",
|
|
22
|
-
"signature": "(options?: Pick<SpeechRecognitionStartOptions, \"language\"> | undefined) => Promise<SpeechRecognitionAvailability>",
|
|
22
|
+
"signature": "(options?: Pick<SpeechRecognitionStartOptions, \"language\" | \"preferLegacyRecognizer\"> | undefined) => Promise<SpeechRecognitionAvailability>",
|
|
23
23
|
"parameters": [
|
|
24
24
|
{
|
|
25
25
|
"name": "options",
|
|
26
26
|
"docs": "",
|
|
27
|
-
"type": "Pick<SpeechRecognitionStartOptions, 'language'> | undefined"
|
|
27
|
+
"type": "Pick<SpeechRecognitionStartOptions, 'language' | 'preferLegacyRecognizer'> | undefined"
|
|
28
28
|
}
|
|
29
29
|
],
|
|
30
30
|
"returns": "Promise<SpeechRecognitionAvailability>",
|
|
31
31
|
"tags": [],
|
|
32
|
-
"docs": "Checks whether
|
|
32
|
+
"docs": "Checks whether on-device speech recognition is available for the selected locale.\n\nThis is the capability check you should use before enabling `useOnDeviceRecognition`.\nOn iOS, the result depends on which recognizer path `start()` will use:\n\n- When `preferLegacyRecognizer` is `false` (default) on iOS 26+, this checks the modern\n `SpeechAnalyzer` path.\n- On older iOS versions, or when `preferLegacyRecognizer` is `true`, this checks\n `SFSpeechRecognizer.supportsOnDeviceRecognition` for the legacy path.\n\nPass the same `preferLegacyRecognizer` value here and in `start()` so the availability\ncheck matches the route that recognition will take.\n\nPlatform SDK docs:\niOS: [Speech](https://developer.apple.com/documentation/speech)\nAndroid: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)",
|
|
33
33
|
"complexTypes": [
|
|
34
34
|
"SpeechRecognitionAvailability",
|
|
35
35
|
"Pick",
|
|
@@ -291,6 +291,30 @@
|
|
|
291
291
|
],
|
|
292
292
|
"slug": "addlistenererror-"
|
|
293
293
|
},
|
|
294
|
+
{
|
|
295
|
+
"name": "addListener",
|
|
296
|
+
"signature": "(eventName: 'audioLevel', listenerFunc: (event: SpeechRecognitionAudioLevelEvent) => void) => Promise<PluginListenerHandle>",
|
|
297
|
+
"parameters": [
|
|
298
|
+
{
|
|
299
|
+
"name": "eventName",
|
|
300
|
+
"docs": "",
|
|
301
|
+
"type": "'audioLevel'"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"name": "listenerFunc",
|
|
305
|
+
"docs": "",
|
|
306
|
+
"type": "(event: SpeechRecognitionAudioLevelEvent) => void"
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
"returns": "Promise<PluginListenerHandle>",
|
|
310
|
+
"tags": [],
|
|
311
|
+
"docs": "Listen for live microphone input level while recognition is active.\n\nEmits roughly 10–20 times per second with a normalized `0..1` level.\nNo events are emitted when recognition is idle.\n\niOS and Android only. Web accepts listener registration but does not emit events.",
|
|
312
|
+
"complexTypes": [
|
|
313
|
+
"PluginListenerHandle",
|
|
314
|
+
"SpeechRecognitionAudioLevelEvent"
|
|
315
|
+
],
|
|
316
|
+
"slug": "addlisteneraudiolevel-"
|
|
317
|
+
},
|
|
294
318
|
{
|
|
295
319
|
"name": "addListener",
|
|
296
320
|
"signature": "(eventName: 'readyForNextSession', listenerFunc: (event: SpeechRecognitionReadyEvent) => void) => Promise<PluginListenerHandle>",
|
|
@@ -397,14 +421,30 @@
|
|
|
397
421
|
{
|
|
398
422
|
"name": "contextualStrings",
|
|
399
423
|
"tags": [],
|
|
400
|
-
"docs": "Words or phrases that should be recognized more accurately by native speech APIs.\n\nOn iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings`\nwhen the plugin uses the legacy `SFSpeechRecognizer` path. That path is the\ndefault on all iOS versions, the fallback below iOS 26, and still available\
|
|
424
|
+
"docs": "Words or phrases that should be recognized more accurately by native speech APIs.\n\nOn iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings`\nwhen the plugin uses the legacy `SFSpeechRecognizer` path. That path is the\ndefault on all iOS versions, the fallback below iOS 26, and still available on\niOS 26+ either by leaving `useOnDeviceRecognition` disabled or by setting\n`preferLegacyRecognizer` — so contextual strings and on-device recognition can\nbe used together.\n\nIgnored by Android and by the iOS 26+ `SpeechAnalyzer` path.",
|
|
401
425
|
"complexTypes": [],
|
|
402
426
|
"type": "string[] | undefined"
|
|
403
427
|
},
|
|
404
428
|
{
|
|
405
429
|
"name": "useOnDeviceRecognition",
|
|
406
430
|
"tags": [],
|
|
407
|
-
"docs": "Opt in to the platform's newer on-device recognition path when available.\n\nOn iOS 26+, this uses Apple's `SpeechAnalyzer` / `SpeechTranscriber` pipeline.\nOn recent Android versions, this uses the on-device `SpeechRecognizer` path.\n\nIt is intentionally opt-in so existing apps keep the legacy flow unless they choose\nto roll out the new behavior.\nOn iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version.\
|
|
431
|
+
"docs": "Opt in to the platform's newer on-device recognition path when available.\n\nOn iOS 26+, this uses Apple's `SpeechAnalyzer` / `SpeechTranscriber` pipeline.\nOn recent Android versions, this uses the on-device `SpeechRecognizer` path.\n\nIt is intentionally opt-in so existing apps keep the legacy flow unless they choose\nto roll out the new behavior.\nOn iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version.\nOn the legacy `SFSpeechRecognizer` path, enabling this rejects with\n`ON_DEVICE_RECOGNITION_UNAVAILABLE` when on-device recognition is not supported for the locale.\nOn iOS 26+, enabling this without `preferLegacyRecognizer` uses the modern `SpeechAnalyzer` path\nwhen available; otherwise recognition falls back to the legacy path with the same rejection rule.\n\nUse {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable} before enabling it in production.\n\nPlatform SDK docs:\niOS: [Speech](https://developer.apple.com/documentation/speech),\n[SpeechAnalyzer](https://developer.apple.com/documentation/speech/speechanalyzer),\n[SpeechTranscriber](https://developer.apple.com/documentation/speech/speechtranscriber)\nAndroid: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)\n\nDefaults to `false`.",
|
|
432
|
+
"complexTypes": [],
|
|
433
|
+
"type": "boolean | undefined"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "preferLegacyRecognizer",
|
|
437
|
+
"tags": [
|
|
438
|
+
{
|
|
439
|
+
"text": "false",
|
|
440
|
+
"name": "default"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"text": "8.1.11",
|
|
444
|
+
"name": "since"
|
|
445
|
+
}
|
|
446
|
+
],
|
|
447
|
+
"docs": "iOS only: skip the modern `SpeechAnalyzer` path even when it is available,\nso that `useOnDeviceRecognition` applies to `SFSpeechRecognizer`\n(`requiresOnDeviceRecognition`) instead.\n\nUseful on iOS 26 devices where the modern path starts and stops a session\nwithout ever emitting `partialResults`.",
|
|
408
448
|
"complexTypes": [],
|
|
409
449
|
"type": "boolean | undefined"
|
|
410
450
|
},
|
|
@@ -720,6 +760,22 @@
|
|
|
720
760
|
}
|
|
721
761
|
]
|
|
722
762
|
},
|
|
763
|
+
{
|
|
764
|
+
"name": "SpeechRecognitionAudioLevelEvent",
|
|
765
|
+
"slug": "speechrecognitionaudiolevelevent",
|
|
766
|
+
"docs": "Live microphone level while recognition is active.\n\n`level` is normalized to `0..1` for easy waveform / meter UI.\n\nEmitted on iOS and Android only. Web accepts listener registration but does not emit events.",
|
|
767
|
+
"tags": [],
|
|
768
|
+
"methods": [],
|
|
769
|
+
"properties": [
|
|
770
|
+
{
|
|
771
|
+
"name": "level",
|
|
772
|
+
"tags": [],
|
|
773
|
+
"docs": "",
|
|
774
|
+
"complexTypes": [],
|
|
775
|
+
"type": "number"
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
},
|
|
723
779
|
{
|
|
724
780
|
"name": "SpeechRecognitionReadyEvent",
|
|
725
781
|
"slug": "speechrecognitionreadyevent",
|
|
@@ -42,8 +42,10 @@ export interface SpeechRecognitionStartOptions {
|
|
|
42
42
|
*
|
|
43
43
|
* On iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings`
|
|
44
44
|
* when the plugin uses the legacy `SFSpeechRecognizer` path. That path is the
|
|
45
|
-
* default on all iOS versions, the fallback below iOS 26, and still available
|
|
46
|
-
*
|
|
45
|
+
* default on all iOS versions, the fallback below iOS 26, and still available on
|
|
46
|
+
* iOS 26+ either by leaving `useOnDeviceRecognition` disabled or by setting
|
|
47
|
+
* `preferLegacyRecognizer` — so contextual strings and on-device recognition can
|
|
48
|
+
* be used together.
|
|
47
49
|
*
|
|
48
50
|
* Ignored by Android and by the iOS 26+ `SpeechAnalyzer` path.
|
|
49
51
|
*/
|
|
@@ -57,7 +59,10 @@ export interface SpeechRecognitionStartOptions {
|
|
|
57
59
|
* It is intentionally opt-in so existing apps keep the legacy flow unless they choose
|
|
58
60
|
* to roll out the new behavior.
|
|
59
61
|
* On iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version.
|
|
60
|
-
*
|
|
62
|
+
* On the legacy `SFSpeechRecognizer` path, enabling this rejects with
|
|
63
|
+
* `ON_DEVICE_RECOGNITION_UNAVAILABLE` when on-device recognition is not supported for the locale.
|
|
64
|
+
* On iOS 26+, enabling this without `preferLegacyRecognizer` uses the modern `SpeechAnalyzer` path
|
|
65
|
+
* when available; otherwise recognition falls back to the legacy path with the same rejection rule.
|
|
61
66
|
*
|
|
62
67
|
* Use {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable} before enabling it in production.
|
|
63
68
|
*
|
|
@@ -70,6 +75,18 @@ export interface SpeechRecognitionStartOptions {
|
|
|
70
75
|
* Defaults to `false`.
|
|
71
76
|
*/
|
|
72
77
|
useOnDeviceRecognition?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* iOS only: skip the modern `SpeechAnalyzer` path even when it is available,
|
|
80
|
+
* so that `useOnDeviceRecognition` applies to `SFSpeechRecognizer`
|
|
81
|
+
* (`requiresOnDeviceRecognition`) instead.
|
|
82
|
+
*
|
|
83
|
+
* Useful on iOS 26 devices where the modern path starts and stops a session
|
|
84
|
+
* without ever emitting `partialResults`.
|
|
85
|
+
*
|
|
86
|
+
* @default false
|
|
87
|
+
* @since 8.1.11
|
|
88
|
+
*/
|
|
89
|
+
preferLegacyRecognizer?: boolean;
|
|
73
90
|
/**
|
|
74
91
|
* Allow a number of milliseconds of silence before splitting the recognition session into segments.
|
|
75
92
|
* Required to be greater than zero and currently supported on Android only.
|
|
@@ -169,6 +186,16 @@ export interface SpeechRecognitionErrorEvent {
|
|
|
169
186
|
message: string;
|
|
170
187
|
sessionId: number;
|
|
171
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Live microphone level while recognition is active.
|
|
191
|
+
*
|
|
192
|
+
* `level` is normalized to `0..1` for easy waveform / meter UI.
|
|
193
|
+
*
|
|
194
|
+
* Emitted on iOS and Android only. Web accepts listener registration but does not emit events.
|
|
195
|
+
*/
|
|
196
|
+
export interface SpeechRecognitionAudioLevelEvent {
|
|
197
|
+
level: number;
|
|
198
|
+
}
|
|
172
199
|
/**
|
|
173
200
|
* Emitted after native resources have been torn down and the plugin is ready for another session.
|
|
174
201
|
*/
|
|
@@ -238,19 +265,24 @@ export interface SpeechRecognitionPlugin {
|
|
|
238
265
|
*/
|
|
239
266
|
available(): Promise<SpeechRecognitionAvailability>;
|
|
240
267
|
/**
|
|
241
|
-
* Checks whether
|
|
268
|
+
* Checks whether on-device speech recognition is available for the selected locale.
|
|
242
269
|
*
|
|
243
270
|
* This is the capability check you should use before enabling `useOnDeviceRecognition`.
|
|
244
|
-
*
|
|
245
|
-
* on-device path for that platform.
|
|
271
|
+
* On iOS, the result depends on which recognizer path `start()` will use:
|
|
246
272
|
*
|
|
247
|
-
*
|
|
273
|
+
* - When `preferLegacyRecognizer` is `false` (default) on iOS 26+, this checks the modern
|
|
274
|
+
* `SpeechAnalyzer` path.
|
|
275
|
+
* - On older iOS versions, or when `preferLegacyRecognizer` is `true`, this checks
|
|
276
|
+
* `SFSpeechRecognizer.supportsOnDeviceRecognition` for the legacy path.
|
|
277
|
+
*
|
|
278
|
+
* Pass the same `preferLegacyRecognizer` value here and in `start()` so the availability
|
|
279
|
+
* check matches the route that recognition will take.
|
|
248
280
|
*
|
|
249
281
|
* Platform SDK docs:
|
|
250
282
|
* iOS: [Speech](https://developer.apple.com/documentation/speech)
|
|
251
283
|
* Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)
|
|
252
284
|
*/
|
|
253
|
-
isOnDeviceRecognitionAvailable(options?: Pick<SpeechRecognitionStartOptions, 'language'>): Promise<SpeechRecognitionAvailability>;
|
|
285
|
+
isOnDeviceRecognitionAvailable(options?: Pick<SpeechRecognitionStartOptions, 'language' | 'preferLegacyRecognizer'>): Promise<SpeechRecognitionAvailability>;
|
|
254
286
|
/**
|
|
255
287
|
* Begins capturing audio and transcribing speech.
|
|
256
288
|
*
|
|
@@ -331,6 +363,15 @@ export interface SpeechRecognitionPlugin {
|
|
|
331
363
|
* Listen for recognition errors.
|
|
332
364
|
*/
|
|
333
365
|
addListener(eventName: 'error', listenerFunc: (event: SpeechRecognitionErrorEvent) => void): Promise<PluginListenerHandle>;
|
|
366
|
+
/**
|
|
367
|
+
* Listen for live microphone input level while recognition is active.
|
|
368
|
+
*
|
|
369
|
+
* Emits roughly 10–20 times per second with a normalized `0..1` level.
|
|
370
|
+
* No events are emitted when recognition is idle.
|
|
371
|
+
*
|
|
372
|
+
* iOS and Android only. Web accepts listener registration but does not emit events.
|
|
373
|
+
*/
|
|
374
|
+
addListener(eventName: 'audioLevel', listenerFunc: (event: SpeechRecognitionAudioLevelEvent) => void): Promise<PluginListenerHandle>;
|
|
334
375
|
/**
|
|
335
376
|
* Listen for the recognizer becoming ready for another session.
|
|
336
377
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Permission map returned by `checkPermissions` and `requestPermissions`.\n *\n * On Android the state maps to the `RECORD_AUDIO` permission.\n * On iOS it combines speech recognition plus microphone permission.\n */\nexport interface SpeechRecognitionPermissionStatus {\n speechRecognition: PermissionState;\n}\n\n/**\n * Configure how the recognizer behaves when calling {@link SpeechRecognitionPlugin.start}.\n */\nexport interface SpeechRecognitionStartOptions {\n /**\n * Locale identifier such as `en-US`. When omitted the device language is used.\n */\n language?: string;\n /**\n * Maximum number of final matches returned by native APIs. Defaults to `5`.\n */\n maxResults?: number;\n /**\n * Prompt message shown inside the Android system dialog (ignored on iOS).\n */\n prompt?: string;\n /**\n * When `true`, Android shows the OS speech dialog instead of running inline recognition.\n * Defaults to `false`.\n */\n popup?: boolean;\n /**\n * Emits partial transcription updates through the `partialResults` listener while audio is captured.\n */\n partialResults?: boolean;\n /**\n * Enables native punctuation handling where supported (iOS 16+).\n */\n addPunctuation?: boolean;\n /**\n * Words or phrases that should be recognized more accurately by native speech APIs.\n *\n * On iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings`\n * when the plugin uses the legacy `SFSpeechRecognizer` path. That path is the\n * default on all iOS versions, the fallback below iOS 26, and still available\n * on iOS 26+ by leaving `useOnDeviceRecognition` disabled.\n *\n * Ignored by Android and by the iOS 26+ `SpeechAnalyzer` path.\n */\n contextualStrings?: string[];\n /**\n * Opt in to the platform's newer on-device recognition path when available.\n *\n * On iOS 26+, this uses Apple's `SpeechAnalyzer` / `SpeechTranscriber` pipeline.\n * On recent Android versions, this uses the on-device `SpeechRecognizer` path.\n *\n * It is intentionally opt-in so existing apps keep the legacy flow unless they choose\n * to roll out the new behavior.\n * On iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version.\n * Enabling it on older iOS versions or unsupported locales falls back to `SFSpeechRecognizer`.\n *\n * Use {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable} before enabling it in production.\n *\n * Platform SDK docs:\n * iOS: [Speech](https://developer.apple.com/documentation/speech),\n * [SpeechAnalyzer](https://developer.apple.com/documentation/speech/speechanalyzer),\n * [SpeechTranscriber](https://developer.apple.com/documentation/speech/speechtranscriber)\n * Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)\n *\n * Defaults to `false`.\n */\n useOnDeviceRecognition?: boolean;\n /**\n * Allow a number of milliseconds of silence before splitting the recognition session into segments.\n * Required to be greater than zero and currently supported on Android only.\n */\n allowForSilence?: number;\n /**\n * EXPERIMENTAL: Keep a PTT session alive across silence by restarting recognition while the button stays held.\n *\n * This restart behavior is implemented for Android inline recognition and iOS native recognition.\n */\n continuousPTT?: boolean;\n /**\n * Suppresses the Android system beep when inline recognition starts or restarts.\n *\n * Uses a best-effort combination of an undocumented recognizer intent extra and\n * temporary notification/system stream volume muting. Some devices ignore the\n * intent extra; the volume fallback is the portable path.\n *\n * Defaults to `true` when `continuousPTT` is enabled.\n */\n muteRecognizerBeep?: boolean;\n}\n\n/**\n * Raised whenever a partial transcription is produced.\n */\nexport interface SpeechRecognitionPartialResultEvent {\n /**\n * Current recognition matches when the native recognizer reports them.\n *\n * This can be omitted for forced or accumulated-only payloads.\n */\n matches?: string[];\n /**\n * Accumulated transcription from earlier continuous PTT cycles.\n */\n accumulated?: string;\n /**\n * Final accumulated text including the current result.\n */\n accumulatedText?: string;\n /**\n * `true` when the plugin is restarting recognition inside a continuous PTT session.\n */\n isRestarting?: boolean;\n /**\n * `true` when the payload was emitted by `forceStop()`.\n */\n forced?: boolean;\n}\n\n/**\n * Raised whenever a segmented result is produced (Android only).\n */\nexport interface SpeechRecognitionSegmentResultEvent {\n matches: string[];\n}\n\n/**\n * Finite state values for the recognition session lifecycle.\n */\nexport type ListeningFiniteState = 'startingListening' | 'started' | 'stoppingListening' | 'stopped';\n\n/**\n * Why a listening state transition happened.\n */\nexport type ListeningReason = 'userStart' | 'userStop' | 'forceStop' | 'results' | 'silence' | 'error' | 'unknown';\n\n/**\n * Raised when the listening state changes.\n *\n * The original `status` field is preserved for backward compatibility and is present\n * on the binary `started` / `stopped` states.\n */\nexport interface SpeechRecognitionListeningEvent {\n /**\n * Finite state of the recognition session.\n */\n state?: ListeningFiniteState;\n /**\n * Unique identifier for the current listening session.\n */\n sessionId?: number;\n /**\n * Why this state transition occurred.\n */\n reason?: ListeningReason;\n /**\n * Error code when the transition is caused by an error.\n */\n errorCode?: string;\n /**\n * Backward-compatible binary state used by earlier releases.\n */\n status?: 'started' | 'stopped';\n}\n\n/**\n * Raised whenever native recognition reports an error.\n */\nexport interface SpeechRecognitionErrorEvent {\n code: string;\n message: string;\n sessionId: number;\n}\n\n/**\n * Emitted after native resources have been torn down and the plugin is ready for another session.\n */\nexport interface SpeechRecognitionReadyEvent {\n sessionId: number;\n}\n\nexport interface SpeechRecognitionAvailability {\n available: boolean;\n}\n\nexport interface SpeechRecognitionMatches {\n matches?: string[];\n}\n\nexport interface SpeechRecognitionLanguages {\n languages: string[];\n}\n\nexport interface SpeechRecognitionListening {\n listening: boolean;\n}\n\n/**\n * Options for {@link SpeechRecognitionPlugin.forceStop}.\n */\nexport interface ForceStopOptions {\n /**\n * Android only: timeout in milliseconds before forcing stop via destroy/recreate.\n *\n * On iOS, the current session is stopped immediately and this value is ignored.\n *\n * Defaults to `1500`.\n */\n timeout?: number;\n}\n\n/**\n * Result from {@link SpeechRecognitionPlugin.getLastPartialResult}.\n */\nexport interface LastPartialResult {\n /**\n * Whether a partial result is currently cached.\n */\n available: boolean;\n /**\n * The most recent transcript text known to the native recognizer.\n */\n text: string;\n /**\n * All current match alternatives when available.\n */\n matches?: string[];\n}\n\n/**\n * Options for {@link SpeechRecognitionPlugin.setPTTState}.\n */\nexport interface PTTStateOptions {\n /**\n * Whether the PTT button is currently held.\n */\n held: boolean;\n /**\n * When set, updates whether Android should suppress the recognizer start beep for the active session.\n *\n * Beep suppression is best-effort and device-specific; see {@link SpeechRecognitionStartOptions.muteRecognizerBeep}.\n */\n mute?: boolean;\n}\n\nexport interface SpeechRecognitionPlugin {\n /**\n * Checks whether the native speech recognition service is usable on the current device.\n */\n available(): Promise<SpeechRecognitionAvailability>;\n /**\n * Checks whether the platform's newer on-device recognition path is available for the selected locale.\n *\n * This is the capability check you should use before enabling `useOnDeviceRecognition`.\n * A `true` result means the current device, OS version, and locale can use the newer\n * on-device path for that platform.\n *\n * Returns `false` when the device only supports the legacy recognizer path.\n *\n * Platform SDK docs:\n * iOS: [Speech](https://developer.apple.com/documentation/speech)\n * Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)\n */\n isOnDeviceRecognitionAvailable(\n options?: Pick<SpeechRecognitionStartOptions, 'language'>,\n ): Promise<SpeechRecognitionAvailability>;\n /**\n * Begins capturing audio and transcribing speech.\n *\n * When `partialResults` is `true`, the returned promise resolves immediately and updates are\n * streamed through the `partialResults` listener until the session ends.\n *\n * The default path keeps the legacy recognizer behavior for backward compatibility.\n * Pass `useOnDeviceRecognition: true` only after checking\n * {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable}.\n */\n start(options?: SpeechRecognitionStartOptions): Promise<SpeechRecognitionMatches>;\n /**\n * Stops listening and tears down native resources.\n */\n stop(): Promise<void>;\n /**\n * Force stops the current session.\n *\n * On Android, this first tries a normal stop and then falls back to destroy/recreate after `timeout`.\n * On iOS, the current session is stopped immediately.\n *\n * If a partial transcript is cached, it is emitted through the `partialResults` listener with `forced: true`.\n */\n forceStop(options?: ForceStopOptions): Promise<void>;\n /**\n * Gets the last cached partial transcription result.\n */\n getLastPartialResult(): Promise<LastPartialResult>;\n /**\n * Updates the current push-to-talk button state.\n *\n * Use this together with `continuousPTT` or with a custom hold-to-talk flow.\n */\n setPTTState(options: PTTStateOptions): Promise<void>;\n /**\n * Gets the locales supported by the underlying recognizer.\n *\n * Android 13+ devices no longer expose this list; in that case `languages` is empty.\n */\n getSupportedLanguages(): Promise<SpeechRecognitionLanguages>;\n /**\n * Returns whether the plugin is actively listening for speech.\n */\n isListening(): Promise<SpeechRecognitionListening>;\n /**\n * Gets the current permission state.\n */\n checkPermissions(): Promise<SpeechRecognitionPermissionStatus>;\n /**\n * Requests the microphone + speech recognition permissions.\n */\n requestPermissions(): Promise<SpeechRecognitionPermissionStatus>;\n /**\n * Returns the native plugin version bundled with this package.\n *\n * Useful when reporting issues to confirm that native and JS versions match.\n */\n getPluginVersion(): Promise<{ version: string }>;\n /**\n * Listen for segmented session completion events (Android only).\n */\n addListener(eventName: 'endOfSegmentedSession', listenerFunc: () => void): Promise<PluginListenerHandle>;\n /**\n * Listen for segmented recognition results (Android only).\n */\n addListener(\n eventName: 'segmentResults',\n listenerFunc: (event: SpeechRecognitionSegmentResultEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for partial transcription updates emitted while `partialResults` is enabled.\n */\n addListener(\n eventName: 'partialResults',\n listenerFunc: (event: SpeechRecognitionPartialResultEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for changes to the native listening state.\n */\n addListener(\n eventName: 'listeningState',\n listenerFunc: (event: SpeechRecognitionListeningEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for recognition errors.\n */\n addListener(\n eventName: 'error',\n listenerFunc: (event: SpeechRecognitionErrorEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for the recognizer becoming ready for another session.\n */\n addListener(\n eventName: 'readyForNextSession',\n listenerFunc: (event: SpeechRecognitionReadyEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Removes every registered listener.\n */\n removeAllListeners(): Promise<void>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Permission map returned by `checkPermissions` and `requestPermissions`.\n *\n * On Android the state maps to the `RECORD_AUDIO` permission.\n * On iOS it combines speech recognition plus microphone permission.\n */\nexport interface SpeechRecognitionPermissionStatus {\n speechRecognition: PermissionState;\n}\n\n/**\n * Configure how the recognizer behaves when calling {@link SpeechRecognitionPlugin.start}.\n */\nexport interface SpeechRecognitionStartOptions {\n /**\n * Locale identifier such as `en-US`. When omitted the device language is used.\n */\n language?: string;\n /**\n * Maximum number of final matches returned by native APIs. Defaults to `5`.\n */\n maxResults?: number;\n /**\n * Prompt message shown inside the Android system dialog (ignored on iOS).\n */\n prompt?: string;\n /**\n * When `true`, Android shows the OS speech dialog instead of running inline recognition.\n * Defaults to `false`.\n */\n popup?: boolean;\n /**\n * Emits partial transcription updates through the `partialResults` listener while audio is captured.\n */\n partialResults?: boolean;\n /**\n * Enables native punctuation handling where supported (iOS 16+).\n */\n addPunctuation?: boolean;\n /**\n * Words or phrases that should be recognized more accurately by native speech APIs.\n *\n * On iOS, these are passed to `SFSpeechRecognitionRequest.contextualStrings`\n * when the plugin uses the legacy `SFSpeechRecognizer` path. That path is the\n * default on all iOS versions, the fallback below iOS 26, and still available on\n * iOS 26+ either by leaving `useOnDeviceRecognition` disabled or by setting\n * `preferLegacyRecognizer` — so contextual strings and on-device recognition can\n * be used together.\n *\n * Ignored by Android and by the iOS 26+ `SpeechAnalyzer` path.\n */\n contextualStrings?: string[];\n /**\n * Opt in to the platform's newer on-device recognition path when available.\n *\n * On iOS 26+, this uses Apple's `SpeechAnalyzer` / `SpeechTranscriber` pipeline.\n * On recent Android versions, this uses the on-device `SpeechRecognizer` path.\n *\n * It is intentionally opt-in so existing apps keep the legacy flow unless they choose\n * to roll out the new behavior.\n * On iOS, leaving this disabled keeps `SFSpeechRecognizer` on every supported OS version.\n * On the legacy `SFSpeechRecognizer` path, enabling this rejects with\n * `ON_DEVICE_RECOGNITION_UNAVAILABLE` when on-device recognition is not supported for the locale.\n * On iOS 26+, enabling this without `preferLegacyRecognizer` uses the modern `SpeechAnalyzer` path\n * when available; otherwise recognition falls back to the legacy path with the same rejection rule.\n *\n * Use {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable} before enabling it in production.\n *\n * Platform SDK docs:\n * iOS: [Speech](https://developer.apple.com/documentation/speech),\n * [SpeechAnalyzer](https://developer.apple.com/documentation/speech/speechanalyzer),\n * [SpeechTranscriber](https://developer.apple.com/documentation/speech/speechtranscriber)\n * Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)\n *\n * Defaults to `false`.\n */\n useOnDeviceRecognition?: boolean;\n /**\n * iOS only: skip the modern `SpeechAnalyzer` path even when it is available,\n * so that `useOnDeviceRecognition` applies to `SFSpeechRecognizer`\n * (`requiresOnDeviceRecognition`) instead.\n *\n * Useful on iOS 26 devices where the modern path starts and stops a session\n * without ever emitting `partialResults`.\n *\n * @default false\n * @since 8.1.11\n */\n preferLegacyRecognizer?: boolean;\n /**\n * Allow a number of milliseconds of silence before splitting the recognition session into segments.\n * Required to be greater than zero and currently supported on Android only.\n */\n allowForSilence?: number;\n /**\n * EXPERIMENTAL: Keep a PTT session alive across silence by restarting recognition while the button stays held.\n *\n * This restart behavior is implemented for Android inline recognition and iOS native recognition.\n */\n continuousPTT?: boolean;\n /**\n * Suppresses the Android system beep when inline recognition starts or restarts.\n *\n * Uses a best-effort combination of an undocumented recognizer intent extra and\n * temporary notification/system stream volume muting. Some devices ignore the\n * intent extra; the volume fallback is the portable path.\n *\n * Defaults to `true` when `continuousPTT` is enabled.\n */\n muteRecognizerBeep?: boolean;\n}\n\n/**\n * Raised whenever a partial transcription is produced.\n */\nexport interface SpeechRecognitionPartialResultEvent {\n /**\n * Current recognition matches when the native recognizer reports them.\n *\n * This can be omitted for forced or accumulated-only payloads.\n */\n matches?: string[];\n /**\n * Accumulated transcription from earlier continuous PTT cycles.\n */\n accumulated?: string;\n /**\n * Final accumulated text including the current result.\n */\n accumulatedText?: string;\n /**\n * `true` when the plugin is restarting recognition inside a continuous PTT session.\n */\n isRestarting?: boolean;\n /**\n * `true` when the payload was emitted by `forceStop()`.\n */\n forced?: boolean;\n}\n\n/**\n * Raised whenever a segmented result is produced (Android only).\n */\nexport interface SpeechRecognitionSegmentResultEvent {\n matches: string[];\n}\n\n/**\n * Finite state values for the recognition session lifecycle.\n */\nexport type ListeningFiniteState = 'startingListening' | 'started' | 'stoppingListening' | 'stopped';\n\n/**\n * Why a listening state transition happened.\n */\nexport type ListeningReason = 'userStart' | 'userStop' | 'forceStop' | 'results' | 'silence' | 'error' | 'unknown';\n\n/**\n * Raised when the listening state changes.\n *\n * The original `status` field is preserved for backward compatibility and is present\n * on the binary `started` / `stopped` states.\n */\nexport interface SpeechRecognitionListeningEvent {\n /**\n * Finite state of the recognition session.\n */\n state?: ListeningFiniteState;\n /**\n * Unique identifier for the current listening session.\n */\n sessionId?: number;\n /**\n * Why this state transition occurred.\n */\n reason?: ListeningReason;\n /**\n * Error code when the transition is caused by an error.\n */\n errorCode?: string;\n /**\n * Backward-compatible binary state used by earlier releases.\n */\n status?: 'started' | 'stopped';\n}\n\n/**\n * Raised whenever native recognition reports an error.\n */\nexport interface SpeechRecognitionErrorEvent {\n code: string;\n message: string;\n sessionId: number;\n}\n\n/**\n * Live microphone level while recognition is active.\n *\n * `level` is normalized to `0..1` for easy waveform / meter UI.\n *\n * Emitted on iOS and Android only. Web accepts listener registration but does not emit events.\n */\nexport interface SpeechRecognitionAudioLevelEvent {\n level: number;\n}\n\n/**\n * Emitted after native resources have been torn down and the plugin is ready for another session.\n */\nexport interface SpeechRecognitionReadyEvent {\n sessionId: number;\n}\n\nexport interface SpeechRecognitionAvailability {\n available: boolean;\n}\n\nexport interface SpeechRecognitionMatches {\n matches?: string[];\n}\n\nexport interface SpeechRecognitionLanguages {\n languages: string[];\n}\n\nexport interface SpeechRecognitionListening {\n listening: boolean;\n}\n\n/**\n * Options for {@link SpeechRecognitionPlugin.forceStop}.\n */\nexport interface ForceStopOptions {\n /**\n * Android only: timeout in milliseconds before forcing stop via destroy/recreate.\n *\n * On iOS, the current session is stopped immediately and this value is ignored.\n *\n * Defaults to `1500`.\n */\n timeout?: number;\n}\n\n/**\n * Result from {@link SpeechRecognitionPlugin.getLastPartialResult}.\n */\nexport interface LastPartialResult {\n /**\n * Whether a partial result is currently cached.\n */\n available: boolean;\n /**\n * The most recent transcript text known to the native recognizer.\n */\n text: string;\n /**\n * All current match alternatives when available.\n */\n matches?: string[];\n}\n\n/**\n * Options for {@link SpeechRecognitionPlugin.setPTTState}.\n */\nexport interface PTTStateOptions {\n /**\n * Whether the PTT button is currently held.\n */\n held: boolean;\n /**\n * When set, updates whether Android should suppress the recognizer start beep for the active session.\n *\n * Beep suppression is best-effort and device-specific; see {@link SpeechRecognitionStartOptions.muteRecognizerBeep}.\n */\n mute?: boolean;\n}\n\nexport interface SpeechRecognitionPlugin {\n /**\n * Checks whether the native speech recognition service is usable on the current device.\n */\n available(): Promise<SpeechRecognitionAvailability>;\n /**\n * Checks whether on-device speech recognition is available for the selected locale.\n *\n * This is the capability check you should use before enabling `useOnDeviceRecognition`.\n * On iOS, the result depends on which recognizer path `start()` will use:\n *\n * - When `preferLegacyRecognizer` is `false` (default) on iOS 26+, this checks the modern\n * `SpeechAnalyzer` path.\n * - On older iOS versions, or when `preferLegacyRecognizer` is `true`, this checks\n * `SFSpeechRecognizer.supportsOnDeviceRecognition` for the legacy path.\n *\n * Pass the same `preferLegacyRecognizer` value here and in `start()` so the availability\n * check matches the route that recognition will take.\n *\n * Platform SDK docs:\n * iOS: [Speech](https://developer.apple.com/documentation/speech)\n * Android: [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer)\n */\n isOnDeviceRecognitionAvailable(\n options?: Pick<SpeechRecognitionStartOptions, 'language' | 'preferLegacyRecognizer'>,\n ): Promise<SpeechRecognitionAvailability>;\n /**\n * Begins capturing audio and transcribing speech.\n *\n * When `partialResults` is `true`, the returned promise resolves immediately and updates are\n * streamed through the `partialResults` listener until the session ends.\n *\n * The default path keeps the legacy recognizer behavior for backward compatibility.\n * Pass `useOnDeviceRecognition: true` only after checking\n * {@link SpeechRecognitionPlugin.isOnDeviceRecognitionAvailable}.\n */\n start(options?: SpeechRecognitionStartOptions): Promise<SpeechRecognitionMatches>;\n /**\n * Stops listening and tears down native resources.\n */\n stop(): Promise<void>;\n /**\n * Force stops the current session.\n *\n * On Android, this first tries a normal stop and then falls back to destroy/recreate after `timeout`.\n * On iOS, the current session is stopped immediately.\n *\n * If a partial transcript is cached, it is emitted through the `partialResults` listener with `forced: true`.\n */\n forceStop(options?: ForceStopOptions): Promise<void>;\n /**\n * Gets the last cached partial transcription result.\n */\n getLastPartialResult(): Promise<LastPartialResult>;\n /**\n * Updates the current push-to-talk button state.\n *\n * Use this together with `continuousPTT` or with a custom hold-to-talk flow.\n */\n setPTTState(options: PTTStateOptions): Promise<void>;\n /**\n * Gets the locales supported by the underlying recognizer.\n *\n * Android 13+ devices no longer expose this list; in that case `languages` is empty.\n */\n getSupportedLanguages(): Promise<SpeechRecognitionLanguages>;\n /**\n * Returns whether the plugin is actively listening for speech.\n */\n isListening(): Promise<SpeechRecognitionListening>;\n /**\n * Gets the current permission state.\n */\n checkPermissions(): Promise<SpeechRecognitionPermissionStatus>;\n /**\n * Requests the microphone + speech recognition permissions.\n */\n requestPermissions(): Promise<SpeechRecognitionPermissionStatus>;\n /**\n * Returns the native plugin version bundled with this package.\n *\n * Useful when reporting issues to confirm that native and JS versions match.\n */\n getPluginVersion(): Promise<{ version: string }>;\n /**\n * Listen for segmented session completion events (Android only).\n */\n addListener(eventName: 'endOfSegmentedSession', listenerFunc: () => void): Promise<PluginListenerHandle>;\n /**\n * Listen for segmented recognition results (Android only).\n */\n addListener(\n eventName: 'segmentResults',\n listenerFunc: (event: SpeechRecognitionSegmentResultEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for partial transcription updates emitted while `partialResults` is enabled.\n */\n addListener(\n eventName: 'partialResults',\n listenerFunc: (event: SpeechRecognitionPartialResultEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for changes to the native listening state.\n */\n addListener(\n eventName: 'listeningState',\n listenerFunc: (event: SpeechRecognitionListeningEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for recognition errors.\n */\n addListener(\n eventName: 'error',\n listenerFunc: (event: SpeechRecognitionErrorEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for live microphone input level while recognition is active.\n *\n * Emits roughly 10–20 times per second with a normalized `0..1` level.\n * No events are emitted when recognition is idle.\n *\n * iOS and Android only. Web accepts listener registration but does not emit events.\n */\n addListener(\n eventName: 'audioLevel',\n listenerFunc: (event: SpeechRecognitionAudioLevelEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Listen for the recognizer becoming ready for another session.\n */\n addListener(\n eventName: 'readyForNextSession',\n listenerFunc: (event: SpeechRecognitionReadyEvent) => void,\n ): Promise<PluginListenerHandle>;\n /**\n * Removes every registered listener.\n */\n removeAllListeners(): Promise<void>;\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
import QuartzCore
|
|
4
|
+
|
|
5
|
+
/// Shared RMS→dB→0..1 metering used by both legacy SFSpeechRecognizer and
|
|
6
|
+
/// modern SpeechAnalyzer recognition paths so calibration stays in sync.
|
|
7
|
+
enum AudioLevelMetering {
|
|
8
|
+
/// Target emit rate (~15 Hz), matching the public audioLevel docs.
|
|
9
|
+
static let emitInterval: CFTimeInterval = 1.0 / 15.0
|
|
10
|
+
|
|
11
|
+
/// Map PCM buffer energy into a normalized 0..1 level.
|
|
12
|
+
/// Typical speech sits roughly between -50 dBFS and 0 dBFS.
|
|
13
|
+
static func normalizedAudioLevel(from buffer: AVAudioPCMBuffer) -> Float {
|
|
14
|
+
guard let channelData = buffer.floatChannelData?[0] else {
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
let frameLength = Int(buffer.frameLength)
|
|
18
|
+
guard frameLength > 0 else {
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var sumSquares: Float = 0
|
|
23
|
+
for i in 0..<frameLength {
|
|
24
|
+
let sample = channelData[i]
|
|
25
|
+
sumSquares += sample * sample
|
|
26
|
+
}
|
|
27
|
+
let rms = sqrt(sumSquares / Float(frameLength))
|
|
28
|
+
let db = 20 * log10(max(rms, 1e-7))
|
|
29
|
+
return max(0, min(1, (db + 50) / 50))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Returns true when enough time has elapsed since `lastEmit`, and updates it.
|
|
33
|
+
static func shouldEmit(now: CFTimeInterval, lastEmit: inout CFTimeInterval) -> Bool {
|
|
34
|
+
guard now - lastEmit >= emitInterval else {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
lastEmit = now
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum LegacyOnDeviceRecognitionRequirement {
|
|
4
|
+
case notRequired
|
|
5
|
+
case required
|
|
6
|
+
case unavailable
|
|
7
|
+
|
|
8
|
+
static let unavailableErrorCode = "ON_DEVICE_RECOGNITION_UNAVAILABLE"
|
|
9
|
+
static let unavailableErrorMessage = "On-device speech recognition is not available on this device."
|
|
10
|
+
|
|
11
|
+
static func evaluate(
|
|
12
|
+
useOnDeviceRecognition: Bool,
|
|
13
|
+
supportsOnDeviceRecognition: Bool
|
|
14
|
+
) -> LegacyOnDeviceRecognitionRequirement {
|
|
15
|
+
guard useOnDeviceRecognition else {
|
|
16
|
+
return .notRequired
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return supportsOnDeviceRecognition ? .required : .unavailable
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Speech
|
|
3
|
+
|
|
4
|
+
enum OnDeviceRecognitionAvailabilityResolver {
|
|
5
|
+
enum Route {
|
|
6
|
+
case modern
|
|
7
|
+
case legacy
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static func route(
|
|
11
|
+
preferLegacyRecognizer: Bool,
|
|
12
|
+
modernPathSupportedOnOS: Bool
|
|
13
|
+
) -> Route {
|
|
14
|
+
if modernPathSupportedOnOS && !preferLegacyRecognizer {
|
|
15
|
+
return .modern
|
|
16
|
+
}
|
|
17
|
+
return .legacy
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static func legacyAvailability(
|
|
21
|
+
recognizerExists: Bool,
|
|
22
|
+
supportsOnDeviceRecognition: Bool
|
|
23
|
+
) -> Bool {
|
|
24
|
+
guard recognizerExists else {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return supportsOnDeviceRecognition
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func legacyAvailability(for locale: Locale) -> Bool {
|
|
31
|
+
guard let recognizer = SFSpeechRecognizer(locale: locale) else {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
return legacyAvailability(
|
|
35
|
+
recognizerExists: true,
|
|
36
|
+
supportsOnDeviceRecognition: recognizer.supportsOnDeviceRecognition
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -52,6 +52,7 @@ final class SpeechAnalyzerRecognitionSession {
|
|
|
52
52
|
typealias ResultHandler = @MainActor ([String], Bool) -> Void
|
|
53
53
|
typealias VoidHandler = @MainActor () -> Void
|
|
54
54
|
typealias ErrorHandler = @MainActor (Error) -> Void
|
|
55
|
+
typealias AudioLevelHandler = @MainActor (Float) -> Void
|
|
55
56
|
|
|
56
57
|
private static let microphoneTapBufferSize: AVAudioFrameCount = 2048
|
|
57
58
|
|
|
@@ -75,6 +76,9 @@ final class SpeechAnalyzerRecognitionSession {
|
|
|
75
76
|
var onListeningStopped: VoidHandler?
|
|
76
77
|
var onResult: ResultHandler?
|
|
77
78
|
var onError: ErrorHandler?
|
|
79
|
+
var onAudioLevel: AudioLevelHandler?
|
|
80
|
+
/// Touched from the audio tap thread for emit throttling; only this session writes it.
|
|
81
|
+
nonisolated(unsafe) private var lastAudioLevelEmitTime: CFTimeInterval = 0
|
|
78
82
|
|
|
79
83
|
var isRunning: Bool {
|
|
80
84
|
audioEngine.isRunning || resultTask != nil || isTearingDown
|
|
@@ -163,6 +167,7 @@ final class SpeechAnalyzerRecognitionSession {
|
|
|
163
167
|
isAudioSessionActive = true
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
|
|
166
171
|
private func startAudioStreaming() throws {
|
|
167
172
|
let inputNode = audioEngine.inputNode
|
|
168
173
|
let inputFormat = inputNode.outputFormat(forBus: 0)
|
|
@@ -177,6 +182,16 @@ final class SpeechAnalyzerRecognitionSession {
|
|
|
177
182
|
return
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
// Throttle on the tap thread first so we skip RMS/log and MainActor
|
|
186
|
+
// hops when emitting faster than ~15 Hz.
|
|
187
|
+
let now = CACurrentMediaTime()
|
|
188
|
+
if AudioLevelMetering.shouldEmit(now: now, lastEmit: &self.lastAudioLevelEmitTime) {
|
|
189
|
+
let level = AudioLevelMetering.normalizedAudioLevel(from: bufferCopy)
|
|
190
|
+
Task { @MainActor [weak self] in
|
|
191
|
+
self?.onAudioLevel?(level)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
let sendableBuffer = SpeechAnalyzerSendablePCMBuffer(buffer: bufferCopy)
|
|
181
196
|
Task {
|
|
182
197
|
do {
|
|
@@ -416,12 +431,14 @@ final class SpeechAnalyzerRecognitionSession: NSObject {
|
|
|
416
431
|
typealias ResultHandler = @MainActor ([String], Bool) -> Void
|
|
417
432
|
typealias VoidHandler = @MainActor () -> Void
|
|
418
433
|
typealias ErrorHandler = @MainActor (Error) -> Void
|
|
434
|
+
typealias AudioLevelHandler = @MainActor (Float) -> Void
|
|
419
435
|
|
|
420
436
|
var isRunning = false
|
|
421
437
|
var onListeningStarted: VoidHandler?
|
|
422
438
|
var onListeningStopped: VoidHandler?
|
|
423
439
|
var onResult: ResultHandler?
|
|
424
440
|
var onError: ErrorHandler?
|
|
441
|
+
var onAudioLevel: AudioLevelHandler?
|
|
425
442
|
|
|
426
443
|
init(locale _: Locale, maxResults _: Int, includePartialResults _: Bool) {}
|
|
427
444
|
|
|
@@ -22,7 +22,7 @@ private enum ListeningReason: String {
|
|
|
22
22
|
// swiftlint:disable type_body_length
|
|
23
23
|
@objc(SpeechRecognitionPlugin)
|
|
24
24
|
public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
25
|
-
private let pluginVersion = "8.
|
|
25
|
+
private let pluginVersion = "8.1.11"
|
|
26
26
|
public let identifier = "SpeechRecognitionPlugin"
|
|
27
27
|
public let jsName = "SpeechRecognition"
|
|
28
28
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -41,6 +41,7 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
41
41
|
]
|
|
42
42
|
|
|
43
43
|
private let audioEngine = AVAudioEngine()
|
|
44
|
+
private var lastAudioLevelEmitTime: CFTimeInterval = 0
|
|
44
45
|
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
|
|
45
46
|
private var recognitionTask: SFSpeechRecognitionTask?
|
|
46
47
|
private var speechRecognizer: SFSpeechRecognizer?
|
|
@@ -69,15 +70,32 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
69
70
|
|
|
70
71
|
@objc func isOnDeviceRecognitionAvailable(_ call: CAPPluginCall) {
|
|
71
72
|
let locale = Locale(identifier: call.getString("language") ?? Locale.current.identifier)
|
|
73
|
+
let preferLegacyRecognizer = call.getBool("preferLegacyRecognizer") ?? false
|
|
74
|
+
let modernPathSupportedOnOS: Bool
|
|
72
75
|
if #available(iOS 26.0, *) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
modernPathSupportedOnOS = true
|
|
77
|
+
} else {
|
|
78
|
+
modernPathSupportedOnOS = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch OnDeviceRecognitionAvailabilityResolver.route(
|
|
82
|
+
preferLegacyRecognizer: preferLegacyRecognizer,
|
|
83
|
+
modernPathSupportedOnOS: modernPathSupportedOnOS
|
|
84
|
+
) {
|
|
85
|
+
case .modern:
|
|
86
|
+
if #available(iOS 26.0, *) {
|
|
87
|
+
Task { @MainActor in
|
|
88
|
+
let isAvailable = await SpeechAnalyzerRecognitionSupport.supports(locale: locale)
|
|
89
|
+
call.resolve(["available": isAvailable])
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
call.resolve(["available": false])
|
|
76
93
|
}
|
|
77
|
-
|
|
94
|
+
case .legacy:
|
|
95
|
+
call.resolve([
|
|
96
|
+
"available": OnDeviceRecognitionAvailabilityResolver.legacyAvailability(for: locale)
|
|
97
|
+
])
|
|
78
98
|
}
|
|
79
|
-
|
|
80
|
-
call.resolve(["available": false])
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
@objc func start(_ call: CAPPluginCall) {
|
|
@@ -102,6 +120,7 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
102
120
|
addPunctuation: call.getBool("addPunctuation") ?? false,
|
|
103
121
|
contextualStrings: contextualStrings,
|
|
104
122
|
useOnDeviceRecognition: call.getBool("useOnDeviceRecognition") ?? false,
|
|
123
|
+
preferLegacyRecognizer: call.getBool("preferLegacyRecognizer") ?? false,
|
|
105
124
|
continuousPTT: call.getBool("continuousPTT") ?? false
|
|
106
125
|
)
|
|
107
126
|
|
|
@@ -279,6 +298,7 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
279
298
|
let locale = Locale(identifier: options.language)
|
|
280
299
|
if #available(iOS 26.0, *),
|
|
281
300
|
options.useOnDeviceRecognition,
|
|
301
|
+
!options.preferLegacyRecognizer,
|
|
282
302
|
await SpeechAnalyzerRecognitionSupport.supports(locale: locale) {
|
|
283
303
|
beginModernRecognition(call: call, options: options, locale: locale, sessionId: sessionId, restarting: restarting)
|
|
284
304
|
} else {
|
|
@@ -314,6 +334,28 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
314
334
|
return
|
|
315
335
|
}
|
|
316
336
|
|
|
337
|
+
let onDeviceRequirement = LegacyOnDeviceRecognitionRequirement.evaluate(
|
|
338
|
+
useOnDeviceRecognition: options.useOnDeviceRecognition,
|
|
339
|
+
supportsOnDeviceRecognition: recognizer.supportsOnDeviceRecognition
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if onDeviceRequirement == .unavailable {
|
|
343
|
+
let message = LegacyOnDeviceRecognitionRequirement.unavailableErrorMessage
|
|
344
|
+
call?.reject(message)
|
|
345
|
+
emitErrorEvent(
|
|
346
|
+
code: LegacyOnDeviceRecognitionRequirement.unavailableErrorCode,
|
|
347
|
+
message: message,
|
|
348
|
+
sessionId: sessionId
|
|
349
|
+
)
|
|
350
|
+
activeCall = nil
|
|
351
|
+
finishSessionIfNeeded(
|
|
352
|
+
sessionId: sessionId,
|
|
353
|
+
reason: .error,
|
|
354
|
+
errorCode: LegacyOnDeviceRecognitionRequirement.unavailableErrorCode
|
|
355
|
+
)
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
317
359
|
speechRecognizer = recognizer
|
|
318
360
|
|
|
319
361
|
do {
|
|
@@ -328,6 +370,17 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
328
370
|
|
|
329
371
|
let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
|
|
330
372
|
recognitionRequest.shouldReportPartialResults = options.partialResults
|
|
373
|
+
|
|
374
|
+
if onDeviceRequirement == .required {
|
|
375
|
+
// Honour `useOnDeviceRecognition` on the legacy path. Without this the
|
|
376
|
+
// option has no effect here and the audio is sent to Apple's servers, even
|
|
377
|
+
// though `SFSpeechRecognizer` has supported on-device recognition since
|
|
378
|
+
// iOS 13. Verified on device: with this line, transcription keeps working
|
|
379
|
+
// in airplane mode.
|
|
380
|
+
if #available(iOS 13.0, *) {
|
|
381
|
+
recognitionRequest.requiresOnDeviceRecognition = true
|
|
382
|
+
}
|
|
383
|
+
}
|
|
331
384
|
if !options.contextualStrings.isEmpty {
|
|
332
385
|
recognitionRequest.contextualStrings = options.contextualStrings
|
|
333
386
|
}
|
|
@@ -341,6 +394,7 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
341
394
|
inputNode.removeTap(onBus: 0)
|
|
342
395
|
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in
|
|
343
396
|
self?.recognitionRequest?.append(buffer)
|
|
397
|
+
self?.emitAudioLevelIfNeeded(from: buffer, sessionId: sessionId)
|
|
344
398
|
}
|
|
345
399
|
hasInstalledTap = true
|
|
346
400
|
|
|
@@ -427,6 +481,13 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
427
481
|
)
|
|
428
482
|
modernRecognitionSession = session
|
|
429
483
|
|
|
484
|
+
session.onAudioLevel = { [weak self, weak session] level in
|
|
485
|
+
guard let self, let session, self.modernRecognitionSession === session, self.activeSessionId == sessionId else {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
self.notifyListeners("audioLevel", data: ["level": level])
|
|
489
|
+
}
|
|
490
|
+
|
|
430
491
|
session.onListeningStarted = { [weak self, weak session] in
|
|
431
492
|
guard let self, let session, self.modernRecognitionSession === session, self.activeSessionId == sessionId else {
|
|
432
493
|
return
|
|
@@ -604,6 +665,21 @@ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
604
665
|
finishSessionIfNeeded(sessionId: sessionId, reason: pendingStopReason ?? .error, errorCode: code)
|
|
605
666
|
}
|
|
606
667
|
|
|
668
|
+
|
|
669
|
+
private func emitAudioLevelIfNeeded(from buffer: AVAudioPCMBuffer, sessionId: Int) {
|
|
670
|
+
let now = CACurrentMediaTime()
|
|
671
|
+
guard AudioLevelMetering.shouldEmit(now: now, lastEmit: &lastAudioLevelEmitTime) else {
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
let level = AudioLevelMetering.normalizedAudioLevel(from: buffer)
|
|
675
|
+
DispatchQueue.main.async { [weak self] in
|
|
676
|
+
guard let self, self.activeSessionId == sessionId else {
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
self.notifyListeners("audioLevel", data: ["level": level])
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
607
683
|
private func buildMatches(from result: SFSpeechRecognitionResult, maxResults: Int) -> [String] {
|
|
608
684
|
var matches: [String] = []
|
|
609
685
|
for transcription in result.transcriptions where matches.count < maxResults {
|
|
@@ -854,5 +930,7 @@ private struct RecognitionOptions {
|
|
|
854
930
|
let addPunctuation: Bool
|
|
855
931
|
let contextualStrings: [String]
|
|
856
932
|
let useOnDeviceRecognition: Bool
|
|
933
|
+
/// Skip the modern (SpeechAnalyzer) path even when it is available.
|
|
934
|
+
let preferLegacyRecognizer: Bool
|
|
857
935
|
let continuousPTT: Bool
|
|
858
936
|
}
|
|
@@ -1,7 +1,155 @@
|
|
|
1
1
|
import XCTest
|
|
2
|
+
@testable import SpeechRecognitionPlugin
|
|
2
3
|
|
|
3
4
|
final class SpeechRecognitionPluginTests: XCTestCase {
|
|
4
|
-
func
|
|
5
|
-
|
|
5
|
+
func testLegacyOnDeviceRequirement_notRequiredWhenFlagDisabled() {
|
|
6
|
+
XCTAssertEqual(
|
|
7
|
+
LegacyOnDeviceRecognitionRequirement.evaluate(
|
|
8
|
+
useOnDeviceRecognition: false,
|
|
9
|
+
supportsOnDeviceRecognition: false
|
|
10
|
+
),
|
|
11
|
+
.notRequired
|
|
12
|
+
)
|
|
13
|
+
XCTAssertEqual(
|
|
14
|
+
LegacyOnDeviceRecognitionRequirement.evaluate(
|
|
15
|
+
useOnDeviceRecognition: false,
|
|
16
|
+
supportsOnDeviceRecognition: true
|
|
17
|
+
),
|
|
18
|
+
.notRequired
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func testLegacyOnDeviceRequirement_requiredWhenSupported() {
|
|
23
|
+
XCTAssertEqual(
|
|
24
|
+
LegacyOnDeviceRecognitionRequirement.evaluate(
|
|
25
|
+
useOnDeviceRecognition: true,
|
|
26
|
+
supportsOnDeviceRecognition: true
|
|
27
|
+
),
|
|
28
|
+
.required
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testLegacyOnDeviceRequirement_unavailableWhenRequestedButNotSupported() {
|
|
33
|
+
XCTAssertEqual(
|
|
34
|
+
LegacyOnDeviceRecognitionRequirement.evaluate(
|
|
35
|
+
useOnDeviceRecognition: true,
|
|
36
|
+
supportsOnDeviceRecognition: false
|
|
37
|
+
),
|
|
38
|
+
.unavailable
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func testLegacyOnDeviceRequirement_unavailableErrorMetadataMatchesAndroid() {
|
|
43
|
+
XCTAssertEqual(
|
|
44
|
+
LegacyOnDeviceRecognitionRequirement.unavailableErrorCode,
|
|
45
|
+
"ON_DEVICE_RECOGNITION_UNAVAILABLE"
|
|
46
|
+
)
|
|
47
|
+
XCTAssertEqual(
|
|
48
|
+
LegacyOnDeviceRecognitionRequirement.unavailableErrorMessage,
|
|
49
|
+
"On-device speech recognition is not available on this device."
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func testOnDeviceAvailabilityRoute_modernWhenModernOSAndLegacyNotPreferred() {
|
|
54
|
+
XCTAssertEqual(
|
|
55
|
+
OnDeviceRecognitionAvailabilityResolver.route(
|
|
56
|
+
preferLegacyRecognizer: false,
|
|
57
|
+
modernPathSupportedOnOS: true
|
|
58
|
+
),
|
|
59
|
+
.modern
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func testOnDeviceAvailabilityRoute_legacyWhenLegacyPreferredOnModernOS() {
|
|
64
|
+
XCTAssertEqual(
|
|
65
|
+
OnDeviceRecognitionAvailabilityResolver.route(
|
|
66
|
+
preferLegacyRecognizer: true,
|
|
67
|
+
modernPathSupportedOnOS: true
|
|
68
|
+
),
|
|
69
|
+
.legacy
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func testOnDeviceAvailabilityRoute_legacyWhenModernOSUnsupported() {
|
|
74
|
+
XCTAssertEqual(
|
|
75
|
+
OnDeviceRecognitionAvailabilityResolver.route(
|
|
76
|
+
preferLegacyRecognizer: false,
|
|
77
|
+
modernPathSupportedOnOS: false
|
|
78
|
+
),
|
|
79
|
+
.legacy
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func testOnDeviceAvailabilityRoute_legacyWhenLegacyPreferredOnOlderOS() {
|
|
84
|
+
XCTAssertEqual(
|
|
85
|
+
OnDeviceRecognitionAvailabilityResolver.route(
|
|
86
|
+
preferLegacyRecognizer: true,
|
|
87
|
+
modernPathSupportedOnOS: false
|
|
88
|
+
),
|
|
89
|
+
.legacy
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func testLegacyOnDeviceAvailability_falseWhenRecognizerMissing() {
|
|
94
|
+
XCTAssertFalse(
|
|
95
|
+
OnDeviceRecognitionAvailabilityResolver.legacyAvailability(
|
|
96
|
+
recognizerExists: false,
|
|
97
|
+
supportsOnDeviceRecognition: true
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func testLegacyOnDeviceAvailability_falseWhenOnDeviceUnsupported() {
|
|
103
|
+
XCTAssertFalse(
|
|
104
|
+
OnDeviceRecognitionAvailabilityResolver.legacyAvailability(
|
|
105
|
+
recognizerExists: true,
|
|
106
|
+
supportsOnDeviceRecognition: false
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func testLegacyOnDeviceAvailability_trueWhenOnDeviceSupported() {
|
|
112
|
+
XCTAssertTrue(
|
|
113
|
+
OnDeviceRecognitionAvailabilityResolver.legacyAvailability(
|
|
114
|
+
recognizerExists: true,
|
|
115
|
+
supportsOnDeviceRecognition: true
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func testBeginRecognitionRouteAlignment_prefersModernOnlyWhenEligible() {
|
|
121
|
+
let modernPathSupportedOnOS = true
|
|
122
|
+
let preferLegacyRecognizer = false
|
|
123
|
+
let useOnDeviceRecognition = true
|
|
124
|
+
|
|
125
|
+
let availabilityRoute = OnDeviceRecognitionAvailabilityResolver.route(
|
|
126
|
+
preferLegacyRecognizer: preferLegacyRecognizer,
|
|
127
|
+
modernPathSupportedOnOS: modernPathSupportedOnOS
|
|
128
|
+
)
|
|
129
|
+
let beginRecognitionUsesModern =
|
|
130
|
+
modernPathSupportedOnOS &&
|
|
131
|
+
useOnDeviceRecognition &&
|
|
132
|
+
!preferLegacyRecognizer
|
|
133
|
+
|
|
134
|
+
XCTAssertEqual(availabilityRoute, .modern)
|
|
135
|
+
XCTAssertTrue(beginRecognitionUsesModern)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func testBeginRecognitionRouteAlignment_prefersLegacyWhenLegacyFlagSet() {
|
|
139
|
+
let modernPathSupportedOnOS = true
|
|
140
|
+
let preferLegacyRecognizer = true
|
|
141
|
+
let useOnDeviceRecognition = true
|
|
142
|
+
|
|
143
|
+
let availabilityRoute = OnDeviceRecognitionAvailabilityResolver.route(
|
|
144
|
+
preferLegacyRecognizer: preferLegacyRecognizer,
|
|
145
|
+
modernPathSupportedOnOS: modernPathSupportedOnOS
|
|
146
|
+
)
|
|
147
|
+
let beginRecognitionUsesModern =
|
|
148
|
+
modernPathSupportedOnOS &&
|
|
149
|
+
useOnDeviceRecognition &&
|
|
150
|
+
!preferLegacyRecognizer
|
|
151
|
+
|
|
152
|
+
XCTAssertEqual(availabilityRoute, .legacy)
|
|
153
|
+
XCTAssertFalse(beginRecognitionUsesModern)
|
|
6
154
|
}
|
|
7
155
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-speech-recognition",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Capacitor plugin for comprehensive on-device speech recognition with live partial results.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
38
|
-
"verify:ios": "
|
|
38
|
+
"verify:ios": "bash scripts/verify-ios.sh",
|
|
39
39
|
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
40
40
|
"verify:web": "npm run build",
|
|
41
41
|
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|