diff --git a/app/src/main/java/com/grammatek/simaromur/AppRepository.java b/app/src/main/java/com/grammatek/simaromur/AppRepository.java index 81f8637..2d65ce5 100644 --- a/app/src/main/java/com/grammatek/simaromur/AppRepository.java +++ b/app/src/main/java/com/grammatek/simaromur/AppRepository.java @@ -46,12 +46,14 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; /** * Abstracted application repository as promoted by the Architecture Guide. @@ -66,8 +68,10 @@ public class AppRepository { private final NormDictEntryDao mNormDictDao; private LiveData mAppData; private LiveData> mAllVoices; + private LiveData> mAllUserDictEntries; private AppData mCachedAppData; private List mAllCachedVoices; + private HashMap mAllCachedUserDictEntries; private final VoiceController mNetworkVoiceController; private final SpeakController mNetworkSpeakController; private final ApiDbUtil mApiDbUtil; @@ -232,6 +236,9 @@ public AppRepository(Application application) throws IOException { }); mAllVoices = mVoiceDao.getAllVoices(); mAllVoices.observeForever(voices -> { + if (voices == null) { + return; + } Log.v(LOG_TAG, "mAllVoices update: " + voices); // Update cached voices if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -240,11 +247,39 @@ public AppRepository(Application application) throws IOException { mAllCachedVoices = Objects.requireNonNullElse(voices, new ArrayList<>()); } }); - + mAllUserDictEntries = mNormDictDao.getSortedEntries(); + mAllUserDictEntries.observeForever(entries -> { + if (entries == null) { + return; + } + Log.v(LOG_TAG, "mAllUserDictEntries update: " + entries.size() + " entries"); + mAllCachedUserDictEntries = createNormDictRegexMap(entries); + }); mMediaPlayer = new MediaPlayObserver(); mScheduler = Executors.newSingleThreadScheduledExecutor(); // only do this once at the beginning mScheduler.schedule(assetVoiceRunnable, 0, TimeUnit.SECONDS); + Log.v(LOG_TAG, "AppRepository() done"); + } + + /** + * Create a map of regex patterns for all normalization dictionary entries for fast + * lookup during normalization. + * + * @param entries list of normalization dictionary entries + * @return map of regex patterns for all normalization dictionary entries + */ + private HashMap createNormDictRegexMap(List entries) { + HashMap regexMap = new HashMap<>(); + for (NormDictEntry entry : entries) { + // make for every NormDictEntry.term a regular expression matching on word boundaries and + // case insensitive. Note: we cannot use \b for the word boundary's term end, + // because of possible trailing punctuation + Pattern regex = Pattern.compile("\\b(?i)" + + Pattern.quote(entry.term.strip().toLowerCase()) + "(?!\\S)"); + regexMap.put(entry, regex); + } + return regexMap; } /** @@ -345,24 +380,17 @@ public LiveData> getAllVoices() { * * @return List of all current normalization dictionary entries as LiveData */ - public LiveData> getNormDictEntries() { - return mNormDictDao.getSortedEntries(); - } - - /** - * Get a LiveData list of all current normalization dictionary entries. - * - * @return List of all current normalization dictionary entries as LiveData - */ - public List getNormDictEntriesDirect() { - return mNormDictDao.getEntries(); + public LiveData> getUserDictEntries() { + Log.v(LOG_TAG, "getUserDictEntries"); + return mAllUserDictEntries; } /** * Creates or updates the given entry inside the Db. */ - public void createOrUpdateNormDictEntry(NormDictEntry entry) { + public void createOrUpdateUserDictEntry(NormDictEntry entry) { mNormDictDao.insert(entry); + mUtteranceCacheManager.clearCache(); } @@ -370,8 +398,21 @@ public void createOrUpdateNormDictEntry(NormDictEntry entry) { * Deletes the given entry from the Db. * @param mEntry the entry to be deleted */ - public void deleteNormDictEntry(NormDictEntry mEntry) { + public void deleteUserDictEntry(NormDictEntry mEntry) { mNormDictDao.delete(mEntry); + mUtteranceCacheManager.clearCache(); + } + + /** + * Returns map of all observed/cached user normalization dictionary entries and the corresponding + * compiled term regular expression pattern. If there are any changes in the model, this map + * is updated automatically. + * + * @return map of all cached voices + */ + public final HashMap getCachedUserDictEntries() { + Log.v(LOG_TAG, "getCachedUserDictEntries"); + return mAllCachedUserDictEntries; } /** @@ -1160,5 +1201,4 @@ protected Void doInBackground(Void... voids) { return null; } } - } diff --git a/app/src/main/java/com/grammatek/simaromur/NormDictViewModel.java b/app/src/main/java/com/grammatek/simaromur/NormDictViewModel.java index 8e1dc69..14aba03 100644 --- a/app/src/main/java/com/grammatek/simaromur/NormDictViewModel.java +++ b/app/src/main/java/com/grammatek/simaromur/NormDictViewModel.java @@ -32,7 +32,7 @@ public NormDictViewModel(Application application) { // Return all entries public LiveData> getEntries() { if (mAllEntries == null) { - mAllEntries = mRepository.getNormDictEntries(); + mAllEntries = mRepository.getUserDictEntries(); } return mAllEntries; } @@ -82,11 +82,11 @@ public void stopSpeaking(Voice voice) { public void createOrUpdate(NormDictEntry mEntry) { Log.v(LOG_TAG, "update: " + mEntry.term + " -> " + mEntry.replacement); - mRepository.createOrUpdateNormDictEntry(mEntry); + mRepository.createOrUpdateUserDictEntry(mEntry); } public void delete(NormDictEntry mEntry) { Log.v(LOG_TAG, "delete: " + mEntry.term + " -> " + mEntry.replacement); - mRepository.deleteNormDictEntry(mEntry); + mRepository.deleteUserDictEntry(mEntry); } } diff --git a/app/src/main/java/com/grammatek/simaromur/Simaromur.java b/app/src/main/java/com/grammatek/simaromur/Simaromur.java index 288dfe7..bc9bef4 100644 --- a/app/src/main/java/com/grammatek/simaromur/Simaromur.java +++ b/app/src/main/java/com/grammatek/simaromur/Simaromur.java @@ -7,16 +7,20 @@ public class Simaromur extends Activity { public Simaromur() { + /** if(BuildConfig.DEBUG) StrictMode.enableDefaults(); + */ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + /** StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()) .detectLeakedClosableObjects() .build()); + */ finish(); } } diff --git a/app/src/main/java/com/grammatek/simaromur/TTSService.java b/app/src/main/java/com/grammatek/simaromur/TTSService.java index fba6f8e..e4e54f8 100644 --- a/app/src/main/java/com/grammatek/simaromur/TTSService.java +++ b/app/src/main/java/com/grammatek/simaromur/TTSService.java @@ -41,10 +41,11 @@ public class TTSService extends TextToSpeechService { @Override public void onCreate() { Log.i(LOG_TAG, "onCreate()"); - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()) - .detectLeakedClosableObjects() - .build()); - + /** + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()) + .detectLeakedClosableObjects() + .build()); + */ mRepository = App.getAppRepository(); // This calls onIsLanguageAvailable() and must run after Initialization super.onCreate(); diff --git a/app/src/main/java/com/grammatek/simaromur/VoiceViewModel.java b/app/src/main/java/com/grammatek/simaromur/VoiceViewModel.java index abc7700..87b2624 100644 --- a/app/src/main/java/com/grammatek/simaromur/VoiceViewModel.java +++ b/app/src/main/java/com/grammatek/simaromur/VoiceViewModel.java @@ -26,7 +26,6 @@ public class VoiceViewModel extends AndroidViewModel { // these variables are for data caching private AppData mAppData; // application data - private LiveData> mAllVoices; // our current voices model public VoiceViewModel(Application application) { super(application); @@ -43,18 +42,13 @@ public AppData getAppData() { // Return all voices public LiveData> getAllVoices() { - if (mAllVoices == null) { - mAllVoices = mRepository.getAllVoices(); - } - return mAllVoices; + return mRepository.getAllVoices(); } // Return specific voice public Voice getVoiceWithId(long voiceId) { - if (mAllVoices == null) { - return null; - } - for(Voice voice: Objects.requireNonNull(mAllVoices.getValue())) { + List voices = getAllVoices().getValue(); + for(Voice voice: Objects.requireNonNull(voices)) { if (voice.voiceId == voiceId) return voice; } diff --git a/app/src/main/java/com/grammatek/simaromur/cache/UtteranceCacheManager.java b/app/src/main/java/com/grammatek/simaromur/cache/UtteranceCacheManager.java index 59b565c..de02a10 100644 --- a/app/src/main/java/com/grammatek/simaromur/cache/UtteranceCacheManager.java +++ b/app/src/main/java/com/grammatek/simaromur/cache/UtteranceCacheManager.java @@ -635,7 +635,6 @@ public static String buildVoiceKey(String voiceName, String voiceVersion) { assert(!voiceName.isEmpty()); assert(!voiceVersion.isEmpty()); final String key = voiceName + ":" + voiceVersion; - Log.v(LOG_TAG, "buildVoiceKey(): " + key); return key; } diff --git a/app/src/main/java/com/grammatek/simaromur/frontend/Abbreviations.java b/app/src/main/java/com/grammatek/simaromur/frontend/Abbreviations.java index 1ca3e73..59da27a 100644 --- a/app/src/main/java/com/grammatek/simaromur/frontend/Abbreviations.java +++ b/app/src/main/java/com/grammatek/simaromur/frontend/Abbreviations.java @@ -37,6 +37,7 @@ public Set getNonEndingAbbr() { } private Set readAbbrFromFile(int resID) { + // TODO: move to FileUtils Set abbrSet = new HashSet<>(); Resources res = context.getResources(); String line; diff --git a/app/src/main/java/com/grammatek/simaromur/frontend/NormalizationManager.java b/app/src/main/java/com/grammatek/simaromur/frontend/NormalizationManager.java index 96e5bea..ea5dfbe 100644 --- a/app/src/main/java/com/grammatek/simaromur/frontend/NormalizationManager.java +++ b/app/src/main/java/com/grammatek/simaromur/frontend/NormalizationManager.java @@ -29,7 +29,7 @@ public class NormalizationManager { private static final String POS_MODEL = "pos/is-pos-reduced-maxent.bin"; private final Context mContext; - private final POSTaggerME mPosTagger; + private POSTaggerME mPosTagger; private final TTSUnicodeNormalizer mUnicodeNormalizer; private final Tokenizer mTokenizer; private final TTSNormalizer mTTSNormalizer; @@ -39,11 +39,6 @@ public NormalizationManager(Context context, Map pronDict mUnicodeNormalizer = new TTSUnicodeNormalizer(context, pronDict); mTokenizer = new Tokenizer(context); mTTSNormalizer = new TTSNormalizer(); - mPosTagger = initPOSTagger(); - if (mPosTagger == null) { - Log.e(LOG_TAG, "Failed to initialize POS tagger"); - throw new RuntimeException("Failed to initialize POS tagger"); - } } /** @@ -113,6 +108,15 @@ private String[] tagText(final String text) { return tags; } + + if (mPosTagger == null) { + // this takes ~2 seconds + mPosTagger = initPOSTagger(); + if (mPosTagger == null) { + Log.e(LOG_TAG, "Failed to initialize POS tagger"); + throw new RuntimeException("Failed to initialize POS tagger"); + } + } tags = mPosTagger.tag(tokens); if (DEBUG) { printProbabilities(tags, mPosTagger, tokens); @@ -127,7 +131,6 @@ private POSTaggerME initPOSTagger() { InputStream iStream = mContext.getAssets().open(POS_MODEL); POSModel posModel = new POSModel(iStream); posTagger = new POSTaggerME(posModel); - } catch(IOException e) { e.printStackTrace(); } diff --git a/app/src/main/java/com/grammatek/simaromur/frontend/Pronunciation.java b/app/src/main/java/com/grammatek/simaromur/frontend/Pronunciation.java index 5bdde98..8235571 100644 --- a/app/src/main/java/com/grammatek/simaromur/frontend/Pronunciation.java +++ b/app/src/main/java/com/grammatek/simaromur/frontend/Pronunciation.java @@ -55,10 +55,13 @@ public class Pronunciation { } public Pronunciation(Context context) { + Log.v(LOG_TAG, "Pronunciation() called"); mContext = context; - mPronDict = readPronDict(); - mIpaPronDict = readIpaPronDict(); mAlphabets = initializeAlphabets(); + // the following members are lazily initialized: + // - mG2P + // - mPronDict + // - mIpaPronDict } public String transcribe(String text) { @@ -133,14 +136,15 @@ private String transcribeString(String text) { StringBuilder sb = new StringBuilder(); for (String tok : tokens) { String transcr = ""; - if (mPronDict.containsKey(tok)) { - transcr = mPronDict.get(tok).getTranscript().trim(); + Map pronDict = GetPronDict(); + if (pronDict.containsKey(tok)) { + transcr = pronDict.get(tok).getTranscript().trim(); } else if (tok.equals(SymbolsLvLIs.TagPause)){ transcr = SymbolsLvLIs.SymbolShortPause; } else { - transcr = mG2P.process(tok).trim(); + transcr = GetG2p().process(tok).trim(); } // bug in Thrax grammar, catch the error here: insert space before C if missing @@ -182,10 +186,25 @@ private List getValidAlphabets() { return List.copyOf(mAlphabets.keySet()); } - public void initializeG2P() { + private NativeG2P initializeG2P() { if (mG2P == null) { mG2P = new NativeG2P(this.mContext); } + return mG2P; + } + + private Map initializePronDict() { + if (mPronDict == null) { + mPronDict = readPronDict(); + } + return mPronDict; + } + + private Map initializeIpaPronDict() { + if (mIpaPronDict == null) { + mIpaPronDict = readIpaPronDict(); + } + return mIpaPronDict; } @@ -229,6 +248,7 @@ private Map>> initializeAlphabets() { } private Map readPronDict() { + Log.v(LOG_TAG, "readPronDict() called"); Map pronDict = new HashMap<>(); final List fileContent = FileUtils.readLinesFromResourceFile(this.mContext, R.raw.ice_pron_dict_standard_clear_2201_extended); @@ -242,6 +262,7 @@ private Map readPronDict() { } private Map readIpaPronDict() { + Log.v(LOG_TAG, "readIpaPronDict() called"); Map pronDict = new HashMap<>(); final List fileContent = FileUtils.readLinesFromResourceFile(this.mContext, R.raw.ice_pron_dict_standard_clear_2201_extended); @@ -255,14 +276,14 @@ private Map readIpaPronDict() { } public NativeG2P GetG2p() { - return mG2P; + return initializeG2P(); } public Map GetPronDict() { - return mPronDict; + return initializePronDict(); } public Map GetIpaPronDict() { - return mIpaPronDict; + return initializeIpaPronDict(); } public Map>> GetAlphabets() { return mAlphabets; diff --git a/app/src/main/java/com/grammatek/simaromur/frontend/PronunciationVits.java b/app/src/main/java/com/grammatek/simaromur/frontend/PronunciationVits.java index a3d6835..1f3e008 100644 --- a/app/src/main/java/com/grammatek/simaromur/frontend/PronunciationVits.java +++ b/app/src/main/java/com/grammatek/simaromur/frontend/PronunciationVits.java @@ -65,9 +65,6 @@ public String transcribe(String text) { * relevant phonemes were found. */ public String transcribe(String text, final String voiceType, final String voiceVersion) { - // lazy initialization of g2p - if (mPronounciation.GetG2p() == null) - mPronounciation.initializeG2P(); mG2P = mPronounciation.GetG2p(); assert(mG2P != null); diff --git a/app/src/main/java/com/grammatek/simaromur/frontend/TTSNormalizer.java b/app/src/main/java/com/grammatek/simaromur/frontend/TTSNormalizer.java index 2646894..437aa8a 100644 --- a/app/src/main/java/com/grammatek/simaromur/frontend/TTSNormalizer.java +++ b/app/src/main/java/com/grammatek/simaromur/frontend/TTSNormalizer.java @@ -143,29 +143,28 @@ public String preNormalize(String text, boolean doIgnoreUserDict) { /** * Replace abbreviations and other patterns from the normalization dictionary via the - * NormDictEntryDao. + * NormDictEntry database. * * @param sentence input sentence * @return normalized sentence with search terms replaced */ private String replaceFromNormDict(String sentence) { // replace abbreviations and other patterns from the normalization dictionary via the - // NormDictEntryDao + // NormDictEntry Db String normalized = sentence; - List entries = App.getAppRepository().getNormDictEntriesDirect(); - - if (entries != null) { - // sort entries descending to match longer strings first. This is important for - // abbreviations, e.g. "Donald Trump" should be replaced before "Trump" - entries.sort((o1, o2) -> o2.term.length() - o1.term.length()); - for (NormDictEntry entry : entries) { - // make for every entry.term a regular expression matching on word boundaries and - // case insensitive - Pattern regex = Pattern.compile("\\b(?i)" + entry.term.strip().toLowerCase() + "\\b"); - if (regex.matcher(normalized).find()) { - //Log.v(LOG_TAG, "replaceFromNormDict() - replacing: " + regex + " with: " + entry.replacement); - normalized = regex.matcher(normalized).replaceAll(entry.replacement); - } + final HashMap entriesMap = App.getAppRepository().getCachedUserDictEntries(); + final List entries = new ArrayList<>(entriesMap.keySet()); + + // sort terms according to their size descendingly to match longer strings first. + // This is important for terms that are contained in other terms, e.g. "Donald Duck" + // should be replaced before "Duck" + entries.sort((o1, o2) -> o2.term.length() - o1.term.length()); + for (NormDictEntry entry : entries) { + Pattern regex = entriesMap.get(entry); + assert regex != null; + if (regex.matcher(normalized).find()) { + //Log.v(LOG_TAG, "replaceFromNormDict() - replacing: " + regex + " with: " + entry.replacement); + normalized = regex.matcher(normalized).replaceAll(entry.replacement); } } if (!normalized.equals(sentence)) { diff --git a/app/src/main/java/com/grammatek/simaromur/frontend/Tokenizer.java b/app/src/main/java/com/grammatek/simaromur/frontend/Tokenizer.java index ad87208..0aa1d7d 100644 --- a/app/src/main/java/com/grammatek/simaromur/frontend/Tokenizer.java +++ b/app/src/main/java/com/grammatek/simaromur/frontend/Tokenizer.java @@ -88,6 +88,10 @@ public Tokenizer(Context context) { * @return a list of sentences as strings */ public List detectSentences(String text) { + // TODO: if the sentence ends with an abbreviation and a dot, then the dot is + // separated from the abbreviation. We should add a heuristics to check + // if the abbreviation is at the end of a sentence, and if so, keep the dot where + // it is. Example: ".... kl." List sentences = new ArrayList<>(); String[] tokensArr = text.split("\\s"); StringBuilder sb = new StringBuilder();