mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-06-28 19:50:55 +02:00
Adds automatic translation to feed and chat.
This commit is contained in:
parent
d168a6c861
commit
229f15ee7f
@ -85,7 +85,7 @@ dependencies {
|
|||||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||||
|
|
||||||
// Json Serialization
|
// 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
|
// link preview
|
||||||
implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
||||||
@ -111,12 +111,21 @@ dependencies {
|
|||||||
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"
|
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"
|
||||||
|
|
||||||
// For QR generation
|
// 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-camera2:1.2.1"
|
||||||
implementation 'androidx.camera:camera-lifecycle:1.2.1'
|
implementation 'androidx.camera:camera-lifecycle:1.2.1'
|
||||||
implementation 'androidx.camera:camera-view: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.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'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
@ -1,30 +1,45 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.util.LruCache
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
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.lnurl.LnInvoiceUtil
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toNote
|
import com.vitorpamplona.amethyst.model.toNote
|
||||||
import com.vitorpamplona.amethyst.service.Nip19
|
import com.vitorpamplona.amethyst.service.Nip19
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.util.Locale
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
|
|
||||||
@ -50,10 +65,25 @@ fun isValidURL(url: String?): Boolean {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextViewer(content: String, tags: List<List<String>>?, navController: NavController) {
|
fun RichTextViewer(content: String, tags: List<List<String>>?, 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
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
content.split('\n').forEach { paragraph ->
|
text?.split('\n')?.forEach { paragraph ->
|
||||||
|
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
paragraph.split(' ').forEach { word: String ->
|
paragraph.split(' ').forEach { word: String ->
|
||||||
@ -88,7 +118,34 @@ fun RichTextViewer(content: String, tags: List<List<String>>?, 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<List<String>>, 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<TranslatorOptions, Translator>(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<String> {
|
||||||
|
return languageIdentification.identifyLanguage(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(text: String, source: String, target: String): Task<ResultOrError> {
|
||||||
|
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<Task<String>>()
|
||||||
|
for (paragraph in text.split("\n")) {
|
||||||
|
tasks.add(translator.translate(paragraph))
|
||||||
|
}
|
||||||
|
|
||||||
|
Tasks.whenAll(tasks).continueWith {
|
||||||
|
val results: MutableList<String> = ArrayList()
|
||||||
|
for (task in tasks) {
|
||||||
|
results.add(task.result)
|
||||||
|
}
|
||||||
|
ResultOrError(results.joinToString("\n"), source, target, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun autoTranslate(text: String, target: String): Task<ResultOrError> {
|
||||||
|
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<ResultOrError> {
|
||||||
|
return autoTranslate(text, usersPreferredLanguage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user