diff --git a/.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch b/.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch index fad02b5f03..e9cf2395fa 100644 --- a/.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch +++ b/.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch @@ -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(); } diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/README b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/README deleted file mode 100644 index ce22e30d78..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/README +++ /dev/null @@ -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 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/am/final.mdl b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/am/final.mdl deleted file mode 100644 index 3ece48fb45..0000000000 Binary files a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/am/final.mdl and /dev/null differ diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/conf/mfcc.conf b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/conf/mfcc.conf deleted file mode 100644 index 12fdad4ed9..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/conf/mfcc.conf +++ /dev/null @@ -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 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/conf/model.conf b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/conf/model.conf deleted file mode 100644 index e66bcae8f0..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/conf/model.conf +++ /dev/null @@ -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 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/Gr.fst b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/Gr.fst deleted file mode 100644 index bf81b42de0..0000000000 Binary files a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/Gr.fst and /dev/null differ diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/HCLr.fst b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/HCLr.fst deleted file mode 100644 index b8ad5671dd..0000000000 Binary files a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/HCLr.fst and /dev/null differ diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/disambig_tid.int b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/disambig_tid.int deleted file mode 100644 index 07298933f9..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/disambig_tid.int +++ /dev/null @@ -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 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/phones/word_boundary.int b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/phones/word_boundary.int deleted file mode 100644 index 9a6c32ed58..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/graph/phones/word_boundary.int +++ /dev/null @@ -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 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.dubm b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.dubm deleted file mode 100644 index cab561c5e6..0000000000 Binary files a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.dubm and /dev/null differ diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.ie b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.ie deleted file mode 100644 index 335fabfb8d..0000000000 Binary files a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.ie and /dev/null differ diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.mat b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.mat deleted file mode 100644 index 2ba204b566..0000000000 Binary files a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/final.mat and /dev/null differ diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/global_cmvn.stats b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/global_cmvn.stats deleted file mode 100644 index 23cafcb5fd..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/global_cmvn.stats +++ /dev/null @@ -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 ] diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/online_cmvn.conf b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/online_cmvn.conf deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/splice.conf b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/splice.conf deleted file mode 100644 index 960cd2e4c2..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/ivector/splice.conf +++ /dev/null @@ -1,2 +0,0 @@ ---left-context=3 ---right-context=3 diff --git a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/uuid b/packages/app-mobile/android/app/src/main/assets/model-fr-fr/uuid deleted file mode 100644 index f736119c45..0000000000 --- a/packages/app-mobile/android/app/src/main/assets/model-fr-fr/uuid +++ /dev/null @@ -1 +0,0 @@ -1b7180e6-e500-4818-adc8-a41fe97a84ce \ No newline at end of file diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index a08fc913cd..c9798fad9a 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -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 ; + return ; }; return ( diff --git a/packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx b/packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx index 285a9a4199..5684ec59bc 100644 --- a/packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx +++ b/packages/app-mobile/components/voiceTyping/VoiceTypingDialog.tsx @@ -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(null); const [error, setError] = useState(null); + const [mustDownloadModel, setMustDownloadModel] = useState(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(null); const [recorderState, setRecorderState] = useState(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 }) => , [RecorderState.Recording]: 'microphone', [RecorderState.Processing]: 'microphone', + [RecorderState.Downloading]: ({ size }: { size: number }) => , [RecorderState.Error]: 'alert-circle-outline', }; @@ -87,16 +106,18 @@ export default (props: Props) => { }; return ( - - {`${_('Voice typing...')}\n${renderContent()}`} - + + + {`${_('Voice typing...')}\n${renderContent()}`} + + ); }; diff --git a/packages/app-mobile/ios/Podfile.lock b/packages/app-mobile/ios/Podfile.lock index 5cfc155e4d..c126dfb154 100644 --- a/packages/app-mobile/ios/Podfile.lock +++ b/packages/app-mobile/ios/Podfile.lock @@ -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 diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index e4eebfd4ca..41ebabfda6 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -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", diff --git a/packages/app-mobile/services/voiceTyping/vosk.android.ts b/packages/app-mobile/services/voiceTyping/vosk.android.ts index 9b30a0594f..a0b911c166 100644 --- a/packages/app-mobile/services/voiceTyping/vosk.android.ts +++ b/packages/app-mobile/services/voiceTyping/vosk.android.ts @@ -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 = {}; + 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); } diff --git a/packages/app-mobile/services/voiceTyping/vosk.ios.ts b/packages/app-mobile/services/voiceTyping/vosk.ios.ts index 08a33d8250..02cb0e3296 100644 --- a/packages/app-mobile/services/voiceTyping/vosk.ios.ts +++ b/packages/app-mobile/services/voiceTyping/vosk.ios.ts @@ -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; }; diff --git a/packages/app-mobile/utils/fs-driver-rn.ts b/packages/app-mobile/utils/fs-driver-rn.ts index b2a1809c64..f87bddea34 100644 --- a/packages/app-mobile/utils/fs-driver-rn.ts +++ b/packages/app-mobile/utils/fs-driver-rn.ts @@ -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); diff --git a/packages/lib/locale.ts b/packages/lib/locale.ts index 49493c23ca..c50aa5c055 100644 --- a/packages/lib/locale.ts +++ b/packages/lib/locale.ts @@ -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 }; diff --git a/packages/tools/release-android.ts b/packages/tools/release-android.ts index 20fddd58d2..79e522eb7d 100644 --- a/packages/tools/release-android.ts +++ b/packages/tools/release-android.ts @@ -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') { diff --git a/yarn.lock b/yarn.lock index 7e90d9fb9b..02ca98b9ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"