You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
This commit is contained in:
@@ -909,8 +909,6 @@ packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -882,8 +882,6 @@ packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 6afcbbf0cc8ca2d69dd78077d61e59a90b2136bb..9f8d72b4ec5b2b3d290975d6a255917c95300854 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -67,19 +67,19 @@ repositories {
|
||||
}
|
||||
|
||||
// Generate UUIDs for each models contained in android/src/main/assets/
|
||||
-tasks.register('genUUID') {
|
||||
- doLast {
|
||||
- fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
|
||||
- if (fileDetails.directory) {
|
||||
- def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
|
||||
- def ofile = file("$odir/uuid")
|
||||
- mkdir odir
|
||||
- ofile.text = UUID.randomUUID().toString()
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-preBuild.dependsOn genUUID
|
||||
+// tasks.register('genUUID') {
|
||||
+// doLast {
|
||||
+// fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
|
||||
+// if (fileDetails.directory) {
|
||||
+// def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
|
||||
+// def ofile = file("$odir/uuid")
|
||||
+// mkdir odir
|
||||
+// ofile.text = UUID.randomUUID().toString()
|
||||
+// }
|
||||
+// }
|
||||
+// }
|
||||
+// }
|
||||
+// preBuild.dependsOn genUUID
|
||||
|
||||
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..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
|
||||
return "Vosk"
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ fun addListener(type: String?) {
|
||||
+ // Keep: Required for RN built in Event Emitter Calls.
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ fun removeListeners(type: Int?) {
|
||||
+ // Keep: Required for RN built in Event Emitter Calls.
|
||||
+ }
|
||||
+
|
||||
override fun onResult(hypothesis: String) {
|
||||
// Get text data from string object
|
||||
val text = getHypothesisText(hypothesis)
|
||||
|
||||
// Stop recording if data found
|
||||
if (text != null && text.isNotEmpty()) {
|
||||
- cleanRecognizer();
|
||||
+ // Don't auto-stop the recogniser - we want to do that when the user
|
||||
+ // presses on "stop" only.
|
||||
+ // cleanRecognizer();
|
||||
sendEvent("onResult", text)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ fun stopOnly() {
|
||||
+ if (speechService != null) {
|
||||
+ speechService!!.stop()
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ fun cleanup() {
|
||||
+ if (speechService != null) {
|
||||
+ speechService!!.shutdown();
|
||||
+ speechService = null
|
||||
+ }
|
||||
+ if (recognizer != null) {
|
||||
+ recognizer!!.close();
|
||||
+ recognizer = null;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@ReactMethod
|
||||
fun unload() {
|
||||
cleanRecognizer();
|
||||
diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts
|
||||
index 441e41cc402cca3a60b34978ef4fea976076259c..a173acebb4b314402550442ad471e0f7c706e3c4 100644
|
||||
--- a/lib/typescript/index.d.ts
|
||||
+++ b/lib/typescript/index.d.ts
|
||||
@@ -10,6 +10,8 @@ export default class Vosk {
|
||||
currentRegisteredEvents: EmitterSubscription[];
|
||||
start: (grammar?: string[] | null) => Promise<String>;
|
||||
stop: () => void;
|
||||
+ stopOnly: () => void;
|
||||
+ cleanup: () => void;
|
||||
unload: () => void;
|
||||
onResult: (onResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
onFinalResult: (onFinalResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
diff --git a/package.json b/package.json
|
||||
index 707eddb8d68007f93071ac659c5b087c935c5f01..90ebe20f224eeec472c377df1fef9b15f2ff8200 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -11,12 +11,9 @@
|
||||
"src",
|
||||
"lib",
|
||||
"android",
|
||||
- "ios",
|
||||
"cpp",
|
||||
- "react-native-vosk.podspec",
|
||||
"!lib/typescript/example",
|
||||
"!android/build",
|
||||
- "!ios/build",
|
||||
"!**/__tests__",
|
||||
"!**/__fixtures__",
|
||||
"!**/__mocks__"
|
||||
diff --git a/react-native-vosk.podspec b/react-native-vosk.podspec
|
||||
deleted file mode 100644
|
||||
index e3d41b90c5eef890c7a5108aaf16ac07d34a698b..0000000000000000000000000000000000000000
|
||||
--- a/react-native-vosk.podspec
|
||||
+++ /dev/null
|
||||
@@ -1,41 +0,0 @@
|
||||
-require "json"
|
||||
-
|
||||
-package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||
-folly_version = '2021.06.28.00-v2'
|
||||
-folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
||||
-
|
||||
-Pod::Spec.new do |s|
|
||||
- s.name = "react-native-vosk"
|
||||
- s.version = package["version"]
|
||||
- s.summary = package["description"]
|
||||
- s.homepage = package["homepage"]
|
||||
- s.license = package["license"]
|
||||
- s.authors = package["author"]
|
||||
-
|
||||
- s.platforms = { :ios => "10.0" }
|
||||
- s.source = { :git => "https://github.com/riderodd/react-native-vosk.git", :tag => "#{s.version}" }
|
||||
-
|
||||
- s.source_files = "ios/**/*.{h,m,mm,swift}"
|
||||
- s.resource_bundles = { 'Vosk' => ['ios/Vosk/*'] }
|
||||
-
|
||||
- s.dependency "React-Core"
|
||||
- s.frameworks = "Accelerate"
|
||||
- s.library = "c++"
|
||||
- s.vendored_frameworks = "ios/libvosk.xcframework"
|
||||
- s.requires_arc = true
|
||||
-
|
||||
- # Don't install the dependencies when we run `pod install` in the old architecture.
|
||||
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
||||
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
||||
- s.pod_target_xcconfig = {
|
||||
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
||||
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
||||
- }
|
||||
-
|
||||
- s.dependency "React-Codegen"
|
||||
- s.dependency "RCT-Folly", folly_version
|
||||
- s.dependency "RCTRequired"
|
||||
- s.dependency "RCTTypeSafety"
|
||||
- s.dependency "ReactCommon/turbomodule/core"
|
||||
- end
|
||||
-end
|
||||
diff --git a/src/index.tsx b/src/index.tsx
|
||||
index d9f90c921d89b1b4d85e145443ed3376546a368a..29e4068dbd7500828a73145bd25497a52c9bf638 100644
|
||||
--- a/src/index.tsx
|
||||
+++ b/src/index.tsx
|
||||
@@ -69,6 +69,15 @@ export default class Vosk {
|
||||
VoskModule.stop();
|
||||
};
|
||||
|
||||
+ stopOnly = () => {
|
||||
+ VoskModule.stopOnly();
|
||||
+ };
|
||||
+
|
||||
+ cleanup = () => {
|
||||
+ this.cleanListeners();
|
||||
+ VoskModule.cleanup();
|
||||
+ };
|
||||
+
|
||||
unload = () => {
|
||||
this.cleanListeners();
|
||||
VoskModule.unload();
|
||||
@@ -101,7 +101,6 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.57.1#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
|
||||
@@ -24,29 +24,8 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
// Seems to be required for react-native-vosk, otherwise the lib looks for it at "https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar" but it's not there. And we get this error:
|
||||
//
|
||||
// Execution failed for task ':app:checkDebugAarMetadata'.
|
||||
// > Could not resolve all files for configuration ':app:debugRuntimeClasspath'.
|
||||
// > Failed to transform vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46) to match attributes {artifactType=android-aar-metadata, org.gradle.status=release}.
|
||||
// > Could not find vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46).
|
||||
// Searched in the following locations:
|
||||
// https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar
|
||||
//
|
||||
// But according to this page, the lib is on the Apache repository:
|
||||
//
|
||||
// https://search.maven.org/artifact/com.alphacephei/vosk-android/0.3.46/aar
|
||||
maven { url "https://maven.apache.org" }
|
||||
|
||||
// Also required for react-native-vosk?
|
||||
maven { url "https://maven.google.com" }
|
||||
|
||||
// Maybe still needed to fetch above package?
|
||||
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
mavenCentral()
|
||||
|
||||
maven {
|
||||
// expo-camera bundles a custom com.google.android:cameraview
|
||||
|
||||
@@ -1172,69 +1172,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
await CommandService.instance().execute('attachFile', filePath);
|
||||
};
|
||||
|
||||
// private vosk_:Vosk;
|
||||
|
||||
// private async getVosk() {
|
||||
// if (this.vosk_) return this.vosk_;
|
||||
// this.vosk_ = new Vosk();
|
||||
// await this.vosk_.loadModel('model-fr-fr');
|
||||
// return this.vosk_;
|
||||
// }
|
||||
|
||||
// private async voiceRecording_onPress() {
|
||||
// logger.info('Vosk: Getting instance...');
|
||||
|
||||
// const vosk = await this.getVosk();
|
||||
|
||||
// this.voskResult_ = [];
|
||||
|
||||
// const eventHandlers: any[] = [];
|
||||
|
||||
// eventHandlers.push(vosk.onResult(e => {
|
||||
// logger.info('Vosk: result', e.data);
|
||||
// this.voskResult_.push(e.data);
|
||||
// }));
|
||||
|
||||
// eventHandlers.push(vosk.onError(e => {
|
||||
// logger.warn('Vosk: error', e.data);
|
||||
// }));
|
||||
|
||||
// eventHandlers.push(vosk.onTimeout(e => {
|
||||
// logger.warn('Vosk: timeout', e.data);
|
||||
// }));
|
||||
|
||||
// eventHandlers.push(vosk.onFinalResult(e => {
|
||||
// logger.info('Vosk: final result', e.data);
|
||||
// }));
|
||||
|
||||
// logger.info('Vosk: Starting recording...');
|
||||
|
||||
// void vosk.start();
|
||||
|
||||
// const buttonId = await dialogs.pop(this, 'Voice recording in progress...', [
|
||||
// { text: 'Stop recording', id: 'stop' },
|
||||
// { text: _('Cancel'), id: 'cancel' },
|
||||
// ]);
|
||||
|
||||
// logger.info('Vosk: Stopping recording...');
|
||||
// vosk.stop();
|
||||
|
||||
// for (const eventHandler of eventHandlers) {
|
||||
// eventHandler.remove();
|
||||
// }
|
||||
|
||||
// logger.info('Vosk: Recording stopped:', this.voskResult_);
|
||||
|
||||
// if (buttonId === 'cancel') return;
|
||||
|
||||
// const newNote: NoteEntity = { ...this.state.note };
|
||||
// newNote.body = `${newNote.body} ${this.voskResult_.join(' ')}`;
|
||||
// this.setState({ note: newNote });
|
||||
// this.scheduleSave();
|
||||
// }
|
||||
|
||||
|
||||
|
||||
public menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
|
||||
@@ -5,9 +5,6 @@ import { _, languageName } from '@joplin/lib/locale';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import VoiceTyping, { OnTextCallback, VoiceTypingSession } from '../../services/voiceTyping/VoiceTyping';
|
||||
import whisper from '../../services/voiceTyping/whisper';
|
||||
import vosk from '../../services/voiceTyping/vosk';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import { RecorderState } from './types';
|
||||
import RecordingControls from './RecordingControls';
|
||||
import { PrimaryButton } from '../buttons';
|
||||
@@ -16,19 +13,17 @@ import shim from '@joplin/lib/shim';
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
provider: string;
|
||||
onDismiss: ()=> void;
|
||||
onText: (text: string)=> void;
|
||||
}
|
||||
|
||||
interface UseVoiceTypingProps {
|
||||
locale: string;
|
||||
provider: string;
|
||||
onSetPreview: OnTextCallback;
|
||||
onText: OnTextCallback;
|
||||
}
|
||||
|
||||
const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingProps) => {
|
||||
const useVoiceTyping = ({ locale, onSetPreview, onText }: UseVoiceTypingProps) => {
|
||||
const [voiceTyping, setVoiceTyping] = useState<VoiceTypingSession>(null);
|
||||
const [error, setError] = useState<Error|null>(null);
|
||||
const [mustDownloadModel, setMustDownloadModel] = useState<boolean | null>(null);
|
||||
@@ -43,8 +38,8 @@ const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypi
|
||||
voiceTypingRef.current = voiceTyping;
|
||||
|
||||
const builder = useMemo(() => {
|
||||
return new VoiceTyping(locale, provider?.startsWith('whisper') ? [whisper] : [vosk]);
|
||||
}, [locale, provider]);
|
||||
return new VoiceTyping(locale, [whisper]);
|
||||
}, [locale]);
|
||||
|
||||
const [redownloadCounter, setRedownloadCounter] = useState(0);
|
||||
|
||||
@@ -121,7 +116,6 @@ const SpeechToTextComponent: React.FC<Props> = props => {
|
||||
locale: props.locale,
|
||||
onSetPreview: setPreview,
|
||||
onText: props.onText,
|
||||
provider: props.provider,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -209,6 +203,4 @@ const SpeechToTextComponent: React.FC<Props> = props => {
|
||||
/>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
provider: state.settings['voiceTyping.preferredProvider'],
|
||||
}))(SpeechToTextComponent);
|
||||
export default SpeechToTextComponent;
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.2.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-zip-archive": "7.0.1",
|
||||
"react-redux": "8.1.3",
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import { languageCodeOnly } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { rtrimSlashes } from '@joplin/lib/path-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Vosk from 'react-native-vosk';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { VoiceTypingProvider, VoiceTypingSession } from './VoiceTyping';
|
||||
import { join } from 'path';
|
||||
|
||||
const logger = Logger.create('voiceTyping/vosk');
|
||||
|
||||
enum State {
|
||||
Idle = 0,
|
||||
Recording,
|
||||
Completing,
|
||||
}
|
||||
|
||||
interface StartOptions {
|
||||
onResult: (text: string)=> void;
|
||||
}
|
||||
|
||||
let vosk_: Record<string, Vosk> = {};
|
||||
|
||||
let state_: State = State.Idle;
|
||||
|
||||
const defaultSupportedLanguages = {
|
||||
'en': 'https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip',
|
||||
'zh': '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(defaultSupportedLanguages).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): string => {
|
||||
const lang = languageCodeOnly(locale).toLowerCase();
|
||||
if (!(lang in defaultSupportedLanguages)) throw new Error(`No language file for: ${locale}`);
|
||||
|
||||
const urlTemplate = rtrimSlashes(Setting.value('voiceTypingBaseUrl').trim());
|
||||
|
||||
if (urlTemplate) {
|
||||
let url = rtrimSlashes(urlTemplate);
|
||||
if (!url.includes('{lang}')) url += '/{lang}.zip';
|
||||
return url.replace(/\{lang\}/g, lang);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return (defaultSupportedLanguages as any)[lang];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const getVosk = async (modelDir: string, locale: string) => {
|
||||
if (vosk_[locale]) return vosk_[locale];
|
||||
|
||||
const vosk = new Vosk();
|
||||
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;
|
||||
};
|
||||
|
||||
export const startRecording = (vosk: Vosk, options: StartOptions): VoiceTypingSession => {
|
||||
if (state_ !== State.Idle) throw new Error('Vosk is already recording');
|
||||
|
||||
state_ = State.Recording;
|
||||
|
||||
const result: string[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const eventHandlers: any[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
const finalResultPromiseResolve: Function = null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
const finalResultPromiseReject: Function = null;
|
||||
const finalResultTimeout = false;
|
||||
|
||||
const completeRecording = (finalResult: string, error: Error) => {
|
||||
logger.info(`Complete recording. Final result: ${finalResult}. Error:`, error);
|
||||
|
||||
for (const eventHandler of eventHandlers) {
|
||||
eventHandler.remove();
|
||||
}
|
||||
|
||||
vosk.cleanup();
|
||||
|
||||
state_ = State.Idle;
|
||||
|
||||
if (error) {
|
||||
if (finalResultPromiseReject) finalResultPromiseReject(error);
|
||||
} else {
|
||||
if (finalResultPromiseResolve) finalResultPromiseResolve(finalResult);
|
||||
}
|
||||
};
|
||||
|
||||
eventHandlers.push(vosk.onResult(e => {
|
||||
const text = e.data;
|
||||
logger.info('Result', text);
|
||||
result.push(text);
|
||||
options.onResult(text);
|
||||
}));
|
||||
|
||||
eventHandlers.push(vosk.onError(e => {
|
||||
logger.warn('Error', e.data);
|
||||
}));
|
||||
|
||||
eventHandlers.push(vosk.onTimeout(e => {
|
||||
logger.warn('Timeout', e.data);
|
||||
}));
|
||||
|
||||
eventHandlers.push(vosk.onFinalResult(e => {
|
||||
logger.info('Final result', e.data);
|
||||
|
||||
if (finalResultTimeout) {
|
||||
logger.warn('Got final result - but already timed out. Not doing anything.');
|
||||
return;
|
||||
}
|
||||
|
||||
completeRecording(e.data, null);
|
||||
}));
|
||||
|
||||
const stopOrCancel = () => {
|
||||
if (state_ === State.Recording) {
|
||||
logger.info('Cancelling...');
|
||||
state_ = State.Completing;
|
||||
vosk.stopOnly();
|
||||
completeRecording('', null);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
start: async () => {
|
||||
logger.info('Starting recording...');
|
||||
await vosk.start();
|
||||
},
|
||||
stop: async () => {
|
||||
stopOrCancel();
|
||||
},
|
||||
cancel: async () => {
|
||||
stopOrCancel();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const vosk: VoiceTypingProvider = {
|
||||
supported: () => true,
|
||||
modelLocalFilepath: (locale: string) => getModelDir(locale),
|
||||
deleteCachedModels: async (locale: string) => {
|
||||
const path = getModelDir(locale);
|
||||
await shim.fsDriver().remove(path, { recursive: true });
|
||||
},
|
||||
getDownloadUrl: (locale) => languageModelUrl(locale),
|
||||
getUuidPath: (locale: string) => join(getModelDir(locale), 'uuid'),
|
||||
build: async ({ callbacks, locale, modelPath }) => {
|
||||
const vosk = await getVosk(modelPath, locale);
|
||||
return startRecording(vosk, { onResult: callbacks.onFinalize });
|
||||
},
|
||||
modelName: 'vosk',
|
||||
};
|
||||
|
||||
export default vosk;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { VoiceTypingProvider } from './VoiceTyping';
|
||||
|
||||
const vosk: VoiceTypingProvider = {
|
||||
supported: () => false,
|
||||
modelLocalFilepath: () => null,
|
||||
getDownloadUrl: () => null,
|
||||
getUuidPath: () => null,
|
||||
deleteCachedModels: () => null,
|
||||
build: async () => {
|
||||
throw new Error('Unsupported!');
|
||||
},
|
||||
modelName: 'vosk',
|
||||
};
|
||||
|
||||
export default vosk;
|
||||
@@ -1933,20 +1933,21 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
section: 'note',
|
||||
},
|
||||
|
||||
// Deprecated and currently unused. For now, the mobile app only supports the Whisper voice typing provider.
|
||||
'voiceTyping.preferredProvider': {
|
||||
value: 'whisper-tiny',
|
||||
type: SettingItemType.String,
|
||||
public: true,
|
||||
public: false,
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => _('Preferred voice typing provider'),
|
||||
label: () => 'Preferred voice typing provider',
|
||||
isEnum: true,
|
||||
show: showVoiceTypingSettings,
|
||||
section: 'note',
|
||||
|
||||
options: () => {
|
||||
return {
|
||||
'vosk': _('Vosk'),
|
||||
'whisper-tiny': _('Whisper'),
|
||||
'vosk': 'Vosk', // No longer supported
|
||||
'whisper-tiny': 'Whisper',
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -1958,7 +1959,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
appTypes: [AppType.Mobile],
|
||||
label: () => _('Voice typing: Glossary'),
|
||||
description: () => _('A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.'),
|
||||
show: (settings) => showVoiceTypingSettings() && settings['voiceTyping.preferredProvider'].startsWith('whisper'),
|
||||
show: () => showVoiceTypingSettings(),
|
||||
section: 'note',
|
||||
},
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ Pre-built models that can be included as `model.bin` are present [on whisper.cpp
|
||||
|
||||
Vosk voice typing can be enabled in settings, under the "Note" tab, by changing the "Preferred voice typing provider" to "Vosk".
|
||||
|
||||
Due to [compatibility issues](https://github.com/laurent22/joplin/issues/13113), voice typing with Vosk is deprecated and will be removed in Joplin >= v3.5.
|
||||
|
||||
### Comparison with Whisper
|
||||
|
||||
- **No punctuation**: Vosk's output is all-lowercase text, without punctuation.
|
||||
|
||||
@@ -221,9 +221,6 @@
|
||||
// https://github.com/TryGhost/node-sqlite3/issues/1747
|
||||
"sqlite3",
|
||||
|
||||
// They refactored it with undocumented breaking changes.
|
||||
"react-native-vosk",
|
||||
|
||||
// Used by onenote-converter but has many breaking changes and we don't test it on CI, so
|
||||
// would need to be manually upgraded and tested.
|
||||
"bat",
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@@ -9309,7 +9309,6 @@ __metadata:
|
||||
react-native-url-polyfill: "npm:2.0.0"
|
||||
react-native-vector-icons: "npm:10.2.0"
|
||||
react-native-version-info: "npm:1.1.1"
|
||||
react-native-vosk: "npm:0.1.12"
|
||||
react-native-web: "npm:0.20.0"
|
||||
react-native-webview: "npm:13.13.5"
|
||||
react-native-zip-archive: "npm:7.0.1"
|
||||
@@ -42018,26 +42017,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-vosk@npm:0.1.12":
|
||||
version: 0.1.12
|
||||
resolution: "react-native-vosk@npm:0.1.12"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/66b4842ab16a10dedfc46e9b7c0f60349ae2b87c3ba8c21d055317cedb35539fcd5eb102fd15538b4d03760e7f1271fc4e1c1a43dab7d8143bd401b998abf5cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"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=5deb2c&locator=root%40workspace%3A."
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 10/90560df25771b161f3d74e989dfbeaca75bad1b435fee471cf9f8c9b278fa15a5279db652f9bb2a2d58109ba3e0666b4cad884c31796207721d63bf887ba00e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-web@npm:0.20.0":
|
||||
version: 0.20.0
|
||||
resolution: "react-native-web@npm:0.20.0"
|
||||
|
||||
Reference in New Issue
Block a user