Skip to content

Commit db1377e

Browse files
committed
Fix VOSK minification bug
1 parent 047863d commit db1377e

4 files changed

Lines changed: 64 additions & 20 deletions

File tree

app/proguard-rules.pro

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
# InputMethodService is referenced from the manifest; keep it.
22
-keep class app.lightphonekeyboard.** { *; }
3+
4+
# Vosk (voice dictation) calls its native library through JNA, which the native code resolves by
5+
# class/field NAME (e.g. com.sun.jna.Pointer.peer). R8 must not strip or rename JNA or Vosk, or the
6+
# release build throws "UnsatisfiedLinkError: Can't obtain peer field ID for class com.sun.jna.Pointer".
7+
-keep class com.sun.jna.** { *; }
8+
-keep class * implements com.sun.jna.Library { *; }
9+
-keepclassmembers class * extends com.sun.jna.Structure { *; }
10+
-keep class org.vosk.** { *; }
11+
# JNA references java.awt (desktop-only, absent on Android); silence the resulting R8 warnings.
12+
-dontwarn java.awt.**

app/src/main/java/app/lightphonekeyboard/LightImeService.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,10 @@ class LightImeService : InputMethodService(), LightKeyboardView.Listener, SpellC
228228
if (dictation.ready) {
229229
dictation.listen(
230230
onPartial = { kb.setListeningStatus(it) },
231-
onResult = { text ->
232-
micActive = false
231+
// Each finished segment commits to the field; dictation keeps going across pauses.
232+
onSegment = { text ->
233233
clearUndo()
234234
currentInputConnection?.commitText(spacedDictation(text), 1)
235-
kb.stopListeningUi()
236235
},
237236
onError = { msg ->
238237
micActive = false
@@ -253,9 +252,11 @@ class LightImeService : InputMethodService(), LightKeyboardView.Listener, SpellC
253252
kb.postDelayed({ startDictationWhenReady(kb, attempts + 1) }, 300)
254253
}
255254

255+
/** Tap on the listening surface = "I'm done": flush the trailing words, then close it. */
256256
override fun onMicCancel() {
257+
if (!micActive) return
257258
micActive = false
258-
dictation.destroy()
259+
dictation.stop()
259260
keyboard?.stopListeningUi()
260261
}
261262

@@ -271,6 +272,7 @@ class LightImeService : InputMethodService(), LightKeyboardView.Listener, SpellC
271272
override fun onWindowHidden() { super.onWindowHidden(); broadcastImeVisible(false) }
272273
override fun onFinishInputView(finishingInput: Boolean) {
273274
super.onFinishInputView(finishingInput)
275+
if (micActive) { micActive = false; dictation.destroy(); keyboard?.stopListeningUi() }
274276
broadcastImeVisible(false)
275277
}
276278

app/src/main/java/app/lightphonekeyboard/LightKeyboardView.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,31 @@ class LightKeyboardView @JvmOverloads constructor(
316316
d.setBounds(left, top, (left + size).toInt(), (top + size).toInt())
317317
d.draw(canvas)
318318
textPaint.textSize = spf(18)
319-
val baseY = midY + dpf(22) - (textPaint.descent() + textPaint.ascent()) / 2f
320-
canvas.drawText(listeningStatus, cx, baseY, textPaint)
319+
// Wrap the live text so a long phrase stacks into lines instead of running off the screen.
320+
drawWrappedCentered(canvas, listeningStatus, cx, midY + dpf(22), width - dpf(48), textPaint)
321321
textPaint.textSize = spf(12)
322-
canvas.drawText("Tap to cancel", cx, height - dpf(18), textPaint)
322+
canvas.drawText("Tap when done", cx, height - dpf(18), textPaint)
323+
}
324+
325+
/** Draw [text] centered on ([cx],[centerY]), wrapping at word boundaries to fit [maxWidth]. */
326+
private fun drawWrappedCentered(
327+
canvas: Canvas, text: String, cx: Float, centerY: Float, maxWidth: Float, paint: Paint,
328+
) {
329+
if (text.isEmpty()) return
330+
val lines = ArrayList<String>()
331+
var line = ""
332+
for (word in text.split(' ')) {
333+
val candidate = if (line.isEmpty()) word else "$line $word"
334+
if (line.isEmpty() || paint.measureText(candidate) <= maxWidth) {
335+
line = candidate
336+
} else {
337+
lines.add(line); line = word
338+
}
339+
}
340+
if (line.isNotEmpty()) lines.add(line)
341+
val lineH = paint.descent() - paint.ascent()
342+
var baseline = centerY - lines.size * lineH / 2f - paint.ascent()
343+
for (l in lines) { canvas.drawText(l, cx, baseline, paint); baseline += lineH }
323344
}
324345

325346
private fun drawKey(canvas: Canvas, pk: PlacedKey) {

app/src/main/java/app/lightphonekeyboard/VoiceDictation.kt

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ class VoiceDictation(private val context: Context) {
3939
}.start()
4040
}
4141

42+
/**
43+
* Continuous dictation: a pause ends one *segment* (delivered via [onSegment]) but keeps listening,
44+
* so a mid-sentence pause no longer stops you. Call [stop] when you're done; the trailing words
45+
* since the last pause then arrive through [onSegment] too. [onPartial] is the live, in-progress text.
46+
*/
4247
fun listen(
4348
onPartial: (String) -> Unit,
44-
onResult: (String) -> Unit,
49+
onSegment: (String) -> Unit,
4550
onError: (String) -> Unit,
4651
) {
4752
val m = model
@@ -51,13 +56,6 @@ class VoiceDictation(private val context: Context) {
5156
return
5257
}
5358
destroy()
54-
var done = false
55-
fun finish(text: String) {
56-
if (done) return
57-
done = true
58-
if (text.isBlank()) onError("Didn't catch that.") else onResult(text)
59-
destroy()
60-
}
6159
try {
6260
val rec = Recognizer(m, SAMPLE_RATE)
6361
recognizer = rec
@@ -67,19 +65,32 @@ class VoiceDictation(private val context: Context) {
6765
override fun onPartialResult(hypothesis: String?) {
6866
field(hypothesis, "partial")?.let { if (it.isNotBlank()) onPartial(it) }
6967
}
70-
override fun onResult(hypothesis: String?) = finish(field(hypothesis, "text").orEmpty())
71-
override fun onFinalResult(hypothesis: String?) = finish(field(hypothesis, "text").orEmpty())
72-
override fun onError(e: Exception?) {
73-
if (!done) { done = true; onError("Voice error."); destroy() }
68+
// A pause ends a segment but NOT the whole dictation — emit it and keep listening.
69+
override fun onResult(hypothesis: String?) {
70+
field(hypothesis, "text")?.let { if (it.isNotBlank()) onSegment(it) }
71+
}
72+
// Fires once when we stop(): the trailing words since the last pause.
73+
override fun onFinalResult(hypothesis: String?) {
74+
field(hypothesis, "text")?.let { if (it.isNotBlank()) onSegment(it) }
7475
}
75-
override fun onTimeout() { if (!done) { done = true; destroy() } }
76+
override fun onError(e: Exception?) { onError("Voice error.") }
77+
override fun onTimeout() {}
7678
})
7779
} catch (e: Throwable) {
7880
Log.e(TAG, "listen failed", e)
7981
onError("Voice error."); destroy()
8082
}
8183
}
8284

85+
/** Finish dictating: flush the words since the last pause (via onSegment), then tear down. */
86+
fun stop() {
87+
val s = speech ?: return destroy()
88+
runCatching { s.stop() }
89+
// Give the final result a moment to arrive before closing the recognizer; skip if a new
90+
// session has since replaced this one.
91+
main.postDelayed({ if (speech === s) destroy() }, 350)
92+
}
93+
8394
fun destroy() {
8495
speech?.let { runCatching { it.stop() }; runCatching { it.shutdown() } }
8596
speech = null

0 commit comments

Comments
 (0)