Updated and tested using the sherpa onnx streaming file - had to download it and extract from tar.bz2 and recompress as zip but that was a start....

This commit is contained in:
2026-01-22 20:58:12 +11:00
parent ce72ef7a16
commit 05787d20d2
4 changed files with 82 additions and 69 deletions

View File

@@ -11,7 +11,7 @@ android {
applicationId = "net.mmanningau.speechtokeyboard" applicationId = "net.mmanningau.speechtokeyboard"
minSdk = 28 minSdk = 28
targetSdk = 36 targetSdk = 36
versionCode = 5 versionCode = 8
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -1,4 +1,4 @@
package net.mmanningau.speechtokeyboard // <--- MAKE SURE THIS MATCHES YOUR PACKAGE NAME package net.mmanningau.speechtokeyboard
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
@@ -9,27 +9,18 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
// import org.vosk.Model
// import org.vosk.android.SpeechService --- removed as part of migratoin to whisper.cpp
import java.io.File import java.io.File
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
// UI Components
private lateinit var statusText: TextView private lateinit var statusText: TextView
// Vosk Components - removed as part of whisper migration // 1. FILE PICKER REGISTRY
// private var model: Model? = null
// private var speechService: SpeechService? = null
// 1. THE FILE PICKER REGISTRY
// This handles the result when the user picks a ZIP file
private val pickZipFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val pickZipFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
// User picked a file. Now we install it. installSherpaModel(uri)
installModelFromUri(uri)
} }
} }
} }
@@ -38,21 +29,23 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
statusText = findViewById(R.id.status_text_view) // Make sure you have a TextView with this ID in your layout statusText = findViewById(R.id.status_text_view)
statusText.text = "Checking for existing model...."
// ADD THIS LINE AT THE BOTTOM: // Auto-check status on launch
// This attempts to load the model immediately if files exist checkModelStatus()
// initVoskModel() - removed as part of whisper migration
// Button listener (if you are using the button layout)
findViewById<android.widget.Button>(R.id.button_load_model)?.setOnClickListener {
openFilePicker()
}
} }
// 2. SETUP THE MENU // 2. MENU SETUP
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu) menuInflater.inflate(R.menu.main_menu, menu)
return true return true
} }
// 3. HANDLE MENU CLICKS
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_load_model -> { R.id.action_load_model -> {
@@ -60,16 +53,17 @@ class MainActivity : AppCompatActivity() {
true true
} }
R.id.action_test_model -> { R.id.action_test_model -> {
// Launch the new Test Activity if (checkModelStatus()) {
val intent = Intent(this, TestModelActivity::class.java) startActivity(Intent(this, TestModelActivity::class.java))
startActivity(intent) } else {
Toast.makeText(this, "Please Load Model First", Toast.LENGTH_SHORT).show()
}
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
// 4. OPEN SYSTEM FILE PICKER
private fun openFilePicker() { private fun openFilePicker() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
@@ -78,75 +72,85 @@ class MainActivity : AppCompatActivity() {
pickZipFile.launch(intent) pickZipFile.launch(intent)
} }
// 5. INSTALLATION LOGIC (Unzip to private storage) // 3. THE SMART UNZIPPER (Renames & Flattens)
private fun installModelFromUri(uri: android.net.Uri) { private fun installSherpaModel(uri: android.net.Uri) {
statusText.text = "Installing model... please wait." statusText.text = "Extracting & Optimizing Model..."
// Run in background thread to avoid freezing UI
Thread { Thread {
try { try {
contentResolver.openInputStream(uri)?.use { inputStream -> contentResolver.openInputStream(uri)?.use { inputStream ->
val zipInputStream = ZipInputStream(inputStream) val zipInputStream = ZipInputStream(inputStream)
val targetDir = File(filesDir, "vosk-model") val targetDir = File(filesDir, "sherpa-model")
// Clean up old model if exists // Clean start
if (targetDir.exists()) targetDir.deleteRecursively() if (targetDir.exists()) targetDir.deleteRecursively()
targetDir.mkdirs() targetDir.mkdirs()
// Unzip loop
var entry = zipInputStream.nextEntry var entry = zipInputStream.nextEntry
var foundEncoder = false
var foundDecoder = false
var foundJoiner = false
var foundTokens = false
while (entry != null) { while (entry != null) {
val outFile = File(targetDir, entry.name) val name = entry.name.lowercase()
if (entry.isDirectory) {
outFile.mkdirs() // Smart Rename Logic
} else { // We ignore the folders and look for keywords in the filename
// Ensure parent dir exists val targetFileName = when {
outFile.parentFile?.mkdirs() name.contains("encoder") && name.endsWith(".onnx") -> "encoder.onnx"
name.contains("decoder") && name.endsWith(".onnx") -> "decoder.onnx"
name.contains("joiner") && name.endsWith(".onnx") -> "joiner.onnx"
name.contains("tokens.txt") -> "tokens.txt"
else -> null
}
if (targetFileName != null) {
val outFile = File(targetDir, targetFileName)
outFile.outputStream().use { output -> outFile.outputStream().use { output ->
zipInputStream.copyTo(output) zipInputStream.copyTo(output)
} }
// Track success
when (targetFileName) {
"encoder.onnx" -> foundEncoder = true
"decoder.onnx" -> foundDecoder = true
"joiner.onnx" -> foundJoiner = true
"tokens.txt" -> foundTokens = true
}
} }
entry = zipInputStream.nextEntry entry = zipInputStream.nextEntry
} }
}
// Back to UI Thread to say success runOnUiThread {
runOnUiThread { if (foundEncoder && foundDecoder && foundJoiner && foundTokens) {
statusText.text = "Model Installed! Initializing..." statusText.text = "Model Installed Successfully!"
// initVoskModel() - removed as part of the whisper migration Toast.makeText(this, "Ready to use!", Toast.LENGTH_SHORT).show()
} else {
statusText.text = "Error: Invalid Model Zip.\nMissing files."
}
}
} }
} catch (e: Exception) { } catch (e: Exception) {
runOnUiThread { runOnUiThread {
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show() statusText.text = "Install Failed: ${e.message}"
statusText.text = "Installation Failed."
} }
} }
}.start() }.start()
} }
// 6. INITIALIZE VOSK "BRAIN" // 4. CHECK IF READY
// Replace your existing initVoskModel with this updated version private fun checkModelStatus(): Boolean {
/* val modelDir = File(filesDir, "sherpa-model")
private fun initVoskModel() { val isReady = File(modelDir, "encoder.onnx").exists() &&
File(modelDir, "tokens.txt").exists()
val modelPath = File(filesDir, "vosk-model") if (isReady) {
statusText.text = "Model Loaded & Ready"
// Check if the directory exists before trying to load } else {
if (!modelPath.exists()) { statusText.text = "No Model Found.\nPlease Load Zip."
statusText.text = "No model found. Please load one."
return
}
val actualModelDir = modelPath.listFiles()?.firstOrNull { it.isDirectory } ?: modelPath
try {
model = Model(actualModelDir.absolutePath)
statusText.text = "Model Loaded & Ready!"
// Optional: Enable your 'Test' button here if you disabled it
} catch (e: Exception) {
statusText.text = "Error loading saved model: ${e.message}"
} }
return isReady
} }
*/
} }

View File

@@ -111,7 +111,8 @@ class TestModelActivity : AppCompatActivity() {
maxActivePaths = 4 maxActivePaths = 4
) )
recognizer = OnlineRecognizer(assetManager = assets, config = config) // recognizer = OnlineRecognizer(assetManager = assets, config = config)
recognizer = OnlineRecognizer(config = config)
stream = recognizer?.createStream() stream = recognizer?.createStream()
outputText.text = "Engine Loaded. Ready to Stream." outputText.text = "Engine Loaded. Ready to Stream."

View File

@@ -10,7 +10,15 @@
android:id="@+id/status_text_view" android:id="@+id/status_text_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Status" android:text="Status: Checking..."
android:textSize="18sp" /> android:textSize="18sp"
android:gravity="center"
android:layout_marginBottom="24dp"/>
<Button
android:id="@+id/button_load_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load Model from Zip" />
</LinearLayout> </LinearLayout>