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"
minSdk = 28
targetSdk = 36
versionCode = 5
versionCode = 8
versionName = "1.0"
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.content.Intent
@@ -9,27 +9,18 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
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.util.zip.ZipInputStream
class MainActivity : AppCompatActivity() {
// UI Components
private lateinit var statusText: TextView
// Vosk Components - removed as part of whisper migration
// 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
// 1. FILE PICKER REGISTRY
private val pickZipFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
// User picked a file. Now we install it.
installModelFromUri(uri)
installSherpaModel(uri)
}
}
}
@@ -38,21 +29,23 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
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.text = "Checking for existing model...."
statusText = findViewById(R.id.status_text_view)
// ADD THIS LINE AT THE BOTTOM:
// This attempts to load the model immediately if files exist
// initVoskModel() - removed as part of whisper migration
// Auto-check status on launch
checkModelStatus()
// 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 {
menuInflater.inflate(R.menu.main_menu, menu)
return true
}
// 3. HANDLE MENU CLICKS
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_load_model -> {
@@ -60,16 +53,17 @@ class MainActivity : AppCompatActivity() {
true
}
R.id.action_test_model -> {
// Launch the new Test Activity
val intent = Intent(this, TestModelActivity::class.java)
startActivity(intent)
if (checkModelStatus()) {
startActivity(Intent(this, TestModelActivity::class.java))
} else {
Toast.makeText(this, "Please Load Model First", Toast.LENGTH_SHORT).show()
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
// 4. OPEN SYSTEM FILE PICKER
private fun openFilePicker() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@@ -78,75 +72,85 @@ class MainActivity : AppCompatActivity() {
pickZipFile.launch(intent)
}
// 5. INSTALLATION LOGIC (Unzip to private storage)
private fun installModelFromUri(uri: android.net.Uri) {
statusText.text = "Installing model... please wait."
// 3. THE SMART UNZIPPER (Renames & Flattens)
private fun installSherpaModel(uri: android.net.Uri) {
statusText.text = "Extracting & Optimizing Model..."
// Run in background thread to avoid freezing UI
Thread {
try {
contentResolver.openInputStream(uri)?.use { 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()
targetDir.mkdirs()
// Unzip loop
var entry = zipInputStream.nextEntry
var foundEncoder = false
var foundDecoder = false
var foundJoiner = false
var foundTokens = false
while (entry != null) {
val outFile = File(targetDir, entry.name)
if (entry.isDirectory) {
outFile.mkdirs()
} else {
// Ensure parent dir exists
outFile.parentFile?.mkdirs()
val name = entry.name.lowercase()
// Smart Rename Logic
// We ignore the folders and look for keywords in the filename
val targetFileName = when {
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 ->
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
}
}
// Back to UI Thread to say success
runOnUiThread {
statusText.text = "Model Installed! Initializing..."
// initVoskModel() - removed as part of the whisper migration
if (foundEncoder && foundDecoder && foundJoiner && foundTokens) {
statusText.text = "Model Installed Successfully!"
Toast.makeText(this, "Ready to use!", Toast.LENGTH_SHORT).show()
} else {
statusText.text = "Error: Invalid Model Zip.\nMissing files."
}
}
}
} catch (e: Exception) {
runOnUiThread {
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
statusText.text = "Installation Failed."
statusText.text = "Install Failed: ${e.message}"
}
}
}.start()
}
// 6. INITIALIZE VOSK "BRAIN"
// Replace your existing initVoskModel with this updated version
/*
private fun initVoskModel() {
// 4. CHECK IF READY
private fun checkModelStatus(): Boolean {
val modelDir = File(filesDir, "sherpa-model")
val isReady = File(modelDir, "encoder.onnx").exists() &&
File(modelDir, "tokens.txt").exists()
val modelPath = File(filesDir, "vosk-model")
// Check if the directory exists before trying to load
if (!modelPath.exists()) {
statusText.text = "No model found. Please load one."
return
if (isReady) {
statusText.text = "Model Loaded & Ready"
} else {
statusText.text = "No Model Found.\nPlease Load Zip."
}
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
)
recognizer = OnlineRecognizer(assetManager = assets, config = config)
// recognizer = OnlineRecognizer(assetManager = assets, config = config)
recognizer = OnlineRecognizer(config = config)
stream = recognizer?.createStream()
outputText.text = "Engine Loaded. Ready to Stream."

View File

@@ -10,7 +10,15 @@
android:id="@+id/status_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textSize="18sp" />
android:text="Status: Checking..."
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>