Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ class ResidualScorer
const val FREQ_TIER_TOP100_BOOST = 1.60f
const val FREQ_TIER_TOP1000_BOOST = 1.35f
const val FREQ_TIER_TOP5000_BOOST = 1.15f
const val SHORT_WORD_FIDELITY_BONUS_4 = 1.08f
const val SHORT_WORD_FIDELITY_BONUS_3 = 1.12f
const val SHORT_WORD_FIDELITY_BONUS_2 = 1.15f
const val SHORT_WORD_FIDELITY_MIN_RATIO_QUALITY = 0.90f
const val SHORT_WORD_SPATIAL_WEIGHT = 0.85f
const val SHORT_WORD_FREQ_WEIGHT = 0.15f
const val SHORT_WORD_SPATIAL_THRESHOLD = 0.60f
const val SHORT_PATH_DAMPENER_MIN_WORD_LENGTH = 4
const val SHORT_PATH_DAMPENER_MIN_EXPECTED_PX = 100f
const val SHORT_PATH_DAMPENER_MILD_RATIO = 0.50f
const val SHORT_PATH_DAMPENER_MODERATE_RATIO = 0.35f
const val SHORT_PATH_DAMPENER_SEVERE_RATIO = 0.20f
const val SHORT_PATH_DAMPENER_MILD_PENALTY = 0.85f
const val SHORT_PATH_DAMPENER_MODERATE_PENALTY = 0.60f
const val SHORT_PATH_DAMPENER_SEVERE_PENALTY = 0.35f

const val REPEATED_LETTER_NO_DWELL_ATTENUATION = 0.40f
const val NON_CONSECUTIVE_REVISIT_MIN_SPAN = 3

}

