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:
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
statusText.text = "Model Installed! Initializing..."
|
if (foundEncoder && foundDecoder && foundJoiner && foundTokens) {
|
||||||
// initVoskModel() - removed as part of the whisper migration
|
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) {
|
} 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
|
|
||||||
}
|
}
|
||||||
|
return isReady
|
||||||
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}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -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."
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user