mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Add resource importing (dict/voice/transformer)
This commit is contained in:
parent
bec40d167c
commit
ff29bf5802
@ -114,6 +114,33 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".uix.ImportResourceActivity"
|
||||
android:label="Import dictionary or model"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="false"
|
||||
android:configChanges="orientation|screenLayout|screenSize|keyboardHidden|keyboard|uiMode|density"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:scheme="content"/>
|
||||
<data android:host="*"/>
|
||||
|
||||
<data android:mimeType="application/octet-stream"/>
|
||||
<data android:mimeType="application/dict"/>
|
||||
<data android:mimeType="application/bin"/>
|
||||
<data android:mimeType="application/gguf"/>
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".uix.voiceinput.downloader.DownloadActivity"
|
||||
android:exported="false"
|
||||
|
@ -41,4 +41,5 @@
|
||||
<string name="blacklist_from_suggestions">Blacklist \"%1$s\" from being suggested?</string>
|
||||
<string name="disable_emoji">Disable emoji suggestions</string>
|
||||
<string name="try_typing">Try typing here…</string>
|
||||
<string name="externally_imported_model">Externally imported model</string>
|
||||
</resources>
|
@ -240,7 +240,7 @@ final public class BinaryDictionaryGetter {
|
||||
* @return The list of addresses of valid dictionary files, or null.
|
||||
*/
|
||||
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
|
||||
final Context context, boolean notifyDictionaryPackForUpdates) {
|
||||
final Context context, boolean notifyDictionaryPackForUpdates, boolean fallback) {
|
||||
if (notifyDictionaryPackForUpdates) {
|
||||
final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
|
||||
context, locale);
|
||||
@ -280,9 +280,12 @@ final public class BinaryDictionaryGetter {
|
||||
if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
|
||||
final int fallbackResId =
|
||||
DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
|
||||
final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
|
||||
if (null != fallbackAsset) {
|
||||
fileList.add(fallbackAsset);
|
||||
|
||||
if(fallback || fallbackResId != R.raw.main) {
|
||||
final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
|
||||
if (null != fallbackAsset) {
|
||||
fileList.add(fallbackAsset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import android.content.Context;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import org.futo.inputmethod.latin.uix.ResourceHelper;
|
||||
import org.futo.inputmethod.latin.utils.DictionaryInfoUtils;
|
||||
|
||||
import java.io.File;
|
||||
@ -52,19 +53,26 @@ public final class DictionaryFactory {
|
||||
}
|
||||
|
||||
final LinkedList<Dictionary> dictList = new LinkedList<>();
|
||||
final ArrayList<AssetFileAddress> assetFileList =
|
||||
BinaryDictionaryGetter.getDictionaryFiles(locale, context, true);
|
||||
if (null != assetFileList) {
|
||||
for (final AssetFileAddress f : assetFileList) {
|
||||
final ReadOnlyBinaryDictionary readOnlyBinaryDictionary =
|
||||
new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength,
|
||||
false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
|
||||
if (readOnlyBinaryDictionary.isValidDictionary()) {
|
||||
dictList.add(readOnlyBinaryDictionary);
|
||||
} else {
|
||||
readOnlyBinaryDictionary.close();
|
||||
// Prevent this dictionary to do any further harm.
|
||||
killDictionary(context, f);
|
||||
|
||||
ReadOnlyBinaryDictionary customDict =
|
||||
ResourceHelper.INSTANCE.tryOpeningCustomMainDictionaryForLocale(context, locale);
|
||||
if(customDict != null) {
|
||||
dictList.add(customDict);
|
||||
} else {
|
||||
final ArrayList<AssetFileAddress> assetFileList =
|
||||
BinaryDictionaryGetter.getDictionaryFiles(locale, context, true, true);
|
||||
if (null != assetFileList) {
|
||||
for (final AssetFileAddress f : assetFileList) {
|
||||
final ReadOnlyBinaryDictionary readOnlyBinaryDictionary =
|
||||
new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength,
|
||||
false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
|
||||
if (readOnlyBinaryDictionary.isValidDictionary()) {
|
||||
dictList.add(readOnlyBinaryDictionary);
|
||||
} else {
|
||||
readOnlyBinaryDictionary.close();
|
||||
// Prevent this dictionary to do any further harm.
|
||||
killDictionary(context, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2049,6 +2049,10 @@ public class LatinIMELegacy implements KeyboardActionListener,
|
||||
return ((LatinIME)(mInputMethodService)).getLanguageModelFacilitator();
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return mLocale;
|
||||
}
|
||||
|
||||
public void onCodePointDeleted(String textBeforeCursor) {
|
||||
((LatinIME)(mInputMethodService)).onEmojiDeleted(textBeforeCursor);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import java.util.Locale
|
||||
|
||||
interface ActionInputTransaction {
|
||||
fun updatePartial(text: String)
|
||||
@ -38,6 +39,7 @@ interface KeyboardManagerForAction {
|
||||
fun cursorRight(steps: Int, stepOverWords: Boolean, select: Boolean)
|
||||
|
||||
fun performHapticAndAudioFeedback(code: Int, view: View)
|
||||
fun getActiveLocale(): Locale
|
||||
}
|
||||
|
||||
interface ActionWindow {
|
||||
|
@ -0,0 +1,326 @@
|
||||
package org.futo.inputmethod.latin.uix
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodSubtype
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.futo.inputmethod.latin.Dictionary
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.ReadOnlyBinaryDictionary
|
||||
import org.futo.inputmethod.latin.RichInputMethodManager
|
||||
import org.futo.inputmethod.latin.uix.settings.DataStoreItem
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItem
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||
import org.futo.inputmethod.latin.uix.theme.StatusBarColorSetter
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
|
||||
import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils
|
||||
import org.futo.inputmethod.latin.xlm.ModelPaths
|
||||
import org.futo.voiceinput.shared.BUILTIN_ENGLISH_MODEL
|
||||
import org.futo.voiceinput.shared.types.ModelFileFile
|
||||
import org.futo.voiceinput.shared.types.ModelLoader
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
data class InputLanguage(
|
||||
val tag: String,
|
||||
val name: String,
|
||||
val inputMethodSubtype: InputMethodSubtype
|
||||
)
|
||||
|
||||
fun getActiveLanguages(context: Context): List<InputLanguage> {
|
||||
RichInputMethodManager.init(context)
|
||||
|
||||
return RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypeList(true).map {
|
||||
val name = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(it)
|
||||
|
||||
InputLanguage(it.locale, name, it)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
|
||||
fun FileKind.preferencesKeyFor(locale: String): Preferences.Key<String> {
|
||||
assert(this != FileKind.Invalid)
|
||||
return stringPreferencesKey("resource_${name}_${locale}")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun resourceOption(language: InputMethodSubtype, kind: FileKind): DataStoreItem<String> {
|
||||
return useDataStore(key = kind.preferencesKeyFor(language.locale), default = "")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImportScreen(fileKind: FileKind, file: String?, onApply: (FileKind, InputMethodSubtype) -> Unit, onCancel: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val importing = remember { mutableStateOf(false) }
|
||||
ScrollableList {
|
||||
ScreenTitle(title = "Resource Importer")
|
||||
|
||||
if(fileKind == FileKind.Invalid) {
|
||||
Text("This file does not appear to be a dictionary, voice input or transformer model. It may be an invalid file or corrupted. Please try a different file.")
|
||||
|
||||
NavigationItem(
|
||||
title = "Close",
|
||||
style = NavigationItemStyle.MiscNoArrow,
|
||||
navigate = {
|
||||
onCancel()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Text("You are importing a ${fileKind.youAreImporting()}.", modifier = Modifier.padding(8.dp))
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if(importing.value) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp)) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
"Which language would you like to set the ${fileKind.youAreImporting()} for?",
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
|
||||
getActiveLanguages(context).forEach {
|
||||
NavigationItem(
|
||||
title = "${it.name} (${it.tag})",
|
||||
style = NavigationItemStyle.MiscNoArrow,
|
||||
navigate = {
|
||||
importing.value = true
|
||||
onApply(fileKind, it.inputMethodSubtype)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class FileKind {
|
||||
VoiceInput,
|
||||
Transformer,
|
||||
Dictionary,
|
||||
Invalid
|
||||
}
|
||||
|
||||
fun FileKind.youAreImporting(): String {
|
||||
return when(this) {
|
||||
FileKind.VoiceInput -> "voice input model"
|
||||
FileKind.Transformer -> "transformer model"
|
||||
FileKind.Dictionary -> "dictionary"
|
||||
FileKind.Invalid -> "invalid file"
|
||||
}
|
||||
}
|
||||
|
||||
fun FileKind.extension(): String {
|
||||
return when(this) {
|
||||
FileKind.VoiceInput -> ".bin"
|
||||
FileKind.Transformer -> ".gguf"
|
||||
FileKind.Dictionary -> ".dict"
|
||||
FileKind.Invalid -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun determineFileKind(context: Context, file: Uri): FileKind {
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
return contentResolver.openInputStream(file)?.use { inputStream ->
|
||||
val array = ByteArray(4)
|
||||
inputStream.read(array)
|
||||
|
||||
val voiceInputMagic = 0x6c6d6767.toUInt()
|
||||
val transformerMagic = 0x47475546.toUInt()
|
||||
val dictionaryMagic = 0x9bc13afe.toUInt()
|
||||
|
||||
val magic = ByteBuffer.wrap(array).getInt().toUInt()
|
||||
|
||||
when(magic) {
|
||||
voiceInputMagic -> FileKind.VoiceInput
|
||||
transformerMagic -> FileKind.Transformer
|
||||
dictionaryMagic -> FileKind.Dictionary
|
||||
else -> FileKind.Invalid
|
||||
}
|
||||
} ?: FileKind.Invalid
|
||||
}
|
||||
|
||||
object ResourceHelper {
|
||||
suspend fun findFileForKind(context: Context, locale: Locale, kind: FileKind): File? {
|
||||
val keysToTry = listOf(
|
||||
locale.language,
|
||||
"${locale.language}_${locale.country}",
|
||||
"${locale.language.lowercase()}_${locale.country.uppercase()}",
|
||||
)
|
||||
|
||||
val settingValue: String = keysToTry.firstNotNullOfOrNull { key ->
|
||||
context.getSetting(kind.preferencesKeyFor(key), "").ifEmpty { null }
|
||||
} ?: return null
|
||||
|
||||
val file = File(context.getExternalFilesDir(null), settingValue)
|
||||
|
||||
if(!file.exists()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fun tryFindingVoiceInputModelForLocale(context: Context, locale: Locale): ModelLoader? {
|
||||
val file = runBlocking { findFileForKind(context, locale, FileKind.VoiceInput) }
|
||||
?: return if(locale.language == "en") {
|
||||
BUILTIN_ENGLISH_MODEL
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return ModelFileFile(R.string.externally_imported_model, file)
|
||||
}
|
||||
|
||||
fun tryOpeningCustomMainDictionaryForLocale(context: Context, locale: Locale): ReadOnlyBinaryDictionary? {
|
||||
val file = runBlocking { findFileForKind(context, locale, FileKind.Dictionary) } ?: return null
|
||||
|
||||
return ReadOnlyBinaryDictionary(
|
||||
file.absolutePath,
|
||||
0,
|
||||
file.length(),
|
||||
false,
|
||||
locale,
|
||||
Dictionary.TYPE_MAIN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ImportResourceActivity : ComponentActivity() {
|
||||
private val themeOption: MutableState<ThemeOption?> = mutableStateOf(null)
|
||||
private val fileBeingImported: MutableState<String?> = mutableStateOf(null)
|
||||
private val fileKind: MutableState<FileKind> = mutableStateOf(FileKind.Invalid)
|
||||
private var uri: Uri? = null
|
||||
|
||||
private fun applySetting(fileKind: FileKind, inputMethodSubtype: InputMethodSubtype) {
|
||||
val outputFileName = "${fileKind.name.lowercase()}_${inputMethodSubtype.locale}${fileKind.extension()}"
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
// This is a special case for now
|
||||
if (fileKind == FileKind.Transformer) {
|
||||
// 1. Copy file
|
||||
val contentResolver = applicationContext.contentResolver
|
||||
val outDirectory = ModelPaths.getModelDirectory(applicationContext)
|
||||
val outputFile = File(outDirectory, outputFileName)
|
||||
contentResolver.openInputStream(uri!!)!!.use { inputStream ->
|
||||
outputFile.outputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream, 1024)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update reference
|
||||
val language = inputMethodSubtype.locale.split("_").first()
|
||||
ModelPaths.updateModelOption(applicationContext, language, outputFile)
|
||||
} else {
|
||||
// 1. Copy file
|
||||
val contentResolver = applicationContext.contentResolver
|
||||
contentResolver.openInputStream(uri!!)!!.use { inputStream ->
|
||||
val outputFile =
|
||||
File(applicationContext.getExternalFilesDir(null), outputFileName)
|
||||
|
||||
outputFile.outputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream, 1024)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update reference
|
||||
val key = fileKind.preferencesKeyFor(inputMethodSubtype.locale)
|
||||
applicationContext.setSetting(key, outputFileName)
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContent() {
|
||||
setContent {
|
||||
themeOption.value?.let { themeOption ->
|
||||
val themeIdx = useDataStore(key = THEME_KEY.key, default = themeOption.key)
|
||||
val theme: ThemeOption = ThemeOptions[themeIdx.value] ?: themeOption
|
||||
UixThemeWrapper(theme.obtainColors(LocalContext.current)) {
|
||||
StatusBarColorSetter()
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ImportScreen(
|
||||
fileKind = fileKind.value,
|
||||
file = fileBeingImported.value,
|
||||
onApply = { fileKind, inputMethodSubtype -> applySetting(fileKind, inputMethodSubtype) },
|
||||
onCancel = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
this.uri = intent?.data!!
|
||||
|
||||
val filePath = intent?.data?.path
|
||||
fileBeingImported.value = filePath
|
||||
fileKind.value = determineFileKind(applicationContext, uri!!)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
|
||||
deferGetSetting(THEME_KEY) {
|
||||
val themeOptionFromSettings = ThemeOptions[it]
|
||||
val themeOption = when {
|
||||
themeOptionFromSettings == null -> VoiceInputTheme
|
||||
!themeOptionFromSettings.available(this) -> VoiceInputTheme
|
||||
else -> themeOptionFromSettings
|
||||
}
|
||||
|
||||
this.themeOption.value = themeOption
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ import org.futo.inputmethod.updates.DEFER_MANUAL_UPDATE_UNTIL
|
||||
import org.futo.inputmethod.updates.MANUAL_UPDATE_PERIOD_MS
|
||||
import org.futo.inputmethod.updates.openManualUpdateCheck
|
||||
import org.futo.inputmethod.updates.retrieveSavedLastUpdateCheckResult
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
private class LatinIMEActionInputTransaction(
|
||||
@ -173,6 +174,10 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
|
||||
override fun performHapticAndAudioFeedback(code: Int, view: View) {
|
||||
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(code, view)
|
||||
}
|
||||
|
||||
override fun getActiveLocale(): Locale {
|
||||
return latinIME.latinIMELegacy.locale
|
||||
}
|
||||
}
|
||||
|
||||
class UixManager(private val latinIME: LatinIME) {
|
||||
|
@ -23,23 +23,16 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.Action
|
||||
import org.futo.inputmethod.latin.uix.ActionInputTransaction
|
||||
import org.futo.inputmethod.latin.uix.ActionWindow
|
||||
import org.futo.inputmethod.latin.uix.DISALLOW_SYMBOLS
|
||||
import org.futo.inputmethod.latin.uix.ENABLE_ENGLISH
|
||||
import org.futo.inputmethod.latin.uix.ENABLE_MULTILINGUAL
|
||||
import org.futo.inputmethod.latin.uix.ENABLE_SOUND
|
||||
import org.futo.inputmethod.latin.uix.ENGLISH_MODEL_INDEX
|
||||
import org.futo.inputmethod.latin.uix.KeyboardManagerForAction
|
||||
import org.futo.inputmethod.latin.uix.LANGUAGE_TOGGLES
|
||||
import org.futo.inputmethod.latin.uix.MULTILINGUAL_MODEL_INDEX
|
||||
import org.futo.inputmethod.latin.uix.PersistentActionState
|
||||
import org.futo.inputmethod.latin.uix.ResourceHelper
|
||||
import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS
|
||||
import org.futo.inputmethod.latin.uix.getSetting
|
||||
import org.futo.inputmethod.latin.uix.voiceinput.downloader.DownloadActivity
|
||||
import org.futo.inputmethod.latin.xlm.UserDictionaryObserver
|
||||
import org.futo.voiceinput.shared.ENGLISH_MODELS
|
||||
import org.futo.voiceinput.shared.MULTILINGUAL_MODELS
|
||||
import org.futo.voiceinput.shared.ModelDoesNotExistException
|
||||
import org.futo.voiceinput.shared.RecognizerView
|
||||
import org.futo.voiceinput.shared.RecognizerViewListener
|
||||
@ -51,6 +44,7 @@ import org.futo.voiceinput.shared.types.getLanguageFromWhisperString
|
||||
import org.futo.voiceinput.shared.whisper.DecodingConfiguration
|
||||
import org.futo.voiceinput.shared.whisper.ModelManager
|
||||
import org.futo.voiceinput.shared.whisper.MultiModelRunConfiguration
|
||||
import java.util.Locale
|
||||
|
||||
val SystemVoiceInputAction = Action(
|
||||
icon = R.drawable.mic_fill,
|
||||
@ -74,7 +68,8 @@ class VoiceInputPersistentState(val manager: KeyboardManagerForAction) : Persist
|
||||
}
|
||||
|
||||
private class VoiceInputActionWindow(
|
||||
val manager: KeyboardManagerForAction, val state: VoiceInputPersistentState
|
||||
val manager: KeyboardManagerForAction, val state: VoiceInputPersistentState,
|
||||
val model: ModelLoader, val locale: Locale
|
||||
) : ActionWindow, RecognizerViewListener {
|
||||
val context = manager.getContext()
|
||||
|
||||
@ -83,25 +78,12 @@ private class VoiceInputActionWindow(
|
||||
val enableSound = async { context.getSetting(ENABLE_SOUND) }
|
||||
val verboseFeedback = async { context.getSetting(VERBOSE_PROGRESS) }
|
||||
val disallowSymbols = async { context.getSetting(DISALLOW_SYMBOLS) }
|
||||
val enableEnglish = async { context.getSetting(ENABLE_ENGLISH) }
|
||||
val englishModelIdx = async { context.getSetting(ENGLISH_MODEL_INDEX) }
|
||||
val enableMultilingual = async { context.getSetting(ENABLE_MULTILINGUAL) }
|
||||
val multilingualModelIdx = async { context.getSetting(MULTILINGUAL_MODEL_INDEX) }
|
||||
val allowedLanguages = async {
|
||||
context.getSetting(LANGUAGE_TOGGLES).mapNotNull { getLanguageFromWhisperString(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
val primaryModel = if (enableMultilingual.await()) {
|
||||
MULTILINGUAL_MODELS[multilingualModelIdx.await()]
|
||||
} else {
|
||||
ENGLISH_MODELS[englishModelIdx.await()]
|
||||
}
|
||||
|
||||
val primaryModel = model
|
||||
val languageSpecificModels = mutableMapOf<Language, ModelLoader>()
|
||||
if (enableEnglish.await()) {
|
||||
languageSpecificModels[Language.English] = ENGLISH_MODELS[englishModelIdx.await()]
|
||||
}
|
||||
val allowedLanguages = setOf(
|
||||
getLanguageFromWhisperString(locale.language)!!
|
||||
)
|
||||
|
||||
shouldPlaySounds = enableSound.await()
|
||||
|
||||
@ -114,7 +96,7 @@ private class VoiceInputActionWindow(
|
||||
),
|
||||
decodingConfiguration = DecodingConfiguration(
|
||||
glossary = state.userDictionaryObserver.getWords().map { it.word },
|
||||
languages = allowedLanguages.await(),
|
||||
languages = allowedLanguages,
|
||||
suppressSymbols = disallowSymbols.await()
|
||||
)
|
||||
)
|
||||
@ -234,14 +216,49 @@ private class VoiceInputActionWindow(
|
||||
}
|
||||
}
|
||||
|
||||
private class VoiceInputNoModelWindow(val locale: Locale) : ActionWindow {
|
||||
@Composable
|
||||
override fun windowName(): String {
|
||||
return stringResource(R.string.voice_input_action_title)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun WindowContents(keyboardShown: Boolean) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(enabled = true,
|
||||
onClickLabel = null,
|
||||
onClick = { TODO() },
|
||||
role = null,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() })) {
|
||||
Text("No model available for ${locale.displayLanguage}, tap to check options?", modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val VoiceInputAction = Action(icon = R.drawable.mic_fill,
|
||||
name = R.string.voice_input_action_title,
|
||||
simplePressImpl = null,
|
||||
keepScreenAwake = true,
|
||||
persistentState = { VoiceInputPersistentState(it) },
|
||||
windowImpl = { manager, persistentState ->
|
||||
VoiceInputActionWindow(
|
||||
manager = manager, state = persistentState as VoiceInputPersistentState
|
||||
)
|
||||
val locale = manager.getActiveLocale()
|
||||
|
||||
val model = ResourceHelper.tryFindingVoiceInputModelForLocale(manager.getContext(), locale)
|
||||
|
||||
if(model == null) {
|
||||
VoiceInputNoModelWindow(locale)
|
||||
} else {
|
||||
VoiceInputActionWindow(
|
||||
manager = manager, state = persistentState as VoiceInputPersistentState,
|
||||
locale = locale, model = model
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
@ -4,11 +4,16 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.futo.inputmethod.latin.BinaryDictionaryGetter
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.RichInputMethodManager
|
||||
import org.futo.inputmethod.latin.uix.FileKind
|
||||
import org.futo.inputmethod.latin.uix.ResourceHelper
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItem
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
@ -16,7 +21,9 @@ import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.Tip
|
||||
import org.futo.inputmethod.latin.uix.settings.openLanguageSettings
|
||||
import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils
|
||||
import org.futo.inputmethod.latin.xlm.ModelPaths
|
||||
import org.futo.inputmethod.updates.openURI
|
||||
import java.util.Locale
|
||||
|
||||
data class LanguageOptions(
|
||||
val voiceInputModel: String?,
|
||||
@ -43,39 +50,57 @@ fun LanguagesScreen(navController: NavHostController = rememberNavController())
|
||||
RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypeList(true).forEach {
|
||||
val name = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(it)
|
||||
|
||||
val dummyOptions = LanguageOptions(
|
||||
voiceInputModel = "Built-in English-39",
|
||||
dictionary = "main.dict",
|
||||
transformerModel = null
|
||||
val locale = Locale.forLanguageTag(it.locale.replace("_", "-"))
|
||||
|
||||
val voiceInputModelName = ResourceHelper.tryFindingVoiceInputModelForLocale(context, locale)?.name?.let { stringResource(it) }
|
||||
val dictionaryName = runBlocking { ResourceHelper.findFileForKind(context, locale, FileKind.Dictionary) }?.let {
|
||||
"Imported Dictionary"
|
||||
} ?: if(BinaryDictionaryGetter.getDictionaryFiles(locale, context, false, false).let {
|
||||
println("DICTIONARIES FOR ${locale.displayLanguage}: ${it.toList().map { it.mFilename }.joinToString(",")}")
|
||||
it
|
||||
}.isNotEmpty()) {
|
||||
"Built-in Dictionary"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val transformerName = runBlocking { ModelPaths.getModelOptions(context) }.get(locale.language)?.let {
|
||||
it.loadDetails()?.name
|
||||
}
|
||||
|
||||
val options = LanguageOptions(
|
||||
voiceInputModel = voiceInputModelName,
|
||||
dictionary = dictionaryName,
|
||||
transformerModel = transformerName
|
||||
)
|
||||
|
||||
ScreenTitle(name)
|
||||
/*
|
||||
|
||||
NavigationItem(
|
||||
title = dummyOptions.voiceInputModel ?: "None",
|
||||
style = dummyOptions.voiceInputModel?.let { NavigationItemStyle.HomeTertiary } ?: NavigationItemStyle.MiscNoArrow,
|
||||
title = options.voiceInputModel ?: "None",
|
||||
style = options.voiceInputModel?.let { NavigationItemStyle.HomeTertiary } ?: NavigationItemStyle.MiscNoArrow,
|
||||
navigate = {
|
||||
context.openURI("https://keyboard.futo.org/voice-input-models", true)
|
||||
},
|
||||
icon = painterResource(id = R.drawable.mic_fill)
|
||||
)
|
||||
NavigationItem(
|
||||
title = dummyOptions.dictionary ?: "None",
|
||||
style = dummyOptions.dictionary?.let { NavigationItemStyle.HomeTertiary } ?: NavigationItemStyle.MiscNoArrow,
|
||||
title = options.dictionary ?: "None",
|
||||
style = options.dictionary?.let { NavigationItemStyle.HomeTertiary } ?: NavigationItemStyle.MiscNoArrow,
|
||||
navigate = {
|
||||
context.openURI("https://codeberg.org/Helium314/aosp-dictionaries#dictionaries", true)
|
||||
},
|
||||
icon = painterResource(id = R.drawable.book)
|
||||
)
|
||||
NavigationItem(
|
||||
title = dummyOptions.transformerModel ?: "None",
|
||||
style = dummyOptions.transformerModel?.let { NavigationItemStyle.HomeTertiary } ?: NavigationItemStyle.MiscNoArrow,
|
||||
title = options.transformerModel ?: "None",
|
||||
style = options.transformerModel?.let { NavigationItemStyle.HomeTertiary } ?: NavigationItemStyle.MiscNoArrow,
|
||||
navigate = {
|
||||
context.openURI("https://keyboard.futo.org/models", true)
|
||||
},
|
||||
icon = painterResource(id = R.drawable.cpu)
|
||||
)
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,10 @@ import org.futo.voiceinput.shared.types.ModelBuiltInAsset
|
||||
import org.futo.voiceinput.shared.types.ModelDownloadable
|
||||
import org.futo.voiceinput.shared.types.ModelLoader
|
||||
|
||||
val BUILTIN_ENGLISH_MODEL: ModelLoader = ModelBuiltInAsset(
|
||||
name = R.string.tiny_en_name,
|
||||
ggmlFile = "tiny_en_acft_q8_0.bin.not.tflite"
|
||||
)
|
||||
|
||||
val ENGLISH_MODELS: List<ModelLoader> = listOf(
|
||||
ModelBuiltInAsset(
|
||||
|
@ -61,8 +61,8 @@ internal class ModelBuiltInAsset(
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun Context.tryOpenDownloadedModel(pathStr: String): MappedByteBuffer {
|
||||
val fis = File(this.filesDir, pathStr).inputStream()
|
||||
private fun tryOpenDownloadedModel(file: File): MappedByteBuffer {
|
||||
val fis = file.inputStream()
|
||||
val channel = fis.channel
|
||||
|
||||
return channel.map(
|
||||
@ -71,6 +71,11 @@ private fun Context.tryOpenDownloadedModel(pathStr: String): MappedByteBuffer {
|
||||
).load()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun Context.tryOpenDownloadedModel(pathStr: String): MappedByteBuffer {
|
||||
return tryOpenDownloadedModel(File(filesDir, pathStr))
|
||||
}
|
||||
|
||||
internal class ModelDownloadable(
|
||||
override val name: Int,
|
||||
val ggmlFile: String,
|
||||
@ -91,3 +96,21 @@ internal class ModelDownloadable(
|
||||
return WhisperGGML(file)
|
||||
}
|
||||
}
|
||||
|
||||
public class ModelFileFile(
|
||||
override val name: Int,
|
||||
val file: File,
|
||||
) : ModelLoader {
|
||||
override fun exists(context: Context): Boolean {
|
||||
return file.exists()
|
||||
}
|
||||
|
||||
override fun getRequiredDownloadList(context: Context): List<String> {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
override fun loadGGML(context: Context): WhisperGGML {
|
||||
val file = tryOpenDownloadedModel(file)
|
||||
return WhisperGGML(file)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user