Skip to content

Commit 20f198a

Browse files
feat(android): add reference parsing, SettingsManager bounds, BPE token cleaning with TDD tests
1 parent c2a30ec commit 20f198a

7 files changed

Lines changed: 222 additions & 417 deletions

File tree

mobile/app/src/main/java/com/bytecats/metanoia/bible/UniversalBibleSearch.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,53 @@ class UniversalBibleSearch {
161161
bySection = bySection.mapKeys { it.key.name }.mapValues { it.value.size }
162162
)
163163
}
164+
165+
/**
166+
* Map common Bible book abbreviations to their canonical full name.
167+
*/
168+
fun resolveBookAbbreviation(input: String): String? {
169+
val clean = input.lowercase().replace(".", "").trim()
170+
val abbreviations = mapOf(
171+
"gen" to "Genesis", "ex" to "Exodus", "exod" to "Exodus", "lev" to "Leviticus",
172+
"num" to "Numbers", "deut" to "Deuteronomy", "dt" to "Deuteronomy", "josh" to "Joshua",
173+
"judg" to "Judges", "ruth" to "Ruth", "1sam" to "1 Samuel", "2sam" to "2 Samuel",
174+
"1kings" to "1 Kings", "2kings" to "2 Kings", "1chron" to "1 Chronicles", "2chron" to "2 Chronicles",
175+
"ezra" to "Ezra", "neh" to "Nehemiah", "esth" to "Esther", "job" to "Job",
176+
"ps" to "Psalms", "pss" to "Psalms", "psalm" to "Psalms", "psalms" to "Psalms",
177+
"prov" to "Proverbs", "eccl" to "Ecclesiastes", "song" to "Song of Solomon", "isa" to "Isaiah",
178+
"jer" to "Jeremiah", "lam" to "Lamentations", "ezek" to "Ezekiel", "dan" to "Daniel",
179+
"hos" to "Hosea", "joel" to "Joel", "amos" to "Amos", "obad" to "Obadiah",
180+
"jonah" to "Jonah", "mic" to "Micah", "nah" to "Nahum", "hab" to "Habakkuk",
181+
"zeph" to "Zephaniah", "hag" to "Haggai", "zech" to "Zechariah", "mal" to "Malachi",
182+
"matt" to "Matthew", "mt" to "Matthew", "mark" to "Mark", "mk" to "Mark",
183+
"luke" to "Luke", "lk" to "Luke", "john" to "John", "jn" to "John",
184+
"acts" to "Acts", "rom" to "Romans", "1cor" to "1 Corinthians", "2cor" to "2 Corinthians",
185+
"gal" to "Galatians", "eph" to "Ephesians", "phil" to "Philippians", "col" to "Colossians",
186+
"1thess" to "1 Thessalonians", "2thess" to "2 Thessalonians", "1tim" to "1 Timothy", "2tim" to "2 Timothy",
187+
"titus" to "Titus", "philem" to "Philemon", "heb" to "Hebrews", "jas" to "James",
188+
"1pet" to "1 Peter", "2pet" to "2 Peter", "1jn" to "1 John", "2jn" to "2 John", "3jn" to "3 John",
189+
"jude" to "Jude", "rev" to "Revelation", "enoch" to "1 Enoch", "1enoch" to "1 Enoch"
190+
)
191+
return abbreviations[clean] ?: BOOKS.find { it.name.equals(input, ignoreCase = true) }?.name
192+
}
193+
194+
/**
195+
* Parse structured reference string like "John 3:16", "1 John 3:16", or "jn 3:16" into components.
196+
*/
197+
fun parseReference(input: String): ParsedReference? {
198+
val regex = Regex("""^((?:\d\s+)?[A-Za-z\s]+)\s+(\d+)(?::(\d+))?$""")
199+
val match = regex.find(input.trim()) ?: return null
200+
val bookRaw = match.groupValues[1].trim()
201+
val chapter = match.groupValues[2].toIntOrNull() ?: return null
202+
val verse = match.groupValues[3].toIntOrNull()
203+
204+
val resolvedBook = resolveBookAbbreviation(bookRaw) ?: bookRaw
205+
return ParsedReference(resolvedBook, chapter, verse)
206+
}
164207
}
165208

