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:
@@ -21,8 +21,8 @@ android {
|
|||||||
applicationId = "net.mmanningau.speechtokeyboard"
|
applicationId = "net.mmanningau.speechtokeyboard"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 13
|
versionCode = 14
|
||||||
versionName = "1.1.1"
|
versionName = "1.1.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.core.app.ActivityCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
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.EndpointConfig
|
||||||
import com.k2fsa.sherpa.onnx.EndpointRule
|
import com.k2fsa.sherpa.onnx.EndpointRule
|
||||||
import com.k2fsa.sherpa.onnx.FeatureConfig
|
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.OnlineTransducerModelConfig
|
||||||
import com.k2fsa.sherpa.onnx.OnlineStream
|
import com.k2fsa.sherpa.onnx.OnlineStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
import com.k2fsa.sherpa.onnx.OfflinePunctuation
|
import com.k2fsa.sherpa.onnx.OfflinePunctuation
|
||||||
import com.k2fsa.sherpa.onnx.OfflinePunctuationConfig
|
import com.k2fsa.sherpa.onnx.OfflinePunctuationConfig
|
||||||
import com.k2fsa.sherpa.onnx.OfflinePunctuationModelConfig
|
import com.k2fsa.sherpa.onnx.OfflinePunctuationModelConfig
|
||||||
@@ -62,7 +60,9 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
micButton = findViewById(R.id.btn_mic_toggle)
|
micButton = findViewById(R.id.btn_mic_toggle)
|
||||||
|
|
||||||
checkAudioPermission()
|
checkAudioPermission()
|
||||||
connectToPico() // Try to auto-connect USB on start
|
|
||||||
|
// Try to connect immediately on startup
|
||||||
|
attemptUsbConnection()
|
||||||
|
|
||||||
// Initialize Engine
|
// Initialize Engine
|
||||||
initSherpaModel()
|
initSherpaModel()
|
||||||
@@ -74,7 +74,7 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// 1. ENGINE INITIALIZATION (The "Missing Code")
|
// 1. ENGINE INITIALIZATION
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
private fun initSherpaModel() {
|
private fun initSherpaModel() {
|
||||||
val modelDir = File(filesDir, "sherpa-model")
|
val modelDir = File(filesDir, "sherpa-model")
|
||||||
@@ -85,14 +85,12 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Define Model Paths
|
|
||||||
val transducerConfig = OnlineTransducerModelConfig(
|
val transducerConfig = OnlineTransducerModelConfig(
|
||||||
encoder = File(modelDir, "encoder.onnx").absolutePath,
|
encoder = File(modelDir, "encoder.onnx").absolutePath,
|
||||||
decoder = File(modelDir, "decoder.onnx").absolutePath,
|
decoder = File(modelDir, "decoder.onnx").absolutePath,
|
||||||
joiner = File(modelDir, "joiner.onnx").absolutePath
|
joiner = File(modelDir, "joiner.onnx").absolutePath
|
||||||
)
|
)
|
||||||
|
|
||||||
// 2. Define General Config
|
|
||||||
val onlineModelConfig = com.k2fsa.sherpa.onnx.OnlineModelConfig(
|
val onlineModelConfig = com.k2fsa.sherpa.onnx.OnlineModelConfig(
|
||||||
transducer = transducerConfig,
|
transducer = transducerConfig,
|
||||||
tokens = File(modelDir, "tokens.txt").absolutePath,
|
tokens = File(modelDir, "tokens.txt").absolutePath,
|
||||||
@@ -101,41 +99,33 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
modelType = "zipformer"
|
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(
|
val silenceRule = EndpointRule(
|
||||||
mustContainNonSilence = false,
|
mustContainNonSilence = false,
|
||||||
minTrailingSilence = 2.4f,
|
minTrailingSilence = 2.4f,
|
||||||
minUtteranceLength = 0.0f
|
minUtteranceLength = 0.0f
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Create Recognizer Config
|
|
||||||
val config = OnlineRecognizerConfig(
|
val config = OnlineRecognizerConfig(
|
||||||
featConfig = FeatureConfig(sampleRate = 16000, featureDim = 80),
|
featConfig = FeatureConfig(sampleRate = 16000, featureDim = 80),
|
||||||
modelConfig = onlineModelConfig,
|
modelConfig = onlineModelConfig,
|
||||||
endpointConfig = EndpointConfig(rule1 = silenceRule), // Pass the rule object here
|
endpointConfig = EndpointConfig(rule1 = silenceRule),
|
||||||
enableEndpoint = true,
|
enableEndpoint = true,
|
||||||
decodingMethod = "greedy_search",
|
decodingMethod = "greedy_search",
|
||||||
maxActivePaths = 4
|
maxActivePaths = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// recognizer = OnlineRecognizer(assetManager = assets, config = config)
|
|
||||||
recognizer = OnlineRecognizer(config = config)
|
recognizer = OnlineRecognizer(config = config)
|
||||||
stream = recognizer?.createStream()
|
stream = recognizer?.createStream()
|
||||||
|
|
||||||
outputText.text = "Engine Loaded. Ready to Stream."
|
outputText.text = "Engine Loaded. Ready."
|
||||||
|
|
||||||
// ... existing recognizer init code ...
|
// Initialize Punctuation Engine
|
||||||
|
|
||||||
// 5. Initialize Punctuation Engine
|
|
||||||
val punctPath = File(modelDir, "punct_model.onnx").absolutePath
|
val punctPath = File(modelDir, "punct_model.onnx").absolutePath
|
||||||
|
|
||||||
if (File(punctPath).exists()) {
|
if (File(punctPath).exists()) {
|
||||||
// CORRECTED: Wrap the path inside 'OfflinePunctuationModelConfig'
|
|
||||||
val punctConfig = OfflinePunctuationConfig(
|
val punctConfig = OfflinePunctuationConfig(
|
||||||
model = OfflinePunctuationModelConfig(ctTransformer = punctPath)
|
model = OfflinePunctuationModelConfig(ctTransformer = punctPath)
|
||||||
)
|
)
|
||||||
|
|
||||||
punctuator = OfflinePunctuation(config = punctConfig)
|
punctuator = OfflinePunctuation(config = punctConfig)
|
||||||
outputText.append("\n+ Punctuation Ready")
|
outputText.append("\n+ Punctuation Ready")
|
||||||
} else {
|
} else {
|
||||||
@@ -149,7 +139,7 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// 2. AUDIO LOOP (The "Manual" Listener)
|
// 2. AUDIO LOOP
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
private fun toggleRecording() {
|
private fun toggleRecording() {
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
@@ -165,9 +155,11 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX 1: CLEAR THE BUFFER
|
// Before we start, check USB connection again!
|
||||||
// This prevents the "ghost text" from the previous session appearing
|
if (usbPort == null) {
|
||||||
// when you hit record again.
|
attemptUsbConnection()
|
||||||
|
}
|
||||||
|
|
||||||
stream?.let { activeStream ->
|
stream?.let { activeStream ->
|
||||||
recognizer?.reset(activeStream)
|
recognizer?.reset(activeStream)
|
||||||
}
|
}
|
||||||
@@ -186,9 +178,6 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
isRecording = false
|
isRecording = false
|
||||||
recordingThread?.join()
|
recordingThread?.join()
|
||||||
micButton.clearColorFilter()
|
micButton.clearColorFilter()
|
||||||
|
|
||||||
// Just show what we have, don't overwrite with "[Stopped]"
|
|
||||||
// to prevent visual jarring.
|
|
||||||
outputText.append("\n[Stopped]")
|
outputText.append("\n[Stopped]")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +185,6 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
val sampleRate = 16000
|
val sampleRate = 16000
|
||||||
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
|
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
|
||||||
// Guard clauses
|
|
||||||
val localRec = recognizer ?: return
|
val localRec = recognizer ?: return
|
||||||
val localStream = stream ?: return
|
val localStream = stream ?: return
|
||||||
|
|
||||||
@@ -213,7 +201,6 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
val ret = record.read(buffer, 0, buffer.size)
|
val ret = record.read(buffer, 0, buffer.size)
|
||||||
if (ret > 0) {
|
if (ret > 0) {
|
||||||
val samples = FloatArray(ret) { buffer[it] / 32768.0f }
|
val samples = FloatArray(ret) { buffer[it] / 32768.0f }
|
||||||
|
|
||||||
localStream.acceptWaveform(samples, sampleRate)
|
localStream.acceptWaveform(samples, sampleRate)
|
||||||
|
|
||||||
while (localRec.isReady(localStream)) {
|
while (localRec.isReady(localStream)) {
|
||||||
@@ -227,28 +214,15 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
val cleanText = text.lowercase()
|
val cleanText = text.lowercase()
|
||||||
|
|
||||||
if (isEndpoint) {
|
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
|
val punctuatedText = punctuator?.addPunctuation(cleanText) ?: cleanText
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
// 2. Commit the BEAUTIFUL text
|
|
||||||
committedText += "$punctuatedText "
|
committedText += "$punctuatedText "
|
||||||
outputText.text = committedText
|
outputText.text = committedText
|
||||||
sendToPico("$punctuatedText ")
|
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)
|
localRec.reset(localStream)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Standard partial update
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
outputText.text = "$committedText $cleanText"
|
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
|
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
|
||||||
// Find the driver
|
// 1. Find Driver
|
||||||
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
|
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
|
||||||
if (availableDrivers.isEmpty()) {
|
if (availableDrivers.isEmpty()) {
|
||||||
outputText.append("\n> No USB Device Found")
|
Log.d("USB", "No drivers found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val driver = availableDrivers[0]
|
val driver = availableDrivers[0]
|
||||||
|
|
||||||
// CHECK PERMISSION
|
// 2. Check Permission
|
||||||
if (!usbManager.hasPermission(driver.device)) {
|
if (!usbManager.hasPermission(driver.device)) {
|
||||||
outputText.append("\n> Requesting Permission...")
|
Log.d("USB", "Requesting Permission...")
|
||||||
val pendingIntent = android.app.PendingIntent.getBroadcast(
|
val pendingIntent = android.app.PendingIntent.getBroadcast(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
@@ -287,31 +263,54 @@ class TestModelActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPEN DEVICE
|
// 3. Open Connection
|
||||||
openUsbDevice(driver, usbManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openUsbDevice(driver: com.hoho.android.usbserial.driver.UsbSerialDriver, manager: UsbManager) {
|
|
||||||
try {
|
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 = driver.ports[0]
|
||||||
usbPort?.open(connection)
|
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)
|
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) {
|
} catch (e: Exception) {
|
||||||
outputText.append("\n> Connection Failed: ${e.message}")
|
Log.e("USB", "Connection Error", e)
|
||||||
|
usbPort = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendToPico(text: String) {
|
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 {
|
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) {
|
} catch (e: Exception) {
|
||||||
// Log error
|
Log.e("USB", "Write Failed", e)
|
||||||
|
// Force reset on next try
|
||||||
|
usbPort = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user