Compare commits

..

5 Commits

Author SHA1 Message Date
a6ccd6d703 Include whisper in letter mode too 2023-12-04 11:57:56 -05:00
4da4eabc19 Include whisper again 2023-12-04 11:56:42 -05:00
23cccf2455 Get sentence position of cursor 2023-12-04 05:48:46 -05:00
48a9a2e822 Commit current word if we're switching modes 2023-12-04 05:22:04 -05:00
0ec4f4b262 Get initial sentence position state 2023-12-04 05:19:38 -05:00
16 changed files with 222 additions and 156 deletions

Binary file not shown.

View File

@@ -31,9 +31,13 @@ interface IME {
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean = false)
fun onGetTextBeforeCursor(n: Int): CharSequence?
fun defaultView()
fun onSwitchInputHandler(inputMode: InputMode)
fun onUpdateStatusIcon(icon: Int?)
fun record()
fun transcribe()
}

View File

@@ -26,8 +26,11 @@ import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.keypad.Event
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.layout.Words
import net.mezimmah.wkt9.layout.LoadingLayout
import net.mezimmah.wkt9.layout.MessageLayout
import net.mezimmah.wkt9.layout.WordsLayout
import net.mezimmah.wkt9.t9.T9
import net.mezimmah.wkt9.voice.Whisper
import java.util.Locale
@@ -35,11 +38,14 @@ class WKT9IME: IME, InputMethodService() {
private val tag = "WKT9"
private val inputModeManager = InputModeManager(this)
private val whisper: Whisper = Whisper(this)
private lateinit var locale: Locale
private var inputHandler: InputHandler? = null
private var wordsView: Words? = null
private var wordsLayoutView: WordsLayout? = null
private var loadingLayoutView: LoadingLayout? = null
private var messageLayoutView: MessageLayout? = null
private val keyDownStats = KeyEventStat(0, 0)
private val keyUpStats = KeyEventStat(0, 0)
@@ -88,9 +94,11 @@ class WKT9IME: IME, InputMethodService() {
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
wordsView = layoutInflater.inflate(R.layout.words, null) as Words
wordsLayoutView = layoutInflater.inflate(R.layout.words, null) as WordsLayout
loadingLayoutView = layoutInflater.inflate(R.layout.loading, null) as LoadingLayout
messageLayoutView = layoutInflater.inflate(R.layout.message, null) as MessageLayout
return wordsView
return wordsLayoutView
}
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) {
@@ -112,10 +120,6 @@ class WKT9IME: IME, InputMethodService() {
inputHandler?.onDeleteWord(word)
}
override fun onGetTextBeforeCursor(n: Int): CharSequence? {
return this.currentInputConnection?.getTextBeforeCursor(n, 0)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyDownStats.keyCode != keyCode) {
keyDownStats.keyCode = keyCode
@@ -276,7 +280,7 @@ class WKT9IME: IME, InputMethodService() {
override fun onWords(words: List<Word>, capMode: Int?) {
this.capMode = capMode
wordsView?.words = words
wordsLayoutView?.words = words
}
override fun onWordSelected(word: Word) {
@@ -292,11 +296,11 @@ class WKT9IME: IME, InputMethodService() {
}
override fun onNextWord() {
wordsView?.next()
wordsLayoutView?.next()
}
override fun onPreviousWord() {
wordsView?.previous()
wordsLayoutView?.previous()
}
private fun deleteText(beforeCursor: Int, afterCursor: Int) {
@@ -305,8 +309,22 @@ class WKT9IME: IME, InputMethodService() {
}
}
override fun record() {
setInputView(messageLayoutView)
whisper.record()
}
override fun transcribe() {
setInputView(loadingLayoutView)
whisper.transcribe()
}
override fun defaultView() {
setInputView(wordsLayoutView)
}
private fun finishComposing() {
wordsView?.clear()
wordsLayoutView?.clear()
inputHandler?.onFinishComposing()
}

View File

@@ -19,14 +19,18 @@ open class DefaultInputHandler(
private var currentCapMode: Int? = null
protected val punctuationMarks = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
protected val sentenceDelimiters = listOf('.', '?', '!')
protected val wordDelimiters = listOf('\t', '\n', ' ', ',', ':', ';')
protected val tag = "WKT9"
protected val keypad: Keypad = Keypad()
protected var wordStart: Boolean = true
protected var sentenceStart: Boolean = true
protected var wordStart: Boolean = false
protected var sentenceStart: Boolean = false
init {
setCursorPositionStatus()
wkt9.currentInputEditorInfo?.let {
val inputType = it.inputType
val typeFlags = inputType.and(InputType.TYPE_MASK_FLAGS)
@@ -83,4 +87,27 @@ open class DefaultInputHandler(
protected fun triggerOriginalKeyEvent(key: Key) {
triggerKeyEvent(key.keyCode)
}
protected fun setCursorPositionStatus() {
val textBeforeCursor = getTextBeforeCursor()
if (
textBeforeCursor.isNullOrEmpty() ||
textBeforeCursor.trim().isEmpty() ||
sentenceDelimiters.contains(textBeforeCursor.trim().last())
) {
sentenceStart = true
wordStart = true
} else if (wordDelimiters.contains(textBeforeCursor.last())) {
sentenceStart = false
wordStart = true
} else {
sentenceStart = false
wordStart = false
}
}
private fun getTextBeforeCursor(): CharSequence? {
return wkt9.currentInputConnection?.getTextBeforeCursor(15, 0)
}
}

View File

@@ -30,7 +30,7 @@ class LetterInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale,
private var composeTimeout: Long
private var composeTimeout: Long,
): SpellCheckerSession.SpellCheckerSessionListener, DefaultInputHandler(wkt9), InputHandler {
private val db = AppDatabase.getInstance(wkt9)
private val wordDao = db.getWordDao()
@@ -59,52 +59,31 @@ class LetterInputHandler(
updateIcon()
}
private fun finalizeWordOrSentence(stats: KeyEventStat) {
if (word.isNotEmpty()) storeWord()
override fun onDeleteWord(word: Word) {}
timeoutJob?.cancel()
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
val index = stats.repeats % punctuationMarks.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % punctuationMarks.count() else null
var beforeCursor = 0
if (lastIndex != null) beforeCursor += punctuationMarks[lastIndex].length
wordStart = true
sentenceStart = index in 1..3
wkt9.onCommit(punctuationMarks[index], beforeCursor)
updateIcon()
}
private fun storeWord() {
val str = word.toString()
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
word.clear()
scope.launch {
val word = Word(
word = str,
code = keypad.getCodeForWord(str),
weight = 0,
length = str.length,
locale = locale.language
)
val result = wordDao.selectWord(str, locale.language)
if (result == null) wordDao.insert(word)
}
}
override fun onFinishComposing() {}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
override fun onSwitchLocale(locale: Locale) {
this.locale = locale
}
override fun onDeleteWord(word: Word) {}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
override fun onFinishComposing() {}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> composeCharacter(key)
Command.DELETE -> delete()
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> wkt9.record()
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> wkt9.transcribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onWordSelected(word: Word) {}
override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
@@ -112,22 +91,6 @@ class LetterInputHandler(
updateIcon()
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> composeCharacter(key)
Command.DELETE -> delete()
Command.INPUT_MODE -> inputMode(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
// Command.RECORD -> wkt9.onRecord(true)
Command.SPACE -> finalizeWordOrSentence(stats)
// Command.TRANSCRIBE -> wkt9.onTranscribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onWordSelected(word: Word) {}
private fun composeCharacter(key: Key) {
val composing = timeoutJob?.isActive ?: false
@@ -157,15 +120,55 @@ class LetterInputHandler(
setComposeTimeout()
}
private fun finalizeWordOrSentence(stats: KeyEventStat) {
if (word.isNotEmpty()) storeWord()
timeoutJob?.cancel()
val index = stats.repeats % punctuationMarks.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % punctuationMarks.count() else null
var beforeCursor = 0
if (lastIndex != null) beforeCursor += punctuationMarks[lastIndex].length
wordStart = true
sentenceStart = index in 1..3
wkt9.onCommit(punctuationMarks[index], beforeCursor)
updateIcon()
}
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun resetKey(key: Key? = null) {
lastKey = key
repeats = 0
}
private fun finishComposingChar() {
val wordDelimiters = listOf(' ', ',', ':', ';')
val sentenceDelimiters = listOf('.', '?', '!')
private fun storeWord() {
val str = word.toString()
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
word.clear()
scope.launch {
val word = Word(
word = str,
code = keypad.getCodeForWord(str),
weight = 0,
length = str.length,
locale = locale.language
)
val result = wordDao.selectWord(str, locale.language)
if (result == null) wordDao.insert(word)
}
}
private fun finishComposingChar() {
wkt9.onCommit()
if (sentenceDelimiters.contains(lastComposeChar)) {

View File

@@ -23,7 +23,7 @@ import java.util.Locale
class WordInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale,
private var locale: Locale
) : DefaultInputHandler(wkt9), InputHandler {
private val codeword = StringBuilder()
private var staleCodeword = false
@@ -90,14 +90,30 @@ class WordInputHandler(
Command.CHARACTER -> buildCodeword(key)
Command.DELETE -> delete(event.repeatCount)
Command.ENTER -> enter(key)
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> record()
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> transcribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
private fun record() {
if (codeword.isNotEmpty()) {
wkt9.onCommit()
codeword.clear()
}
wkt9.record()
}
private fun transcribe() {
wkt9.transcribe()
}
override fun onWordSelected(word: Word) {
lastSelectedWord = word
}
@@ -131,6 +147,11 @@ class WordInputHandler(
}
}
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun enter(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit("")
else triggerOriginalKeyEvent(key)
@@ -158,6 +179,8 @@ class WordInputHandler(
}
private fun inputMode(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit()
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Number)
else wkt9.onSwitchInputHandler(InputMode.Letter)
}

View File

@@ -7,6 +7,7 @@ enum class Command {
DELETE,
DIAL,
ENTER,
FINISH_DELETE,
INPUT_MODE,
MOVE_CURSOR,
NUMBER,

View File

@@ -388,6 +388,12 @@ enum class Key(
command = Command.DELETE
),
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Word, InputMode.Letter),
command = Command.FINISH_DELETE
),
CommandMapping(
inputModes = listOf(InputMode.Number),
overrideConsume = true,

View File

@@ -0,0 +1,9 @@
package net.mezimmah.wkt9.layout
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
class LoadingLayout(context: Context, attributeSet: AttributeSet): LinearLayout(context, attributeSet) {
}

View File

@@ -0,0 +1,9 @@
package net.mezimmah.wkt9.layout
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
class MessageLayout(context: Context, attributeSet: AttributeSet): LinearLayout(context, attributeSet) {
}

View File

@@ -13,7 +13,7 @@ import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.entity.Word
class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
class WordsLayout(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
private var wkt9: WKT9IME
private var wordCount: Int = 0
private var current: Int = 0

View File

@@ -2,17 +2,12 @@ package net.mezimmah.wkt9.voice
import android.media.MediaRecorder
import android.util.Log
import android.view.View
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.inputhandler.InputHandler
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
@@ -23,17 +18,11 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
class Whisper(
private val context: WKT9IME,
private val inputHandler: InputHandler?,
private val ui: View
private val wkt9: WKT9IME,
) {
private val tag = "WKT9"
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var ioJob: Job? = null
private var recorder: MediaRecorder? = null
private var recording: File? = null
@@ -47,19 +36,18 @@ class Whisper(
stopRecording()
val recording = this.recording ?: return
showTranscribing()
val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
ioJob?.cancel()
ioJob = ioScope.launch {
try {
val transcription = run(recording)
val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
mainScope.launch {
showCandidates()
wkt9.onCommit(transcription)
wkt9.defaultView()
}
// inputHandler?.onInsertText(transcription.plus(" "))
} catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
}
@@ -70,9 +58,7 @@ class Whisper(
fun record() {
if (recorder != null) stopRecording()
showMessage()
recording = File.createTempFile("recording.3gp", null, context.cacheDir)
recording = File.createTempFile("recording.3gp", null, wkt9.cacheDir)
recorder = MediaRecorder().also {
it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
@@ -88,36 +74,6 @@ class Whisper(
}
}
private fun showCandidates() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.VISIBLE
// loadingView.visibility = View.GONE
// messageView.visibility = View.GONE
}
private fun showMessage() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.GONE
// loadingView.visibility = View.GONE
// messageView.visibility = View.VISIBLE
}
private fun showTranscribing() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.GONE
// loadingView.visibility = View.VISIBLE
// messageView.visibility = View.GONE
}
private fun stopRecording() {
recorder?.run {
stop()

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.LoadingLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:theme="@style/Theme.AppCompat.DayNight"
android:gravity="bottom"
android:background="@color/black"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/suggestions"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="40dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing..." />
</net.mezimmah.wkt9.layout.LoadingLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<net.mezimmah.wkt9.layout.MessageLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
@@ -10,11 +10,11 @@
android:background="@color/black"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/suggestions"
style="?android:attr/progressBarStyleLarge"
<ImageView
android:layout_height="40dp"
android:layout_width="wrap_content"
android:layout_height="40dp" />
android:src="@drawable/mic"/>
<TextView
android:layout_width="wrap_content"
@@ -24,6 +24,6 @@
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing, please wait..." />
android:text="Recording..." />
</LinearLayout>
</net.mezimmah.wkt9.layout.MessageLayout>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp">
<TextView
android:id="@+id/suggestion_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:minWidth="40dp"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400" />
</LinearLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.Words xmlns:android="http://schemas.android.com/apk/res/android"
<net.mezimmah.wkt9.layout.WordsLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
@@ -14,4 +14,4 @@
android:layout_height="44dp"
android:orientation="horizontal" />
</net.mezimmah.wkt9.layout.Words>
</net.mezimmah.wkt9.layout.WordsLayout>