Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
37 changes: 21 additions & 16 deletions app/src/main/java/com/ethran/notable/io/InboxSyncEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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()) {
Expand Down Expand Up @@ -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<Boolean> { 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<Void?> { 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
}

/**
Expand All @@ -180,7 +185,7 @@ object InboxSyncEngine {
*/
private suspend fun recognizeStrokes(strokes: List<Stroke>): 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down