mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-27 08:21:03 +02:00
Android: Add support for Voice Typing for most languages (#8309)
This commit is contained in:
parent
377e35b1af
commit
5841596acb
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
Binary file not shown.
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 ]
|
@ -1,2 +0,0 @@
|
||||
--left-context=3
|
||||
--right-context=3
|
@ -1 +0,0 @@
|
||||
1b7180e6-e500-4818-adc8-a41fe97a84ce
|
@ -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';
|
||||
@ -988,7 +989,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: () => {
|
||||
@ -1295,7 +1296,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 (
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -402,7 +402,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.0.0):
|
||||
- RNDateTimePicker (7.0.1):
|
||||
- React-Core
|
||||
- RNExitApp (1.1.0):
|
||||
- React
|
||||
@ -420,7 +420,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)
|
||||
@ -511,6 +519,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:
|
||||
@ -529,6 +538,7 @@ SPEC REPOS:
|
||||
- libevent
|
||||
- OpenSSL-Universal
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
- YogaKit
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@ -654,6 +664,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"
|
||||
|
||||
@ -679,6 +691,7 @@ SPEC CHECKSUMS:
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||
<<<<<<< HEAD
|
||||
RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a
|
||||
RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e
|
||||
React: bb3e06418d2cc48a84f9666a576c7b38e89cd7db
|
||||
@ -692,6 +705,21 @@ SPEC CHECKSUMS:
|
||||
React-jsiexecutor: b4a65947391c658450151275aa406f2b8263178f
|
||||
React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b
|
||||
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
|
||||
=======
|
||||
RCTRequired: 8ef706f91e2b643cd32c26a57700b5f24fab0585
|
||||
RCTTypeSafety: 5fbddd8eb9242b91ac0d901c01da3673f358b1b7
|
||||
React: e5d2d559e89d256a1d6da64d51adaecda9c8ddae
|
||||
React-callinvoker: 352ecbafbdccca5fdf4aed99c98ae5b7fc28e39b
|
||||
React-Codegen: fa660a71e24078b2e52a62ecc2f3048c2f8ae6d7
|
||||
React-Core: 4ec45c2d537fe58e6d878bec6a13e3e2bed9c182
|
||||
React-CoreModules: 63f7f9fda3d4b214040a80e3f47ab4fb9a3e88e6
|
||||
React-cxxreact: 1a729807190ebf98ce5fb0c3d2ed211e8b5f2f87
|
||||
React-hermes: eb93eb6e7921ecd4abcc6e741b327f40763e850f
|
||||
React-jsi: 1995961abdff0c9af9aae8a6b24468f21811000e
|
||||
React-jsiexecutor: 4bb480a183a354e4dbfb1012936b1a2bb9357de7
|
||||
React-jsinspector: cdc854f8b13abd202afa54bc12578e5afb9cfae1
|
||||
React-logger: ef2269b3afa6ba868da90496c3e17a4ec4f4cee0
|
||||
>>>>>>> 13cda9a72 (Android: Add support for Voice Typing for most languages (#8309))
|
||||
react-native-alarm-notification: 26527410a6162d07a9dc57f4bbc62e94ff48e65d
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-document-picker: 69ca2094d8780cfc1e7e613894d15290fdc54bba
|
||||
@ -723,7 +751,7 @@ SPEC CHECKSUMS:
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: 66cb21772d462d02efc6826adac37f1fd40984cf
|
||||
RNDateTimePicker: e073697ac3e8a378968d68ab0581fef542b8af8a
|
||||
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
@ -732,8 +760,14 @@ SPEC CHECKSUMS:
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
<<<<<<< HEAD
|
||||
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc
|
||||
=======
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
Yoga: e7ea9e590e27460d28911403b894722354d73479
|
||||
>>>>>>> 13cda9a72 (Android: Add support for Voice Typing for most languages (#8309))
|
||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||
|
||||
PODFILE CHECKSUM: 0235ffbfa2e655de806a80d996148182dd493d8d
|
||||
|
@ -69,6 +69,7 @@
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "11.26.1",
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-redux": "7.2.9",
|
||||
"redux": "4.2.1",
|
||||
"rn-fetch-blob": "0.12.0",
|
||||
|
@ -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,11 +31,113 @@ export interface Recorder {
|
||||
cleanup: ()=> void;
|
||||
}
|
||||
|
||||
export const getVosk = async () => {
|
||||
if (vosk_) return vosk_;
|
||||
vosk_ = new Vosk();
|
||||
await vosk_.loadModel('model-fr-fr');
|
||||
return vosk_;
|
||||
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);
|
||||
|
||||
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 => {
|
||||
@ -109,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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 };
|
||||
|
@ -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') {
|
||||
|
15
yarn.lock
15
yarn.lock
@ -4799,6 +4799,7 @@ __metadata:
|
||||
react-native-version-info: 1.1.1
|
||||
react-native-vosk: 0.1.12
|
||||
react-native-webview: 11.26.1
|
||||
react-native-zip-archive: 6.0.9
|
||||
react-redux: 7.2.9
|
||||
redux: 4.2.1
|
||||
rn-fetch-blob: 0.12.0
|
||||
@ -27051,11 +27052,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
|
||||
|
||||
@ -27072,6 +27073,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"
|
||||
|
Loading…
Reference in New Issue
Block a user