From 372d6846500c81a46b3582e46f938d0c218883fe Mon Sep 17 00:00:00 2001 From: Nehemiah of Zebulun Date: Sun, 27 Aug 2023 16:20:58 +0200 Subject: [PATCH] Big steps --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 15 ++ app/src/main/java/net/mezimmah/wkt9/WKT9.kt | 140 ++++++++++++++++-- .../mezimmah/wkt9/inputmode/AlphaInputMode.kt | 3 + .../net/mezimmah/wkt9/inputmode/InputMode.kt | 2 + .../wkt9/inputmode/NumericInputMode.kt | 3 + .../net/mezimmah/wkt9/inputmode/Status.kt | 11 ++ .../mezimmah/wkt9/inputmode/WordInputMode.kt | 98 +++++++----- .../wkt9/keypad/KeyCommandResolver.kt | 3 +- .../mezimmah/wkt9/keypad/KeyEventResult.kt | 5 +- .../wkt9/preferences/PreferencesActivity.kt | 17 +++ .../wkt9/preferences/PreferencesFragment.kt | 59 ++++++++ .../java/net/mezimmah/wkt9/voice/Whisper.kt | 41 +++++ .../main/res/layout/preferences_container.xml | 9 ++ app/src/main/res/values/strings.xml | 12 ++ app/src/main/res/xml/preferences.xml | 19 +++ 16 files changed, 387 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesActivity.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt create mode 100644 app/src/main/java/net/mezimmah/wkt9/voice/Whisper.kt create mode 100644 app/src/main/res/layout/preferences_container.xml create mode 100644 app/src/main/res/xml/preferences.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae70889..38ebc66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,6 +42,8 @@ dependencies { implementation("com.google.android.material:material:1.9.0") implementation("androidx.room:room-common:2.5.2") implementation("androidx.room:room-ktx:2.5.2") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.10") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7db66de..c0fe91f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,10 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/WKT9.kt b/app/src/main/java/net/mezimmah/wkt9/WKT9.kt index 706290a..c093cde 100644 --- a/app/src/main/java/net/mezimmah/wkt9/WKT9.kt +++ b/app/src/main/java/net/mezimmah/wkt9/WKT9.kt @@ -1,7 +1,9 @@ package net.mezimmah.wkt9 import android.annotation.SuppressLint +import android.content.Intent import android.inputmethodservice.InputMethodService +import android.media.MediaRecorder import android.text.InputType import android.util.Log import android.view.KeyEvent @@ -10,6 +12,7 @@ import android.view.ViewConfiguration import android.view.inputmethod.EditorInfo import android.widget.LinearLayout import android.widget.TextView +import android.widget.Toast import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -21,6 +24,7 @@ import net.mezimmah.wkt9.db.AppDatabase import net.mezimmah.wkt9.inputmode.InputMode import net.mezimmah.wkt9.inputmode.AlphaInputMode import net.mezimmah.wkt9.inputmode.NumericInputMode +import net.mezimmah.wkt9.inputmode.Status import net.mezimmah.wkt9.inputmode.WordInputMode import net.mezimmah.wkt9.inputmode.WKT9InputMode import net.mezimmah.wkt9.keypad.KeyCodeMapping @@ -28,6 +32,9 @@ import net.mezimmah.wkt9.keypad.KeyEventResult import net.mezimmah.wkt9.keypad.KeyLayout import net.mezimmah.wkt9.keypad.Keypad import net.mezimmah.wkt9.t9.T9 +import net.mezimmah.wkt9.voice.Whisper +import okio.IOException +import java.io.File import java.lang.StringBuilder class WKT9: InputMethodService() { @@ -39,9 +46,10 @@ class WKT9: InputMethodService() { private lateinit var settingDao: SettingDao // Coroutines - private val job = SupervisorJob() - private val scope = CoroutineScope(Dispatchers.Main + job) + private val queryScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private var queryJob: Job? = null + private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var ioJob: Job? = null private var cursorPosition = 0 private var longPressTimeout = 700 @@ -62,9 +70,16 @@ class WKT9: InputMethodService() { private var composing = false private val candidates: MutableList = mutableListOf() private var candidateIndex = 0 + private var sentenceStart = false // UI private lateinit var inputView: View + private var toast: Toast? = null + + // Whisper + private val whisper: Whisper = Whisper() + private var recorder: MediaRecorder? = null + private var recording: File? = null override fun onCreate() { Log.d(tag, "WKT9 is loading") @@ -145,6 +160,9 @@ class WKT9: InputMethodService() { val inputType = attribute?.inputType?.and(InputType.TYPE_MASK_CLASS) ?: 0 cursorPosition = attribute?.initialSelEnd ?: 0 + sentenceStart = + if (cursorPosition == 0) true + else isSentenceStart() when (inputType) { InputType.TYPE_CLASS_DATETIME, @@ -179,15 +197,6 @@ class WKT9: InputMethodService() { ) } - private fun cancelComposing() { - composing = false - - currentInputConnection.let { - it.setComposingText("", 1) - it.finishComposingText() - } - } - private fun clearCandidates() { clearCandidateUI() @@ -201,8 +210,8 @@ class WKT9: InputMethodService() { candidatesView.removeAllViews() } - private fun commitText(text: CharSequence, start: Int, end: Int, cursorPosition: Int): Boolean { - return (markComposingRegion(start, end) && composeText(text, cursorPosition) && finishComposingText()) + private fun commitText(text: CharSequence, start: Int, end: Int): Boolean { + return (markComposingRegion(start, end) && composeText(text, 1) && finishComposingText()) } private fun composeText(text: CharSequence, cursorPosition: Int = 1): Boolean { @@ -213,12 +222,17 @@ class WKT9: InputMethodService() { private fun deleteText(beforeCursor: Int, afterCursor: Int) { currentInputConnection?.deleteSurroundingText(beforeCursor, afterCursor) + + sentenceStart = isSentenceStart() } // Todo: inputType private fun enableInputMode(mode: WKT9InputMode, inputType: Int) { lastInputMode = mode + if (inputType.and(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) + Log.d(tag, "InputConnection expects email address") + inputMode = when(mode) { WKT9InputMode.ALPHA -> alphaInputMode WKT9InputMode.NUMERIC -> numericInputMode @@ -229,22 +243,47 @@ class WKT9: InputMethodService() { private fun finishComposingText(): Boolean { return if (composing) { composing = false + sentenceStart = isSentenceStart() currentInputConnection?.finishComposingText() ?: false } else false } + private fun goHome() { + with(Intent(Intent.ACTION_MAIN)) { + this.addCategory(Intent.CATEGORY_HOME) + this.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + startActivity(this) + } + } + private fun handleKeyEventResult(res: KeyEventResult): Boolean { if (res.finishComposing) finishComposingText() if (res.startComposing) markComposingRegion() if (!res.codeWord.isNullOrEmpty()) onCodeWordUpdate(res.codeWord) if (!res.candidates.isNullOrEmpty()) onCandidates(res.candidates) if (res.deleteBeforeCursor > 0 || res.deleteAfterCursor > 0) onDelete(res.deleteBeforeCursor, res.deleteAfterCursor) + if (res.goHome) goHome() if (res.left) onLeft() if (res.right) onRight() + if (res.record) onRecord() + if (res.transcribe) onTranscribe() return res.consumed } + private fun isSentenceStart(): Boolean { + if (cursorPosition == 0) return true + + val textBeforeCursor = currentInputConnection?.getTextBeforeCursor(10, 0) ?: return false + + if ( + textBeforeCursor.trimEnd().isEmpty() || + listOf('.', '!', '?').contains(textBeforeCursor.trimEnd().last())) return true + + return false + } + private fun loadCandidates(highLight: Int? = null) { val candidatesView = inputView.findViewById(R.id.suggestions) @@ -284,7 +323,7 @@ class WKT9: InputMethodService() { clearCandidates() queryJob?.cancel() - queryJob = scope.launch { + queryJob = queryScope.launch { val hasCandidates = queryT9Candidates(codeWord, 10) if (!hasCandidates) return@launch @@ -311,6 +350,41 @@ class WKT9: InputMethodService() { composeText(candidates[candidateIndex]) } + private fun onRecord() { + // The recorder must be busy... + if (recorder !== null) return + + clearCandidates() + + recording?.delete() + + // Toast settings + val text = "Recording now.\nRelease the button to start transcribing." + val duration = Toast.LENGTH_SHORT + + // Instantiate recorder and start recording + recorder = MediaRecorder(this).also { + recording = File.createTempFile("recording.3gp", null, cacheDir) + + it.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION) + it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + it.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + it.setOutputFile(recording) + + try { + it.prepare() + it.start() + + toast?.cancel() + toast = Toast.makeText(this, text, duration).apply { + this.show() + } + } catch (e: Exception) { + Log.d(tag, "Failed to start recording", e) + } + } + } + private fun onRight() { if (candidates.isEmpty()) return @@ -323,11 +397,45 @@ class WKT9: InputMethodService() { composeText(candidates[candidateIndex]) } + private fun onTranscribe() { + val recorder = this.recorder ?: return + + recorder.stop() + recorder.reset() + recorder.release() + + this.recorder = null + + val text = "Sending recording to speech-to-text server for transcription." + val duration = Toast.LENGTH_SHORT + + toast?.cancel() + toast = Toast.makeText(this, text, duration).apply { + this.show() + } + + ioJob?.cancel() + ioJob = ioScope.launch { + try { + val transcription = whisper.run(recording!!) + + commitText(transcription, cursorPosition, cursorPosition) + } catch (e: IOException) { + Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e) + } + } + } + private suspend fun queryT9Candidates(codeWord: StringBuilder, limit: Int = 10): Boolean { val words = wordDao.findCandidates(codeWord.toString(), limit) - words.forEach { - candidates.add(it.word) + words.forEach { word -> + val candidate = + if (sentenceStart && inputMode?.status == Status.WORD_CAP) word.word.replaceFirstChar { it.uppercase() } + else if (inputMode?.status == Status.WORD_UPPER) word.word.uppercase() + else word.word + + candidates.add(candidate) } return words.isNotEmpty() diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt index 7c50beb..9e6edb8 100644 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/AlphaInputMode.kt @@ -4,6 +4,9 @@ import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.KeyEventResult class AlphaInputMode: InputMode { + override var status: Status = Status.ALPHA_CAP + private set + override fun onKeyDown(key: Key): KeyEventResult { return KeyEventResult(consumed = false) } diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt index b24e3a4..b467b6f 100644 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/InputMode.kt @@ -4,6 +4,8 @@ import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.KeyEventResult interface InputMode { + val status: Status + fun onKeyDown(key: Key): KeyEventResult fun onKeyLongDown(key: Key): KeyEventResult diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt index e855931..a9cadb9 100644 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/NumericInputMode.kt @@ -12,6 +12,9 @@ class NumericInputMode: InputMode { private val keyCommandResolver: KeyCommandResolver = KeyCommandResolver.getBasic() private val codeWord = StringBuilder() + override var status: Status = Status.NUM + private set + override fun onKeyDown(key: Key): KeyEventResult { return when(keyCommandResolver.getCommand(key)) { Command.CHARACTER -> buildCodeWord(key) diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt new file mode 100644 index 0000000..7c497f7 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/Status.kt @@ -0,0 +1,11 @@ +package net.mezimmah.wkt9.inputmode + +enum class Status(val idx: Int) { + WORD(0), + WORD_CAP(1), + WORD_UPPER(2), + ALPHA(3), + ALPHA_CAP(4), + ALPHA_UPPER(5), + NUM(6) +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt b/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt index 5e6b909..ce5aa5f 100644 --- a/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt +++ b/app/src/main/java/net/mezimmah/wkt9/inputmode/WordInputMode.kt @@ -6,7 +6,6 @@ import net.mezimmah.wkt9.keypad.Key import net.mezimmah.wkt9.keypad.KeyCommandResolver import net.mezimmah.wkt9.keypad.KeyEventResult import java.lang.StringBuilder -import java.lang.annotation.Native class WordInputMode: InputMode { private val tag = "WKT9" @@ -16,66 +15,54 @@ class WordInputMode: InputMode { private var keyIndex = 0 private var lastKey: Key? = null + override var status: Status = Status.WORD_CAP + private set + + init { + Log.d(tag, "Started word input mode.") + } + override fun onKeyDown(key: Key): KeyEventResult { keyStats(key) - val command = keyCommandResolver.getCommand(key) - - Log.d(tag, "Command: $command") - return when(keyCommandResolver.getCommand(key)) { Command.BACK -> KeyEventResult(false) Command.CHARACTER -> buildCodeWord(key) -// Command.SELECT -> true Command.DELETE -> deleteCharacter() Command.SPACE -> finalizeWordOrSentence() Command.LEFT -> navigateLeft() Command.RIGHT -> navigateRight() -// Command.CYCLE_CANDIDATES -> cycleCandidates() else -> KeyEventResult() } } override fun onKeyLongDown(key: Key): KeyEventResult { -// Log.d(tag, "onKeyLongDown") - - val command = keyCommandResolver.getCommand(key = key, longPress = true) - - - - Log.d(tag, "Command: $command") - - return KeyEventResult() + return when(keyCommandResolver.getCommand(key, true)) { + Command.RECORD -> record() + else -> KeyEventResult(true) + } } override fun onKeyDownRepeatedly(key: Key, repeat: Int): KeyEventResult { return when(keyCommandResolver.getCommand(key, repeat = repeat)) { + Command.HOME -> goHome(repeat) Command.DELETE -> deleteCharacter(repeat) else -> KeyEventResult() } } override fun afterKeyDown(key: Key): KeyEventResult { -// Log.d(tag, "afterKeyDown") - -// return when(keyCommandResolver.getCommand(key, after = true)) { -// Command.DELETE -> deleteCharacter(repeat) -// else -> KeyEventResult() -// } - - val command = keyCommandResolver.getCommand(key, after = true) - - - - Log.d(tag, "Command: $command") - - return KeyEventResult(false) + return when(keyCommandResolver.getCommand(key, after = true)) { + Command.BACK -> goBack() + else -> KeyEventResult() + } } override fun afterKeyLongDown(key: Key, keyDownMS: Long): KeyEventResult { -// Log.d(tag, "afterKeyLongDown") - - return KeyEventResult() + return when(keyCommandResolver.getCommand(key, after = true, longPress = true)) { + Command.TRANSCRIBE -> transcribe() + else -> KeyEventResult() + } } private fun buildCodeWord(key: Key): KeyEventResult { @@ -115,6 +102,27 @@ class WordInputMode: InputMode { ) } + private fun goBack(): KeyEventResult { + reset() + + return KeyEventResult( + consumed = false, + finishComposing = true + ) + } + + private fun goHome(repeat: Int): KeyEventResult { + if (repeat > 1) return KeyEventResult(true) + + reset() + + return KeyEventResult( + consumed = true, + finishComposing = true, + goHome = true + ) + } + private fun keyStats(key: Key) { when (key != lastKey) { true -> { @@ -137,4 +145,28 @@ class WordInputMode: InputMode { private fun navigateRight(): KeyEventResult { return KeyEventResult(right = true) } + + private fun record(): KeyEventResult { + codeWord.clear() + + return KeyEventResult( + consumed = true, + finishComposing = true, + record = true + ) + } + + private fun reset() { + codeWord.clear() + newKey = true + keyIndex = 0 + lastKey = null + } + + private fun transcribe(): KeyEventResult { + return KeyEventResult( + consumed = true, + transcribe = true + ) + } } \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt index 60b6a7f..8470d40 100644 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyCommandResolver.kt @@ -51,8 +51,6 @@ class KeyCommandResolver ( )), onLong = HashMap(mapOf( - Key.BACK to Command.HOME, - Key.N0 to Command.NUMBER, Key.N1 to Command.NUMBER, Key.N2 to Command.NUMBER, @@ -78,6 +76,7 @@ class KeyCommandResolver ( )), onRepeat = HashMap(mapOf( + Key.BACK to Command.HOME, Key.STAR to Command.DELETE, )) ) diff --git a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt index ef6bbf0..d3d91db 100644 --- a/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt +++ b/app/src/main/java/net/mezimmah/wkt9/keypad/KeyEventResult.kt @@ -10,6 +10,9 @@ data class KeyEventResult( val candidates: List? = null, val deleteBeforeCursor: Int = 0, val deleteAfterCursor: Int = 0, + val goHome: Boolean = false, val left: Boolean = false, - val right: Boolean = false + val right: Boolean = false, + val record: Boolean = false, + val transcribe: Boolean = false ) diff --git a/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesActivity.kt b/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesActivity.kt new file mode 100644 index 0000000..c7c94be --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesActivity.kt @@ -0,0 +1,17 @@ +package net.mezimmah.wkt9.preferences + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import net.mezimmah.wkt9.R + +class PreferencesActivity: AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.preferences_container) + supportFragmentManager + .beginTransaction() + .replace(R.id.preferences_container, PreferencesFragment()) + .commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt b/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt new file mode 100644 index 0000000..d733eda --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/preferences/PreferencesFragment.kt @@ -0,0 +1,59 @@ +package net.mezimmah.wkt9.preferences + +import android.content.SharedPreferences +import android.os.Bundle +import android.Manifest +import android.os.Build +import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference +import net.mezimmah.wkt9.R + +class PreferencesFragment: PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener { + private val tag = "WKT9" + private val requestPermissionLauncher = registerForActivityResult(RequestMultiplePermissions()) { isGranted: Map -> + // If any permission got denied we programmatically disable the option + if (isGranted.containsValue(false)) { + val key = getString(R.string.preference_setting_speech_to_text_key) + + findPreference(key)?.isChecked = false + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + Log.d(tag, "Loading preferences") + + setPreferencesFromResource(R.xml.preferences, rootKey) + } + + override fun onResume() { + super.onResume() + + preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + + preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(p0: SharedPreferences?, key: String?) { + when (key) { + getString(R.string.preference_setting_speech_to_text_key) -> { + if (findPreference(key)?.isChecked == true) { + val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.POST_NOTIFICATIONS + ) + } else arrayOf(Manifest.permission.RECORD_AUDIO) + + requestPermissionLauncher.launch(permissions) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mezimmah/wkt9/voice/Whisper.kt b/app/src/main/java/net/mezimmah/wkt9/voice/Whisper.kt new file mode 100644 index 0000000..e68ec61 --- /dev/null +++ b/app/src/main/java/net/mezimmah/wkt9/voice/Whisper.kt @@ -0,0 +1,41 @@ +package net.mezimmah.wkt9.voice + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit + +class Whisper { + private val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(2, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .readTimeout(25, TimeUnit.SECONDS) + .callTimeout(32, TimeUnit.SECONDS) + .build() + + fun run(recording: File): String { + val mediaType = "audio/3gpp".toMediaType() + + val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("language", "en") + .addFormDataPart("model_size", "tiny.en") + .addFormDataPart("files", "recording.3gp", recording.asRequestBody(mediaType)) + .build() + + val request = Request.Builder() + .url("https://voice.mezimmah.net") + .post(requestBody) + .build() + + return client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + response.body.string().trim() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/preferences_container.xml b/app/src/main/res/layout/preferences_container.xml new file mode 100644 index 0000000..e0f6b1b --- /dev/null +++ b/app/src/main/res/layout/preferences_container.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3607dd..f807f0c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,15 @@ WKT9 + + WKT9 Preferences + + Speech to Text + + speech_to_text + Enable Speech to Text + For this feature to work net.mezimmah.wkt9.WKT9 needs permission to show notifications and record audio. You will be asked to grant these permissions if you haven\'t already permitted it. + + whisper_url + Whisper Server URL + Provide an URL to the Whisper server. \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml new file mode 100644 index 0000000..5639099 --- /dev/null +++ b/app/src/main/res/xml/preferences.xml @@ -0,0 +1,19 @@ + + + + + + + + + +