1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-02-19 20:00:20 +02:00

Android: Add support for Voice Typing for most languages (#8309)

This commit is contained in:
Laurent Cozic 2023-06-13 18:06:54 +01:00 committed by GitHub
parent 92c68882e2
commit ce7da882ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 228 additions and 299 deletions

View File

@ -36,7 +36,7 @@ index 6afcbbf0cc8ca2d69dd78077d61e59a90b2136bb..9f8d72b4ec5b2b3d290975d6a255917c
def kotlin_version = getExtOrDefault('kotlinVersion')
diff --git a/android/src/main/java/com/reactnativevosk/VoskModule.kt b/android/src/main/java/com/reactnativevosk/VoskModule.kt
index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..f3da440bc2863a59db6d2d1691c54d8d4870cb3f 100644
index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..5a8539b9cce8951967640dba755e29a4e3ff404a 100644
--- a/android/src/main/java/com/reactnativevosk/VoskModule.kt
+++ b/android/src/main/java/com/reactnativevosk/VoskModule.kt
@@ -19,13 +19,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
@ -66,7 +66,25 @@ index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..f3da440bc2863a59db6d2d1691c54d8d
sendEvent("onResult", text)
}
}
@@ -153,6 +165,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
@@ -93,12 +105,11 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
@ReactMethod
fun loadModel(path: String, promise: Promise) {
cleanModel();
- StorageService.unpack(context, path, "models",
- { model: Model? ->
- this.model = model
- promise.resolve("Model successfully loaded")
- }
- ) { e: IOException ->
+
+ try {
+ this.model = Model(path);
+ promise.resolve("Model successfully loaded")
+ } catch (e: IOException) {
this.model = null
promise.reject(e)
}
@@ -153,6 +164,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
cleanRecognizer();
}

View File

@ -1,7 +0,0 @@
French small model for Vosk
WER
%WER 23.95 [ 37203 / 155330, 5373 ins, 4427 del, 27403 sub ] exp/chain_a/tdnn/decode_test_cv/wer_12_0.0
%WER 19.30 [ 2975 / 15412, 683 ins, 672 del, 1620 sub ] exp/chain_a/tdnn/decode_test_mtedx/wer_10_0.0
%WER 27.25 [ 20208 / 74145, 2647 ins, 5852 del, 11709 sub ] exp/chain_a/tdnn/decode_test_podcast_reseg/wer_10_0.0

View File

@ -1,8 +0,0 @@
--use-energy=false
--sample-frequency=16000
--num-mel-bins=40
--num-ceps=40
--low-freq=40
--high-freq=-200
--allow-upsample=true
--allow-downsample=true

View File

@ -1,10 +0,0 @@
--min-active=200
--max-active=7000
--beam=13.0
--lattice-beam=4.0
--acoustic-scale=1.0
--frame-subsampling-factor=3
--endpoint.silence-phones=1:2:3:4:5:6:7:8:9:10
--endpoint.rule2.min-trailing-silence=0.5
--endpoint.rule3.min-trailing-silence=1.0
--endpoint.rule4.min-trailing-silence=2.0

View File

@ -1,76 +0,0 @@
9365
9366
9367
9368
9369
9370
9371
9372
9373
9374
9375
9376
9377
9378
9379
9380
9381
9382
9383
9384
9385
9386
9387
9388
9389
9390
9391
9392
9393
9394
9395
9396
9397
9398
9399
9400
9401
9402
9403
9404
9405
9406
9407
9408
9409
9410
9411
9412
9413
9414
9415
9416
9417
9418
9419
9420
9421
9422
9423
9424
9425
9426
9427
9428
9429
9430
9431
9432
9433
9434
9435
9436
9437
9438
9439
9440

View File

@ -1,154 +0,0 @@
1 nonword
2 begin
3 end
4 internal
5 singleton
6 nonword
7 begin
8 end
9 internal
10 singleton
11 begin
12 end
13 internal
14 singleton
15 begin
16 end
17 internal
18 singleton
19 begin
20 end
21 internal
22 singleton
23 begin
24 end
25 internal
26 singleton
27 begin
28 end
29 internal
30 singleton
31 begin
32 end
33 internal
34 singleton
35 begin
36 end
37 internal
38 singleton
39 begin
40 end
41 internal
42 singleton
43 begin
44 end
45 internal
46 singleton
47 begin
48 end
49 internal
50 singleton
51 begin
52 end
53 internal
54 singleton
55 begin
56 end
57 internal
58 singleton
59 begin
60 end
61 internal
62 singleton
63 begin
64 end
65 internal
66 singleton
67 begin
68 end
69 internal
70 singleton
71 begin
72 end
73 internal
74 singleton
75 begin
76 end
77 internal
78 singleton
79 begin
80 end
81 internal
82 singleton
83 begin
84 end
85 internal
86 singleton
87 begin
88 end
89 internal
90 singleton
91 begin
92 end
93 internal
94 singleton
95 begin
96 end
97 internal
98 singleton
99 begin
100 end
101 internal
102 singleton
103 begin
104 end
105 internal
106 singleton
107 begin
108 end
109 internal
110 singleton
111 begin
112 end
113 internal
114 singleton
115 begin
116 end
117 internal
118 singleton
119 begin
120 end
121 internal
122 singleton
123 begin
124 end
125 internal
126 singleton
127 begin
128 end
129 internal
130 singleton
131 begin
132 end
133 internal
134 singleton
135 begin
136 end
137 internal
138 singleton
139 begin
140 end
141 internal
142 singleton
143 begin
144 end
145 internal
146 singleton
147 begin
148 end
149 internal
150 singleton
151 begin
152 end
153 internal
154 singleton

View File

@ -1,3 +0,0 @@
[
1.022245e+11 -6.33291e+09 -2.480997e+09 8.290258e+09 -9.084483e+09 -8.092173e+09 -1.4735e+10 -7.041795e+09 -1.171205e+10 -2.976464e+08 -1.009425e+10 -6765179 -7.821326e+09 1.449499e+09 -6.413975e+09 -5.303802e+08 -4.998635e+09 9.521598e+07 -3.073041e+09 1.56756e+08 -1.287956e+09 1.738752e+08 -2.382392e+08 -2.716675e+07 4.404485e+08 -1.913359e+08 7.780919e+08 -4.006922e+08 7.895809e+08 -5.401082e+08 5.17605e+08 -6.227134e+08 6.58271e+08 -6.204593e+07 5.187754e+08 -4.497048e+08 4.219366e+07 -2.78742e+08 -1.797385e+07 -3.604475e+07 1.053647e+09
1.040194e+13 6.245521e+11 4.223293e+11 6.831219e+11 6.078478e+11 6.3425e+11 7.943839e+11 6.013323e+11 6.781652e+11 5.272091e+11 5.810814e+11 4.353831e+11 4.473305e+11 3.42063e+11 3.083377e+11 2.14257e+11 1.892057e+11 1.163827e+11 8.367058e+10 4.203224e+10 2.297476e+10 7.596307e+09 1.099877e+09 2.886651e+08 3.797438e+09 9.372847e+09 1.629059e+10 2.196351e+10 2.747149e+10 3.072878e+10 3.238528e+10 3.330232e+10 3.407238e+10 3.230687e+10 2.676914e+10 2.252055e+10 1.914305e+10 1.565974e+10 1.224627e+10 8.415393e+09 0 ]

View File

@ -1,2 +0,0 @@
--left-context=3
--right-context=3

View File

@ -1 +0,0 @@
1b7180e6-e500-4818-adc8-a41fe97a84ce

View File

@ -46,6 +46,7 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
import Logger from '@joplin/lib/Logger';
import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog';
import { voskEnabled } from '../../services/voiceTyping/vosk';
import { isSupportedLanguage } from '../../services/voiceTyping/vosk.android';
const urlUtils = require('@joplin/lib/urlUtils');
// import Vosk from 'react-native-vosk';
@ -1000,7 +1001,7 @@ class NoteScreenComponent extends BaseScreenComponent {
});
// Voice typing is enabled only for French language and on Android for now
if (voskEnabled && shim.mobilePlatform() === 'android' && currentLocale() === 'fr_FR') {
if (voskEnabled && shim.mobilePlatform() === 'android' && isSupportedLanguage(currentLocale())) {
output.push({
title: _('Voice typing...'),
onPress: () => {
@ -1307,7 +1308,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const renderVoiceTypingDialog = () => {
if (!this.state.voiceTypingDialogShown) return null;
return <VoiceTypingDialog onText={this.voiceTypingDialog_onText} onDismiss={this.voiceTypingDialog_onDismiss}/>;
return <VoiceTypingDialog locale={currentLocale()} onText={this.voiceTypingDialog_onText} onDismiss={this.voiceTypingDialog_onDismiss}/>;
};
return (

View File

@ -1,12 +1,14 @@
import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Banner, ActivityIndicator } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import { Banner, ActivityIndicator, Modal } from 'react-native-paper';
import { _, languageName } from '@joplin/lib/locale';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { getVosk, Recorder, startRecording, Vosk } from '../../services/voiceTyping/vosk';
import { IconSource } from 'react-native-paper/lib/typescript/src/components/Icon';
import { modelIsDownloaded } from '../../services/voiceTyping/vosk.android';
interface Props {
locale: string;
onDismiss: ()=> void;
onText: (text: string)=> void;
}
@ -16,30 +18,39 @@ enum RecorderState {
Recording = 2,
Processing = 3,
Error = 4,
Downloading = 5,
}
const useVosk = (): [Error | null, Vosk|null] => {
const useVosk = (locale: string): [Error | null, boolean, Vosk|null] => {
const [vosk, setVosk] = useState<Vosk>(null);
const [error, setError] = useState<Error>(null);
const [mustDownloadModel, setMustDownloadModel] = useState<boolean | null>(null);
useAsyncEffect(async (event: AsyncEffectEvent) => {
if (mustDownloadModel === null) return;
try {
const v = await getVosk();
const v = await getVosk(locale);
if (event.cancelled) return;
setVosk(v);
} catch (error) {
setError(error);
} finally {
setMustDownloadModel(false);
}
}, []);
}, [locale, mustDownloadModel]);
return [error, vosk];
useAsyncEffect(async (_event: AsyncEffectEvent) => {
setMustDownloadModel(!(await modelIsDownloaded(locale)));
}, [locale]);
return [error, mustDownloadModel, vosk];
};
export default (props: Props) => {
const [recorder, setRecorder] = useState<Recorder>(null);
const [recorderState, setRecorderState] = useState<RecorderState>(RecorderState.Loading);
const [voskError, vosk] = useVosk();
const [voskError, mustDownloadModel, vosk] = useVosk(props.locale);
useEffect(() => {
if (voskError) {
@ -49,6 +60,12 @@ export default (props: Props) => {
}
}, [vosk, voskError]);
useEffect(() => {
if (mustDownloadModel) {
setRecorderState(RecorderState.Downloading);
}
}, [mustDownloadModel]);
useEffect(() => {
if (recorderState === RecorderState.Recording) {
setRecorder(startRecording(vosk, {
@ -60,7 +77,7 @@ export default (props: Props) => {
}, [recorderState, vosk, props.onText]);
const onDismiss = useCallback(() => {
recorder.cleanup();
if (recorder) recorder.cleanup();
props.onDismiss();
}, [recorder, props.onDismiss]);
@ -69,6 +86,7 @@ export default (props: Props) => {
[RecorderState.Loading]: () => _('Loading...'),
[RecorderState.Recording]: () => _('Please record your voice...'),
[RecorderState.Processing]: () => _('Converting speech to text...'),
[RecorderState.Downloading]: () => _('Downloading %s language files...', languageName(props.locale)),
[RecorderState.Error]: () => _('Error: %s', voskError.message),
};
@ -80,6 +98,7 @@ export default (props: Props) => {
[RecorderState.Loading]: ({ size }: { size: number }) => <ActivityIndicator animating={true} style={{ width: size, height: size }} />,
[RecorderState.Recording]: 'microphone',
[RecorderState.Processing]: 'microphone',
[RecorderState.Downloading]: ({ size }: { size: number }) => <ActivityIndicator animating={true} style={{ width: size, height: size }} />,
[RecorderState.Error]: 'alert-circle-outline',
};
@ -87,16 +106,18 @@ export default (props: Props) => {
};
return (
<Banner
visible={true}
icon={renderIcon()}
actions={[
{
label: _('Done'),
onPress: onDismiss,
},
]}>
{`${_('Voice typing...')}\n${renderContent()}`}
</Banner>
<Modal visible={true} style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
<Banner
visible={true}
icon={renderIcon()}
actions={[
{
label: _('Done'),
onPress: onDismiss,
},
]}>
{`${_('Voice typing...')}\n${renderContent()}`}
</Banner>
</Modal>
);
};

View File

@ -483,7 +483,15 @@ PODS:
- React-Core
- RNVectorIcons (9.2.0):
- React-Core
- RNZipArchive (6.0.9):
- React-Core
- RNZipArchive/Core (= 6.0.9)
- SSZipArchive (~> 2.2)
- RNZipArchive/Core (6.0.9):
- React-Core
- SSZipArchive (~> 2.2)
- SocketRocket (0.6.0)
- SSZipArchive (2.4.3)
- Yoga (1.14.0)
- YogaKit (1.18.1):
- Yoga (~> 1.14)
@ -577,6 +585,7 @@ DEPENDENCIES:
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
- RNShare (from `../node_modules/react-native-share`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- RNZipArchive (from `../node_modules/react-native-zip-archive`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@ -595,6 +604,7 @@ SPEC REPOS:
- libevent
- OpenSSL-Universal
- SocketRocket
- SSZipArchive
- YogaKit
EXTERNAL SOURCES:
@ -724,6 +734,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-share"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
RNZipArchive:
:path: "../node_modules/react-native-zip-archive"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@ -804,7 +816,9 @@ SPEC CHECKSUMS:
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
Yoga: e7ea9e590e27460d28911403b894722354d73479
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

View File

@ -69,6 +69,7 @@
"react-native-version-info": "1.1.1",
"react-native-vosk": "0.1.12",
"react-native-webview": "12.4.0",
"react-native-zip-archive": "6.0.9",
"react-redux": "7.2.9",
"redux": "4.2.1",
"rn-fetch-blob": "0.12.0",

View File

@ -1,17 +1,25 @@
import { languageCodeOnly } from '@joplin/lib/locale';
import Logger from '@joplin/lib/Logger';
import shim from '@joplin/lib/shim';
import Vosk from 'react-native-vosk';
import { unzip } from 'react-native-zip-archive';
import RNFetchBlob from 'rn-fetch-blob';
const md5 = require('md5');
const logger = Logger.create('voiceTyping/vosk');
enum State {
Idle = 0,
Recording,
Completing,
}
interface StartOptions {
onResult: (text: string)=> void;
}
let vosk_: Vosk|null = null;
let vosk_: Record<string, Vosk> = {};
let state_: State = State.Idle;
export const voskEnabled = true;
@ -23,12 +31,113 @@ export interface Recorder {
cleanup: ()=> void;
}
export const getVosk = async () => {
if (vosk_) return vosk_;
vosk_ = new Vosk();
const result = await vosk_.loadModel('model-fr-fr');
const supportedLanguages = {
'en': 'https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip',
'cn': 'https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip',
'ru': 'https://alphacephei.com/vosk/models/vosk-model-small-ru-0.22.zip',
'fr': 'https://alphacephei.com/vosk/models/vosk-model-small-fr-0.22.zip',
'de': 'https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip',
'es': 'https://alphacephei.com/vosk/models/vosk-model-small-es-0.42.zip',
'pt': 'https://alphacephei.com/vosk/models/vosk-model-small-pt-0.3.zip',
'tr': 'https://alphacephei.com/vosk/models/vosk-model-small-tr-0.3.zip',
'vn': 'https://alphacephei.com/vosk/models/vosk-model-small-vn-0.4.zip',
'it': 'https://alphacephei.com/vosk/models/vosk-model-small-it-0.22.zip',
'nl': 'https://alphacephei.com/vosk/models/vosk-model-small-nl-0.22.zip',
'uk': 'https://alphacephei.com/vosk/models/vosk-model-small-uk-v3-small.zip',
'ja': 'https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip',
'hi': 'https://alphacephei.com/vosk/models/vosk-model-small-hi-0.22.zip',
'cs': 'https://alphacephei.com/vosk/models/vosk-model-small-cs-0.4-rhasspy.zip',
'pl': 'https://alphacephei.com/vosk/models/vosk-model-small-pl-0.22.zip',
'uz': 'https://alphacephei.com/vosk/models/vosk-model-small-uz-0.22.zip',
'ko': 'https://alphacephei.com/vosk/models/vosk-model-small-ko-0.22.zip',
};
export const isSupportedLanguage = (locale: string) => {
const l = languageCodeOnly(locale).toLowerCase();
return Object.keys(supportedLanguages).includes(l);
};
// Where all the models files for all the languages are
const getModelRootDir = () => {
return `${RNFetchBlob.fs.dirs.DocumentDir}/vosk-models`;
};
// Where we unzip a model after downloading it
const getUnzipDir = (locale: string) => {
return `${getModelRootDir()}/${locale}`;
};
// Where the model for a particular language is
const getModelDir = (locale: string) => {
return `${getUnzipDir(locale)}/model`;
};
const languageModelUrl = (locale: string) => {
const l = languageCodeOnly(locale).toLowerCase();
if (!(l in supportedLanguages)) throw new Error(`No language file for: ${locale}`);
return (supportedLanguages as any)[l];
};
export const modelIsDownloaded = async (locale: string) => {
const uuidFile = `${getModelDir(locale)}/uuid`;
return shim.fsDriver().exists(uuidFile);
};
export const getVosk = async (locale: string) => {
if (vosk_[locale]) return vosk_[locale];
const vosk = new Vosk();
const modelDir = await downloadModel(locale);
logger.info(`Loading model from ${modelDir}`);
await shim.fsDriver().readDirStats(modelDir);
const result = await vosk.loadModel(modelDir);
logger.info('getVosk:', result);
return vosk_;
vosk_ = { [locale]: vosk };
return vosk;
};
const downloadModel = async (locale: string) => {
const modelUrl = languageModelUrl(locale);
const unzipDir = getUnzipDir(locale);
const zipFilePath = `${unzipDir}.zip`;
const modelDir = getModelDir(locale);
const uuidFile = `${modelDir}/uuid`;
if (await modelIsDownloaded(locale)) {
logger.info(`Model for ${locale} already exists at ${modelDir}`);
return modelDir;
}
await shim.fsDriver().remove(unzipDir);
logger.info(`Downloading model from: ${modelUrl}`);
await shim.fetchBlob(languageModelUrl(locale), {
path: zipFilePath,
});
logger.info(`Unzipping ${zipFilePath} => ${unzipDir}`);
await unzip(zipFilePath, unzipDir);
const dirs = await shim.fsDriver().readDirStats(unzipDir);
if (dirs.length !== 1) {
logger.error('Expected 1 directory but got', dirs);
throw new Error(`Expected 1 directory, but got ${dirs.length}`);
}
const fullUnzipPath = `${unzipDir}/${dirs[0].path}`;
logger.info(`Moving ${fullUnzipPath} => ${modelDir}`);
await shim.fsDriver().rename(fullUnzipPath, modelDir);
await shim.fsDriver().writeFile(uuidFile, md5(modelUrl));
await shim.fsDriver().remove(zipFilePath);
return modelDir;
};
export const startRecording = (vosk: Vosk, options: StartOptions): Recorder => {
@ -110,8 +219,9 @@ export const startRecording = (vosk: Vosk, options: StartOptions): Recorder => {
});
},
cleanup: () => {
if (state_ !== State.Idle) {
if (state_ === State.Recording) {
logger.info('Cancelling...');
state_ = State.Completing;
vosk.stopOnly();
completeRecording('', null);
}

View File

@ -13,7 +13,15 @@ export interface Recorder {
cleanup: ()=> void;
}
export const getVosk = async () => {
export const isSupportedLanguage = (_locale: string) => {
return false;
};
export const modelIsDownloaded = async (_locale: string) => {
return false;
};
export const getVosk = async (_locale: string) => {
return {} as any;
};

View File

@ -116,6 +116,13 @@ export default class FsDriverRN extends FsDriverBase {
return RNFS.moveFile(source, dest);
}
public async rename(source: string, dest: string) {
if (isScopedUri(source) || isScopedUri(dest)) {
await RNSAF.rename(source, dest);
}
return RNFS.moveFile(source, dest);
}
public async exists(path: string) {
if (isScopedUri(path)) {
return RNSAF.exists(path);

View File

@ -508,7 +508,8 @@ function languageNameInEnglish(languageCode: string) {
return codeToLanguageE_[languageCode] ? codeToLanguageE_[languageCode] : '';
}
function languageName(languageCode: string, defaultToEnglish: boolean = true) {
function languageName(canonicalName: string, defaultToEnglish: boolean = true) {
const languageCode = languageCodeOnly(canonicalName);
if (codeToLanguage_[languageCode]) return codeToLanguage_[languageCode];
if (defaultToEnglish) return languageNameInEnglish(languageCode);
return '';
@ -603,4 +604,4 @@ const stringByLocale = (locale: string, s: string, ...args: any[]): string => {
}
};
export { _, _n, supportedLocales, currentLocale, localesFromLanguageCode, languageCodeOnly, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
export { _, _n, supportedLocales, languageName, currentLocale, localesFromLanguageCode, languageCodeOnly, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };

View File

@ -118,8 +118,6 @@ async function createRelease(projectName: string, name: string, tagName: string,
content = content.replace(/\s+"react-native-vosk": ".*",/, '');
return content;
});
await patcher.removeFile(`${rnDir}/android/app/src/main/assets/model-fr-fr`);
}
if (name === 'vosk') {

View File

@ -4425,6 +4425,7 @@ __metadata:
react-native-version-info: 1.1.1
react-native-vosk: 0.1.12
react-native-webview: 12.4.0
react-native-zip-archive: 6.0.9
react-redux: 7.2.9
redux: 4.2.1
rn-fetch-blob: 0.12.0
@ -27380,11 +27381,11 @@ __metadata:
"react-native-vosk@patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch::locator=root%40workspace%3A.":
version: 0.1.12
resolution: "react-native-vosk@patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch::version=0.1.12&hash=b82215&locator=root%40workspace%3A."
resolution: "react-native-vosk@patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch::version=0.1.12&hash=5deb2c&locator=root%40workspace%3A."
peerDependencies:
react: "*"
react-native: "*"
checksum: 9451534de711f28274e117087a2c27266ee60ffc46723136841669c02345fd5644333a204c98c18d82e2c4221bb65906ac20529fd63fdcd5439c8aad86710e3b
checksum: ca31cf6345f422b778c18bc2365f5114ab2c3e20214e684bbb77220e6476cd877bb5c256d88070b2235f01714e8cd8e5bee999b854894b066d31834b9b17dca4
languageName: node
linkType: hard
@ -27401,6 +27402,16 @@ __metadata:
languageName: node
linkType: hard
"react-native-zip-archive@npm:6.0.9":
version: 6.0.9
resolution: "react-native-zip-archive@npm:6.0.9"
peerDependencies:
react: ">=16.8.6"
react-native: ">=0.60.0"
checksum: 96c83864a596fcd8a82f55a01fa5e97822c9e80c7cf55de6b011a27cc436fbf58371207cb61c63b19dbd22b852ee62b95935358b4b9a73686695133dffdcfe27
languageName: node
linkType: hard
"react-native@npm:0.70.6":
version: 0.70.6
resolution: "react-native@npm:0.70.6"