diff --git a/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt b/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt index c65b5cf6..ab04594b 100644 --- a/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt +++ b/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt @@ -51,6 +51,7 @@ data class AppSettings( // Inbox Capture val obsidianInboxPath: String = "Documents/primary/inbox", + val hwrLanguage: String = "en-US", // Debug val showWelcome: Boolean = true, diff --git a/app/src/main/java/com/ethran/notable/io/InboxSyncEngine.kt b/app/src/main/java/com/ethran/notable/io/InboxSyncEngine.kt index d853ca95..d6ec2e4e 100644 --- a/app/src/main/java/com/ethran/notable/io/InboxSyncEngine.kt +++ b/app/src/main/java/com/ethran/notable/io/InboxSyncEngine.kt @@ -29,15 +29,9 @@ private val log = ShipBook.getLogger("InboxSyncEngine") object InboxSyncEngine { - private val modelIdentifier = - DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US") - private val model = - modelIdentifier?.let { DigitalInkRecognitionModel.builder(it).build() } - private val recognizer = model?.let { - DigitalInkRecognition.getClient( - DigitalInkRecognizerOptions.builder(it).build() - ) - } + @Volatile private var currentLanguage: String? = null + private var recognizer: com.google.mlkit.vision.digitalink.recognition.DigitalInkRecognizer? = null + private var currentModel: DigitalInkRecognitionModel? = null /** * Sync an inbox page to Obsidian. Tags come from the UI (pill selection), @@ -61,7 +55,7 @@ object InboxSyncEngine { return } - ensureModelDownloaded() + ensureRecognizer(GlobalAppSettings.current.hwrLanguage) // 1. Recognize ALL strokes together to preserve natural text flow var fullText = if (allStrokes.isNotEmpty()) { @@ -153,25 +147,36 @@ object InboxSyncEngine { log.i("Inbox sync complete for page $pageId") } - private suspend fun ensureModelDownloaded() { - val m = model ?: throw IllegalStateException("ML Kit model identifier not found") + private suspend fun ensureRecognizer(language: String) { + if (language == currentLanguage && recognizer != null) return + + val modelIdentifier = DigitalInkRecognitionModelIdentifier.fromLanguageTag(language) + ?: throw IllegalStateException("Unsupported HWR language: $language") + val model = DigitalInkRecognitionModel.builder(modelIdentifier).build() val manager = RemoteModelManager.getInstance() val isDownloaded = suspendCancellableCoroutine { cont -> - manager.isModelDownloaded(m) + manager.isModelDownloaded(model) .addOnSuccessListener { cont.resume(it) } .addOnFailureListener { cont.resumeWithException(it) } } if (!isDownloaded) { - log.i("Downloading ML Kit model...") + log.i("Downloading ML Kit model for $language...") suspendCancellableCoroutine { cont -> - manager.download(m, DownloadConditions.Builder().build()) + manager.download(model, DownloadConditions.Builder().build()) .addOnSuccessListener { cont.resume(null) } .addOnFailureListener { cont.resumeWithException(it) } } log.i("Model downloaded") } + + recognizer?.close() + currentModel = model + recognizer = DigitalInkRecognition.getClient( + DigitalInkRecognizerOptions.builder(model).build() + ) + currentLanguage = language } /** @@ -180,7 +185,7 @@ object InboxSyncEngine { */ private suspend fun recognizeStrokes(strokes: List): String { val rec = recognizer - ?: throw IllegalStateException("ML Kit recognizer not initialized") + ?: throw IllegalStateException("ML Kit recognizer not initialized — call ensureRecognizer first") val lines = segmentIntoLines(strokes) log.i("Segmented ${strokes.size} strokes into ${lines.size} lines") diff --git a/app/src/main/java/com/ethran/notable/ui/components/GeneralSettings.kt b/app/src/main/java/com/ethran/notable/ui/components/GeneralSettings.kt index cb42ea3d..779147dd 100644 --- a/app/src/main/java/com/ethran/notable/ui/components/GeneralSettings.kt +++ b/app/src/main/java/com/ethran/notable/ui/components/GeneralSettings.kt @@ -247,6 +247,41 @@ private fun InboxCaptureSettings( style = MaterialTheme.typography.caption, color = Color.Gray ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + "Handwriting recognition language", + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(4.dp)) + + val hwrLanguages = listOf( + "en-US" to "English (US)", + "en-GB" to "English (UK)", + "fr-FR" to "French", + "de-DE" to "German", + "es-ES" to "Spanish", + "it-IT" to "Italian", + "pt-BR" to "Portuguese (Brazil)", + "pt-PT" to "Portuguese (Portugal)", + "nl-NL" to "Dutch", + "ru-RU" to "Russian", + "zh-CN" to "Chinese (Simplified)", + "zh-TW" to "Chinese (Traditional)", + "ja-JP" to "Japanese", + "ko-KR" to "Korean", + "ar-SA" to "Arabic", + ) + SelectorRow( + label = "", + options = hwrLanguages, + value = settings.hwrLanguage, + onValueChange = { onSettingsChange(settings.copy(hwrLanguage = it)) }, + labelMaxLines = 1 + ) } SettingsDivider()