⎇ 8380e44a0947bafb169773353ce2b2036aff9e70
keyboard/src/main/java/com/rizoma/trazo/data/SuggestionEngine.kt
History
raw
Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2)
1package com.rizoma.trazo.data
2
3import android.content.Context
4import androidx.datastore.core.DataStore
5import androidx.datastore.preferences.core.Preferences
6import androidx.datastore.preferences.core.edit
7import androidx.datastore.preferences.core.intPreferencesKey
8import androidx.datastore.preferences.preferencesDataStore
9import kotlinx.coroutines.CoroutineScope
10import kotlinx.coroutines.Dispatchers
11import kotlinx.coroutines.flow.first
12import kotlinx.coroutines.launch
13import java.util.concurrent.ConcurrentHashMap
14
15private val Context.trazoLearnedStore: DataStore<Preferences> by preferencesDataStore(name = "trazo_learned")
16
17/**
18 * Predicción por prefijo. Mezcla un diccionario base (assets) con las palabras que el
19 * usuario escribe (aprendidas y persistidas). El peso ordena las sugerencias.
20 */
21class SuggestionEngine(context: Context, private val scope: CoroutineScope) {
22
23 private val appContext = context.applicationContext
24 private val store = appContext.trazoLearnedStore
25 private val freq = ConcurrentHashMap<String, Int>()
26
27 init {
28 scope.launch(Dispatchers.IO) {
29 loadBase()
30 loadLearned()
31 }
32 }
33
34 private fun loadBase() {
35 runCatching {
36 appContext.assets.open("dict_es.txt").bufferedReader().useLines { lines ->
37 var i = 0
38 lines.forEach { line ->
39 val w = line.trim().lowercase()
40 if (w.isNotEmpty()) {
41 freq.merge(w, maxOf(1, BASE_TOP - i)) { a, b -> maxOf(a, b) }
42 i++
43 }
44 }
45 }
46 }
47 }
48
49 private suspend fun loadLearned() {
50 runCatching {
51 store.data.first().asMap().forEach { (key, value) ->
52 if (key.name.startsWith(W) && value is Int) {
53 val w = key.name.removePrefix(W)
54 freq.merge(w, value * LEARN_INC) { a, b -> a + b }
55 }
56 }
57 }
58 }
59
60 /** Completa el prefijo con las palabras de mayor peso (más cortas primero a igual peso). */
61 fun suggest(prefix: String, limit: Int = 3): List<String> {
62 val p = prefix.trim().lowercase()
63 if (p.isEmpty()) return emptyList()
64 return freq.entries.asSequence()
65 .filter { it.key.length > p.length && it.key.startsWith(p) }
66 .sortedWith(compareByDescending<Map.Entry<String, Int>> { it.value }.thenBy { it.key.length })
67 .map { it.key }
68 .take(limit)
69 .toList()
70 }
71
72 /** Aprende/refuerza una palabra escrita por el usuario. */
73 fun learn(raw: String) {
74 val w = raw.trim().lowercase()
75 if (w.length < 2 || !w.all { it.isLetter() }) return
76 freq.merge(w, LEARN_INC) { a, _ -> a + LEARN_INC }
77 scope.launch(Dispatchers.IO) {
78 runCatching {
79 store.edit { prefs ->
80 val key = intPreferencesKey(W + w)
81 prefs[key] = (prefs[key] ?: 0) + 1
82 }
83 }
84 }
85 }
86
87 private companion object {
88 const val BASE_TOP = 5000
89 const val LEARN_INC = 400
90 const val W = "w:"
91 }
92}