From 229f15ee7f9c9247104d0c4eddb4733f1f23e942 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 4 Feb 2023 19:43:31 -0500 Subject: [PATCH] Adds automatic translation to feed and chat. --- app/build.gradle | 15 +- .../amethyst/ui/components/RichTextViewer.kt | 156 +++++++++++++++++- 2 files changed, 162 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cf624e38c..248a9b47b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' // Json Serialization - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2' // link preview implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1' @@ -111,12 +111,21 @@ dependencies { implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version" // For QR generation - implementation "com.google.zxing:core:3.5.0" + implementation 'com.google.zxing:core:3.5.1' implementation "androidx.camera:camera-camera2:1.2.1" implementation 'androidx.camera:camera-lifecycle:1.2.1' implementation 'androidx.camera:camera-view:1.2.1' + + // For QR Scanning implementation 'com.google.mlkit:vision-common:17.3.0' - implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0' + implementation 'com.google.mlkit:barcode-scanning:17.0.3' + + // Use this dependency to use the dynamically downloaded model in Google Play Services + implementation 'com.google.android.gms:play-services-mlkit-language-id:17.0.0' + + // Use this dependency to use the translate text + implementation 'com.google.mlkit:translate:17.0.1' + implementation 'com.google.mlkit:language-id-common:16.1.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 601fb51a5..88ced0693 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -1,30 +1,45 @@ package com.vitorpamplona.amethyst.ui.components +import android.content.res.Resources +import android.util.LruCache import android.util.Patterns import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.core.os.ConfigurationCompat import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.mlkit.nl.languageid.LanguageIdentification +import com.google.mlkit.nl.translate.TranslateLanguage +import com.google.mlkit.nl.translate.Translation +import com.google.mlkit.nl.translate.Translator +import com.google.mlkit.nl.translate.TranslatorOptions import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.ui.note.toShortenHex -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL +import java.util.Locale import java.util.regex.Pattern import nostr.postr.toNpub @@ -50,10 +65,25 @@ fun isValidURL(url: String?): Boolean { @Composable fun RichTextViewer(content: String, tags: List>?, navController: NavController) { - Column(modifier = Modifier.padding(top = 5.dp)) { + val translatedTextState = remember { + mutableStateOf(ResultOrError(content, null, null, null)) + } + var showOriginal by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + LanguageTranslatorService.autoTranslate(content).addOnCompleteListener { task -> + if (task.isSuccessful) { + translatedTextState.value = task.result + } + } + } + + val text = if (showOriginal) content else translatedTextState.value.result + + Column(modifier = Modifier.padding(top = 5.dp)) { // FlowRow doesn't work well with paragraphs. So we need to split them - content.split('\n').forEach { paragraph -> + text?.split('\n')?.forEach { paragraph -> FlowRow() { paragraph.split(' ').forEach { word: String -> @@ -88,7 +118,34 @@ fun RichTextViewer(content: String, tags: List>?, navController: Na } } } + } + val target = translatedTextState.value.targetLang + val source = translatedTextState.value.sourceLang + + if (source != null && target != null) { + if (source != target) { + Row() { + Text( + text = "Auto-translated from ", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + ) + ClickableText( + text = AnnotatedString("${Locale(source).displayName}"), + onClick = { showOriginal = true }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) + ) + Text( + text = " to ", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + ) + ClickableText( + text = AnnotatedString("${Locale(target).displayName}"), + onClick = { showOriginal = false }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) + ) + } + } } } } @@ -165,3 +222,90 @@ fun TagLink(word: String, tags: List>, navController: NavController } } + +class ResultOrError( + var result: String?, + var sourceLang: String?, + var targetLang: String?, + var error: Exception? +) + +object LanguageTranslatorService { + private val languageIdentification = LanguageIdentification.getClient() + + private val languagesSpokenByTheUser = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()).toLanguageTags() + private val usersPreferredLanguage = Locale.getDefault().language + + init { + println("LanguagesAAA: ${languagesSpokenByTheUser}") + } + + private val translators = + object : LruCache(10) { + override fun create(options: TranslatorOptions): Translator { + return Translation.getClient(options) + } + + override fun entryRemoved( + evicted: Boolean, + key: TranslatorOptions, + oldValue: Translator, + newValue: Translator? + ) { + oldValue.close() + } + } + + fun identifyLanguage(text: String): Task { + return languageIdentification.identifyLanguage(text) + } + + fun translate(text: String, source: String, target: String): Task { + val sourceLangCode = TranslateLanguage.fromLanguageTag(source) + val targetLangCode = TranslateLanguage.fromLanguageTag(target) + if (sourceLangCode == null || targetLangCode == null) { + return Tasks.forCanceled() + } + + val options = TranslatorOptions.Builder() + .setSourceLanguage(sourceLangCode) + .setTargetLanguage(targetLangCode) + .build() + + val translator = translators[options] + + return translator.downloadModelIfNeeded().onSuccessTask { + + val tasks = mutableListOf>() + for (paragraph in text.split("\n")) { + tasks.add(translator.translate(paragraph)) + } + + Tasks.whenAll(tasks).continueWith { + val results: MutableList = ArrayList() + for (task in tasks) { + results.add(task.result) + } + ResultOrError(results.joinToString("\n"), source, target, null) + } + } + } + + fun autoTranslate(text: String, target: String): Task { + return identifyLanguage(text).onSuccessTask { + if (it == target) { + Tasks.forCanceled() + } else if (it != "und" && !languagesSpokenByTheUser.contains(it)) { + translate(text, it, target) + } else { + Tasks.forCanceled() + } + } + } + + fun autoTranslate(text: String): Task { + return autoTranslate(text, usersPreferredLanguage) + } +} + +