Compare commits
10 Commits
813441645c
...
2a8f004916
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a8f004916 | |||
| ac7d51b46e | |||
| f17c6ab84e | |||
| cce093db4e | |||
| 6471f642c4 | |||
| 05787d20d2 | |||
| ce72ef7a16 | |||
| 404bc55ed3 | |||
| 12c0508713 | |||
| 8f178d16e9 |
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-01-22T04:36:45.393638454Z">
|
||||
<DropdownSelection timestamp="2026-01-23T01:29:57.710335816Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/michael/.android/avd/Pixel_5_API_31_Android_12_.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=DKTAB13NEU0019483" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "net.mmanningau.speechtokeyboard"
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 4
|
||||
versionName = "1.0"
|
||||
versionCode = 12
|
||||
versionName = "1.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -25,6 +25,11 @@ android {
|
||||
"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 {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
@@ -47,12 +52,17 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
implementation("com.github.mik3y:usb-serial-for-android:3.7.0")
|
||||
}
|
||||
@@ -37,6 +37,8 @@
|
||||
<activity
|
||||
android:name=".TestModelActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:label="Test Microphone" />
|
||||
|
||||
</application>
|
||||
|
||||
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.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
|
||||
import java.io.File
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
// UI Components
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
// Vosk Components
|
||||
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()
|
||||
// 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,72 +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()
|
||||
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() {
|
||||
val modelPath = File(filesDir, "vosk-model")
|
||||
// 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()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,56 @@
|
||||
package net.mmanningau.speechtokeyboard
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
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.util.Log
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
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.UsbSerialProber
|
||||
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 {
|
||||
import com.k2fsa.sherpa.onnx.OfflinePunctuation
|
||||
import com.k2fsa.sherpa.onnx.OfflinePunctuationConfig
|
||||
import com.k2fsa.sherpa.onnx.OfflinePunctuationModelConfig
|
||||
|
||||
class TestModelActivity : AppCompatActivity() {
|
||||
|
||||
// UI Components
|
||||
private lateinit var outputText: TextView
|
||||
private lateinit var micButton: ImageButton
|
||||
|
||||
// Vosk Components
|
||||
private var model: Model? = null
|
||||
private var speechService: SpeechService? = null
|
||||
private var isListening = false
|
||||
// Sherpa (Whisper) Components
|
||||
private var recognizer: OnlineRecognizer? = null
|
||||
private var stream: OnlineStream? = null
|
||||
private var isRecording = false
|
||||
private var recordingThread: Thread? = null
|
||||
|
||||
// Punctuation variables
|
||||
private var punctuator: OfflinePunctuation? = null
|
||||
|
||||
// USB Components
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -46,209 +60,249 @@ class TestModelActivity : AppCompatActivity(), RecognitionListener {
|
||||
outputText = findViewById(R.id.text_output_log)
|
||||
micButton = findViewById(R.id.btn_mic_toggle)
|
||||
|
||||
// Check Permissions immediately
|
||||
checkAudioPermission()
|
||||
connectToPico() // Try to auto-connect USB on start
|
||||
|
||||
// Setup Button Listener
|
||||
// Initialize Engine
|
||||
initSherpaModel()
|
||||
|
||||
// Setup Button
|
||||
micButton.setOnClickListener {
|
||||
toggleListening()
|
||||
toggleRecording()
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToPico() {
|
||||
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
// ----------------------------------------------------------------
|
||||
// 1. ENGINE INITIALIZATION (The "Missing Code")
|
||||
// ----------------------------------------------------------------
|
||||
private fun initSherpaModel() {
|
||||
val modelDir = File(filesDir, "sherpa-model")
|
||||
|
||||
// 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.")
|
||||
if (!File(modelDir, "encoder.onnx").exists()) {
|
||||
outputText.text = "Error: Sherpa Model files missing in /sherpa-model/"
|
||||
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)
|
||||
// 1. Define Model Paths
|
||||
val transducerConfig = OnlineTransducerModelConfig(
|
||||
encoder = File(modelDir, "encoder.onnx").absolutePath,
|
||||
decoder = File(modelDir, "decoder.onnx").absolutePath,
|
||||
joiner = File(modelDir, "joiner.onnx").absolutePath
|
||||
)
|
||||
|
||||
outputText.append("\n> USB Connected to Pico!")
|
||||
// 2. Define General Config
|
||||
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."
|
||||
|
||||
// ... existing recognizer init code ...
|
||||
|
||||
// 5. Initialize Punctuation Engine
|
||||
val punctPath = File(modelDir, "punct_model.onnx").absolutePath
|
||||
|
||||
if (File(punctPath).exists()) {
|
||||
// CORRECTED: Wrap the path inside 'OfflinePunctuationModelConfig'
|
||||
val punctConfig = OfflinePunctuationConfig(
|
||||
model = OfflinePunctuationModelConfig(ctTransformer = punctPath)
|
||||
)
|
||||
|
||||
punctuator = OfflinePunctuation(config = punctConfig)
|
||||
outputText.append("\n+ Punctuation Ready")
|
||||
} else {
|
||||
outputText.append("\n(No Punctuation model found)")
|
||||
}
|
||||
|
||||
} 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)
|
||||
}
|
||||
|
||||
val text = localRec.getResult(localStream).text
|
||||
val isEndpoint = localRec.isEndpoint(localStream)
|
||||
|
||||
if (text.isNotEmpty()) {
|
||||
val cleanText = text.lowercase()
|
||||
|
||||
if (isEndpoint) {
|
||||
// FIX 2: THE ORDER OF OPERATIONS
|
||||
|
||||
// A. Update UI first
|
||||
// 1. PUNCTUATE
|
||||
// We pass the raw text to the punctuator
|
||||
val punctuatedText = punctuator?.addPunctuation(cleanText) ?: cleanText
|
||||
|
||||
runOnUiThread {
|
||||
// 2. Commit the BEAUTIFUL text
|
||||
committedText += "$punctuatedText "
|
||||
outputText.text = committedText
|
||||
sendToPico("$punctuatedText ")
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the model in background
|
||||
initModel()
|
||||
}
|
||||
|
||||
private fun initModel() {
|
||||
// We look for the folder inside private storage (same logic as MainActivity)
|
||||
val modelPath = File(filesDir, "vosk-model")
|
||||
|
||||
if (!modelPath.exists()) {
|
||||
outputText.text = "Error: Model not found. Please go back and load a model first."
|
||||
micButton.isEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
Thread {
|
||||
try {
|
||||
// Find the actual model folder inside
|
||||
val actualModelDir = modelPath.listFiles()?.firstOrNull { it.isDirectory } ?: modelPath
|
||||
model = Model(actualModelDir.absolutePath)
|
||||
|
||||
runOnUiThread {
|
||||
outputText.append("\n\n> Model Loaded. Ready.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
outputText.text = "Error loading model: ${e.message}"
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun sendToPico(text: String) {
|
||||
if (usbPort == null) return // Safety check
|
||||
|
||||
if (usbPort == null) return
|
||||
try {
|
||||
// Convert text to bytes and send
|
||||
val data = text.toByteArray(Charsets.UTF_8)
|
||||
usbPort?.write(data, 1000) // 1000ms timeout
|
||||
usbPort?.write(text.toByteArray(Charsets.UTF_8), 500)
|
||||
} catch (e: Exception) {
|
||||
outputText.append("\n[Send Failed: ${e.message}]")
|
||||
// Log error
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleListening() {
|
||||
if (model == null) {
|
||||
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
|
||||
// ----------------------------------------------------------------
|
||||
// 4. CLEANUP
|
||||
// ----------------------------------------------------------------
|
||||
private fun checkAudioPermission() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on exit
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
speechService?.shutdown()
|
||||
|
||||
// Close USB
|
||||
try {
|
||||
usbPort?.close()
|
||||
} catch (e: Exception) {
|
||||
// Ignore errors on close
|
||||
}
|
||||
isRecording = false
|
||||
stream?.release()
|
||||
recognizer?.release()
|
||||
try { usbPort?.close() } catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
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")
|
||||