Increase target size of preferred letters while typing.

This increases the chance of hitting the correct letter when typing a word
that exists in the dictionary, rather than only correct it after the fact.
It is most effective after 2 or 3 letters of a word have been typed and gets
more accurate with more typed letters in the word.

If 2 adjacent letters have similar probabilities of occuring, then there is no
hit correction applied.
main
Amith Yamasani 2010-02-05 14:07:04 -08:00
parent 8fa317a61a
commit 1b62ff1a3d
12 changed files with 215 additions and 20 deletions

View File

@ -89,7 +89,7 @@ static jint latinime_BinaryDictionary_open
static int latinime_BinaryDictionary_getSuggestions(
JNIEnv *env, jobject object, jint dict, jintArray inputArray, jint arraySize,
jcharArray outputArray, jintArray frequencyArray, jint maxWordLength, jint maxWords,
jint maxAlternatives, jint skipPos)
jint maxAlternatives, jint skipPos, jintArray nextLettersArray, jint nextLettersSize)
{
Dictionary *dictionary = (Dictionary*) dict;
if (dictionary == NULL)
@ -98,13 +98,16 @@ static int latinime_BinaryDictionary_getSuggestions(
int *frequencies = env->GetIntArrayElements(frequencyArray, NULL);
int *inputCodes = env->GetIntArrayElements(inputArray, NULL);
jchar *outputChars = env->GetCharArrayElements(outputArray, NULL);
int *nextLetters = nextLettersArray != NULL ? env->GetIntArrayElements(nextLettersArray, NULL)
: NULL;
int count = dictionary->getSuggestions(inputCodes, arraySize, (unsigned short*) outputChars, frequencies,
maxWordLength, maxWords, maxAlternatives, skipPos);
maxWordLength, maxWords, maxAlternatives, skipPos, nextLetters, nextLettersSize);
env->ReleaseIntArrayElements(frequencyArray, frequencies, 0);
env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT);
env->ReleaseCharArrayElements(outputArray, outputChars, 0);
env->ReleaseIntArrayElements(nextLettersArray, nextLetters, 0);
return count;
}
@ -136,7 +139,7 @@ static JNINativeMethod gMethods[] = {
{"openNative", "(Landroid/content/res/AssetManager;Ljava/lang/String;II)I",
(void*)latinime_BinaryDictionary_open},
{"closeNative", "(I)V", (void*)latinime_BinaryDictionary_close},
{"getSuggestionsNative", "(I[II[C[IIIII)I", (void*)latinime_BinaryDictionary_getSuggestions},
{"getSuggestionsNative", "(I[II[C[IIIII[II)I", (void*)latinime_BinaryDictionary_getSuggestions},
{"isValidWordNative", "(I[CI)Z", (void*)latinime_BinaryDictionary_isValidWord}
};

View File

