Updated to continually check for the a valid USB connection on each press of the mircophone and also totally rewrote the send to Pico code as well to me more robust...

This commit is contained in:
2026-02-03 20:39:00 +11:00
parent 9f6d67a567
commit 75b63f91ea
2 changed files with 60 additions and 61 deletions

View File

@@ -21,8 +21,8 @@ android {
applicationId = "net.mmanningau.speechtokeyboard"
minSdk = 28
targetSdk = 36
versionCode = 13
versionName = "1.1.1"
versionCode = 14
versionName = "1.1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -17,7 +17,6 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
import com.k2fsa.sherpa.onnx.EndpointConfig
import com.k2fsa.sherpa.onnx.EndpointRule
import com.k2fsa.sherpa.onnx.FeatureConfig
@@ -26,7 +25,6 @@ import com.k2fsa.sherpa.onnx.OnlineRecognizerConfig
import com.k2fsa.sherpa.onnx.OnlineTransducerModelConfig
import com.k2fsa.sherpa.onnx.OnlineStream
import java.io.File
import com.k2fsa.sherpa.onnx.OfflinePunctuation
import com.k2fsa.sherpa.onnx.OfflinePunctuationConfig
import com.k2fsa.sherpa.onnx.OfflinePunctuationModelConfig
@@ -62,7 +60,9 @@ class TestModelActivity : AppCompatActivity() {
micButton = findViewById(R.id.btn_mic_toggle)
checkAudioPermission()
connectToPico() // Try to auto-connect USB on start
// Try to connect immediately on startup
attemptUsbConnection()
// Initialize Engine
initSherpaModel()
@@ -74,7 +74,7 @@ class TestModelActivity : AppCompatActivity() {
}
// ----------------------------------------------------------------
// 1. ENGINE INITIALIZATION (The "Missing Code")
// 1. ENGINE INITIALIZATION
// ----------------------------------------------------------------
private fun initSherpaModel() {
val modelDir = File(filesDir, "sherpa-model")
@@ -85,14 +85,12 @@ class TestModelActivity : AppCompatActivity() {
}
try {
// 1. Define Model Paths
val transducerConfig = OnlineTransducerModelConfig(
encoder = File(modelDir, "encoder.onnx").absolutePath,
decoder = File(modelDir, "decoder.onnx").absolutePath,
joiner = File(modelDir, "joiner.onnx").absolutePath
)
// 2. Define General Config
val onlineModelConfig = com.k2fsa.sherpa.onnx.OnlineModelConfig(
transducer = transducerConfig,
tokens = File(modelDir, "tokens.txt").absolutePath,
@@ -101,41 +99,33 @@ class TestModelActivity : AppCompatActivity() {
modelType = "zipformer"
)
// 3. Define Endpoint Rule (The fix for your error)
// rule1 = detected silence after speech. We set this to 2.4 seconds.
val silenceRule = EndpointRule(
mustContainNonSilence = false,
minTrailingSilence = 2.4f,
minUtteranceLength = 0.0f
)
// 4. Create Recognizer Config
val config = OnlineRecognizerConfig(
featConfig = FeatureConfig(sampleRate = 16000, featureDim = 80),
modelConfig = onlineModelConfig,
endpointConfig = EndpointConfig(rule1 = silenceRule), // Pass the rule object here
endpointConfig = EndpointConfig(rule1 = silenceRule),
enableEndpoint = true,
decodingMethod = "greedy_search",
maxActivePaths = 4
)
// recognizer = OnlineRecognizer(assetManager = assets, config = config)
recognizer = OnlineRecognizer(config = config)
stream = recognizer?.createStream()
outputText.text = "Engine Loaded. Ready to Stream."
outputText.text = "Engine Loaded. Ready."
// ... existing recognizer init code ...
// 5. Initialize Punctuation Engine
// Initialize Punctuation Engine
val punctPath = File(modelDir, "punct_model.onnx").absolutePath
if (File(punctPath).exists()) {
// CORRECTED: Wrap the path inside 'OfflinePunctuationModelConfig'
val punctConfig = OfflinePunctuationConfig(
model = OfflinePunctuationModelConfig(ctTransformer = punctPath)
)
punctuator = OfflinePunctuation(config = punctConfig)
outputText.append("\n+ Punctuation Ready")
} else {
@@ -149,7 +139,7 @@ class TestModelActivity : AppCompatActivity() {
}
// ----------------------------------------------------------------
// 2. AUDIO LOOP (The "Manual" Listener)
// 2. AUDIO LOOP
// ----------------------------------------------------------------
private fun toggleRecording() {
if (isRecording) {
@@ -165,9 +155,11 @@ class TestModelActivity : AppCompatActivity() {
return
}
// FIX 1: CLEAR THE BUFFER
// This prevents the "ghost text" from the previous session appearing
// when you hit record again.
// Before we start, check USB connection again!
if (usbPort == null) {
attemptUsbConnection()
}
stream?.let { activeStream ->
recognizer?.reset(activeStream)
}
@@ -186,9 +178,6 @@ class TestModelActivity : AppCompatActivity() {
isRecording = false
recordingThread?.join()
micButton.clearColorFilter()
// Just show what we have, don't overwrite with "[Stopped]"
// to prevent visual jarring.
outputText.append("\n[Stopped]")
}
@@ -196,7 +185,6 @@ class TestModelActivity : AppCompatActivity() {
val sampleRate = 16000
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
// Guard clauses
val localRec = recognizer ?: return
val localStream = stream ?: return
@@ -213,7 +201,6 @@ class TestModelActivity : AppCompatActivity() {
val ret = record.read(buffer, 0, buffer.size)
if (ret > 0) {
val samples = FloatArray(ret) { buffer[it] / 32768.0f }
localStream.acceptWaveform(samples, sampleRate)
while (localRec.isReady(localStream)) {
@@ -227,28 +214,15 @@ class TestModelActivity : AppCompatActivity() {
val cleanText = text.lowercase()
if (isEndpoint) {
// FIX 2: THE ORDER OF OPERATIONS
// A. Update UI first
// 1. PUNCTUATE
// We pass the raw text to the punctuator
val punctuatedText = punctuator?.addPunctuation(cleanText) ?: cleanText
runOnUiThread {
// 2. Commit the BEAUTIFUL text
committedText += "$punctuatedText "
outputText.text = committedText
sendToPico("$punctuatedText ")
}
// B. RESET IMMEDIATELY ON BACKGROUND THREAD
// We do this HERE, not inside runOnUiThread.
// This guarantees the stream is clean BEFORE the loop
// reads the next chunk of audio.
localRec.reset(localStream)
} else {
// Standard partial update
runOnUiThread {
outputText.text = "$committedText $cleanText"
}
@@ -261,22 +235,24 @@ class TestModelActivity : AppCompatActivity() {
}
// ----------------------------------------------------------------
// REVISED USB LOGIC (With Permission Request)
// 3. ROBUST USB LOGIC (FIXED)
// ----------------------------------------------------------------
private fun connectToPico() {
// RENAMED from 'connectToPico' to be clearer
private fun attemptUsbConnection() {
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
// Find the driver
// 1. Find Driver
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
if (availableDrivers.isEmpty()) {
outputText.append("\n> No USB Device Found")
Log.d("USB", "No drivers found")
return
}
val driver = availableDrivers[0]
// CHECK PERMISSION
// 2. Check Permission
if (!usbManager.hasPermission(driver.device)) {
outputText.append("\n> Requesting Permission...")
Log.d("USB", "Requesting Permission...")
val pendingIntent = android.app.PendingIntent.getBroadcast(
this,
0,
@@ -287,31 +263,54 @@ class TestModelActivity : AppCompatActivity() {
return
}
// OPEN DEVICE
openUsbDevice(driver, usbManager)
}
private fun openUsbDevice(driver: com.hoho.android.usbserial.driver.UsbSerialDriver, manager: UsbManager) {
// 3. Open Connection
try {
val connection = manager.openDevice(driver.device) ?: return
val connection = usbManager.openDevice(driver.device)
if (connection == null) {
Log.e("USB", "openDevice returned null")
return
}
// Clean up old port if exists
try { usbPort?.close() } catch (e: Exception) {}
usbPort = driver.ports[0]
usbPort?.open(connection)
// CRITICAL: MATCHING BAUD RATE
// We are sticking with 115200. You MUST update your Pico code to match this.
usbPort?.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
outputText.append("\n> USB Connected (115200)!")
Log.d("USB", "Success! Connected at 115200")
// UI Feedback
runOnUiThread {
Toast.makeText(this, "USB Connected", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
outputText.append("\n> Connection Failed: ${e.message}")
Log.e("USB", "Connection Error", e)
usbPort = null
}
}
private fun sendToPico(text: String) {
if (usbPort == null) return
// AUTO-RECONNECT FEATURE
if (usbPort == null) {
Log.d("USB", "Port null, trying to reconnect...")
attemptUsbConnection()
if (usbPort == null) {
Log.e("USB", "Reconnect failed.")
return
}
}
try {
usbPort?.write(text.toByteArray(Charsets.UTF_8), 500)
val data = text.toByteArray(Charsets.UTF_8)
usbPort?.write(data, 500)
Log.d("USB", "Sent: $text")
} catch (e: Exception) {
// Log error
Log.e("USB", "Write Failed", e)
// Force reset on next try
usbPort = null
}
}