Compare commits
8 Commits
813441645c
...
StreamingO
| Author | SHA1 | Date | |
|---|---|---|---|
| f17c6ab84e | |||
| cce093db4e | |||
| 6471f642c4 | |||
| 05787d20d2 | |||
| ce72ef7a16 | |||
| 404bc55ed3 | |||
| 12c0508713 | |||
| 8f178d16e9 |
@@ -11,7 +11,7 @@ android {
|
|||||||
applicationId = "net.mmanningau.speechtokeyboard"
|
applicationId = "net.mmanningau.speechtokeyboard"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 4
|
versionCode = 10
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -25,6 +25,11 @@ android {
|
|||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix = ".streaming"
|
||||||
|
// This changes the app name on your homescreen to "MyApp (Dev)"
|
||||||
|
resValue("string", "app_name", "Speech To Keyboard (Streaming)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
@@ -47,12 +52,17 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
// 1. The "Brain": Vosk Offline Speech Recognition
|
// 1. The "Brain": Vosk Offline Speech Recognition
|
||||||
implementation("com.alphacephei:vosk-android:0.3.47")
|
//implementation("com.alphacephei:vosk-android:0.3.47") - removed as part of the migration to whisper
|
||||||
|
|
||||||
// (Optional) Helper for memory management if needed later
|
// (Optional) Helper for memory management if needed later
|
||||||
// Removed the following as it was listed as optional and it did cause errors -
|
// Removed the following as it was listed as optional and it did cause errors -
|
||||||
// so to avoid a whole list of duplicate class found errors - this is already required via the VOSK libraries
|
// so to avoid a whole list of duplicate class found errors - this is already required via the VOSK libraries
|
||||||
// implementation("net.java.dev.jna:jna:5.13.0")
|
// implementation("net.java.dev.jna:jna:5.13.0")
|
||||||
|
|
||||||
|
// New Whisper include...
|
||||||
|
// implementation("com.k2fsa.sherpa.onnx:sherpa-onnx:1.12.23") // The engine
|
||||||
|
implementation("com.github.k2-fsa:sherpa-onnx:v1.12.23")
|
||||||
|
|
||||||
// 2. The "Mouth": USB Serial Driver for Android
|
// 2. The "Mouth": USB Serial Driver for Android
|
||||||
implementation("com.github.mik3y:usb-serial-for-android:3.7.0")
|
implementation("com.github.mik3y:usb-serial-for-android:3.7.0")
|
||||||
}
|
}
|
||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 411 KiB |
@@ -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
|
|
||||||
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
|
// 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()
|
|
||||||
|
// 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,72 +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()
|
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 {
|
||||||
private fun initVoskModel() {
|
val modelDir = File(filesDir, "sherpa-model")
|
||||||
val modelPath = File(filesDir, "vosk-model")
|
val isReady = File(modelDir, "encoder.onnx").exists() &&
|
||||||
|
File(modelDir, "tokens.txt").exists()
|
||||||
|
|
||||||
// Check if the directory exists before trying to load
|
if (isReady) {
|
||||||
if (!modelPath.exists()) {
|
statusText.text = "Model Loaded & Ready"
|
||||||
statusText.text = "No model found. Please load one."
|
} else {
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,49 @@
|
|||||||
package net.mmanningau.speechtokeyboard
|
package net.mmanningau.speechtokeyboard
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import android.media.AudioFormat
|
||||||
|
import android.media.AudioRecord
|
||||||
|
import android.media.MediaRecorder
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import org.json.JSONObject
|
|
||||||
import org.vosk.Model
|
|
||||||
import org.vosk.Recognizer
|
|
||||||
import org.vosk.android.RecognitionListener
|
|
||||||
import org.vosk.android.SpeechService
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.hardware.usb.UsbManager
|
|
||||||
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.hoho.android.usbserial.util.SerialInputOutputManager
|
||||||
|
import com.k2fsa.sherpa.onnx.EndpointConfig
|
||||||
|
import com.k2fsa.sherpa.onnx.EndpointRule
|
||||||
|
import com.k2fsa.sherpa.onnx.FeatureConfig
|
||||||
|
import com.k2fsa.sherpa.onnx.OnlineRecognizer
|
||||||
|
import com.k2fsa.sherpa.onnx.OnlineRecognizerConfig
|
||||||
|
import com.k2fsa.sherpa.onnx.OnlineTransducerModelConfig
|
||||||
|
import com.k2fsa.sherpa.onnx.OnlineStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class TestModelActivity : AppCompatActivity(), RecognitionListener {
|
class TestModelActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
// UI Components
|
||||||
private lateinit var outputText: TextView
|
private lateinit var outputText: TextView
|
||||||
private lateinit var micButton: ImageButton
|
private lateinit var micButton: ImageButton
|
||||||
|
|
||||||
// Vosk Components
|
// Sherpa (Whisper) Components
|
||||||
private var model: Model? = null
|
private var recognizer: OnlineRecognizer? = null
|
||||||
private var speechService: SpeechService? = null
|
private var stream: OnlineStream? = null
|
||||||
private var isListening = false
|
private var isRecording = false
|
||||||
|
private var recordingThread: Thread? = null
|
||||||
|
|
||||||
// USB Components
|
// USB Components
|
||||||
private var usbPort: UsbSerialPort? = null
|
private var usbPort: UsbSerialPort? = null
|
||||||
private var usbIoManager: SerialInputOutputManager? = null // Handles the data flow
|
|
||||||
|
|
||||||
private var committedText = "" // Stores the finalized sentences
|
// Text History
|
||||||
|
private var committedText = ""
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -46,209 +53,227 @@ class TestModelActivity : AppCompatActivity(), RecognitionListener {
|
|||||||
outputText = findViewById(R.id.text_output_log)
|
outputText = findViewById(R.id.text_output_log)
|
||||||
micButton = findViewById(R.id.btn_mic_toggle)
|
micButton = findViewById(R.id.btn_mic_toggle)
|
||||||
|
|
||||||
// Check Permissions immediately
|
|
||||||
checkAudioPermission()
|
checkAudioPermission()
|
||||||
|
connectToPico() // Try to auto-connect USB on start
|
||||||
|
|
||||||
// Setup Button Listener
|
// Initialize Engine
|
||||||
|
initSherpaModel()
|
||||||
|
|
||||||
|
// Setup Button
|
||||||
micButton.setOnClickListener {
|
micButton.setOnClickListener {
|
||||||
toggleListening()
|
toggleRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectToPico() {
|
|
||||||
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
|
||||||
|
|
||||||
// 1. Find the Device
|
|
||||||
// (This probes specifically for devices listed in your device_filter.xml)
|
|
||||||
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
|
|
||||||
if (availableDrivers.isEmpty()) {
|
|
||||||
outputText.append("\n> No USB device found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume the first device found is the Pico
|
|
||||||
val driver = availableDrivers[0]
|
|
||||||
val connection = usbManager.openDevice(driver.device)
|
|
||||||
|
|
||||||
if (connection == null) {
|
|
||||||
outputText.append("\n> Permission denied. Re-plug device?")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Open the Port
|
|
||||||
// Most Picos use port 0.
|
|
||||||
usbPort = driver.ports[0]
|
|
||||||
|
|
||||||
try {
|
|
||||||
usbPort?.open(connection)
|
|
||||||
// 3. Set Parameters (Must match your Pico's C/Python code!)
|
|
||||||
// 115200 Baud, 8 Data bits, 1 Stop bit, No Parity
|
|
||||||
usbPort?.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
|
||||||
|
|
||||||
outputText.append("\n> USB Connected to Pico!")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
outputText.append("\n> USB Error: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the model in background
|
|
||||||
initModel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initModel() {
|
// ----------------------------------------------------------------
|
||||||
// We look for the folder inside private storage (same logic as MainActivity)
|
// 1. ENGINE INITIALIZATION (The "Missing Code")
|
||||||
val modelPath = File(filesDir, "vosk-model")
|
// ----------------------------------------------------------------
|
||||||
|
private fun initSherpaModel() {
|
||||||
|
val modelDir = File(filesDir, "sherpa-model")
|
||||||
|
|
||||||
if (!modelPath.exists()) {
|
if (!File(modelDir, "encoder.onnx").exists()) {
|
||||||
outputText.text = "Error: Model not found. Please go back and load a model first."
|
outputText.text = "Error: Sherpa Model files missing in /sherpa-model/"
|
||||||
micButton.isEnabled = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread {
|
try {
|
||||||
try {
|
// 1. Define Model Paths
|
||||||
// Find the actual model folder inside
|
val transducerConfig = OnlineTransducerModelConfig(
|
||||||
val actualModelDir = modelPath.listFiles()?.firstOrNull { it.isDirectory } ?: modelPath
|
encoder = File(modelDir, "encoder.onnx").absolutePath,
|
||||||
model = Model(actualModelDir.absolutePath)
|
decoder = File(modelDir, "decoder.onnx").absolutePath,
|
||||||
|
joiner = File(modelDir, "joiner.onnx").absolutePath
|
||||||
|
)
|
||||||
|
|
||||||
runOnUiThread {
|
// 2. Define General Config
|
||||||
outputText.append("\n\n> Model Loaded. Ready.")
|
val onlineModelConfig = com.k2fsa.sherpa.onnx.OnlineModelConfig(
|
||||||
|
transducer = transducerConfig,
|
||||||
|
tokens = File(modelDir, "tokens.txt").absolutePath,
|
||||||
|
numThreads = 1,
|
||||||
|
debug = false,
|
||||||
|
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(
|
||||||
|
mustContainNonSilence = false,
|
||||||
|
minTrailingSilence = 2.4f,
|
||||||
|
minUtteranceLength = 0.0f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. Create Recognizer Config
|
||||||
|
val config = OnlineRecognizerConfig(
|
||||||
|
featConfig = FeatureConfig(sampleRate = 16000, featureDim = 80),
|
||||||
|
modelConfig = onlineModelConfig,
|
||||||
|
endpointConfig = EndpointConfig(rule1 = silenceRule), // Pass the rule object here
|
||||||
|
enableEndpoint = true,
|
||||||
|
decodingMethod = "greedy_search",
|
||||||
|
maxActivePaths = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// recognizer = OnlineRecognizer(assetManager = assets, config = config)
|
||||||
|
recognizer = OnlineRecognizer(config = config)
|
||||||
|
stream = recognizer?.createStream()
|
||||||
|
|
||||||
|
outputText.text = "Engine Loaded. Ready to Stream."
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Sherpa", "Init Error", e)
|
||||||
|
outputText.text = "Init Error: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 2. AUDIO LOOP (The "Manual" Listener)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private fun toggleRecording() {
|
||||||
|
if (isRecording) {
|
||||||
|
stopRecording()
|
||||||
|
} else {
|
||||||
|
startRecording()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startRecording() {
|
||||||
|
if (recognizer == null) {
|
||||||
|
Toast.makeText(this, "Engine not ready", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX 1: CLEAR THE BUFFER
|
||||||
|
// This prevents the "ghost text" from the previous session appearing
|
||||||
|
// when you hit record again.
|
||||||
|
stream?.let { activeStream ->
|
||||||
|
recognizer?.reset(activeStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRecording = true
|
||||||
|
micButton.setColorFilter(android.graphics.Color.RED)
|
||||||
|
outputText.text = "$committedText [Listening...]"
|
||||||
|
|
||||||
|
recordingThread = Thread {
|
||||||
|
processAudioLoop()
|
||||||
|
}
|
||||||
|
recordingThread?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopRecording() {
|
||||||
|
isRecording = false
|
||||||
|
recordingThread?.join()
|
||||||
|
micButton.clearColorFilter()
|
||||||
|
|
||||||
|
// Just show what we have, don't overwrite with "[Stopped]"
|
||||||
|
// to prevent visual jarring.
|
||||||
|
outputText.append("\n[Stopped]")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processAudioLoop() {
|
||||||
|
val sampleRate = 16000
|
||||||
|
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
|
||||||
|
// Guard clauses
|
||||||
|
val localRec = recognizer ?: return
|
||||||
|
val localStream = stream ?: return
|
||||||
|
|
||||||
|
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val record = AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)
|
||||||
|
record.startRecording()
|
||||||
|
|
||||||
|
val buffer = ShortArray(bufferSize)
|
||||||
|
|
||||||
|
while (isRecording) {
|
||||||
|
val ret = record.read(buffer, 0, buffer.size)
|
||||||
|
if (ret > 0) {
|
||||||
|
val samples = FloatArray(ret) { buffer[it] / 32768.0f }
|
||||||
|
|
||||||
|
localStream.acceptWaveform(samples, sampleRate)
|
||||||
|
|
||||||
|
while (localRec.isReady(localStream)) {
|
||||||
|
localRec.decode(localStream)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
runOnUiThread {
|
val text = localRec.getResult(localStream).text
|
||||||
outputText.text = "Error loading model: ${e.message}"
|
val isEndpoint = localRec.isEndpoint(localStream)
|
||||||
|
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
val cleanText = text.lowercase()
|
||||||
|
|
||||||
|
if (isEndpoint) {
|
||||||
|
// FIX 2: THE ORDER OF OPERATIONS
|
||||||
|
|
||||||
|
// A. Update UI first
|
||||||
|
runOnUiThread {
|
||||||
|
committedText += "$cleanText "
|
||||||
|
outputText.text = committedText
|
||||||
|
sendToPico("$cleanText ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Standard partial update
|
||||||
|
runOnUiThread {
|
||||||
|
outputText.text = "$committedText $cleanText"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
}
|
||||||
|
record.stop()
|
||||||
|
record.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 3. USB LOGIC (Unchanged from before)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
private fun connectToPico() {
|
||||||
|
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
|
||||||
|
if (availableDrivers.isEmpty()) return
|
||||||
|
|
||||||
|
val driver = availableDrivers[0]
|
||||||
|
val connection = usbManager.openDevice(driver.device) ?: return
|
||||||
|
|
||||||
|
usbPort = driver.ports[0]
|
||||||
|
try {
|
||||||
|
usbPort?.open(connection)
|
||||||
|
usbPort?.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
||||||
|
outputText.append("\n> USB Connected")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
outputText.append("\n> USB Error: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendToPico(text: String) {
|
private fun sendToPico(text: String) {
|
||||||
if (usbPort == null) return // Safety check
|
if (usbPort == null) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert text to bytes and send
|
usbPort?.write(text.toByteArray(Charsets.UTF_8), 500)
|
||||||
val data = text.toByteArray(Charsets.UTF_8)
|
|
||||||
usbPort?.write(data, 1000) // 1000ms timeout
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
outputText.append("\n[Send Failed: ${e.message}]")
|
// Log error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleListening() {
|
// ----------------------------------------------------------------
|
||||||
if (model == null) {
|
// 4. CLEANUP
|
||||||
Toast.makeText(this, "Model not loaded yet", Toast.LENGTH_SHORT).show()
|
// ----------------------------------------------------------------
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isListening) {
|
|
||||||
stopRecognition()
|
|
||||||
} else {
|
|
||||||
startRecognition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startRecognition() {
|
|
||||||
try {
|
|
||||||
val recognizer = Recognizer(model, 16000.0f) // 16kHz is standard for Vosk
|
|
||||||
speechService = SpeechService(recognizer, 16000.0f)
|
|
||||||
//speechService?.addListener(this) <----- removed this as it generated an error
|
|
||||||
speechService?.startListening(this)
|
|
||||||
|
|
||||||
isListening = true
|
|
||||||
micButton.setColorFilter(android.graphics.Color.RED) // Turn button red
|
|
||||||
outputText.text = "" // Clear previous text
|
|
||||||
outputText.append("> Listening...\n")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
outputText.append("\nError starting mic: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopRecognition() {
|
|
||||||
speechService?.stop()
|
|
||||||
speechService = null
|
|
||||||
isListening = false
|
|
||||||
micButton.clearColorFilter() // Reset button color
|
|
||||||
outputText.append("\n> Stopped.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Vosk Listener Callbacks ---
|
|
||||||
|
|
||||||
override fun onResult(hypothesis: String?) {
|
|
||||||
hypothesis?.let {
|
|
||||||
val text = parseVoskResult(it)
|
|
||||||
if (text.isNotEmpty()) {
|
|
||||||
// 1. Update the UI History
|
|
||||||
// Add the new sentence to our history
|
|
||||||
committedText += "$text. "
|
|
||||||
// Update screen
|
|
||||||
outputText.text = "$committedText"
|
|
||||||
|
|
||||||
// 2. SEND TO PICO
|
|
||||||
// We append a space because speech engines strip trailing spaces,
|
|
||||||
// and you don't want "helloworld" typed into your computer.
|
|
||||||
sendToPico("$text ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPartialResult(hypothesis: String?) {
|
|
||||||
// Optional: Shows words as they are being spoken (streaming)
|
|
||||||
// You can enable this if you want to see "typing" effect
|
|
||||||
hypothesis?.let {
|
|
||||||
// Parse the "partial" JSON key
|
|
||||||
val partial = JSONObject(it).optString("partial", "")
|
|
||||||
|
|
||||||
if (partial.isNotEmpty()) {
|
|
||||||
// Display: [History] + [Current Streaming Guess]
|
|
||||||
outputText.text = "$committedText $partial..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinalResult(hypothesis: String?) {
|
|
||||||
// Final flush when stopping
|
|
||||||
hypothesis?.let {
|
|
||||||
val text = parseVoskResult(it)
|
|
||||||
if (text.isNotEmpty()) {
|
|
||||||
outputText.append("$text\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(exception: Exception?) {
|
|
||||||
outputText.append("\nError: ${exception?.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimeout() {
|
|
||||||
outputText.append("\nTimeout.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to clean JSON: {"text": "hello world"} -> "hello world"
|
|
||||||
private fun parseVoskResult(json: String): String {
|
|
||||||
return try {
|
|
||||||
JSONObject(json).optString("text", "")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission Helper
|
|
||||||
private fun checkAudioPermission() {
|
private fun checkAudioPermission() {
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1)
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on exit
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
speechService?.shutdown()
|
isRecording = false
|
||||||
|
stream?.release()
|
||||||
// Close USB
|
recognizer?.release()
|
||||||
try {
|
try { usbPort?.close() } catch (e: Exception) {}
|
||||||
usbPort?.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Ignore errors on close
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 19 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 25 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#0878F5</color>
|
||||||
|
</resources>
|
||||||
5
build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
}
|
||||||
23
gradle.properties
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
27
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "8.13.2"
|
||||||
|
kotlin = "2.0.21"
|
||||||
|
coreKtx = "1.17.0"
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitVersion = "1.3.0"
|
||||||
|
espressoCore = "3.7.0"
|
||||||
|
appcompat = "1.7.1"
|
||||||
|
material = "1.13.0"
|
||||||
|
activity = "1.12.2"
|
||||||
|
constraintlayout = "2.2.1"
|
||||||
|
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#Thu Jan 22 13:35:52 GMT+11:00 2026
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
185
gradlew
vendored
Executable file
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
89
gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
25
settings.gradle.kts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
// Add this line:
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "Speech To Keyboard"
|
||||||
|
include(":app")
|
||||||