209+
data class ParsedReference(val book: String, val chapter: Int, val verse: Int?)
210+
166211
/**
167212
* Statistics about the complete biblical corpus.
168213
*/

mobile/app/src/main/java/com/bytecats/metanoia/settings/SettingsManager.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ class SettingsManager(context: Context) {
5555
set(value) = prefs.edit().putBoolean("speak_defs_on_tap", value).apply()
5656

5757
// --- Reader UI ---
58+
var themeMode: String
59+
get() = prefs.getString("theme_mode", "system") ?: "system"
60+
set(value) = prefs.edit().putString("theme_mode", value).apply()
61+
5862
var englishFontSize: Int
59-
get() = prefs.getInt("english_font_size", 20)
60-
set(value) = prefs.edit().putInt("english_font_size", value).apply()
63+
get() = prefs.getInt("english_font_size", 20).coerceIn(10, 48)
64+
set(value) = prefs.edit().putInt("english_font_size", value.coerceIn(10, 48)).apply()
6165

6266
var ancientFontSize: Int
63-
get() = prefs.getInt("ancient_font_size", 22)
64-
set(value) = prefs.edit().putInt("ancient_font_size", value).apply()
67+
get() = prefs.getInt("ancient_font_size", 22).coerceIn(10, 48)
68+
set(value) = prefs.edit().putInt("ancient_font_size", value.coerceIn(10, 48)).apply()
6569

6670
var hapticFeedbackEnabled: Boolean
6771
get() = prefs.getBoolean("haptic_enabled", true)

mobile/app/src/main/java/com/bytecats/metanoia/tts/BPETokenizer.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,15 @@ class BPETokenizer(
4343
val cacheKey = text
4444
cache[cacheKey]?.let { return it }
4545

46+
// Fallback text cleaning: remove non-printable characters, normalize whitespace
47+
val cleaned = text.replace(Regex("[\\p{C}&&[^\\r\\n\\t]]"), "")
48+
.replace(Regex("\\s+"), " ")
49+
.trim()
50+
4651
// Convert to lowercase (common for TTS)
47-
val normalized = text.lowercase()
52+
val normalized = cleaned.lowercase()
4853

49-
// Basic tokenization (character-based with bigram merging)
54+
// Basic tokenization (character-based with bigram matching)
5055
val tokens = mutableListOf<Int>()
5156
var i = 0
5257

@@ -58,7 +63,8 @@ class BPETokenizer(
5863
val substr = normalized.substring(i, i + len)
5964
val tokenId = vocab[substr]
6065

61-
if (tokenId != null) {
66+
// Vocabulary boundary checks - ensure tokenId is valid
67+
if (tokenId != null && tokenId >= 0) {
6268
tokens.add(tokenId)
6369
i += len
6470
matched = true
@@ -68,7 +74,7 @@ class BPETokenizer(
6874

6975
if (!matched) {
7076
// Unknown character, use UNK token
71-
vocab[UNK_TOKEN]?.let { tokens.add(it) }
77+
vocab[UNK_TOKEN]?.let { if (it >= 0) tokens.add(it) }
7278
i++
7379
}
7480
}

mobile/app/src/main/java/com/bytecats/metanoia/tts/Qwen3TTSEngine.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,17 @@ class Qwen3TTSEngine(
119119
temperature: Float = 0.5f,
120120
cfgScale: Float = 2.0f
121121
): FloatArray {
122-
val tokenIds = text.map { it.code % config.vocabSize }
123-
val maxTokens = config.maxTokens(text.length)
122+
// Fallback text cleaning
123+
val cleanedText = text.replace(Regex("[\\p{C}&&[^\\r\\n\\t]]"), "")
124+
.replace(Regex("\\s+"), " ")
125+
.trim()
126+
127+
// Resilient tokenization with boundary checks
128+
val tokenIds = cleanedText.map { char ->
129+
val code = char.code
130+
if (code >= 0) code % config.vocabSize else 0
131+
}
132+
val maxTokens = config.maxTokens(cleanedText.length)
124133

125134
// Create embeddings (matching Zig's embedding lookup)
126135
val embeddings = createEmbeddings(tokenIds)

0 commit comments

Comments
 (0)