From 05787d20d2e9c750f025bcea5842d1ff0ed0b216 Mon Sep 17 00:00:00 2001 From: mmanningau Date: Thu, 22 Jan 2026 20:58:12 +1100 Subject: [PATCH] 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.... --- app/build.gradle.kts | 2 +- .../speechtokeyboard/MainActivity.kt | 134 +++++++++--------- .../speechtokeyboard/TestModelActivity.kt | 3 +- app/src/main/res/layout/activity_main.xml | 12 +- 4 files changed, 82 insertions(+), 69 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e5a2cb..aedd00a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/app/src/main/java/net/mmanningau/speechtokeyboard/MainActivity.kt b/app/src/main/java/net/mmanningau/speechtokeyboard/MainActivity.kt index 2d75a2c..ed3a182 100644 --- a/app/src/main/java/net/mmanningau/speechtokeyboard/MainActivity.kt +++ b/app/src/main/java/net/mmanningau/speechtokeyboard/MainActivity.kt @@ -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(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 + runOnUiThread { + 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 - } - - 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}" + if (isReady) { + statusText.text = "Model Loaded & Ready" + } else { + statusText.text = "No Model Found.\nPlease Load Zip." } + return isReady } -*/ } \ No newline at end of file diff --git a/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt b/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt index ca98dcc..bb1f78f 100644 --- a/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt +++ b/app/src/main/java/net/mmanningau/speechtokeyboard/TestModelActivity.kt @@ -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." diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 181dcd1..a53a346 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -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"/> + +