diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 47994d1..148cf66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } diff --git a/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt b/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt index f5fc30a..15b7e6c 100644 --- a/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt +++ b/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt @@ -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 } }