Skip to content
Closed
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
129 changes: 129 additions & 0 deletions app/src/main/assets/common/predictions/en_next_words.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"fallback": [
"the",
"to",
"and",
"you",
"it",
"that",
"for",
"with",
"this",
"have",
"your",
"from"
],
"bigrams": {
"a": ["lot", "little", "few", "bit", "good", "great", "new"],
"about": ["the", "this", "that", "it", "what", "how"],
"all": ["the", "of", "you", "this", "that"],
"am": ["not", "going", "trying", "glad", "sure"],
"and": ["the", "i", "you", "we", "it", "then", "that"],
"are": ["you", "we", "they", "there", "not", "going"],
"as": ["a", "well", "soon", "long", "much", "if"],
"at": ["the", "all", "least", "first", "home", "work"],
"be": ["able", "there", "sure", "back", "careful", "good"],
"been": ["a", "there", "trying", "working", "doing"],
"but": ["i", "it", "you", "we", "the", "that"],
"by": ["the", "far", "then", "default", "my"],
"can": ["you", "we", "i", "do", "get", "see", "help"],
"could": ["you", "we", "be", "do", "get", "try"],
"did": ["you", "we", "they", "it", "not"],
"do": ["you", "we", "i", "not", "that", "this"],
"does": ["it", "that", "this", "not"],
"for": ["the", "you", "me", "this", "that", "now"],
"from": ["the", "you", "my", "there", "here"],
"get": ["the", "it", "back", "started", "to", "a"],
"good": ["luck", "idea", "morning", "night", "to"],
"got": ["it", "to", "the", "a", "some"],
"had": ["to", "a", "been", "no", "the"],
"has": ["been", "to", "a", "the", "no"],
"have": ["a", "to", "been", "the", "you", "we"],
"he": ["was", "is", "said", "has", "will"],
"hello": ["there", "everyone", "again"],
"here": ["is", "are", "we", "you", "it"],
"how": ["are", "do", "to", "can", "would", "much", "long"],
"i": ["am", "have", "think", "need", "want", "will", "can", "was"],
"if": ["you", "we", "it", "that", "there"],
"in": ["the", "a", "my", "this", "that"],
"is": ["the", "a", "it", "not", "there", "that"],
"it": ["is", "was", "would", "will", "has", "can", "should"],
"just": ["a", "the", "to", "let", "be", "wanted"],
"know": ["if", "what", "that", "how", "you"],
"let": ["me", "us", "it", "them"],
"like": ["to", "a", "the", "this", "that", "you"],
"me": ["know", "if", "when", "what", "and"],
"my": ["phone", "keyboard", "app", "friend", "device"],
"need": ["to", "a", "the", "help", "more"],
"new": ["one", "version", "build", "feature", "keyboard"],
"not": ["sure", "working", "bad", "the", "a"],
"of": ["the", "this", "that", "it", "course"],
"on": ["the", "my", "this", "that", "your"],
"or": ["the", "you", "we", "not", "maybe"],
"please": ["let", "check", "send", "help"],
"should": ["be", "have", "work", "not"],
"so": ["i", "you", "we", "it", "that"],
"thanks": ["for", "again", "so", "you"],
"that": ["is", "was", "would", "should", "you"],
"the": ["same", "best", "app", "keyboard", "phone", "issue", "other"],
"there": ["is", "are", "was", "will", "should"],
"they": ["are", "have", "will", "can", "were"],
"this": ["is", "was", "would", "should", "one"],
"to": ["the", "be", "do", "get", "make", "use", "see"],
"want": ["to", "a", "the", "it", "more"],
"was": ["the", "a", "not", "going", "just"],
"we": ["can", "should", "are", "need", "have", "will"],
"what": ["is", "do", "are", "you", "we"],
"when": ["you", "we", "it", "the", "i"],
"where": ["is", "are", "do", "can"],
"why": ["is", "does", "do", "are"],
"will": ["be", "have", "not", "the", "it"],
"with": ["the", "this", "that", "you", "a"],
"would": ["be", "like", "have", "you", "it"],
"you": ["are", "can", "should", "will", "have", "need", "know", "want"],
"your": ["phone", "keyboard", "app", "settings", "device"]
},
"trigrams": {
"a lot": ["of"],
"all of": ["the", "this", "that"],
"can i": ["get", "use", "have", "see"],
"can you": ["help", "check", "send", "see", "try"],
"do you": ["want", "know", "think", "have", "need"],
"for the": ["same", "keyboard", "app", "phone"],
"how are": ["you", "we", "things"],
"how do": ["you", "we", "i"],
"i am": ["not", "going", "trying", "glad", "sure"],
"i can": ["do", "see", "try", "help"],
"i did": ["not", "see", "try"],
"i do": ["not", "think", "want"],
"i had": ["to", "a", "been"],
"i have": ["to", "a", "been", "no"],
"i just": ["wanted", "need", "have"],
"i know": ["that", "what", "you"],
"i need": ["to", "a", "the", "help"],
"i think": ["it", "we", "you", "that"],
"i want": ["to", "a", "the", "it"],
"i will": ["be", "have", "try", "check"],
"if you": ["want", "need", "can", "have"],
"in the": ["same", "app", "keyboard", "settings"],
"is the": ["same", "best", "keyboard", "app"],
"it is": ["not", "the", "a", "just"],
"let me": ["know", "check", "see", "try"],
"need to": ["be", "do", "get", "make"],
"one of": ["the", "my", "your"],
"please let": ["me"],
"so i": ["can", "have", "know"],
"thank you": ["for", "so", "again"],
"there is": ["a", "no", "the", "nothing"],
"this is": ["the", "a", "not", "what"],
"to the": ["same", "app", "keyboard"],
"want to": ["be", "do", "get", "make"],
"we are": ["not", "going", "trying", "ready"],
"we can": ["do", "try", "see", "make"],
"what do": ["you", "we", "i"],
"you are": ["the", "not", "going"],
"you can": ["do", "see", "try", "use"],
"you have": ["to", "a", "the"],
"you need": ["to", "a", "the"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package it.palsoftware.pastiera.core.suggestions

import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap

class BundledPhrasePredictor(
context: Context,
private val locale: Locale = Locale.getDefault(),
private val isValidWord: ((String) -> Boolean)? = null
) {
private val appContext = context.applicationContext

companion object {
private const val PREDICTION_ASSET_PREFIX = "common/predictions"
private val normalizeRegex = Regex("[^\\p{L}']")
private val cache = ConcurrentHashMap<String, PhraseData>()
private val emptyPhraseData = PhraseData(emptyMap(), emptyMap(), emptyList())
}

fun predictContextual(previousWord: String, previousPreviousWord: String?, limit: Int): List<String> {
if (limit <= 0) return emptyList()
val data = loadData()
if (data === emptyPhraseData) return emptyList()

val previous = normalize(previousWord) ?: return emptyList()
val previousPrevious = normalize(previousPreviousWord)
val results = linkedSetOf<String>()

if (previousPrevious != null) {
data.trigrams["$previousPrevious $previous"]
?.asSequence()
?.filter(::isPlausibleWord)
?.forEach(results::add)
}
data.bigrams[previous]
?.asSequence()
?.filter(::isPlausibleWord)
?.forEach(results::add)

return results.take(limit)
}

fun fallbackWords(limit: Int): List<String> {
if (limit <= 0) return emptyList()
return loadData().fallback
.asSequence()
.filter(::isPlausibleWord)
.take(limit)
.toList()
}

private fun loadData(): PhraseData {
val language = locale.language.lowercase(Locale.ROOT)
if (language != "en") return emptyPhraseData
return cache.getOrPut(language) {
val path = "$PREDICTION_ASSET_PREFIX/${language}_next_words.json"
runCatching {
appContext.assets.open(path).bufferedReader().use { reader ->
val root = JSONObject(reader.readText())
PhraseData(
bigrams = parseMap(root.optJSONObject("bigrams")),
trigrams = parseMap(root.optJSONObject("trigrams")),
fallback = parseArray(root.optJSONArray("fallback"))
)
}
}.getOrElse {
emptyPhraseData
}
}
}

private fun parseMap(source: JSONObject?): Map<String, List<String>> {
if (source == null) return emptyMap()
return source.keys().asSequence().associateWith { key ->
parseArray(source.optJSONArray(key))
}
}

private fun parseArray(source: JSONArray?): List<String> {
if (source == null) return emptyList()
return buildList {
for (index in 0 until source.length()) {
source.optString(index)
.takeIf { it.isNotBlank() }
?.let(::add)
}
}
}

private fun normalize(word: String?): String? {
if (word.isNullOrBlank()) return null
val normalized = word
.replace('’', '\'')
.replace('‘', '\'')
.replace('ʼ', '\'')
.lowercase(locale)
.replace(normalizeRegex, "")
.trim('\'')
return normalized.takeIf { it.isNotBlank() }
}

private fun isPlausibleWord(word: String): Boolean {
val normalized = normalize(word) ?: return false
if (normalized.length < 2) return false
return isValidWord?.invoke(normalized) ?: true
}

private data class PhraseData(
val bigrams: Map<String, List<String>>,
val trigrams: Map<String, List<String>>,
val fallback: List<String>
)
}
Loading