tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Definitions

⎇ 8380e44a0947bafb169773353ce2b2036aff9e70 keyboard/src/main/java/com/rizoma/trazo/data/SuggestionEngine.kt 92 lines · 3.1 KB History raw

Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2) 8380e44a0 · sergio ·

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}