@ -49,7 +49,8 @@ Dictionary::~Dictionary()
}
int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies,
int maxWordLength, int maxWords, int maxAlternatives, int skipPos)
int maxWordLength, int maxWords, int maxAlternatives, int skipPos,
int *nextLetters, int nextLettersSize)
{
int suggWords;
mFrequencies = frequencies;
@ -61,6 +62,8 @@ int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWor
mMaxWords = maxWords;
mSkipPos = skipPos;
mMaxEditDistance = mInputLength < 5 ? 2 : mInputLength / 2;
mNextLettersFrequencies = nextLetters;
mNextLettersSize = nextLettersSize;
getWordsRec(0, 0, mInputLength * 3, false, 1, 0, 0);
@ -68,9 +71,27 @@ int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWor
suggWords = 0;
while (suggWords < mMaxWords && mFrequencies[suggWords] > 0) suggWords++;
if (DEBUG_DICT) LOGI("Returning %d words", suggWords);
if (DEBUG_DICT) {
LOGI("Next letters: ");
for (int k = 0; k < nextLettersSize; k++) {
if (mNextLettersFrequencies[k] > 0) {
LOGI("%c = %d,", k, mNextLettersFrequencies[k]);
}
}
LOGI("\n");
}
return suggWords;
}
void
Dictionary::registerNextLetter(unsigned short c)
{
if (c < mNextLettersSize) {
mNextLettersFrequencies[c]++;
}
}
unsigned short
Dictionary::getChar(int *pos)
{
@ -210,6 +231,9 @@ Dictionary::getWordsRec(int pos, int depth, int maxDepth, bool completion, int s
mWord[depth] = c;
if (terminal) {
addWord(mWord, depth + 1, freq * snr);
if (depth >= mInputLength && mSkipPos < 0) {
registerNextLetter(mWord[mInputLength]);
}
}
if (childrenAddress != 0) {
getWordsRec(childrenAddress, depth + 1, maxDepth,

View File

@ -32,7 +32,8 @@ class Dictionary {
public:
Dictionary(void *dict, int typedLetterMultipler, int fullWordMultiplier);
int getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies,
int maxWordLength, int maxWords, int maxAlternatives, int skipPos);
int maxWordLength, int maxWords, int maxAlternatives, int skipPos,
int *nextLetters, int nextLettersSize);
bool isValidWord(unsigned short *word, int length);
void setAsset(void *asset) { mAsset = asset; }
void *getAsset() { return mAsset; }
@ -53,6 +54,7 @@ private:
void getWordsRec(int pos, int depth, int maxDepth, bool completion, int frequency,
int inputIndex, int diffs);
bool isValidWordRec(int pos, unsigned short *word, int offset, int length);
void registerNextLetter(unsigned short c);
unsigned char *mDict;
void *mAsset;
@ -70,6 +72,8 @@ private:
int mFullWordMultiplier;
int mTypedLetterMultiplier;
int *mNextLettersFrequencies;
int mNextLettersSize;
};
// ----------------------------------------------------------------------------

View File

@ -65,7 +65,8 @@ public class BinaryDictionary extends Dictionary {
private native boolean isValidWordNative(int nativeData, char[] word, int wordLength);
private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize,
char[] outputChars, int[] frequencies,
int maxWordLength, int maxWords, int maxAlternatives, int skipPos);
int maxWordLength, int maxWords, int maxAlternatives, int skipPos,
int[] nextLettersFrequencies, int nextLettersSize);
private final void loadDictionary(Context context, int resId) {
AssetManager am = context.getResources().getAssets();
@ -74,7 +75,8 @@ public class BinaryDictionary extends Dictionary {
}
@Override
public void getWords(final WordComposer codes, final WordCallback callback) {
public void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
final int codesSize = codes.size();
// Wont deal with really long words.
if (codesSize > MAX_WORD_LENGTH - 1) return;
@ -90,7 +92,9 @@ public class BinaryDictionary extends Dictionary {
int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize,
mOutputChars, mFrequencies,
MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1);
MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1,
nextLettersFrequencies,
nextLettersFrequencies != null ? nextLettersFrequencies.length : 0);
// If there aren't sufficient suggestions, search for words by allowing wild cards at
// the different character positions. This feature is not ready for prime-time as we need
@ -100,7 +104,8 @@ public class BinaryDictionary extends Dictionary {
for (int skip = 0; skip < codesSize; skip++) {
int tempCount = getSuggestionsNative(mNativeDict, mInputCodes, codesSize,
mOutputChars, mFrequencies,
MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip);
MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip,
null, 0);
count = Math.max(count, tempCount);
if (tempCount > 0) break;
}

View File

@ -84,14 +84,15 @@ public class ContactsDictionary extends ExpandableDictionary {
}
@Override
public synchronized void getWords(final WordComposer codes, final WordCallback callback) {
public synchronized void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
synchronized (mUpdatingLock) {
// If we need to update, start off a background task
if (mRequiresReload) loadDictionaryAsyncLocked();
// Currently updating contacts, don't return any results.
if (mUpdatingContacts) return;
}
super.getWords(codes, callback);
super.getWords(codes, callback, nextLettersFrequencies);
}
@Override

View File

@ -55,9 +55,14 @@ abstract public class Dictionary {
* words are added through the callback object.
* @param composer the key sequence to match
* @param callback the callback object to send matched words to as possible candidates
* @param nextLettersFrequencies array of frequencies of next letters that could follow the
* word so far. For instance, "bracke" can be followed by "t", so array['t'] will have
* a non-zero value on returning from this method.
* Pass in null if you don't want the dictionary to look up next letters.
* @see WordCallback#addWord(char[], int, int)
*/
abstract public void getWords(final WordComposer composer, final WordCallback callback);
abstract public void getWords(final WordComposer composer, final WordCallback callback,
int[] nextLettersFrequencies);
/**
* Checks if the given word occurs in the dictionary

View File

@ -27,6 +27,7 @@ public class ExpandableDictionary extends Dictionary {
private char[] mWordBuilder = new char[MAX_WORD_LENGTH];
private int mMaxDepth;
private int mInputLength;
private int[] mNextLettersFrequencies;
public static final int MAX_WORD_LENGTH = 32;
private static final char QUOTE = '\'';
@ -116,8 +117,10 @@ public class ExpandableDictionary extends Dictionary {
}
@Override
public void getWords(final WordComposer codes, final WordCallback callback) {
public void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
mInputLength = codes.size();
mNextLettersFrequencies = nextLettersFrequencies;
if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
// Cache the codes so that we don't have to lookup an array list
for (int i = 0; i < mInputLength; i++) {
@ -216,6 +219,11 @@ public class ExpandableDictionary extends Dictionary {
if (!callback.addWord(word, 0, depth + 1, freq * snr)) {
return;
}
// Add to frequency of next letters for predictive correction
if (mNextLettersFrequencies != null && depth >= inputIndex && skipPos < 0
&& mNextLettersFrequencies.length > word[inputIndex]) {
mNextLettersFrequencies[word[inputIndex]]++;
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,

View File

@ -1340,6 +1340,8 @@ public class LatinIME extends InputMethodService
private void updateSuggestions() {
mSuggestionShouldReplaceCurrentWord = false;
((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null);
// Check if we have a suggestion engine attached.
if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) {
return;
@ -1351,6 +1353,10 @@ public class LatinIME extends InputMethodService
}
List<CharSequence> stringList = mSuggest.getSuggestions(mInputView, mWord, false);
int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies();
((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(nextLettersFrequencies);
boolean correctionAvailable = mSuggest.hasMinimalCorrection();
//|| mCorrectionMode == mSuggest.CORRECTION_FULL;
CharSequence typedWord = mWord.getTypedWord();

View File

@ -35,11 +35,15 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.Keyboard;
import android.text.TextPaint;
import android.util.Log;
import android.view.ViewConfiguration;
import android.view.inputmethod.EditorInfo;
public class LatinKeyboard extends Keyboard {
private static final boolean DEBUG_PREFERRED_LETTER = false;
private static final String TAG = "LatinKeyboard";
private Drawable mShiftLockIcon;
private Drawable mShiftLockPreviewIcon;
private Drawable mOldShiftIcon;
@ -69,6 +73,12 @@ public class LatinKeyboard extends Keyboard {
private boolean mCurrentlyInSpace;
private SlidingLocaleDrawable mSlidingLocaleIcon;
private Rect mBounds = new Rect();
private int[] mPrefLetterFrequencies;
private boolean mPreemptiveCorrection;
private int mPrefLetter;
private int mPrefLetterX;
private int mPrefLetterY;
private int mPrefDistance;
private int mExtensionResId;
@ -79,6 +89,8 @@ public class LatinKeyboard extends Keyboard {
private int mShiftState = SHIFT_OFF;
private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f;
private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f;
static int sSpacebarVerticalCorrection;
@ -409,9 +421,18 @@ public class LatinKeyboard extends Keyboard {
return mCurrentlyInSpace;
}
void setPreferredLetters(int[] frequencies) {
mPrefLetterFrequencies = frequencies;
mPrefLetter = 0;
}
void keyReleased() {
mCurrentlyInSpace = false;
mSpaceDragLastDiff = 0;
mPrefLetter = 0;
mPrefLetterX = 0;
mPrefLetterY = 0;
mPrefDistance = Integer.MAX_VALUE;
if (mSpaceKey != null) {
updateLocaleDrag(Integer.MAX_VALUE);
}
@ -448,6 +469,79 @@ public class LatinKeyboard extends Keyboard {
return insideSpace;
}
}
} else if (mPrefLetterFrequencies != null) {
// New coordinate? Reset
if (mPrefLetterX != x || mPrefLetterY != y) {
mPrefLetter = 0;
mPrefDistance = Integer.MAX_VALUE;
}
// Handle preferred next letter
final int[] pref = mPrefLetterFrequencies;
if (mPrefLetter > 0) {
if (DEBUG_PREFERRED_LETTER && mPrefLetter == code
&& !key.isInsideSuper(x, y)) {
Log.d(TAG, "CORRECTED !!!!!!");
}
return mPrefLetter == code;
} else {
final boolean inside = key.isInsideSuper(x, y);
int[] nearby = getNearestKeys(x, y);
List<Key> nearbyKeys = getKeys();
if (inside) {
// If it's a preferred letter
if (inPrefList(code, pref)) {
// Check if its frequency is much lower than a nearby key
mPrefLetter = code;
mPrefLetterX = x;
mPrefLetterY = y;
for (int i = 0; i < nearby.length; i++) {
Key k = nearbyKeys.get(nearby[i]);
if (k != key && inPrefList(k.codes[0], pref)) {
final int dist = distanceFrom(k, x, y);
if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) &&
(pref[k.codes[0]] > pref[mPrefLetter] * 3)) {
mPrefLetter = k.codes[0];
mPrefDistance = dist;
if (DEBUG_PREFERRED_LETTER) {
Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!");
}
break;
}
}
}
return mPrefLetter == code;
}
}
// Get the surrounding keys and intersect with the preferred list
// For all in the intersection
// if distance from touch point is within a reasonable distance
// make this the pref letter
// If no pref letter
// return inside;
// else return thiskey == prefletter;
for (int i = 0; i < nearby.length; i++) {
Key k = nearbyKeys.get(nearby[i]);
if (inPrefList(k.codes[0], pref)) {
final int dist = distanceFrom(k, x, y);
if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB)
&& dist < mPrefDistance) {
mPrefLetter = k.codes[0];
mPrefLetterX = x;
mPrefLetterY = y;
mPrefDistance = dist;
}
}
}
// Didn't find any
if (mPrefLetter == 0) {
return inside;
} else {
return mPrefLetter == code;
}
}
}
// Lock into the spacebar
@ -456,6 +550,19 @@ public class LatinKeyboard extends Keyboard {
return key.isInsideSuper(x, y);
}
private boolean inPrefList(int code, int[] pref) {
if (code < pref.length && code >= 0) return pref[code] > 0;
return false;
}
private int distanceFrom(Key k, int x, int y) {
if (y > k.y && y < k.y + k.height) {
return Math.abs(k.x + k.width / 2 - x);
} else {
return Integer.MAX_VALUE;
}
}
@Override
public int[] getNearestKeys(int x, int y) {
if (mCurrentlyInSpace) {
@ -512,7 +619,8 @@ public class LatinKeyboard extends Keyboard {
*/
@Override
public boolean isInside(int x, int y) {
return LatinKeyboard.this.isInside(this, x, y);
boolean result = LatinKeyboard.this.isInside(this, x, y);
return result;
}
boolean isInsideSuper(int x, int y) {

View File

@ -20,6 +20,7 @@ import java.util.List;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.inputmethodservice.Keyboard.Key;
@ -80,6 +81,11 @@ public class LatinKeyboardView extends KeyboardView {
@Override
public boolean onTouchEvent(MotionEvent me) {
LatinKeyboard keyboard = (LatinKeyboard) getKeyboard();
if (DEBUG_LINE) {
mLastX = (int) me.getX();
mLastY = (int) me.getY();
invalidate();
}
// Reset any bounding box controls in the keyboard
if (me.getAction() == MotionEvent.ACTION_DOWN) {
keyboard.keyReleased();
@ -203,6 +209,7 @@ public class LatinKeyboardView extends KeyboardView {
/**************************** INSTRUMENTATION *******************************/
static final boolean DEBUG_AUTO_PLAY = false;
static final boolean DEBUG_LINE = false;
private static final int MSG_TOUCH_DOWN = 1;
private static final int MSG_TOUCH_UP = 2;
@ -213,6 +220,9 @@ public class LatinKeyboardView extends KeyboardView {
private boolean mDownDelivered;
private Key[] mAsciiKeys = new Key[256];
private boolean mPlaying;
private int mLastX;
private int mLastY;
private Paint mPaint;
@Override
public void setKeyboard(Keyboard k) {
@ -309,5 +319,14 @@ public class LatinKeyboardView extends KeyboardView {
mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20);
}
}
if (DEBUG_LINE) {
if (mPaint == null) {
mPaint = new Paint();
mPaint.setColor(0x80FFFFFF);
mPaint.setAntiAlias(false);
}
c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint);
c.drawLine(0, mLastY, getWidth(), mLastY, mPaint);
}
}
}

View File

@ -52,6 +52,12 @@ public class Suggest implements Dictionary.WordCallback {
private int mPrefMaxSuggestions = 12;
private int[] mPriorities = new int[mPrefMaxSuggestions];
// Handle predictive correction for only the first 1280 characters for performance reasons
// If we support scripts that need latin characters beyond that, we should probably use some
// kind of a sparse array or language specific list with a mapping lookup table.
// 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of
// latin characters.
private int[] mNextLettersFrequencies = new int[1280];
private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
private boolean mHaveCorrection;
@ -162,6 +168,7 @@ public class Suggest implements Dictionary.WordCallback {
mCapitalize = wordComposer.isCapitalized();
collectGarbage();
Arrays.fill(mPriorities, 0);
Arrays.fill(mNextLettersFrequencies, 0);
// Save a lowercase version of the original word
mOriginalWord = wordComposer.getTypedWord();
@ -175,17 +182,17 @@ public class Suggest implements Dictionary.WordCallback {
if (wordComposer.size() > 1) {
if (mUserDictionary != null || mContactsDictionary != null) {
if (mUserDictionary != null) {
mUserDictionary.getWords(wordComposer, this);
mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
}
if (mContactsDictionary != null) {
mContactsDictionary.getWords(wordComposer, this);
mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
}
if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)) {
mHaveCorrection = true;
}
}
mMainDict.getWords(wordComposer, this);
mMainDict.getWords(wordComposer, this, mNextLettersFrequencies);
if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) {
mHaveCorrection = true;
}
@ -229,6 +236,10 @@ public class Suggest implements Dictionary.WordCallback {
return mSuggestions;
}
public int[] getNextLettersFrequencies() {
return mNextLettersFrequencies;
}
private void removeDupes() {
final ArrayList<CharSequence> suggestions = mSuggestions;
if (suggestions.size() < 2) return;

View File

@ -94,9 +94,10 @@ public class UserDictionary extends ExpandableDictionary {
}
@Override
public synchronized void getWords(final WordComposer codes, final WordCallback callback) {
public synchronized void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
if (mRequiresReload) loadDictionary();
super.getWords(codes, callback);
super.getWords(codes, callback, nextLettersFrequencies);
}
@Override