data class CandidateResult(
Expand Down Expand Up @@ -132,7 +151,7 @@ class ResidualScorer
val isClusteredWord = pathGeometryAnalyzer.isClusteredWord(entry.word, keyPositions)

val pointsPerLetter = signal.rawPointCount.toFloat() / entry.word.length.toFloat()
val optimalRatio = if (entry.word.length <= 3) 3.0f else 4.0f
val optimalRatio = if (entry.word.length <= 3) 6.0f else 4.0f
val ratioQuality = pointsPerLetter / optimalRatio

val spatialScore = calculateGeometricSpatialScore(
Expand Down Expand Up @@ -249,6 +268,9 @@ class ResidualScorer
val pathResidualPenalty = calculatePathResidualPenalty(
signal.pathLength, expectedPathLen,
)
val shortPathDampener = calculateShortPathDampener(
signal.pathLength, expectedPathLen, entry.word.length, isClusteredWord,
)

val passthroughPenalty = calculatePassthroughPenalty(entry.word, signal)

Expand All @@ -260,7 +282,7 @@ class ResidualScorer
traversalPenalty * orderPenalty * vertexLengthPenalty *
pathCoherenceMultiplier * boundsPenalty *
pathLengthMultiplier * pathResidualPenalty *
passthroughPenalty
passthroughPenalty * shortPathDampener

val residual = 1.0f - combinedScore.coerceIn(0f, 1f)

Expand Down Expand Up @@ -450,11 +472,36 @@ class ResidualScorer
letterScore = maxOf(letterScore, TRAVERSAL_FLOOR_SCORE)
}

if (letterIndex > 0 && word[letterIndex] == word[letterIndex - 1]) {
val repeatedLetterBoost = pathGeometryAnalyzer.detectRepeatedLetterSignal(
swipePath, keyPos, reuseLetterPathIndices.lastOrNull() ?: 0, closestPointIndex,
)
letterScore *= (1f + repeatedLetterBoost)
if (letterIndex > 0) {
val isConsecutiveRepeat = word[letterIndex] == word[letterIndex - 1]
var prevOccurrence = -1
if (!isConsecutiveRepeat) {
for (k in letterIndex - 1 downTo 0) {
if (word[k].lowercaseChar() == lowerChar) {
prevOccurrence = k
break
}
}
}
if (isConsecutiveRepeat) {
val repeatedLetterBoost = pathGeometryAnalyzer.detectRepeatedLetterSignal(
swipePath, keyPos, reuseLetterPathIndices.lastOrNull() ?: 0, closestPointIndex,
)
letterScore *= (1f + repeatedLetterBoost)
} else if (prevOccurrence >= 0) {
val prevPathIdx = reuseLetterPathIndices[prevOccurrence]
val pathSpan = kotlin.math.abs(closestPointIndex - prevPathIdx)
val repeatedLetterBoost = pathGeometryAnalyzer.detectRepeatedLetterSignal(
swipePath, keyPos, prevPathIdx, closestPointIndex,
)
if (repeatedLetterBoost > 0f) {
letterScore *= (1f + repeatedLetterBoost)
} else if (pathSpan >= NON_CONSECUTIVE_REVISIT_MIN_SPAN) {
letterScore *= 1f
} else {
letterScore *= REPEATED_LETTER_NO_DWELL_ATTENUATION
}
}
}

reuseLetterPathIndices.add(closestPointIndex)
Expand Down Expand Up @@ -657,15 +704,16 @@ class ResidualScorer
private fun calculateLengthBonus(
wordLength: Int,
ratioQuality: Float,
): Float {
if (ratioQuality < LENGTH_BONUS_MIN_RATIO_QUALITY) return 1.0f
return when {
wordLength >= 8 -> 1.25f
wordLength == 7 -> 1.18f
wordLength == 6 -> 1.12f
wordLength == 5 -> 1.06f
else -> 1.0f
}
): Float = when {
ratioQuality >= SHORT_WORD_FIDELITY_MIN_RATIO_QUALITY && wordLength == 2 -> SHORT_WORD_FIDELITY_BONUS_2
ratioQuality >= SHORT_WORD_FIDELITY_MIN_RATIO_QUALITY && wordLength == 3 -> SHORT_WORD_FIDELITY_BONUS_3
ratioQuality >= SHORT_WORD_FIDELITY_MIN_RATIO_QUALITY && wordLength == 4 -> SHORT_WORD_FIDELITY_BONUS_4
ratioQuality < LENGTH_BONUS_MIN_RATIO_QUALITY -> 1.0f
wordLength >= 8 -> 1.25f
wordLength == 7 -> 1.18f
wordLength == 6 -> 1.12f
wordLength == 5 -> 1.06f
else -> 1.0f
}

private fun calculateBoundsPenalty(
Expand Down Expand Up @@ -731,6 +779,7 @@ class ResidualScorer
}
return when {
entry.word.length == 2 && adjustedSpatialScore > 0.75f -> 0.88f to 0.12f
entry.word.length in 3..4 && adjustedSpatialScore >= SHORT_WORD_SPATIAL_THRESHOLD -> SHORT_WORD_SPATIAL_WEIGHT to SHORT_WORD_FREQ_WEIGHT
frequencyRatio >= 10.0f -> minOf(baselineSpatialWeight, 0.50f) to maxOf(baselineFreqWeight, 0.50f)
frequencyRatio >= 5.0f -> minOf(baselineSpatialWeight, 0.55f) to maxOf(baselineFreqWeight, 0.45f)
frequencyRatio >= 3.0f -> minOf(baselineSpatialWeight, 0.58f) to maxOf(baselineFreqWeight, 0.42f)
Expand Down Expand Up @@ -790,6 +839,24 @@ class ResidualScorer
}
}

private fun calculateShortPathDampener(
physicalPathLength: Float,
expectedWordPathLength: Float,
wordLength: Int,
isClusteredWord: Boolean,
): Float {
if (wordLength < SHORT_PATH_DAMPENER_MIN_WORD_LENGTH) return 1.0f
if (isClusteredWord) return 1.0f
if (expectedWordPathLength < SHORT_PATH_DAMPENER_MIN_EXPECTED_PX) return 1.0f
val ratio = physicalPathLength / expectedWordPathLength
if (ratio >= SHORT_PATH_DAMPENER_MILD_RATIO) return 1.0f
return when {
ratio < SHORT_PATH_DAMPENER_SEVERE_RATIO -> SHORT_PATH_DAMPENER_SEVERE_PENALTY
ratio < SHORT_PATH_DAMPENER_MODERATE_RATIO -> SHORT_PATH_DAMPENER_MODERATE_PENALTY
else -> SHORT_PATH_DAMPENER_MILD_PENALTY
}
}

private fun calculatePassthroughPenalty(word: String, signal: SwipeSignal): Float {
if (signal.passthroughKeys.isEmpty()) return 1.0f
val intentionalKeys = signal.traversedKeys - signal.passthroughKeys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class StreamingScoringEngine
if (currentKeyPositions.isEmpty()) return

val elapsedMs = (System.nanoTime() - gestureStartTimeNanos) / 1_000_000L
val beforeSize = liveCandidates.size

when {
elapsedMs >= TRAVERSAL_PRUNE_MS && tickCount >= 3 -> {
Expand Down Expand Up @@ -421,7 +422,7 @@ class StreamingScoringEngine
private const val START_ANCHOR_RADIUS = 85f
private const val BOUNDS_MARGIN = 60f
private const val TRAVERSAL_RADIUS = 40f
private const val BOUNDS_SAFETY_MARGIN = 1
private const val BOUNDS_SAFETY_MARGIN = 2
private const val TRAVERSAL_MIN_OVERLAP = 0.30f
private const val LIVE_SET_CAPACITY = 50
private const val EXCELLENT_CANDIDATE_THRESHOLD = 0.95f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,13 @@ class SwipeDetector

cachedTransformPoint.set(firstPointX, firstPointY)
_swipeListener?.onSwipeStart(cachedTransformPoint)

val buffered = ringBuffer.snapshot()
for (i in 1 until buffered.size) {
cachedTransformPoint.set(buffered[i].x, buffered[i].y)
_swipeListener?.onSwipeUpdate(cachedTransformPoint)
}
lastUpdateTime = System.currentTimeMillis()
}
}

Expand Down Expand Up @@ -807,7 +814,7 @@ class SwipeDetector
private const val SLOW_MOVEMENT_VELOCITY_THRESHOLD = 0.5f
private const val UI_UPDATE_INTERVAL_MS = 16
private const val TAP_DURATION_THRESHOLD_MS = 350L
private const val MAX_SWIPE_VELOCITY_PX_PER_MS = 5f
private const val MAX_SWIPE_VELOCITY_PX_PER_MS = 9f
private const val PECK_LATE_DISPLACEMENT_RATIO = 0.95f
private const val HIGH_VELOCITY_DISTANCE_MULTIPLIER = 1.5f
private const val GHOST_DENSITY_VELOCITY_GATE = 2.0f
Expand Down
Loading