diff --git a/.gitignore b/.gitignore index b3c33ec..162da8d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ build # JDT-specific (Eclipse Java Development Tools) .classpath -.vscode \ No newline at end of file +.vscode +local.properties \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index f2222f8..03e29f2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,8 +2,9 @@ apply plugin: 'com.android.application' repositories { google() + mavenCentral() maven { - url "https://dl.bintray.com/alphacep/vosk" + url 'https://alphacephei.com/maven/' } maven { url "https://jitpack.io" @@ -31,9 +32,10 @@ android { } dependencies { - implementation 'com.alphacep:vosk-android:0.3.15' + implementation 'com.alphacephei:vosk-android:0.3.30' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.code.gson:gson:2.8.6' + implementation 'net.java.dev.jna:jna:5.8.0@aar' + implementation 'com.google.code.gson:gson:2.8.7' implementation 'org.mozilla.deepspeech:libdeepspeech:0.8.2' implementation 'com.github.gkonovalov:android-vad:1.0.0' } diff --git a/app/src/main/java/cat/oreilly/localstt/Assets.java b/app/src/main/java/cat/oreilly/localstt/Assets.java new file mode 100644 index 0000000..df9d029 --- /dev/null +++ b/app/src/main/java/cat/oreilly/localstt/Assets.java @@ -0,0 +1,261 @@ +// Copyright 2019 Alpha Cephei Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cat.oreilly.localstt; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +import android.content.Context; +import android.content.res.AssetManager; +import android.os.Environment; + +/** + * Provides utility methods to keep asset files to external storage to allow + * further JNI code access assets from a filesystem. + * + * There must be special file {@value #ASSET_LIST_NAME} among the application + * assets containing relative paths of assets to synchronize. If the + * corresponding path does not exist on the external storage it is copied. If + * the path exists checksums are compared and the asset is copied only if there + * is a mismatch. Checksum is stored in a separate asset with the name that + * consists of the original name and a suffix that depends on the checksum + * algorithm (e.g. MD5). Checksum files are copied along with the corresponding + * asset files. + * + * @author Alexander Solovets + */ +public class Assets { + + protected static final String TAG = Assets.class.getSimpleName(); + + public static final String ASSET_LIST_NAME = "assets.lst"; + public static final String SYNC_DIR = "sync"; + public static final String HASH_EXT = ".md5"; + + private final AssetManager assetManager; + private final File externalDir; + + /** + * Creates new instance for asset synchronization + * + * @param context + * application context + * + * @throws IOException + * if the directory does not exist + * + * @see android.content.Context#getExternalFilesDir + * @see android.os.Environment#getExternalStorageState + */ + public Assets(Context context) throws IOException { + File appDir = context.getExternalFilesDir(null); + if (null == appDir) + throw new IOException("cannot get external files dir, " + + "external storage state is " + Environment.getExternalStorageState()); + externalDir = new File(appDir, SYNC_DIR); + assetManager = context.getAssets(); + } + + /** + * Creates new instance with specified destination for assets + * + * @param context + * application context to retrieve the assets + * @param path + * path to sync the files + */ + public Assets(Context context, String dest) { + externalDir = new File(dest); + assetManager = context.getAssets(); + } + + /** + * Returns destination path on external storage where assets are copied. + * + * @return path to application directory or null if it does not exists + */ + public File getExternalDir() { + return externalDir; + } + + /** + * Returns the map of asset paths to the files checksums. + * + * @return path to the root of resources directory on external storage + * @throws IOException + * if an I/O error occurs or "assets.lst" is missing + */ + public Map getItems() throws IOException { + Map items = new HashMap(); + for (String path : readLines(openAsset(ASSET_LIST_NAME))) { + Reader reader = new InputStreamReader(openAsset(path + HASH_EXT)); + items.put(path, new BufferedReader(reader).readLine()); + } + return items; + } + + /** + * Returns path to hash mappings for the previously copied files. This + * method can be used to find out assets which must be updated. + */ + public Map getExternalItems() { + try { + Map items = new HashMap(); + File assetFile = new File(externalDir, ASSET_LIST_NAME); + for (String line : readLines(new FileInputStream(assetFile))) { + String[] fields = line.split(" "); + items.put(fields[0], fields[1]); + } + return items; + } catch (IOException e) { + return Collections.emptyMap(); + } + } + + /** + * In case you want to create more smart sync implementation, this method + * returns the list of items which must be synchronized. + */ + public Collection getItemsToCopy(String path) throws IOException { + Collection items = new ArrayList(); + Queue queue = new ArrayDeque(); + queue.offer(path); + + while (!queue.isEmpty()) { + path = queue.poll(); + String[] list = assetManager.list(path); + for (String nested : list) + queue.offer(nested); + + if (list.length == 0) + items.add(path); + } + + return items; + } + + private List readLines(InputStream source) throws IOException { + List lines = new ArrayList(); + BufferedReader br = new BufferedReader(new InputStreamReader(source)); + String line; + while (null != (line = br.readLine())) + lines.add(line); + return lines; + } + + private InputStream openAsset(String asset) throws IOException { + return assetManager.open(new File(SYNC_DIR, asset).getPath()); + } + + /** + * Saves the list of synchronized items. The list is stored as a two-column + * space-separated list of items in a text file. The file is located at the + * root of synchronization directory in the external storage. + * + * @param items + * the items + * @throws IOException + * if an I/O error occurs + */ + public void updateItemList(Map items) throws IOException { + File assetListFile = new File(externalDir, ASSET_LIST_NAME); + PrintWriter pw = new PrintWriter(new FileOutputStream(assetListFile)); + for (Map.Entry entry : items.entrySet()) + pw.format("%s %s\n", entry.getKey(), entry.getValue()); + pw.close(); + } + + /** + * Copies raw asset resource to external storage of the device. + * + * @param path + * path of the asset to copy + * @throws IOException + * if an I/O error occurs + */ + public File copy(String asset) throws IOException { + InputStream source = openAsset(asset); + File destinationFile = new File(externalDir, asset); + destinationFile.getParentFile().mkdirs(); + OutputStream destination = new FileOutputStream(destinationFile); + byte[] buffer = new byte[1024]; + int nread; + + while ((nread = source.read(buffer)) != -1) { + if (nread == 0) { + nread = source.read(); + if (nread < 0) + break; + destination.write(nread); + continue; + } + destination.write(buffer, 0, nread); + } + destination.close(); + return destinationFile; + } + + /** + * Performs the sync of assets in the application and on the external + * storage + * + * @return The folder on external storage with data + * @throws IOException + */ + public File syncAssets() throws IOException { + Collection newItems = new ArrayList(); + Collection unusedItems = new ArrayList(); + Map items = getItems(); + Map externalItems = getExternalItems(); + + for (String path : items.keySet()) { + if (!items.get(path).equals(externalItems.get(path)) + || !(new File(externalDir, path).exists())) + newItems.add(path); + } + + unusedItems.addAll(externalItems.keySet()); + unusedItems.removeAll(items.keySet()); + + for (String path : newItems) { + File file = copy(path); + } + + for (String path : unusedItems) { + File file = new File(externalDir, path); + file.delete(); + } + + updateItemList(items); + return externalDir; + } + +} diff --git a/app/src/main/java/cat/oreilly/localstt/DeepSpeechRecognitionService.java b/app/src/main/java/cat/oreilly/localstt/DeepSpeechRecognitionService.java index 5bc8177..f81e27e 100644 --- a/app/src/main/java/cat/oreilly/localstt/DeepSpeechRecognitionService.java +++ b/app/src/main/java/cat/oreilly/localstt/DeepSpeechRecognitionService.java @@ -15,23 +15,17 @@ package cat.oreilly.localstt; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; import android.os.Handler; import android.os.Looper; import android.speech.RecognitionService; import android.util.Log; -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.MediaRecorder; -import org.kaldi.Assets; -import org.kaldi.RecognitionListener; +import org.vosk.android.RecognitionListener; import org.mozilla.deepspeech.libdeepspeech.DeepSpeechModel; import java.io.File; -import java.util.Map; import java.util.ArrayList; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -170,6 +164,14 @@ public class DeepSpeechRecognitionService extends RecognitionService implements } } + @Override + public void onFinalResult(String hypothesis) { + if (hypothesis != null) { + Log.i(TAG, hypothesis); + results(createResultsBundle(hypothesis), true); + } + } + @Override public void onPartialResult(String hypothesis) { if (hypothesis != null) { diff --git a/app/src/main/java/cat/oreilly/localstt/DeepSpeechService.java b/app/src/main/java/cat/oreilly/localstt/DeepSpeechService.java index 93d4159..bfac344 100644 --- a/app/src/main/java/cat/oreilly/localstt/DeepSpeechService.java +++ b/app/src/main/java/cat/oreilly/localstt/DeepSpeechService.java @@ -16,9 +16,6 @@ package cat.oreilly.localstt; -import static java.lang.String.format; - -import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.HashSet; @@ -30,7 +27,7 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; -import org.kaldi.RecognitionListener; +import org.vosk.android.RecognitionListener; import org.mozilla.deepspeech.libdeepspeech.DeepSpeechModel; import org.mozilla.deepspeech.libdeepspeech.DeepSpeechStreamingState; diff --git a/app/src/main/java/cat/oreilly/localstt/VoskRecognitionService.java b/app/src/main/java/cat/oreilly/localstt/VoskRecognitionService.java index c3e5f93..283c7ea 100644 --- a/app/src/main/java/cat/oreilly/localstt/VoskRecognitionService.java +++ b/app/src/main/java/cat/oreilly/localstt/VoskRecognitionService.java @@ -17,7 +17,6 @@ package cat.oreilly.localstt; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -26,12 +25,12 @@ import android.speech.RecognitionService; import android.util.Log; import com.google.gson.Gson; -import org.kaldi.Assets; -import org.kaldi.KaldiRecognizer; -import org.kaldi.Model; -import org.kaldi.RecognitionListener; -import org.kaldi.SpeechService; -import org.kaldi.Vosk; +import org.vosk.Recognizer; +import org.vosk.Model; +import org.vosk.android.RecognitionListener; +import org.vosk.android.SpeechService; +import org.vosk.LibVosk; +import org.vosk.LogLevel; import java.io.File; import java.util.Map; @@ -44,7 +43,7 @@ public class VoskRecognitionService extends RecognitionService implements Recogn private final static String TAG = VoskRecognitionService.class.getSimpleName(); private final Handler handler = new Handler(Looper.getMainLooper()); private final Executor executor = Executors.newSingleThreadExecutor(); - private KaldiRecognizer recognizer; + private Recognizer recognizer; private SpeechService speechService; private Model model; @@ -77,7 +76,7 @@ public class VoskRecognitionService extends RecognitionService implements Recogn if (model == null) { Assets assets = new Assets(VoskRecognitionService.this); File assetDir = assets.syncAssets(); - Vosk.SetLogLevel(0); + LibVosk.setLogLevel(LogLevel.INFO); Log.i(TAG, "Loading model"); model = new Model(assetDir.toString() + "/vosk-catala"); @@ -111,23 +110,22 @@ public class VoskRecognitionService extends RecognitionService implements Recogn } } - private void setupRecognizer() throws IOException { + private void setupRecognizer() { try { if (recognizer == null) { Log.i(TAG, "Creating recognizer"); - recognizer = new KaldiRecognizer(model, 16000.0f); + recognizer = new Recognizer(model, 16000.0f); } if (speechService == null) { Log.i(TAG, "Creating speechService"); speechService = new SpeechService(recognizer, 16000.0f); - speechService.addListener(this); } else { speechService.cancel(); } - speechService.startListening(); + speechService.startListening(this); } catch (IOException e) { Log.e(TAG, e.getMessage()); } @@ -171,7 +169,9 @@ public class VoskRecognitionService extends RecognitionService implements Recogn } private void error(int errorCode) { - speechService.cancel(); + if (speechService != null) { + speechService.cancel(); + } try { mCallback.error(errorCode); } catch (RemoteException e) { @@ -190,6 +190,17 @@ public class VoskRecognitionService extends RecognitionService implements Recogn } } + @Override + public void onFinalResult(String hypothesis) { + if (hypothesis != null) { + Log.i(TAG, hypothesis); + Gson gson = new Gson(); + Map map = gson.fromJson(hypothesis, Map.class); + String text = map.get("text"); + results(createResultsBundle(text), true); + } + } + @Override public void onPartialResult(String hypothesis) { if (hypothesis != null) { @@ -210,6 +221,6 @@ public class VoskRecognitionService extends RecognitionService implements Recogn @Override public void onTimeout() { speechService.cancel(); - speechService.startListening(); + speechService.startListening(this); } } diff --git a/build.gradle b/build.gradle index 8010f99..30e817e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,17 +2,17 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:4.2.0' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/gradle.properties b/gradle.properties index fdd4c96..2fa2013 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,7 @@ +org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=4096m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true android.enableD8=true android.enableJetifier=true android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a224..cfedb47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Aug 03 00:12:16 CEST 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME