Skip to content

Commit 297ca73

Browse files
authored
Make suggestion typeahead a bit smarter (#723)
1 parent f0827b8 commit 297ca73

1 file changed

Lines changed: 80 additions & 1 deletion

File tree

app/src/main/java/com/urik/keyboard/service/SpellCheckManager.kt

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ constructor(
776776
COMPLETION_LENGTH_WEIGHT * lengthRatio +
777777
COMPLETION_FREQUENCY_WEIGHT * frequencyScore
778778

779+
val spatialScore = calculateFullWordSpatialScore(
780+
normalizedWord,
781+
word.lowercase()
782+
)
783+
baseConfidence += spatialScore * COMPLETION_SPATIAL_WEIGHT
784+
779785
if (userFrequency > 0) {
780786
val userFreqBoost = calculateFrequencyBoost(userFrequency)
781787
baseConfidence += userFreqBoost
@@ -1253,7 +1259,67 @@ constructor(
12531259
return scoreSameLengthAlignment(input, candidate, keyPositions, twoSigmaSquared)
12541260
}
12551261

1256-
return scoreDifferentLengthAlignment(input, candidate, keyPositions, twoSigmaSquared)
1262+
if (input.length > candidate.length) {
1263+
return scoreDifferentLengthAlignment(input, candidate, keyPositions, twoSigmaSquared)
1264+
}
1265+
1266+
return scorePrefixWithLookahead(input, candidate, keyPositions, twoSigmaSquared, avgKeySpacing)
1267+
}
1268+
1269+
private fun scorePrefixWithLookahead(
1270+
input: String,
1271+
candidate: String,
1272+
keyPositions: Map<Char, android.graphics.PointF>,
1273+
twoSigmaSquared: Double,
1274+
avgKeySpacing: Double
1275+
): Double {
1276+
if (input.isEmpty()) return 0.0
1277+
1278+
val lengthDiff = candidate.length - input.length
1279+
val typedScore = if (lengthDiff <= 2) {
1280+
maxOf(
1281+
scoreDifferentLengthAlignment(input, candidate, keyPositions, twoSigmaSquared),
1282+
scoreDirectPrefix(input, candidate, keyPositions, twoSigmaSquared)
1283+
)
1284+
} else {
1285+
scoreDirectPrefix(input, candidate, keyPositions, twoSigmaSquared)
1286+
}
1287+
1288+
val lookaheadSigma = avgKeySpacing * LOOKAHEAD_SIGMA_MULTIPLIER
1289+
val lookaheadTwoSigmaSquared = 2.0 * lookaheadSigma * lookaheadSigma
1290+
1291+
var lookaheadScoreSum = 0.0
1292+
var lookaheadWeightSum = 0.0
1293+
for (i in input.length until candidate.length) {
1294+
val positionsAhead = i - input.length
1295+
val decayIndex = minOf(positionsAhead, LOOKAHEAD_DECAY_TABLE.lastIndex)
1296+
val weight = LOOKAHEAD_BASE_WEIGHT * LOOKAHEAD_DECAY_TABLE[decayIndex]
1297+
val transitionScore = scoreCharPair(
1298+
candidate[i - 1],
1299+
candidate[i],
1300+
keyPositions,
1301+
lookaheadTwoSigmaSquared
1302+
)
1303+
lookaheadScoreSum += transitionScore * weight
1304+
lookaheadWeightSum += weight
1305+
}
1306+
1307+
val typedWeight = input.length.toDouble()
1308+
val totalWeight = typedWeight + lookaheadWeightSum
1309+
return (typedScore * typedWeight + lookaheadScoreSum) / totalWeight
1310+
}
1311+
1312+
private fun scoreDirectPrefix(
1313+
input: String,
1314+
candidate: String,
1315+
keyPositions: Map<Char, android.graphics.PointF>,
1316+
twoSigmaSquared: Double
1317+
): Double {
1318+
var totalScore = 0.0
1319+
for (i in input.indices) {
1320+
totalScore += scoreCharPair(input[i], candidate[i], keyPositions, twoSigmaSquared)
1321+
}
1322+
return if (input.isNotEmpty()) totalScore / input.length else 0.0
12571323
}
12581324

12591325
private fun scoreSameLengthAlignment(
@@ -1430,6 +1496,19 @@ constructor(
14301496
const val PROXIMITY_SIGMA_MULTIPLIER = 2.0
14311497
const val MINIMUM_LENGTH_RATIO = 0.6
14321498

1499+
const val LOOKAHEAD_BASE_WEIGHT = 0.5
1500+
const val LOOKAHEAD_DECAY = 0.7
1501+
const val LOOKAHEAD_SIGMA_MULTIPLIER = 4.0
1502+
const val COMPLETION_SPATIAL_WEIGHT = 0.15
1503+
private const val MAX_LOOKAHEAD_POSITIONS = 20
1504+
1505+
@JvmField
1506+
val LOOKAHEAD_DECAY_TABLE = DoubleArray(MAX_LOOKAHEAD_POSITIONS) { i ->
1507+
var result = 1.0
1508+
repeat(i) { result *= LOOKAHEAD_DECAY }
1509+
result
1510+
}
1511+
14331512
const val MAX_PREFIX_COMPLETION_RESULTS = 10
14341513
const val MAX_INPUT_CODEPOINTS = 100
14351514

0 commit comments

Comments
 (0)