diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index e25812552..eb764e8ff 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -33,6 +33,7 @@ + 3 36% - 100 - 300 + 0 12dp diff --git a/java/src/org/futo/inputmethod/latin/Dictionary.java b/java/src/org/futo/inputmethod/latin/Dictionary.java index b2cfde048..a826087e0 100644 --- a/java/src/org/futo/inputmethod/latin/Dictionary.java +++ b/java/src/org/futo/inputmethod/latin/Dictionary.java @@ -64,7 +64,6 @@ public abstract class Dictionary { // User history dictionary internal to LatinIME. public static final String TYPE_USER_HISTORY = "history"; - public static final String TYPE_GGML = "ggml"; public final String mDictType; // The locale for this dictionary. May be null if unknown (phony dictionary for example). public final Locale mLocale; diff --git a/java/src/org/futo/inputmethod/latin/DictionaryFacilitator.java b/java/src/org/futo/inputmethod/latin/DictionaryFacilitator.java index 09f1ed4e3..70f13c1ca 100644 --- a/java/src/org/futo/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/org/futo/inputmethod/latin/DictionaryFacilitator.java @@ -45,7 +45,6 @@ import javax.annotation.Nullable; public interface DictionaryFacilitator { public static final String[] ALL_DICTIONARY_TYPES = new String[] { - Dictionary.TYPE_GGML, Dictionary.TYPE_MAIN, Dictionary.TYPE_CONTACTS, Dictionary.TYPE_USER_HISTORY, diff --git a/java/src/org/futo/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/org/futo/inputmethod/latin/DictionaryFacilitatorImpl.java index c4891aed8..5b107f5d4 100644 --- a/java/src/org/futo/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/org/futo/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -136,8 +136,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { @Nullable public final String mAccount; @Nullable private Dictionary mMainDict; - - @Nullable private LanguageModel mGGMLDict = null; // Confidence that the most probable language is actually the language the user is // typing in. For now, this is simply the number of times a word from this language // has been committed in a row. @@ -185,9 +183,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { if (Dictionary.TYPE_MAIN.equals(dictType)) { return mMainDict; } - if (Dictionary.TYPE_GGML.equals(dictType)) { - return mGGMLDict; - } return getSubDict(dictType); } @@ -199,9 +194,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { if (Dictionary.TYPE_MAIN.equals(dictType)) { return mMainDict != null; } - if (Dictionary.TYPE_GGML.equals(dictType)) { - return mGGMLDict != null; - } if (Dictionary.TYPE_USER_HISTORY.equals(dictType) && !TextUtils.equals(account, mAccount)) { // If the dictionary type is user history, & if the account doesn't match, @@ -358,7 +350,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { DictionaryGroup newDictionaryGroup = new DictionaryGroup(newLocale, mainDict, account, subDicts); - newDictionaryGroup.mGGMLDict = new LanguageModel(context, Dictionary.TYPE_GGML, newLocale); // Replace Dictionaries. final DictionaryGroup oldDictionaryGroup; synchronized (mLock) { @@ -416,7 +407,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { synchronized (mLock) { if (locale.equals(dictionaryGroup.mLocale)) { dictionaryGroup.setMainDict(mainDict); - dictionaryGroup.mGGMLDict = new LanguageModel(context, Dictionary.TYPE_GGML, locale); } else { // Dictionary facilitator has been reset for another locale. mainDict.close(); @@ -628,6 +618,13 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { NgramContext ngramContext, @Nonnull final Keyboard keyboard, SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId, int inputStyle) { + + if(settingsValuesForSuggestion.mUseTransformerLM) { + throw new IllegalStateException("Invalid code path TransformerLM"); + } + + + long proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo(); final SuggestionResults suggestionResults = new SuggestionResults( SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext(), @@ -635,9 +632,6 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { final float[] weightOfLangModelVsSpatialModel = new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; for (final String dictType : ALL_DICTIONARY_TYPES) { - if(settingsValuesForSuggestion.mUseTransformerLM && dictType != Dictionary.TYPE_GGML) continue; - else if(!settingsValuesForSuggestion.mUseTransformerLM && dictType == Dictionary.TYPE_GGML) continue; - final Dictionary dictionary = mDictionaryGroup.getDict(dictType); if (null == dictionary) continue; final float weightForLocale = composedData.mIsBatchMode diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index eddafb015..71f8bea2c 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -60,9 +60,19 @@ import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import org.futo.inputmethod.latin.common.Constants +import org.futo.inputmethod.latin.common.ComposedData import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.ActionBar import org.futo.inputmethod.latin.uix.ActionInputTransaction @@ -87,6 +97,11 @@ import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.presets.ClassicMaterialDark import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme +import org.futo.inputmethod.latin.settings.SettingsValues; +import org.futo.inputmethod.latin.settings.SettingsValuesForSuggestion +import org.futo.inputmethod.latin.xlm.LanguageModel; +import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator; +import org.futo.inputmethod.latin.utils.SuggestionResults class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, KeyboardManagerForAction { @@ -127,6 +142,15 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save this as LatinIMELegacy.SuggestionStripController ) + public val languageModelFacilitator = LanguageModelFacilitator( + this, + latinIMELegacy.mInputLogic, + latinIMELegacy.mDictionaryFacilitator, + latinIMELegacy.mSettings, + latinIMELegacy.mKeyboardSwitcher, + lifecycleScope + ) + private var activeThemeOption: ThemeOption? = null private var activeColorScheme = DarkColorScheme private var colorSchemeLoaderJob: Job? = null @@ -209,6 +233,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save handleLifecycleEvent(Lifecycle.Event.ON_RESUME) latinIMELegacy.onCreate() + + languageModelFacilitator.launchProcessor() } override fun onDestroy() { @@ -698,4 +724,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save return true } + + fun postUpdateSuggestionStrip(inputStyle: Int) { + languageModelFacilitator.updateSuggestionStripAsync(inputStyle); + } } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index bdcfbb884..668566ed7 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -101,6 +101,7 @@ import org.futo.inputmethod.latin.utils.StatsUtils; import org.futo.inputmethod.latin.utils.StatsUtilsManager; import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils; import org.futo.inputmethod.latin.utils.ViewLayoutUtils; +import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -157,7 +158,7 @@ public class LatinIMELegacy implements KeyboardActionListener, private static final String SCHEME_PACKAGE = "package"; final Settings mSettings; - private final DictionaryFacilitator mDictionaryFacilitator = + final DictionaryFacilitator mDictionaryFacilitator = DictionaryFacilitatorProvider.getDictionaryFacilitator( false /* isNeededForSpellChecking */); final InputLogic mInputLogic; @@ -220,7 +221,7 @@ public class LatinIMELegacy implements KeyboardActionListener, public static final class UIHandler extends LeakGuardHandlerWrapper { private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; - private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; + private static final int MSG_UPDATE_SUGGESTION_STRIP_LEGACY = 2; private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int MSG_REOPEN_DICTIONARIES = 5; @@ -266,7 +267,7 @@ public class LatinIMELegacy implements KeyboardActionListener, } final KeyboardSwitcher switcher = latinImeLegacy.mKeyboardSwitcher; switch (msg.what) { - case MSG_UPDATE_SUGGESTION_STRIP: + case MSG_UPDATE_SUGGESTION_STRIP_LEGACY: cancelUpdateSuggestionStrip(); latinImeLegacy.mInputLogic.performUpdateSuggestionStripSync( latinImeLegacy.mSettings.getCurrent(), msg.arg1 /* inputStyle */); @@ -332,8 +333,13 @@ public class LatinIMELegacy implements KeyboardActionListener, } public void postUpdateSuggestionStrip(final int inputStyle) { - sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP, inputStyle, - 0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions); + final LatinIMELegacy latinImeLegacy = getOwnerInstance(); + if(latinImeLegacy.mSettings.getCurrent().mTransformerPredictionEnabled) { + ((LatinIME)latinImeLegacy.getInputMethodService()).postUpdateSuggestionStrip(inputStyle); + } else { + sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP_LEGACY, inputStyle, + 0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions); + } } public void postReopenDictionaries() { @@ -389,11 +395,16 @@ public class LatinIMELegacy implements KeyboardActionListener, } public void cancelUpdateSuggestionStrip() { - removeMessages(MSG_UPDATE_SUGGESTION_STRIP); + removeMessages(MSG_UPDATE_SUGGESTION_STRIP_LEGACY); } public boolean hasPendingUpdateSuggestions() { - return hasMessages(MSG_UPDATE_SUGGESTION_STRIP); + return hasMessages(MSG_UPDATE_SUGGESTION_STRIP_LEGACY); + } + + public LanguageModelFacilitator getLanguageModelFacilitator() { + final LatinIMELegacy latinImeLegacy = getOwnerInstance(); + return ((LatinIME)(latinImeLegacy.mInputMethodService)).getLanguageModelFacilitator(); } public boolean hasPendingReopenDictionaries() { @@ -1562,12 +1573,17 @@ public class LatinIMELegacy implements KeyboardActionListener, // TODO[IL]: Move this out of LatinIME. public void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { + SettingsValues settings = mSettings.getCurrent(); + if(settings.mTransformerPredictionEnabled) { + ((LatinIME)getInputMethodService()).postUpdateSuggestionStrip(inputStyle); + return; + } final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (keyboard == null) { callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance()); return; } - mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard, + mInputLogic.getSuggestedWords(settings, keyboard, mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback); } diff --git a/java/src/org/futo/inputmethod/latin/Suggest.java b/java/src/org/futo/inputmethod/latin/Suggest.java index 06f47fa4c..eb6a2f3e0 100644 --- a/java/src/org/futo/inputmethod/latin/Suggest.java +++ b/java/src/org/futo/inputmethod/latin/Suggest.java @@ -147,13 +147,12 @@ public final class Suggest { return firstSuggestedWordInfo; } - // Retrieves suggestions for non-batch input (typing, recorrection, predictions...) - // and calls the callback function with the suggestions. - private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer, - final NgramContext ngramContext, final Keyboard keyboard, - final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, - final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { + public static SuggestedWords obtainNonBatchedInputSuggestedWords( + final WordComposer wordComposer, final int inputStyleIfNotPrediction, + final boolean isCorrectionEnabled, final int sequenceNumber, + final Locale locale, final SuggestionResults suggestionResults, + final float autoCorrectionThreshold + ) { final String typedWordString = wordComposer.getTypedWord(); final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWordString); @@ -161,10 +160,6 @@ public final class Suggest { ? typedWordString.substring(0, typedWordString.length() - trailingSingleQuotesCount) : typedWordString; - final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( - wordComposer.getComposedDataSnapshot(), ngramContext, keyboard, - settingsValuesForSuggestion, SESSION_ID_TYPING, inputStyleIfNotPrediction); - final Locale locale = mDictionaryFacilitator.getLocale(); final ArrayList suggestionsContainer = getTransformedSuggestedWordInfoList(wordComposer, suggestionResults, trailingSingleQuotesCount, locale); @@ -223,7 +218,7 @@ public final class Suggest { // list, "will" would always auto-correct to "Will" which is unwanted. Hence, no // main dict => no auto-correct. Also, it would probably get obnoxious quickly. // TODO: now that we have personalization, we may want to re-evaluate this decision - || !mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary() + //|| !mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary() // If the first suggestion is a shortcut we never auto-correct to it, regardless // of how strong it is (allowlist entries are not KIND_SHORTCUT but KIND_WHITELIST). // TODO: we may want to have shortcut-only entries auto-correct in the future. @@ -235,7 +230,7 @@ public final class Suggest { && firstOcurrenceOfTypedWordInSuggestions != 0) { hasAutoCorrection = true; } else if (!AutoCorrectionUtils.suggestionExceedsThreshold( - firstSuggestion, consideredWord, mAutoCorrectionThreshold)) { + firstSuggestion, consideredWord, autoCorrectionThreshold)) { // Score is too low for autocorrect hasAutoCorrection = false; } else { @@ -277,11 +272,30 @@ public final class Suggest { final boolean isTypedWordValid = firstOcurrenceOfTypedWordInSuggestions > -1 || (!resultsArePredictions && !allowsToBeAutoCorrected); - callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, + return new SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, typedWordInfo, isTypedWordValid, hasAutoCorrection /* willAutoCorrect */, - false /* isObsoleteSuggestions */, inputStyle, sequenceNumber)); + false /* isObsoleteSuggestions */, inputStyle, sequenceNumber); + } + + + // Retrieves suggestions for non-batch input (typing, recorrection, predictions...) + // and calls the callback function with the suggestions. + private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer, + final NgramContext ngramContext, final Keyboard keyboard, + final SettingsValuesForSuggestion settingsValuesForSuggestion, + final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, + final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { + final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( + wordComposer.getComposedDataSnapshot(), ngramContext, keyboard, + settingsValuesForSuggestion, SESSION_ID_TYPING, inputStyleIfNotPrediction); + final Locale locale = mDictionaryFacilitator.getLocale(); + + callback.onGetSuggestedWords( + obtainNonBatchedInputSuggestedWords(wordComposer, inputStyleIfNotPrediction, + isCorrectionEnabled, sequenceNumber, locale, suggestionResults, mAutoCorrectionThreshold) + ); } // Retrieves suggestions for the batch input diff --git a/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java b/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java index a6ada04bb..e21b11cb6 100644 --- a/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/org/futo/inputmethod/latin/inputlogic/InputLogic.java @@ -58,6 +58,7 @@ import org.futo.inputmethod.latin.utils.InputTypeUtils; import org.futo.inputmethod.latin.utils.RecapitalizeStatus; import org.futo.inputmethod.latin.utils.StatsUtils; import org.futo.inputmethod.latin.utils.TextRange; +import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator; import java.util.ArrayList; import java.util.Locale; @@ -74,7 +75,7 @@ public final class InputLogic { // TODO : Remove this member when we can. final LatinIMELegacy mLatinIMELegacy; - private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; + public final SuggestionStripViewAccessor mSuggestionStripViewAccessor; // Never null. private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER; @@ -88,8 +89,8 @@ public final class InputLogic { private final DictionaryFacilitator mDictionaryFacilitator; public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - // This has package visibility so it can be accessed from InputLogicHandler. - /* package */ final WordComposer mWordComposer; + + public final WordComposer mWordComposer; public final RichInputConnection mConnection; private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); @@ -1443,62 +1444,90 @@ public final class InputLogic { ngramContext, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); } + private void ensureSuggestionStripCompleted(final SettingsValues settingsValues, + final String separator, final LatinIMELegacy.UIHandler handler) { + if(settingsValues.mTransformerPredictionEnabled) { + LanguageModelFacilitator facilitator = handler.getLanguageModelFacilitator(); + if(facilitator.hasPendingUpdate()) { + facilitator.blockUntilComplete(); + } + } else { + if (handler.hasPendingUpdateSuggestions()) { + handler.cancelUpdateSuggestionStrip(); + // To know the input style here, we should retrieve the in-flight "update suggestions" + // message and read its arg1 member here. However, the Handler class does not let + // us retrieve this message, so we can't do that. But in fact, we notice that + // we only ever come here when the input style was typing. In the case of batch + // input, we update the suggestions synchronously when the tail batch comes. Likewise + // for application-specified completions. As for recorrections, we never auto-correct, + // so we don't come here either. Hence, the input style is necessarily + // INPUT_STYLE_TYPING. + performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING); + } + } + } + public void performUpdateSuggestionStripSync(final SettingsValues settingsValues, final int inputStyle) { - long startTimeMillis = 0; - if (DebugFlags.DEBUG_ENABLED) { - startTimeMillis = System.currentTimeMillis(); - Log.d(TAG, "performUpdateSuggestionStripSync()"); - } - // Check if we have a suggestion engine attached. - if (!settingsValues.needsToLookupSuggestions()) { - if (mWordComposer.isComposingWord()) { - Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " - + "requested!"); + if(settingsValues.mTransformerPredictionEnabled) { + throw new IllegalStateException("called performUpdateSuggestionStripSync during TransformerLM"); + } else { + long startTimeMillis = 0; + if (DebugFlags.DEBUG_ENABLED) { + startTimeMillis = System.currentTimeMillis(); + Log.d(TAG, "performUpdateSuggestionStripSync()"); + } + // Check if we have a suggestion engine attached. + if (!settingsValues.needsToLookupSuggestions()) { + if (mWordComposer.isComposingWord()) { + Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " + + "requested!"); + } + // Clear the suggestions strip. + mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()); + return; + } + + if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + return; } - // Clear the suggestions strip. - mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()); - return; - } - if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); - return; - } - - final AsyncResultHolder holder = new AsyncResultHolder<>("Suggest"); - mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - final String typedWordString = mWordComposer.getTypedWord(); - final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo( - typedWordString, "" /* prevWordsContext */, - SuggestedWordInfo.MAX_SCORE, - SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE); - // Show new suggestions if we have at least one. Otherwise keep the old - // suggestions with the new typed word. Exception: if the length of the - // typed word is <= 1 (after a deletion typically) we clear old suggestions. - if (suggestedWords.size() > 1 || typedWordString.length() <= 1) { - holder.set(suggestedWords); - } else { - holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords)); + final AsyncResultHolder holder = new AsyncResultHolder<>("Suggest"); + mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER, + new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + final String typedWordString = mWordComposer.getTypedWord(); + final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo( + typedWordString, "" /* prevWordsContext */, + SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE); + // Show new suggestions if we have at least one. Otherwise keep the old + // suggestions with the new typed word. Exception: if the length of the + // typed word is <= 1 (after a deletion typically) we clear old suggestions. + if (suggestedWords.size() > 1 || typedWordString.length() <= 1) { + holder.set(suggestedWords); + } else { + holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords)); + } } } - } - ); + ); - // This line may cause the current thread to wait. - final SuggestedWords suggestedWords = holder.get(null, - Constants.GET_SUGGESTED_WORDS_TIMEOUT); - if (suggestedWords != null) { - mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); - } - if (DebugFlags.DEBUG_ENABLED) { - long runTimeMillis = System.currentTimeMillis() - startTimeMillis; - Log.d(TAG, "performUpdateSuggestionStripSync() : " + runTimeMillis + " ms to finish"); + // This line may cause the current thread to wait. + final SuggestedWords suggestedWords = holder.get(null, + Constants.GET_SUGGESTED_WORDS_TIMEOUT); + if (suggestedWords != null) { + mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); + } + if (DebugFlags.DEBUG_ENABLED) { + long runTimeMillis = System.currentTimeMillis() - startTimeMillis; + Log.d(TAG, "performUpdateSuggestionStripSync() : " + runTimeMillis + " ms to finish"); + } } } @@ -2094,18 +2123,7 @@ public final class InputLogic { private void commitCurrentAutoCorrection(final SettingsValues settingsValues, final String separator, final LatinIMELegacy.UIHandler handler) { // Complete any pending suggestions query first - if (handler.hasPendingUpdateSuggestions()) { - handler.cancelUpdateSuggestionStrip(); - // To know the input style here, we should retrieve the in-flight "update suggestions" - // message and read its arg1 member here. However, the Handler class does not let - // us retrieve this message, so we can't do that. But in fact, we notice that - // we only ever come here when the input style was typing. In the case of batch - // input, we update the suggestions synchronously when the tail batch comes. Likewise - // for application-specified completions. As for recorrections, we never auto-correct, - // so we don't come here either. Hence, the input style is necessarily - // INPUT_STYLE_TYPING. - performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING); - } + ensureSuggestionStripCompleted(settingsValues, separator, handler); final SuggestedWordInfo autoCorrectionOrNull = mWordComposer.getAutoCorrectionOrNull(); final String typedWord = mWordComposer.getTypedWord(); final String stringToCommit = (autoCorrectionOrNull != null) diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java index 354651b99..dd28b39c8 100644 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java @@ -55,7 +55,6 @@ public class SettingsValues { // From resources: public final SpacingAndPunctuations mSpacingAndPunctuations; - public final int mDelayInMillisecondsToUpdateOldSuggestions; public final long mDoubleSpacePeriodTimeout; // From configuration: public final Locale mLocale; @@ -124,8 +123,6 @@ public class SettingsValues { @Nonnull final InputAttributes inputAttributes) { mLocale = res.getConfiguration().locale; // Get the resources - mDelayInMillisecondsToUpdateOldSuggestions = - res.getInteger(R.integer.config_delay_in_milliseconds_to_update_old_suggestions); mSpacingAndPunctuations = new SpacingAndPunctuations(res); // Store the input attributes @@ -381,8 +378,6 @@ public class SettingsValues { final StringBuilder sb = new StringBuilder("Current settings :"); sb.append("\n mSpacingAndPunctuations = "); sb.append("" + mSpacingAndPunctuations.dump()); - sb.append("\n mDelayInMillisecondsToUpdateOldSuggestions = "); - sb.append("" + mDelayInMillisecondsToUpdateOldSuggestions); sb.append("\n mAutoCap = "); sb.append("" + mAutoCap); sb.append("\n mVibrateOn = "); diff --git a/java/src/org/futo/inputmethod/latin/xlm/LanguageModel.java b/java/src/org/futo/inputmethod/latin/xlm/LanguageModel.java index ebf67eafb..8e144d0de 100644 --- a/java/src/org/futo/inputmethod/latin/xlm/LanguageModel.java +++ b/java/src/org/futo/inputmethod/latin/xlm/LanguageModel.java @@ -95,16 +95,16 @@ public class LanguageModel extends Dictionary { @Override public void run() { if(mNativeState != 0) return; - String modelPath = getPathToModelResource(context, R.raw.ml4_f16, R.raw.ml3_tokenizer, true); + String modelPath = getPathToModelResource(context, R.raw.ml4_1_f16, R.raw.ml3_tokenizer, true); mNativeState = openNative(modelPath); if(mNativeState == 0){ - modelPath = getPathToModelResource(context, R.raw.ml4_f16, R.raw.ml3_tokenizer, true); + modelPath = getPathToModelResource(context, R.raw.ml4_1_f16, R.raw.ml3_tokenizer, true); mNativeState = openNative(modelPath); } if(mNativeState == 0){ - throw new RuntimeException("Failed to load R.raw.ml4_f16, R.raw.ml3_tokenizer model"); + throw new RuntimeException("Failed to load R.raw.ml4_1_f16, R.raw.ml3_tokenizer model"); } } }; diff --git a/java/src/org/futo/inputmethod/latin/xlm/LanguageModelFacilitator.kt b/java/src/org/futo/inputmethod/latin/xlm/LanguageModelFacilitator.kt new file mode 100644 index 000000000..72c3896fc --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/xlm/LanguageModelFacilitator.kt @@ -0,0 +1,246 @@ +package org.futo.inputmethod.latin.xlm; + +import android.content.ComponentCallbacks2 +import android.content.Context +import android.content.res.Configuration +import android.inputmethodservice.InputMethodService +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.CompletionInfo +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InlineSuggestion +import android.view.inputmethod.InlineSuggestionsRequest +import android.view.inputmethod.InlineSuggestionsResponse +import android.view.inputmethod.InputMethodSubtype +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.findViewTreeSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Semaphore +import org.futo.inputmethod.latin.common.Constants +import org.futo.inputmethod.latin.common.ComposedData +import org.futo.inputmethod.latin.uix.Action +import org.futo.inputmethod.latin.uix.ActionBar +import org.futo.inputmethod.latin.uix.ActionInputTransaction +import org.futo.inputmethod.latin.uix.ActionWindow +import org.futo.inputmethod.latin.uix.BasicThemeProvider +import org.futo.inputmethod.latin.uix.DynamicThemeProvider +import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner +import org.futo.inputmethod.latin.uix.KeyboardManagerForAction +import org.futo.inputmethod.latin.uix.PersistentActionState +import org.futo.inputmethod.latin.uix.THEME_KEY +import org.futo.inputmethod.latin.uix.actions.VoiceInputAction +import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest +import org.futo.inputmethod.latin.uix.deferGetSetting +import org.futo.inputmethod.latin.uix.deferSetSetting +import org.futo.inputmethod.latin.uix.differsFrom +import org.futo.inputmethod.latin.uix.inflateInlineSuggestion +import org.futo.inputmethod.latin.uix.theme.DarkColorScheme +import org.futo.inputmethod.latin.uix.theme.ThemeOption +import org.futo.inputmethod.latin.uix.theme.ThemeOptions +import org.futo.inputmethod.latin.uix.theme.Typography +import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper +import org.futo.inputmethod.latin.uix.theme.presets.ClassicMaterialDark +import org.futo.inputmethod.latin.uix.theme.presets.DynamicSystemTheme +import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme +import org.futo.inputmethod.latin.settings.SettingsValues; +import org.futo.inputmethod.latin.settings.SettingsValuesForSuggestion +import org.futo.inputmethod.latin.settings.Settings +import org.futo.inputmethod.latin.xlm.LanguageModel; +import org.futo.inputmethod.latin.utils.SuggestionResults +import org.futo.inputmethod.latin.NgramContext +import org.futo.inputmethod.latin.LatinIMELegacy +import org.futo.inputmethod.latin.inputlogic.InputLogic +import org.futo.inputmethod.latin.DictionaryFacilitator +import org.futo.inputmethod.latin.Suggest +import org.futo.inputmethod.latin.SuggestedWords +import org.futo.inputmethod.keyboard.KeyboardSwitcher + +public class LanguageModelFacilitator( + val context: Context, + val inputLogic: InputLogic, + val dictionaryFacilitator: DictionaryFacilitator, + val settings: Settings, + val keyboardSwitcher: KeyboardSwitcher, + val lifecycleScope: LifecycleCoroutineScope +) { + private var languageModel: LanguageModel? = null + data class PredictionInputValues( + val composedData: ComposedData, + val ngramContext: NgramContext, + val inputStyle: Int, + val sequenceId: Int + ) + private val sharedFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1) + + private var currentSequenceId = 0 + private val sequenceIdFinishedFlow = MutableSharedFlow(replay = 4, extraBufferCapacity = 4) + + private val computationSemaphore = Semaphore(1) + public fun hasPendingUpdate(): Boolean = + computationSemaphore.availablePermits == 0 + + public fun blockUntilComplete() { + runBlocking { + computationSemaphore.acquire() + computationSemaphore.release() + try { + sequenceIdFinishedFlow.first { it >= currentSequenceId } + } catch(ignored: Exception) { + + } + } + } + + private suspend fun processUpdateSuggestionStrip(values: PredictionInputValues) { + computationSemaphore.acquire() + try { + val job = Job() + CoroutineScope(Dispatchers.Default + job).launch { + delay(200) + inputLogic.mSuggestionStripViewAccessor.setNeutralSuggestionStrip() + } + + val locale = dictionaryFacilitator.locale + if(languageModel == null) { + languageModel = LanguageModel(context, "lm", locale) + } + + val settingsValues = settings.current + + val keyboard = keyboardSwitcher.getKeyboard() + val settingsForPrediction = SettingsValuesForSuggestion( + settingsValues.mBlockPotentiallyOffensive, + settingsValues.mTransformerPredictionEnabled + ) + val proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo() + + val suggestionResults = SuggestionResults( + 3, values.ngramContext.isBeginningOfSentenceContext(), false) + + val lmSuggestions = languageModel!!.getSuggestions( + values.composedData, + values.ngramContext, + proximityInfoHandle, + settingsForPrediction, + -1, + 0.0f, + floatArrayOf()) + + if(lmSuggestions == null) { + job.cancel() + inputLogic.mSuggestionStripViewAccessor.setNeutralSuggestionStrip() + return + } + + suggestionResults.addAll(lmSuggestions) + if(suggestionResults.mRawSuggestions != null) { + suggestionResults.mRawSuggestions.addAll(lmSuggestions) + } + + val wordComposer = inputLogic.mWordComposer + val suggestedWords = Suggest.obtainNonBatchedInputSuggestedWords( + wordComposer, values.inputStyle, true, -1, locale, suggestionResults, settingsValues.mAutoCorrectionThreshold) + + job.cancel() + inputLogic.mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords) + sequenceIdFinishedFlow.emit(values.sequenceId) + } finally { + computationSemaphore.release() + } + } + + public fun launchProcessor() = lifecycleScope.launch { + println("LatinIME: Starting processor") + withContext(Dispatchers.Default) { + sharedFlow.conflate().collect { value -> + println("LatinIME: Collecting") + processUpdateSuggestionStrip(value) + } + } + } + + public fun updateSuggestionStripAsync(inputStyle: Int) { + val settingsValues = settings.current + if (!settingsValues.needsToLookupSuggestions()) { + inputLogic.mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()) + return + } + + if(!settingsValues.mTransformerPredictionEnabled) { + // TODO: Call old path + return + } + + val wordComposer = inputLogic.mWordComposer + val ngramContext = inputLogic.getNgramContextFromNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations, 2) + + val values = PredictionInputValues( + wordComposer.getComposedDataSnapshot(), + ngramContext, + inputStyle, + ++currentSequenceId + ) + + lifecycleScope.launch { + println("LatinIME: Emitting values") + sharedFlow.emit(values) + } + } +} \ No newline at end of file