diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index cab135b40..803201151 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -36,11 +36,8 @@ jobs: path: mv3/android/MV3Demo - name: YoloDemo path: Yolo/android - - name: WhisperDemo - path: whisper/android/WhisperApp - require_aar: true - - name: ParakeetDemo - path: parakeet/android/ParakeetApp + - name: AsrDemo + path: asr/android/AsrApp require_aar: true @@ -62,7 +59,7 @@ jobs: uses: gradle/actions/setup-gradle@v4 - name: Download local AAR - if: ${{ inputs.local_aar && (matrix.name == 'LlamaDemo' || matrix.name == 'YoloDemo' || matrix.name == 'WhisperDemo' || matrix.name == 'ParakeetDemo') }} + if: ${{ inputs.local_aar && (matrix.name == 'LlamaDemo' || matrix.name == 'YoloDemo' || matrix.name == 'AsrDemo') }} run: | mkdir -p ${{ matrix.path }}/app/libs curl -fL -o ${{ matrix.path }}/app/libs/executorch.aar "${{ inputs.local_aar }}" @@ -71,7 +68,7 @@ jobs: if: ${{ !matrix.require_aar || inputs.local_aar }} working-directory: ${{ matrix.path }} run: | - if [ -n "${{ inputs.local_aar }}" ] && ([ "${{ matrix.name }}" == "LlamaDemo" ] || [ "${{ matrix.name }}" == "YoloDemo" ] || [ "${{ matrix.name }}" == "WhisperDemo" ] || [ "${{ matrix.name }}" == "ParakeetDemo" ]); then + if [ -n "${{ inputs.local_aar }}" ] && ([ "${{ matrix.name }}" == "LlamaDemo" ] || [ "${{ matrix.name }}" == "YoloDemo" ] || [ "${{ matrix.name }}" == "AsrDemo" ]); then ./gradlew build --no-daemon -PuseLocalAar=true else ./gradlew build --no-daemon diff --git a/parakeet/android/ParakeetApp/.gitignore b/asr/android/AsrApp/.gitignore similarity index 100% rename from parakeet/android/ParakeetApp/.gitignore rename to asr/android/AsrApp/.gitignore diff --git a/asr/android/AsrApp/README.md b/asr/android/AsrApp/README.md new file mode 100644 index 000000000..8a54b7f2b --- /dev/null +++ b/asr/android/AsrApp/README.md @@ -0,0 +1,37 @@ +# ASR Demo App + +This app demonstrates running speech recognition models on Android using ExecuTorch. It supports both **Whisper** and **Parakeet** model families. + +## Supported Models + +| Model | Type | Details | +|-------|------|---------| +| Whisper Tiny/Small/Medium (INT8/INT4) | Streaming | Requires model, tokenizer, and preprocessor | +| Whisper Tiny/Small/Medium (FP32) | Streaming | Requires model, tokenizer, and preprocessor | +| Parakeet TDT 0.6B (INT4) | Synchronous | Requires model and tokenizer | + +## Export Model Files + +- **Whisper**: Follow the instructions at https://github.com/pytorch/executorch/tree/main/examples/models/whisper +- **Parakeet**: Follow the instructions at https://github.com/pytorch/executorch/tree/main/examples/models/parakeet + +## Run the App + +1. Open AsrApp in Android Studio +2. Copy the `executorch.aar` library (with ASR and Parakeet JNI bindings) into `app/libs/` +3. Build and run on device + +## Download Models + +The app includes a built-in download screen to fetch models from HuggingFace. Alternatively, push files manually: + +```bash +adb push model.pte /data/local/tmp/asr/ +adb push tokenizer.json /data/local/tmp/asr/ +adb push whisper_preprocessor.pte /data/local/tmp/asr/ # Whisper only +``` + +## Recording Behavior + +- **Whisper**: Click to start recording; automatically stops after 30 seconds +- **Parakeet**: Click to start recording; click again to stop (no time limit) diff --git a/whisper/android/WhisperApp/app/build.gradle.kts b/asr/android/AsrApp/app/build.gradle.kts similarity index 94% rename from whisper/android/WhisperApp/app/build.gradle.kts rename to asr/android/AsrApp/app/build.gradle.kts index c1f70356c..85075d4e8 100644 --- a/whisper/android/WhisperApp/app/build.gradle.kts +++ b/asr/android/AsrApp/app/build.gradle.kts @@ -7,11 +7,11 @@ plugins { val useLocalAar: Boolean? = (project.findProperty("useLocalAar") as? String)?.toBoolean() android { - namespace = "com.example.whisperapp" + namespace = "com.example.asrapp" compileSdk = 35 defaultConfig { - applicationId = "com.example.whisperapp" + applicationId = "com.example.asrapp" minSdk = 24 targetSdk = 35 versionCode = 1 @@ -56,4 +56,4 @@ dependencies { implementation("org.pytorch:executorch-android:1.1.0") } implementation("com.facebook.fbjni:fbjni:0.5.1") -} \ No newline at end of file +} diff --git a/parakeet/android/ParakeetApp/app/proguard-rules.pro b/asr/android/AsrApp/app/proguard-rules.pro similarity index 100% rename from parakeet/android/ParakeetApp/app/proguard-rules.pro rename to asr/android/AsrApp/app/proguard-rules.pro diff --git a/parakeet/android/ParakeetApp/app/src/main/AndroidManifest.xml b/asr/android/AsrApp/app/src/main/AndroidManifest.xml similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/AndroidManifest.xml rename to asr/android/AsrApp/app/src/main/AndroidManifest.xml diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/MainActivity.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/MainActivity.kt similarity index 81% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/MainActivity.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/MainActivity.kt index 265e0d8c9..e3a136a23 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/MainActivity.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp +package com.example.asrapp import android.Manifest import android.content.pm.PackageManager @@ -43,9 +43,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider -import com.example.whisperapp.ui.theme.WhisperAppTheme +import com.example.asrapp.ui.theme.AsrTheme import org.pytorch.executorch.extension.asr.AsrCallback import org.pytorch.executorch.extension.asr.AsrModule +import org.pytorch.executorch.extension.parakeet.ParakeetModule import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -56,7 +57,7 @@ class MainActivity : ComponentActivity(), AsrCallback { companion object { private const val TAG = "MainActivity" private const val RECORDING_DURATION_MS = 30000L // 30 seconds - // Token lengths to remove from transcription output + // Whisper token lengths to remove from transcription output private const val START_TOKEN_LENGTH = 37 private const val END_TOKEN_LENGTH = 13 } @@ -68,6 +69,7 @@ class MainActivity : ComponentActivity(), AsrCallback { private var statusText by mutableStateOf("") private var currentScreen by mutableStateOf(Screen.MAIN) private var showWavFileDialog by mutableStateOf(false) + private var currentModelType by mutableStateOf(ModelType.PARAKEET) private var isRecording = false private var audioRecord: AudioRecord? = null @@ -106,8 +108,8 @@ class MainActivity : ComponentActivity(), AsrCallback { // Initialize view models viewModel = ViewModelProvider(this)[ModelSettingsViewModel::class.java] - viewModel.setAppStorageDirectory(filesDir.absolutePath) - viewModel.initialize() + viewModel.setAppStorageDirectory("${filesDir.absolutePath}/asr") + viewModel.initialize("/data/local/tmp/asr", modelType = ModelType.PARAKEET) downloadViewModel = ViewModelProvider(this)[ModelDownloadViewModel::class.java] downloadViewModel.initialize(filesDir.absolutePath) @@ -126,7 +128,7 @@ class MainActivity : ComponentActivity(), AsrCallback { } setContent { - WhisperAppTheme { + AsrTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background @@ -146,12 +148,13 @@ class MainActivity : ComponentActivity(), AsrCallback { ) } Screen.MAIN -> { - WhisperScreen( + AsrScreen( buttonText = buttonText, buttonEnabled = buttonEnabled && viewModel.isReadyForInference(), statusText = statusText, transcriptionResult = transcriptionOutput, modelSettings = viewModel.modelSettings, + defaultDirectory = viewModel.defaultDirectory, availableWavFiles = viewModel.availableWavFiles, showWavFileDialog = showWavFileDialog, onRecordClick = { onRecordButtonClick() }, @@ -161,7 +164,7 @@ class MainActivity : ComponentActivity(), AsrCallback { }, onWavFileSelected = { wavPath -> showWavFileDialog = false - runWhisperFromFile(wavPath) + runFromFile(wavPath) }, onWavDialogDismiss = { showWavFileDialog = false }, onSettingsClick = { @@ -174,7 +177,9 @@ class MainActivity : ComponentActivity(), AsrCallback { ModelSettingsScreen( viewModel = viewModel, onBackClick = { currentScreen = Screen.MAIN }, - onDownloadClick = { currentScreen = Screen.DOWNLOAD } + onDownloadClick = { currentScreen = Screen.DOWNLOAD }, + showModelTypeSelector = true, + onModelTypeChanged = { type -> currentModelType = type } ) } } @@ -184,9 +189,17 @@ class MainActivity : ComponentActivity(), AsrCallback { } private fun applyDownloadedModelPaths() { + val preset = downloadViewModel.getSelectedPreset() viewModel.selectModel(downloadViewModel.getModelPath()) viewModel.selectTokenizer(downloadViewModel.getTokenizerPath()) - viewModel.selectPreprocessor(downloadViewModel.getPreprocessorPath()) + val preprocessorPath = downloadViewModel.getPreprocessorPath() + if (preprocessorPath.isNotEmpty()) { + viewModel.selectPreprocessor(preprocessorPath) + } else { + viewModel.clearPreprocessor() + } + viewModel.selectModelType(preset.modelType) + currentModelType = preset.modelType } private fun onRecordButtonClick() { @@ -198,23 +211,18 @@ class MainActivity : ComponentActivity(), AsrCallback { } /** - * Run Whisper inference on the recorded audio file. + * Run inference on a WAV file from disk. */ - private fun runWhisper() { - val wavFile = File(getExternalFilesDir(null), "audio_record.wav") - runWhisperOnWavFile(wavFile.absolutePath) - } - - /** - * Run Whisper inference on a WAV file from /data/local/tmp/whisper. - */ - private fun runWhisperFromFile(wavFilePath: String) { + private fun runFromFile(wavFilePath: String) { buttonEnabled = false statusText = "Loading WAV file..." Thread { try { - runWhisperOnWavFile(wavFilePath) + when (currentModelType) { + ModelType.WHISPER -> runWhisperOnWavFile(wavFilePath) + ModelType.PARAKEET -> runParakeetOnWavFile(wavFilePath) + } } catch (e: Exception) { Log.e(TAG, "Error processing WAV file", e) runOnUiThread { @@ -226,8 +234,18 @@ class MainActivity : ComponentActivity(), AsrCallback { } /** - * Common method to run Whisper on a WAV file path. + * Run inference on the recorded audio file. */ + private fun runInference() { + val wavFile = File(getExternalFilesDir(null), "audio_record.wav") + when (currentModelType) { + ModelType.WHISPER -> runWhisperOnWavFile(wavFile.absolutePath) + ModelType.PARAKEET -> runParakeetOnWavFile(wavFile.absolutePath) + } + } + + // --- Whisper inference --- + private fun runWhisperOnWavFile(wavFilePath: String) { val settings = viewModel.modelSettings @@ -254,7 +272,7 @@ class MainActivity : ComponentActivity(), AsrCallback { preprocessorPath = settings.preprocessorPath.ifBlank { null } ) - Log.v(TAG, "Starting transcribe for: $wavFilePath") + Log.v(TAG, "Starting Whisper transcribe for: $wavFilePath") runOnUiThread { statusText = "Transcribing..." } @@ -262,10 +280,8 @@ class MainActivity : ComponentActivity(), AsrCallback { whisperModule.transcribe(wavFilePath, callback = this@MainActivity) val elapsedTime = System.currentTimeMillis() - startTime val elapsedSeconds = elapsedTime / 1000.0 - Log.v(TAG, "Finished transcribe in ${elapsedSeconds}s") + Log.v(TAG, "Finished Whisper transcribe in ${elapsedSeconds}s") - // Display result in Text view instead of Toast - // hack to remove start and end tokens; ideally the runner should not do callback on these tokens runOnUiThread { val minLength = START_TOKEN_LENGTH + END_TOKEN_LENGTH if (rawTranscriptionOutput.length > minLength) { @@ -284,7 +300,6 @@ class MainActivity : ComponentActivity(), AsrCallback { Log.v(TAG, "Called callback: here's the current output") rawTranscriptionOutput += result runOnUiThread { - // Strip start token prefix for display while transcribing if (rawTranscriptionOutput.length > START_TOKEN_LENGTH) { transcriptionOutput = rawTranscriptionOutput.substring(START_TOKEN_LENGTH) } @@ -292,13 +307,60 @@ class MainActivity : ComponentActivity(), AsrCallback { Log.v(TAG, rawTranscriptionOutput) } + // --- Parakeet inference --- + + private fun runParakeetOnWavFile(wavFilePath: String) { + val settings = viewModel.modelSettings + + if (!settings.isValid()) { + runOnUiThread { + statusText = "Please select model and tokenizer in Settings" + buttonEnabled = true + } + return + } + + runOnUiThread { + transcriptionOutput = "" + statusText = "Loading model..." + buttonText = "Transcribing..." + buttonEnabled = false + } + + val parakeetModule = ParakeetModule( + modelPath = settings.modelPath, + tokenizerPath = settings.tokenizerPath, + dataPath = settings.dataPath.ifBlank { null } + ) + + Log.v(TAG, "Starting Parakeet transcribe for: $wavFilePath") + runOnUiThread { + statusText = "Transcribing..." + } + val startTime = System.currentTimeMillis() + val result = parakeetModule.transcribe(wavFilePath) + val elapsedTime = System.currentTimeMillis() - startTime + val elapsedSeconds = elapsedTime / 1000.0 + Log.v(TAG, "Finished Parakeet transcribe in ${elapsedSeconds}s") + + parakeetModule.close() + + runOnUiThread { + transcriptionOutput = result + statusText = "Transcription complete (%.2fs)".format(elapsedSeconds) + buttonText = "Record" + buttonEnabled = true + } + } + + // --- Recording --- + private fun startRecording() { when { ContextCompat.checkSelfPermission( this, Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED -> { - // Permission already granted, start recording try { audioRecord = AudioRecord( MediaRecorder.AudioSource.MIC, @@ -317,14 +379,18 @@ class MainActivity : ComponentActivity(), AsrCallback { audioRecord?.startRecording() isRecording = true - buttonText = "Recording... (30s)" - buttonEnabled = false + if (currentModelType == ModelType.WHISPER) { + buttonText = "Recording... (30s)" + buttonEnabled = false - // Schedule automatic stop after 30 seconds - stopRecordingRunnable = Runnable { - stopRecording() + // Schedule automatic stop after 30 seconds for Whisper + stopRecordingRunnable = Runnable { + stopRecording() + } + handler.postDelayed(stopRecordingRunnable!!, RECORDING_DURATION_MS) + } else { + buttonText = "Recording... (tap to stop)" } - handler.postDelayed(stopRecordingRunnable!!, RECORDING_DURATION_MS) val pcmFile = File(getExternalFilesDir(null), "audio_record.pcm") @@ -345,7 +411,7 @@ class MainActivity : ComponentActivity(), AsrCallback { runOnUiThread { writeWavFile(pcmFile) statusText = "Recording saved" - runWhisper() + runInference() } } catch (e: IOException) { @@ -376,8 +442,14 @@ class MainActivity : ComponentActivity(), AsrCallback { } private fun stopRecording() { + if (!isRecording) return + isRecording = false + // Cancel any pending auto-stop + stopRecordingRunnable?.let { handler.removeCallbacks(it) } + stopRecordingRunnable = null + try { audioRecord?.stop() audioRecord?.release() @@ -386,8 +458,8 @@ class MainActivity : ComponentActivity(), AsrCallback { } audioRecord = null - buttonText = "Record" - buttonEnabled = true + buttonText = "Processing..." + buttonEnabled = false recordingThread?.join() recordingThread = null @@ -400,7 +472,6 @@ class MainActivity : ComponentActivity(), AsrCallback { val wavOut = FileOutputStream(wavFile) - // Write WAV header for 16-bit mono audio at 16 kHz writeWavHeader(wavOut, pcmData.size.toLong(), sampleRate, 1, 16) wavOut.write(pcmData) wavOut.flush() @@ -521,12 +592,13 @@ class MainActivity : ComponentActivity(), AsrCallback { } @Composable -fun WhisperScreen( +fun AsrScreen( buttonText: String, buttonEnabled: Boolean, statusText: String, transcriptionResult: String, modelSettings: ModelSettings, + defaultDirectory: String, availableWavFiles: List, showWavFileDialog: Boolean, onRecordClick: () -> Unit, @@ -546,7 +618,7 @@ fun WhisperScreen( Spacer(modifier = Modifier.height(32.dp)) Text( - text = "Whisper Demo", + text = "ASR Demo", style = MaterialTheme.typography.headlineMedium ) @@ -567,6 +639,11 @@ fun WhisperScreen( Spacer(modifier = Modifier.height(8.dp)) if (modelSettings.isValid()) { + Text( + text = "Type: ${modelSettings.modelType.name.lowercase() + .replaceFirstChar { it.uppercase() }}", + style = MaterialTheme.typography.bodySmall + ) Text( text = "Model: ${modelSettings.modelPath.substringAfterLast('/')}", style = MaterialTheme.typography.bodySmall @@ -575,17 +652,19 @@ fun WhisperScreen( text = "Tokenizer: ${modelSettings.tokenizerPath.substringAfterLast('/')}", style = MaterialTheme.typography.bodySmall ) - if (modelSettings.hasPreprocessor()) { - Text( - text = "Preprocessor: ${modelSettings.preprocessorPath.substringAfterLast('/')}", - style = MaterialTheme.typography.bodySmall - ) - } else { - Text( - text = "Preprocessor: None (raw WAV mode)", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (modelSettings.modelType == ModelType.WHISPER) { + if (modelSettings.hasPreprocessor()) { + Text( + text = "Preprocessor: ${modelSettings.preprocessorPath.substringAfterLast('/')}", + style = MaterialTheme.typography.bodySmall + ) + } else { + Text( + text = "Preprocessor: None (raw WAV mode)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } if (modelSettings.dataPath.isNotBlank()) { Text( @@ -680,6 +759,7 @@ fun WhisperScreen( if (showWavFileDialog) { WavFileSelectionDialog( files = availableWavFiles, + defaultDirectory = defaultDirectory, onDismiss = onWavDialogDismiss, onSelect = onWavFileSelected ) @@ -689,6 +769,7 @@ fun WhisperScreen( @Composable fun WavFileSelectionDialog( files: List, + defaultDirectory: String, onDismiss: () -> Unit, onSelect: (String) -> Unit ) { @@ -700,10 +781,10 @@ fun WavFileSelectionDialog( text = { if (files.isEmpty()) { Column { - Text("No WAV files found in ${ModelSettings.DEFAULT_DIRECTORY}") + Text("No WAV files found in $defaultDirectory") Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Use adb to push WAV files:\nadb push audio.wav ${ModelSettings.DEFAULT_DIRECTORY}/", + text = "Use adb to push WAV files:\nadb push audio.wav $defaultDirectory/", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelDownloadScreen.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelDownloadScreen.kt similarity index 65% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelDownloadScreen.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelDownloadScreen.kt index b8dd51f2b..526922f99 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelDownloadScreen.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelDownloadScreen.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp +package com.example.asrapp import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -53,7 +53,7 @@ fun ModelDownloadScreen( Spacer(modifier = Modifier.height(24.dp)) - // Preset selection + // Preset selection grouped by model type Surface( modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.surfaceVariant, @@ -66,43 +66,56 @@ fun ModelDownloadScreen( ) Spacer(modifier = Modifier.height(8.dp)) - ModelDownloadViewModel.MODEL_PRESETS.forEachIndexed { index, preset -> - val alreadyDownloaded = downloadViewModel.isPresetDownloaded(preset) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = index == selectedIndex, - onClick = { - if (!isDownloading) { - downloadViewModel.selectPreset(index) - downloadViewModel.resetStatus() - } - }, - enabled = !isDownloading + // Group presets by model type + val groupedPresets = ModelDownloadViewModel.MODEL_PRESETS + .withIndex() + .groupBy { it.value.modelType } + + // Whisper Models section + groupedPresets[ModelType.WHISPER]?.let { whisperPresets -> + Text( + text = "Whisper Models", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp) + ) + + whisperPresets.forEach { (index, preset) -> + PresetRow( + preset = preset, + isSelected = index == selectedIndex, + isDownloaded = downloadViewModel.isPresetDownloaded(preset), + isDownloading = isDownloading, + onSelect = { + downloadViewModel.selectPreset(index) + downloadViewModel.resetStatus() + } ) - Spacer(modifier = Modifier.width(4.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = preset.displayName, - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = preset.modelFile.filename, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (alreadyDownloaded) { - Text( - text = "✓", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } + Spacer(modifier = Modifier.height(4.dp)) } - if (index < ModelDownloadViewModel.MODEL_PRESETS.size - 1) { + } + + // Parakeet Models section + groupedPresets[ModelType.PARAKEET]?.let { parakeetPresets -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Parakeet Models", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp) + ) + + parakeetPresets.forEach { (index, preset) -> + PresetRow( + preset = preset, + isSelected = index == selectedIndex, + isDownloaded = downloadViewModel.isPresetDownloaded(preset), + isDownloading = isDownloading, + onSelect = { + downloadViewModel.selectPreset(index) + downloadViewModel.resetStatus() + } + ) Spacer(modifier = Modifier.height(4.dp)) } } @@ -124,15 +137,14 @@ fun ModelDownloadScreen( ) Spacer(modifier = Modifier.height(8.dp)) - // Shared files - ModelDownloadViewModel.SHARED_FILES.forEach { fileInfo -> - FileStatusRow(fileInfo.description, fileInfo.filename, downloadViewModel) - Spacer(modifier = Modifier.height(4.dp)) - } - - // Model-specific file val preset = downloadViewModel.getSelectedPreset() - FileStatusRow(preset.modelFile.description, preset.modelFile.filename, downloadViewModel) + + preset.files.forEachIndexed { index, file -> + FileStatusRow(file.description, file.filename, downloadViewModel) + if (index < preset.files.size - 1) { + Spacer(modifier = Modifier.height(4.dp)) + } + } // Download progress if (isDownloading) { @@ -227,6 +239,46 @@ fun ModelDownloadScreen( } } +@Composable +private fun PresetRow( + preset: AsrModelPreset, + isSelected: Boolean, + isDownloaded: Boolean, + isDownloading: Boolean, + onSelect: () -> Unit +) { + val modelFile = preset.files.first { it.type == FileType.MODEL } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = { if (!isDownloading) onSelect() }, + enabled = !isDownloading + ) + Spacer(modifier = Modifier.width(4.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = preset.displayName, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = modelFile.filename, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (isDownloaded) { + Text( + text = "\u2713", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + } +} + @Composable private fun FileStatusRow( description: String, @@ -252,9 +304,9 @@ private fun FileStatusRow( } Text( text = when { - fileExists || status == DownloadStatus.COMPLETED -> "✓" - status == DownloadStatus.DOWNLOADING && currentFileName == filename -> "⬇" - else -> "○" + fileExists || status == DownloadStatus.COMPLETED -> "\u2713" + status == DownloadStatus.DOWNLOADING && currentFileName == filename -> "\u2B07" + else -> "\u25CB" }, style = MaterialTheme.typography.bodyMedium ) diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelDownloadViewModel.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelDownloadViewModel.kt similarity index 51% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelDownloadViewModel.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelDownloadViewModel.kt index 37cd1ba23..e80a808bf 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelDownloadViewModel.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelDownloadViewModel.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp +package com.example.asrapp import android.util.Log import androidx.compose.runtime.getValue @@ -16,16 +16,20 @@ import java.io.FileOutputStream import java.net.HttpURLConnection import java.net.URL -data class ModelFileInfo( +enum class FileType { MODEL, TOKENIZER, PREPROCESSOR } + +data class PresetFile( val url: String, val filename: String, - val description: String + val description: String, + val type: FileType ) -data class WhisperModelPreset( +data class AsrModelPreset( val id: String, val displayName: String, - val modelFile: ModelFileInfo + val modelType: ModelType, + val files: List ) enum class DownloadStatus { @@ -39,8 +43,9 @@ class ModelDownloadViewModel : ViewModel() { companion object { private const val TAG = "ModelDownloadViewModel" - private const val MODELS_SUBDIRECTORY = "whisper" + private const val MODELS_SUBDIRECTORY = "asr" + // Whisper URLs private const val TINY_INT8_URL = "https://huggingface.co/larryliu0820/whisper-tiny-INT8-INT4-ExecuTorch-XNNPACK/resolve/main" private const val SMALL_INT8_URL = @@ -54,72 +59,134 @@ class ModelDownloadViewModel : ViewModel() { private const val MEDIUM_FP32_URL = "https://huggingface.co/larryliu0820/whisper-medium-ExecuTorch-XNNPACK/resolve/main" - val SHARED_FILES = listOf( - ModelFileInfo( - url = "$TINY_INT8_URL/tokenizer.json", - filename = "tokenizer.json", - description = "Tokenizer" - ), - ModelFileInfo( - url = "$TINY_INT8_URL/whisper_preprocessor.pte", - filename = "whisper_preprocessor.pte", - description = "Preprocessor" - ) + // Parakeet URLs + private const val PARAKEET_BASE_URL = + "https://huggingface.co/larryliu0820/parakeet-tdt-0.6b-v3-executorch/resolve/main/xnnpack/int4" + + // Shared Whisper files (tokenizer + preprocessor) + private val WHISPER_TOKENIZER = PresetFile( + url = "$TINY_INT8_URL/tokenizer.json", + filename = "tokenizer.json", + description = "Tokenizer", + type = FileType.TOKENIZER + ) + private val WHISPER_PREPROCESSOR = PresetFile( + url = "$TINY_INT8_URL/whisper_preprocessor.pte", + filename = "whisper_preprocessor.pte", + description = "Preprocessor", + type = FileType.PREPROCESSOR ) val MODEL_PRESETS = listOf( - WhisperModelPreset( - id = "tiny_int8", + // Whisper presets + AsrModelPreset( + id = "whisper_tiny_int8", displayName = "Whisper Tiny (INT8/INT4)", - modelFile = ModelFileInfo( - url = "$TINY_INT8_URL/model.pte", - filename = "whisper_tiny_int8int4.pte", - description = "Whisper Tiny INT8/INT4 Model" + modelType = ModelType.WHISPER, + files = listOf( + PresetFile( + url = "$TINY_INT8_URL/model.pte", + filename = "whisper_tiny_int8int4.pte", + description = "Whisper Tiny INT8/INT4 Model", + type = FileType.MODEL + ), + WHISPER_TOKENIZER, + WHISPER_PREPROCESSOR ) ), - WhisperModelPreset( - id = "small_int8", + AsrModelPreset( + id = "whisper_small_int8", displayName = "Whisper Small (INT8/INT4)", - modelFile = ModelFileInfo( - url = "$SMALL_INT8_URL/model.pte", - filename = "whisper_small_int8int4.pte", - description = "Whisper Small INT8/INT4 Model" + modelType = ModelType.WHISPER, + files = listOf( + PresetFile( + url = "$SMALL_INT8_URL/model.pte", + filename = "whisper_small_int8int4.pte", + description = "Whisper Small INT8/INT4 Model", + type = FileType.MODEL + ), + WHISPER_TOKENIZER, + WHISPER_PREPROCESSOR ) ), - WhisperModelPreset( - id = "medium_int8", + AsrModelPreset( + id = "whisper_medium_int8", displayName = "Whisper Medium (INT8/INT4)", - modelFile = ModelFileInfo( - url = "$MEDIUM_INT8_URL/model.pte", - filename = "whisper_medium_int8int4.pte", - description = "Whisper Medium INT8/INT4 Model" + modelType = ModelType.WHISPER, + files = listOf( + PresetFile( + url = "$MEDIUM_INT8_URL/model.pte", + filename = "whisper_medium_int8int4.pte", + description = "Whisper Medium INT8/INT4 Model", + type = FileType.MODEL + ), + WHISPER_TOKENIZER, + WHISPER_PREPROCESSOR ) ), - WhisperModelPreset( - id = "tiny_fp32", + AsrModelPreset( + id = "whisper_tiny_fp32", displayName = "Whisper Tiny (FP32)", - modelFile = ModelFileInfo( - url = "$TINY_FP32_URL/model.pte", - filename = "whisper_tiny_fp32.pte", - description = "Whisper Tiny FP32 Model" + modelType = ModelType.WHISPER, + files = listOf( + PresetFile( + url = "$TINY_FP32_URL/model.pte", + filename = "whisper_tiny_fp32.pte", + description = "Whisper Tiny FP32 Model", + type = FileType.MODEL + ), + WHISPER_TOKENIZER, + WHISPER_PREPROCESSOR ) ), - WhisperModelPreset( - id = "small_fp32", + AsrModelPreset( + id = "whisper_small_fp32", displayName = "Whisper Small (FP32)", - modelFile = ModelFileInfo( - url = "$SMALL_FP32_URL/model.pte", - filename = "whisper_small_fp32.pte", - description = "Whisper Small FP32 Model" + modelType = ModelType.WHISPER, + files = listOf( + PresetFile( + url = "$SMALL_FP32_URL/model.pte", + filename = "whisper_small_fp32.pte", + description = "Whisper Small FP32 Model", + type = FileType.MODEL + ), + WHISPER_TOKENIZER, + WHISPER_PREPROCESSOR ) ), - WhisperModelPreset( - id = "medium_fp32", + AsrModelPreset( + id = "whisper_medium_fp32", displayName = "Whisper Medium (FP32)", - modelFile = ModelFileInfo( - url = "$MEDIUM_FP32_URL/model.pte", - filename = "whisper_medium_fp32.pte", - description = "Whisper Medium FP32 Model" + modelType = ModelType.WHISPER, + files = listOf( + PresetFile( + url = "$MEDIUM_FP32_URL/model.pte", + filename = "whisper_medium_fp32.pte", + description = "Whisper Medium FP32 Model", + type = FileType.MODEL + ), + WHISPER_TOKENIZER, + WHISPER_PREPROCESSOR + ) + ), + // Parakeet preset + AsrModelPreset( + id = "parakeet_int4", + displayName = "Parakeet TDT 0.6B (INT4)", + modelType = ModelType.PARAKEET, + files = listOf( + PresetFile( + url = "$PARAKEET_BASE_URL/model.pte", + filename = "parakeet_int4.pte", + description = "Parakeet TDT 0.6B INT4 Model", + type = FileType.MODEL + ), + PresetFile( + url = "$PARAKEET_BASE_URL/tokenizer.model", + filename = "parakeet_tokenizer.model", + description = "Tokenizer", + type = FileType.TOKENIZER + ) ) ) ) @@ -158,16 +225,25 @@ class ModelDownloadViewModel : ViewModel() { selectedPresetIndex = index } - fun getSelectedPreset(): WhisperModelPreset = MODEL_PRESETS[selectedPresetIndex] + fun getSelectedPreset(): AsrModelPreset = MODEL_PRESETS[selectedPresetIndex] + + fun getModelPath(): String { + val file = getSelectedPreset().files.first { it.type == FileType.MODEL } + return "$modelsDir/${file.filename}" + } + + fun getTokenizerPath(): String { + val file = getSelectedPreset().files.first { it.type == FileType.TOKENIZER } + return "$modelsDir/${file.filename}" + } - fun getModelPath(): String = "$modelsDir/${getSelectedPreset().modelFile.filename}" - fun getTokenizerPath(): String = "$modelsDir/${SHARED_FILES[0].filename}" - fun getPreprocessorPath(): String = "$modelsDir/${SHARED_FILES[1].filename}" + fun getPreprocessorPath(): String { + val file = getSelectedPreset().files.firstOrNull { it.type == FileType.PREPROCESSOR } + return if (file != null) "$modelsDir/${file.filename}" else "" + } - fun isPresetDownloaded(preset: WhisperModelPreset): Boolean { - val modelExists = File("$modelsDir/${preset.modelFile.filename}").exists() - val sharedExist = SHARED_FILES.all { File("$modelsDir/${it.filename}").exists() } - return modelExists && sharedExist + fun isPresetDownloaded(preset: AsrModelPreset): Boolean { + return preset.files.all { File("$modelsDir/${it.filename}").exists() } } fun downloadSelectedPreset() { @@ -175,18 +251,8 @@ class ModelDownloadViewModel : ViewModel() { val preset = getSelectedPreset() - val filesToDownload = mutableListOf() - - // Add shared files if not already present - for (shared in SHARED_FILES) { - if (!File("$modelsDir/${shared.filename}").exists()) { - filesToDownload.add(shared) - } - } - - // Add model file if not already present - if (!File("$modelsDir/${preset.modelFile.filename}").exists()) { - filesToDownload.add(preset.modelFile) + val filesToDownload = preset.files.filter { file -> + !File("$modelsDir/${file.filename}").exists() } if (filesToDownload.isEmpty()) { @@ -235,7 +301,7 @@ class ModelDownloadViewModel : ViewModel() { } private suspend fun downloadFile( - fileInfo: ModelFileInfo, + fileInfo: PresetFile, targetFile: File ): Boolean = withContext(Dispatchers.IO) { try { diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettings.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettings.kt similarity index 67% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettings.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettings.kt index 74c6e816a..23bffdd4d 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettings.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettings.kt @@ -1,19 +1,29 @@ -package com.example.whisperapp +package com.example.asrapp /** - * Data class representing the model file settings for Whisper inference. + * The type of ASR model being used, which determines inference behavior. + */ +enum class ModelType { + WHISPER, + PARAKEET +} + +/** + * Data class representing the model file settings for ASR inference. * - * @param modelPath Path to the main Whisper model (.pte file) - * @param tokenizerPath Path to the tokenizer file (.json or .bin file) + * @param modelPath Path to the main model (.pte file) + * @param tokenizerPath Path to the tokenizer file (.json, .bin, or .model file) * @param preprocessorPath Optional path to the preprocessor model (.pte file). * If empty, raw WAV audio will be used directly. * @param dataPath Optional path to external data file (.ptd file) + * @param modelType The type of ASR model (WHISPER or PARAKEET) */ data class ModelSettings( val modelPath: String = "", val tokenizerPath: String = "", val preprocessorPath: String = "", - val dataPath: String = "" + val dataPath: String = "", + val modelType: ModelType = ModelType.PARAKEET ) { /** * Check if the minimum required files are set for inference. @@ -32,7 +42,6 @@ data class ModelSettings( } companion object { - const val DEFAULT_DIRECTORY = "/data/local/tmp/whisper" val MODEL_EXTENSIONS = arrayOf(".pte") val TOKENIZER_EXTENSIONS = arrayOf(".json", ".bin", ".model") val DATA_EXTENSIONS = arrayOf(".ptd") diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettingsScreen.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettingsScreen.kt similarity index 72% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettingsScreen.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettingsScreen.kt index 0f582b268..dd09269bd 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettingsScreen.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettingsScreen.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp +package com.example.asrapp import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,18 +12,28 @@ import androidx.compose.ui.unit.dp /** * Settings screen for selecting model files. + * + * @param showPreprocessor Whether to show the preprocessor file selection row. + * Defaults to auto-deriving from the current model type. + * @param showModelTypeSelector Whether to show the model type selector (Whisper/Parakeet). + * @param onModelTypeChanged Callback when the user changes the model type. */ @Composable fun ModelSettingsScreen( viewModel: ModelSettingsViewModel, onBackClick: () -> Unit, - onDownloadClick: () -> Unit + onDownloadClick: () -> Unit, + showPreprocessor: Boolean = viewModel.modelSettings.modelType == ModelType.WHISPER, + showModelTypeSelector: Boolean = false, + onModelTypeChanged: ((ModelType) -> Unit)? = null ) { var showModelDialog by remember { mutableStateOf(false) } var showTokenizerDialog by remember { mutableStateOf(false) } var showPreprocessorDialog by remember { mutableStateOf(false) } var showDataDialog by remember { mutableStateOf(false) } + val defaultDirectory = viewModel.defaultDirectory + Column( modifier = Modifier .fillMaxSize() @@ -37,6 +47,41 @@ fun ModelSettingsScreen( modifier = Modifier.padding(bottom = 24.dp) ) + // Model type selector + if (showModelTypeSelector) { + Text( + text = "Model Type", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + ModelType.entries.forEach { type -> + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = viewModel.modelSettings.modelType == type, + onClick = { + viewModel.selectModelType(type) + onModelTypeChanged?.invoke(type) + } + ) + Text( + text = type.name.lowercase() + .replaceFirstChar { it.uppercase() }, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + } + // Model file selection FileSelectionRow( label = "Model File (.pte)", @@ -63,21 +108,23 @@ fun ModelSettingsScreen( Spacer(modifier = Modifier.height(16.dp)) - // Preprocessor file selection (optional) - FileSelectionRow( - label = "Preprocessor (.pte) - Optional", - selectedPath = viewModel.modelSettings.preprocessorPath, - required = false, - onClick = { - viewModel.refreshFileLists() - showPreprocessorDialog = true - }, - onClear = if (viewModel.modelSettings.hasPreprocessor()) { - { viewModel.clearPreprocessor() } - } else null - ) + // Preprocessor file selection (optional, Whisper only) + if (showPreprocessor) { + FileSelectionRow( + label = "Preprocessor (.pte) - Optional", + selectedPath = viewModel.modelSettings.preprocessorPath, + required = false, + onClick = { + viewModel.refreshFileLists() + showPreprocessorDialog = true + }, + onClear = if (viewModel.modelSettings.hasPreprocessor()) { + { viewModel.clearPreprocessor() } + } else null + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) + } // Data file selection (optional) FileSelectionRow( @@ -108,13 +155,13 @@ fun ModelSettingsScreen( // Status indicator if (viewModel.isReadyForInference()) { Text( - text = "✓ Ready for inference", + text = "\u2713 Ready for inference", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary ) } else { Text( - text = "⚠ Model and Tokenizer are required", + text = "\u26A0 Model and Tokenizer are required", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error ) @@ -133,25 +180,27 @@ fun ModelSettingsScreen( // Info text Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Scanning: ${ModelSettings.DEFAULT_DIRECTORY} and app storage", + text = "Scanning: $defaultDirectory and app storage", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) - if (viewModel.modelSettings.hasPreprocessor()) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Preprocessor will convert WAV to mel-spectrogram", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "No preprocessor: WAV audio will be used directly", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (showPreprocessor) { + if (viewModel.modelSettings.hasPreprocessor()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Preprocessor will convert WAV to mel-spectrogram", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "No preprocessor: WAV audio will be used directly", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } @@ -161,6 +210,7 @@ fun ModelSettingsScreen( title = "Select Model File", files = viewModel.availableModels, currentSelection = viewModel.modelSettings.modelPath, + defaultDirectory = defaultDirectory, onDismiss = { showModelDialog = false }, onSelect = { viewModel.selectModel(it) @@ -174,6 +224,7 @@ fun ModelSettingsScreen( title = "Select Tokenizer File", files = viewModel.availableTokenizers, currentSelection = viewModel.modelSettings.tokenizerPath, + defaultDirectory = defaultDirectory, onDismiss = { showTokenizerDialog = false }, onSelect = { viewModel.selectTokenizer(it) @@ -182,11 +233,12 @@ fun ModelSettingsScreen( ) } - if (showPreprocessorDialog) { + if (showPreprocessor && showPreprocessorDialog) { FileSelectionDialog( title = "Select Preprocessor (Optional)", files = viewModel.availablePreprocessors, currentSelection = viewModel.modelSettings.preprocessorPath, + defaultDirectory = defaultDirectory, onDismiss = { showPreprocessorDialog = false }, onSelect = { viewModel.selectPreprocessor(it) @@ -201,6 +253,7 @@ fun ModelSettingsScreen( title = "Select Data File (Optional)", files = viewModel.availableDataFiles, currentSelection = viewModel.modelSettings.dataPath, + defaultDirectory = defaultDirectory, onDismiss = { showDataDialog = false }, onSelect = { viewModel.selectDataFile(it) @@ -278,6 +331,7 @@ fun FileSelectionDialog( title: String, files: List, currentSelection: String, + defaultDirectory: String, onDismiss: () -> Unit, onSelect: (String) -> Unit, allowNone: Boolean = false @@ -288,10 +342,10 @@ fun FileSelectionDialog( text = { if (files.isEmpty()) { Column { - Text("No files found. Download from the setup screen or use adb push to ${ModelSettings.DEFAULT_DIRECTORY}") + Text("No files found. Download from the setup screen or use adb push to $defaultDirectory") Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Use adb to push files:\nadb push ${ModelSettings.DEFAULT_DIRECTORY}/", + text = "Use adb to push files:\nadb push $defaultDirectory/", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettingsViewModel.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettingsViewModel.kt similarity index 70% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettingsViewModel.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettingsViewModel.kt index 46ef6209b..e6f341061 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ModelSettingsViewModel.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ModelSettingsViewModel.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp +package com.example.asrapp import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,20 +33,43 @@ class ModelSettingsViewModel : ViewModel() { var errorMessage by mutableStateOf(null) private set + var defaultDirectory: String = "" + private set + + private var supportsPreprocessor: Boolean = false private var appStorageDirectory: String? = null /** - * Initialize the ViewModel by scanning for available files. + * Initialize the ViewModel with app-specific configuration. + * + * @param defaultDirectory The default directory to scan for model files + * (e.g., "/data/local/tmp/asr") + * @param modelType The type of ASR model, used to derive preprocessor support + */ + fun initialize(defaultDirectory: String, modelType: ModelType = ModelType.PARAKEET) { + this.defaultDirectory = defaultDirectory + this.supportsPreprocessor = (modelType == ModelType.WHISPER) + modelSettings = modelSettings.copy(modelType = modelType) + refreshFileLists() + } + + /** + * Change the model type and update preprocessor support accordingly. */ - fun initialize() { + fun selectModelType(type: ModelType) { + modelSettings = modelSettings.copy(modelType = type) + supportsPreprocessor = (type == ModelType.WHISPER) refreshFileLists() } /** - * Set the app-internal storage directory (filesDir/whisper) so we can scan it too. + * Set the app-internal storage directory so we can scan it too. + * + * @param dir The full path to the app storage directory + * (e.g., "${filesDir}/asr") */ - fun setAppStorageDirectory(filesDir: String) { - appStorageDirectory = "$filesDir/whisper" + fun setAppStorageDirectory(dir: String) { + appStorageDirectory = dir refreshFileLists() } @@ -55,13 +78,15 @@ class ModelSettingsViewModel : ViewModel() { */ fun refreshFileLists() { val directories = buildList { - add(ModelSettings.DEFAULT_DIRECTORY) + if (defaultDirectory.isNotEmpty()) add(defaultDirectory) appStorageDirectory?.let { add(it) } } availableModels = listLocalFilesFromDirs(directories, ModelSettings.MODEL_EXTENSIONS) availableTokenizers = listLocalFilesFromDirs(directories, ModelSettings.TOKENIZER_EXTENSIONS) - availablePreprocessors = listLocalFilesFromDirs(directories, ModelSettings.MODEL_EXTENSIONS) + if (supportsPreprocessor) { + availablePreprocessors = listLocalFilesFromDirs(directories, ModelSettings.MODEL_EXTENSIONS) + } availableDataFiles = listLocalFilesFromDirs(directories, ModelSettings.DATA_EXTENSIONS) availableWavFiles = listLocalFilesFromDirs(directories, WAV_EXTENSIONS) diff --git a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ui/theme/Theme.kt b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ui/theme/AsrTheme.kt similarity index 93% rename from whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ui/theme/Theme.kt rename to asr/android/AsrApp/app/src/main/java/com/example/asrapp/ui/theme/AsrTheme.kt index 39b806169..01224ce42 100644 --- a/whisper/android/WhisperApp/app/src/main/java/com/example/whisperapp/ui/theme/Theme.kt +++ b/asr/android/AsrApp/app/src/main/java/com/example/asrapp/ui/theme/AsrTheme.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp.ui.theme +package com.example.asrapp.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme @@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext @Composable -fun WhisperAppTheme( +fun AsrTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/asr/android/AsrApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to asr/android/AsrApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/asr/android/AsrApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to asr/android/AsrApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/asr/android/AsrApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/asr/android/AsrApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/asr/android/AsrApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/asr/android/AsrApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/asr/android/AsrApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/asr/android/AsrApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/asr/android/AsrApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/asr/android/AsrApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/asr/android/AsrApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/asr/android/AsrApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from parakeet/android/ParakeetApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to asr/android/AsrApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/asr/android/AsrApp/app/src/main/res/values/strings.xml b/asr/android/AsrApp/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..2ff600bb9 --- /dev/null +++ b/asr/android/AsrApp/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + ASR App + diff --git a/whisper/android/WhisperApp/app/src/test/java/com/example/whisperapp/ModelSettingsTest.kt b/asr/android/AsrApp/app/src/test/java/com/example/asrapp/ModelSettingsTest.kt similarity index 55% rename from whisper/android/WhisperApp/app/src/test/java/com/example/whisperapp/ModelSettingsTest.kt rename to asr/android/AsrApp/app/src/test/java/com/example/asrapp/ModelSettingsTest.kt index 4060459d3..58fbfe9e9 100644 --- a/whisper/android/WhisperApp/app/src/test/java/com/example/whisperapp/ModelSettingsTest.kt +++ b/asr/android/AsrApp/app/src/test/java/com/example/asrapp/ModelSettingsTest.kt @@ -1,4 +1,4 @@ -package com.example.whisperapp +package com.example.asrapp import org.junit.Assert.* import org.junit.Test @@ -12,7 +12,7 @@ class ModelSettingsTest { fun `isValid returns false when model path is empty`() { val settings = ModelSettings( modelPath = "", - tokenizerPath = "/data/local/tmp/whisper/tokenizer.json" + tokenizerPath = "/data/local/tmp/asr/tokenizer.json" ) assertFalse(settings.isValid()) } @@ -20,7 +20,7 @@ class ModelSettingsTest { @Test fun `isValid returns false when tokenizer path is empty`() { val settings = ModelSettings( - modelPath = "/data/local/tmp/whisper/model.pte", + modelPath = "/data/local/tmp/asr/model.pte", tokenizerPath = "" ) assertFalse(settings.isValid()) @@ -35,8 +35,8 @@ class ModelSettingsTest { @Test fun `isValid returns true when model and tokenizer are set`() { val settings = ModelSettings( - modelPath = "/data/local/tmp/whisper/model.pte", - tokenizerPath = "/data/local/tmp/whisper/tokenizer.json" + modelPath = "/data/local/tmp/asr/model.pte", + tokenizerPath = "/data/local/tmp/asr/tokenizer.json" ) assertTrue(settings.isValid()) } @@ -44,18 +44,28 @@ class ModelSettingsTest { @Test fun `isValid returns true even without preprocessor`() { val settings = ModelSettings( - modelPath = "/data/local/tmp/whisper/model.pte", - tokenizerPath = "/data/local/tmp/whisper/tokenizer.json", + modelPath = "/data/local/tmp/asr/model.pte", + tokenizerPath = "/data/local/tmp/asr/tokenizer.json", preprocessorPath = "" ) assertTrue(settings.isValid()) } + @Test + fun `isValid returns true with data path`() { + val settings = ModelSettings( + modelPath = "/data/local/tmp/asr/model.pte", + tokenizerPath = "/data/local/tmp/asr/tokenizer.model", + dataPath = "/data/local/tmp/asr/data.ptd" + ) + assertTrue(settings.isValid()) + } + @Test fun `hasPreprocessor returns false when preprocessor is empty`() { val settings = ModelSettings( - modelPath = "/data/local/tmp/whisper/model.pte", - tokenizerPath = "/data/local/tmp/whisper/tokenizer.json", + modelPath = "/data/local/tmp/asr/model.pte", + tokenizerPath = "/data/local/tmp/asr/tokenizer.json", preprocessorPath = "" ) assertFalse(settings.hasPreprocessor()) @@ -64,8 +74,8 @@ class ModelSettingsTest { @Test fun `hasPreprocessor returns false when preprocessor is blank`() { val settings = ModelSettings( - modelPath = "/data/local/tmp/whisper/model.pte", - tokenizerPath = "/data/local/tmp/whisper/tokenizer.json", + modelPath = "/data/local/tmp/asr/model.pte", + tokenizerPath = "/data/local/tmp/asr/tokenizer.json", preprocessorPath = " " ) assertFalse(settings.hasPreprocessor()) @@ -74,18 +84,13 @@ class ModelSettingsTest { @Test fun `hasPreprocessor returns true when preprocessor is set`() { val settings = ModelSettings( - modelPath = "/data/local/tmp/whisper/model.pte", - tokenizerPath = "/data/local/tmp/whisper/tokenizer.json", - preprocessorPath = "/data/local/tmp/whisper/preprocess.pte" + modelPath = "/data/local/tmp/asr/model.pte", + tokenizerPath = "/data/local/tmp/asr/tokenizer.json", + preprocessorPath = "/data/local/tmp/asr/preprocess.pte" ) assertTrue(settings.hasPreprocessor()) } - @Test - fun `default directory constant is correct`() { - assertEquals("/data/local/tmp/whisper", ModelSettings.DEFAULT_DIRECTORY) - } - @Test fun `model extensions include pte`() { assertTrue(ModelSettings.MODEL_EXTENSIONS.contains(".pte")) @@ -116,4 +121,38 @@ class ModelSettingsTest { assertEquals("/path/to/preprocess.pte", updated.preprocessorPath) assertEquals("", original.preprocessorPath) // Original unchanged } + + @Test + fun `modelType defaults to PARAKEET`() { + val settings = ModelSettings() + assertEquals(ModelType.PARAKEET, settings.modelType) + } + + @Test + fun `modelType can be set to WHISPER`() { + val settings = ModelSettings(modelType = ModelType.WHISPER) + assertEquals(ModelType.WHISPER, settings.modelType) + } + + @Test + fun `copy preserves modelType`() { + val original = ModelSettings( + modelPath = "/path/to/model.pte", + tokenizerPath = "/path/to/tokenizer.json", + modelType = ModelType.WHISPER + ) + val updated = original.copy(modelPath = "/path/to/other.pte") + + assertEquals(ModelType.WHISPER, updated.modelType) + assertEquals("/path/to/other.pte", updated.modelPath) + } + + @Test + fun `copy can change modelType`() { + val original = ModelSettings(modelType = ModelType.WHISPER) + val updated = original.copy(modelType = ModelType.PARAKEET) + + assertEquals(ModelType.WHISPER, original.modelType) + assertEquals(ModelType.PARAKEET, updated.modelType) + } } diff --git a/whisper/android/WhisperApp/app/src/test/java/com/example/whisperapp/ModelSettingsViewModelTest.kt b/asr/android/AsrApp/app/src/test/java/com/example/asrapp/ModelSettingsViewModelTest.kt similarity index 98% rename from whisper/android/WhisperApp/app/src/test/java/com/example/whisperapp/ModelSettingsViewModelTest.kt rename to asr/android/AsrApp/app/src/test/java/com/example/asrapp/ModelSettingsViewModelTest.kt index f1ccb2766..413c5496b 100644 --- a/whisper/android/WhisperApp/app/src/test/java/com/example/whisperapp/ModelSettingsViewModelTest.kt +++ b/asr/android/AsrApp/app/src/test/java/com/example/asrapp/ModelSettingsViewModelTest.kt @@ -1,8 +1,7 @@ -package com.example.whisperapp +package com.example.asrapp import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder diff --git a/parakeet/android/ParakeetApp/build.gradle.kts b/asr/android/AsrApp/build.gradle.kts similarity index 100% rename from parakeet/android/ParakeetApp/build.gradle.kts rename to asr/android/AsrApp/build.gradle.kts diff --git a/parakeet/android/ParakeetApp/gradle.properties b/asr/android/AsrApp/gradle.properties similarity index 100% rename from parakeet/android/ParakeetApp/gradle.properties rename to asr/android/AsrApp/gradle.properties diff --git a/parakeet/android/ParakeetApp/gradle/libs.versions.toml b/asr/android/AsrApp/gradle/libs.versions.toml similarity index 99% rename from parakeet/android/ParakeetApp/gradle/libs.versions.toml rename to asr/android/AsrApp/gradle/libs.versions.toml index caafb09e9..b03b80b75 100644 --- a/parakeet/android/ParakeetApp/gradle/libs.versions.toml +++ b/asr/android/AsrApp/gradle/libs.versions.toml @@ -31,4 +31,3 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } - diff --git a/parakeet/android/ParakeetApp/gradle/wrapper/gradle-wrapper.jar b/asr/android/AsrApp/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from parakeet/android/ParakeetApp/gradle/wrapper/gradle-wrapper.jar rename to asr/android/AsrApp/gradle/wrapper/gradle-wrapper.jar diff --git a/parakeet/android/ParakeetApp/gradle/wrapper/gradle-wrapper.properties b/asr/android/AsrApp/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from parakeet/android/ParakeetApp/gradle/wrapper/gradle-wrapper.properties rename to asr/android/AsrApp/gradle/wrapper/gradle-wrapper.properties diff --git a/parakeet/android/ParakeetApp/gradlew b/asr/android/AsrApp/gradlew similarity index 100% rename from parakeet/android/ParakeetApp/gradlew rename to asr/android/AsrApp/gradlew diff --git a/parakeet/android/ParakeetApp/gradlew.bat b/asr/android/AsrApp/gradlew.bat similarity index 100% rename from parakeet/android/ParakeetApp/gradlew.bat rename to asr/android/AsrApp/gradlew.bat diff --git a/whisper/android/WhisperApp/settings.gradle.kts b/asr/android/AsrApp/settings.gradle.kts similarity index 93% rename from whisper/android/WhisperApp/settings.gradle.kts rename to asr/android/AsrApp/settings.gradle.kts index 9b591175b..5c1aef0aa 100644 --- a/whisper/android/WhisperApp/settings.gradle.kts +++ b/asr/android/AsrApp/settings.gradle.kts @@ -19,6 +19,5 @@ dependencyResolutionManagement { } } -rootProject.name = "Whisper App" +rootProject.name = "ASR App" include(":app") - \ No newline at end of file diff --git a/parakeet/android/ParakeetApp/README.md b/parakeet/android/ParakeetApp/README.md deleted file mode 100644 index 41575f414..000000000 --- a/parakeet/android/ParakeetApp/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Parakeet Demo App - -This app demonstrates running the Parakeet TDT speech recognition model on Android using ExecuTorch. - -## Download ExecuTorch AAR - -Download the prebuilt ExecuTorch AAR (with Parakeet JNI bindings) and place it in `app/libs/`: - -```bash -mkdir -p app/libs -curl -L -o app/libs/executorch.aar https://gha-artifacts.s3.amazonaws.com/pytorch/executorch/21934561658/artifacts/executorch.aar -``` - -## Export Model Files - -Export the model `.pte` and tokenizer files following the instructions at: -https://github.com/pytorch/executorch/tree/main/examples/models/parakeet - -This app requires a model `.pte` and a tokenizer `.model` file. - -## Run the App - -1. Download the ExecuTorch AAR (see above) -2. Open ParakeetApp in Android Studio -3. Build and run on device - -## Download Models - -The app includes a built-in download screen to fetch the Parakeet TDT 0.6B (INT4) model from HuggingFace: -- Model: `parakeet_int4.pte` -- Tokenizer: `tokenizer.model` - -Alternatively, push files manually: -```bash -adb push model.pte /data/local/tmp/parakeet/ -adb push tokenizer.model /data/local/tmp/parakeet/ -``` diff --git a/parakeet/android/ParakeetApp/app/.gitignore b/parakeet/android/ParakeetApp/app/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/parakeet/android/ParakeetApp/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/parakeet/android/ParakeetApp/app/build.gradle.kts b/parakeet/android/ParakeetApp/app/build.gradle.kts deleted file mode 100644 index 4a5b06de3..000000000 --- a/parakeet/android/ParakeetApp/app/build.gradle.kts +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) -} - -val useLocalAar: Boolean? = (project.findProperty("useLocalAar") as? String)?.toBoolean() - -android { - namespace = "com.example.parakeetapp" - compileSdk = 35 - - defaultConfig { - applicationId = "com.example.parakeetapp" - minSdk = 24 - targetSdk = 35 - versionCode = 1 - versionName = "1.0" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } - buildFeatures { - compose = true - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - testImplementation(libs.junit) - debugImplementation(libs.androidx.ui.tooling) - if (useLocalAar == true) { - implementation(files("libs/executorch.aar")) - } else { - implementation("org.pytorch:executorch-android:1.1.0") - } - implementation("com.facebook.fbjni:fbjni:0.5.1") -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/MainActivity.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/MainActivity.kt deleted file mode 100644 index 183a9f907..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/MainActivity.kt +++ /dev/null @@ -1,735 +0,0 @@ -package com.example.parakeetapp - -import android.Manifest -import android.content.pm.PackageManager -import android.media.AudioFormat -import android.media.AudioRecord -import android.media.MediaRecorder -import android.os.Bundle -import android.system.ErrnoException -import android.system.Os -import android.util.Log -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider -import com.example.parakeetapp.ui.theme.ParakeetAppTheme -import org.pytorch.executorch.extension.parakeet.ParakeetModule -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.OutputStream - -class MainActivity : ComponentActivity() { - - companion object { - private const val TAG = "MainActivity" - } - - private var transcriptionOutput by mutableStateOf("") - private var buttonText by mutableStateOf("Hold to Record") - private var buttonEnabled by mutableStateOf(true) - private var statusText by mutableStateOf("") - private var currentScreen by mutableStateOf(Screen.MAIN) - private var showWavFileDialog by mutableStateOf(false) - - private var isRecording = false - private var audioRecord: AudioRecord? = null - private var recordingThread: Thread? = null - - private val sampleRate = 16000 - private val channelConfig = AudioFormat.CHANNEL_IN_MONO - private val audioFormat = AudioFormat.ENCODING_PCM_16BIT - - private val bufferSize = AudioRecord.getMinBufferSize( - sampleRate, - channelConfig, - audioFormat - ) - - private lateinit var viewModel: ModelSettingsViewModel - private lateinit var downloadViewModel: ModelDownloadViewModel - - enum class Screen { - DOWNLOAD, - MAIN, - SETTINGS - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - try { - Os.setenv("ADSP_LIBRARY_PATH", applicationInfo.nativeLibraryDir, true) - Os.setenv("LD_LIBRARY_PATH", applicationInfo.nativeLibraryDir, true) - } catch (e: ErrnoException) { - finish() - } - - // Initialize view models - viewModel = ViewModelProvider(this)[ModelSettingsViewModel::class.java] - viewModel.setAppStorageDirectory(filesDir.absolutePath) - viewModel.initialize() - - downloadViewModel = ViewModelProvider(this)[ModelDownloadViewModel::class.java] - downloadViewModel.initialize(filesDir.absolutePath) - - // If the first preset is already downloaded, auto-select its paths - val firstPreset = ModelDownloadViewModel.MODEL_PRESETS[0] - if (downloadViewModel.isPresetDownloaded(firstPreset)) { - downloadViewModel.selectPreset(0) - applyDownloadedModelPaths() - } - - // Check if minimum buffer size is valid - if (bufferSize == AudioRecord.ERROR_BAD_VALUE || bufferSize == AudioRecord.ERROR) { - Log.e(TAG, "Invalid buffer size") - statusText = "Audio recording not supported on this device" - } - - setContent { - ParakeetAppTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - when (currentScreen) { - Screen.DOWNLOAD -> { - ModelDownloadScreen( - downloadViewModel = downloadViewModel, - onDownloadComplete = { - applyDownloadedModelPaths() - viewModel.refreshFileLists() - currentScreen = Screen.SETTINGS - }, - onSkip = { - currentScreen = Screen.SETTINGS - } - ) - } - Screen.MAIN -> { - ParakeetScreen( - buttonText = buttonText, - buttonEnabled = buttonEnabled && viewModel.isReadyForInference(), - statusText = statusText, - transcriptionResult = transcriptionOutput, - modelSettings = viewModel.modelSettings, - availableWavFiles = viewModel.availableWavFiles, - showWavFileDialog = showWavFileDialog, - onRecordStart = { startRecording() }, - onRecordStop = { stopRecording() }, - onUseWavFileClick = { - viewModel.refreshFileLists() - showWavFileDialog = true - }, - onWavFileSelected = { wavPath -> - showWavFileDialog = false - runParakeetFromFile(wavPath) - }, - onWavDialogDismiss = { showWavFileDialog = false }, - onSettingsClick = { - viewModel.refreshFileLists() - currentScreen = Screen.SETTINGS - } - ) - } - Screen.SETTINGS -> { - ModelSettingsScreen( - viewModel = viewModel, - onBackClick = { currentScreen = Screen.MAIN }, - onDownloadClick = { currentScreen = Screen.DOWNLOAD } - ) - } - } - } - } - } - } - - private fun applyDownloadedModelPaths() { - viewModel.selectModel(downloadViewModel.getModelPath()) - viewModel.selectTokenizer(downloadViewModel.getTokenizerPath()) - } - - /** - * Run Parakeet inference on the recorded audio file. - */ - private fun runParakeet() { - val wavFile = File(getExternalFilesDir(null), "audio_record.wav") - runParakeetOnWavFile(wavFile.absolutePath) - } - - /** - * Run Parakeet inference on a WAV file. - */ - private fun runParakeetFromFile(wavFilePath: String) { - buttonEnabled = false - statusText = "Loading WAV file..." - - Thread { - try { - runParakeetOnWavFile(wavFilePath) - } catch (e: Exception) { - Log.e(TAG, "Error processing WAV file", e) - runOnUiThread { - statusText = "Error: ${e.message}" - buttonEnabled = true - } - } - }.start() - } - - /** - * Common method to run Parakeet on a WAV file path. - */ - private fun runParakeetOnWavFile(wavFilePath: String) { - val settings = viewModel.modelSettings - - if (!settings.isValid()) { - runOnUiThread { - statusText = "Please select model and tokenizer in Settings" - buttonEnabled = true - } - return - } - - runOnUiThread { - transcriptionOutput = "" - statusText = "Loading model..." - buttonText = "Transcribing..." - buttonEnabled = false - } - - val parakeetModule = ParakeetModule( - modelPath = settings.modelPath, - tokenizerPath = settings.tokenizerPath, - dataPath = settings.dataPath.ifBlank { null } - ) - - Log.v(TAG, "Starting transcribe for: $wavFilePath") - runOnUiThread { - statusText = "Transcribing..." - } - val startTime = System.currentTimeMillis() - val result = parakeetModule.transcribe(wavFilePath) - val elapsedTime = System.currentTimeMillis() - startTime - val elapsedSeconds = elapsedTime / 1000.0 - Log.v(TAG, "Finished transcribe in ${elapsedSeconds}s") - - parakeetModule.close() - - runOnUiThread { - transcriptionOutput = result - statusText = "Transcription complete (%.2fs)".format(elapsedSeconds) - buttonText = "Hold to Record" - buttonEnabled = true - } - } - - private fun startRecording() { - when { - ContextCompat.checkSelfPermission( - this, - Manifest.permission.RECORD_AUDIO - ) == PackageManager.PERMISSION_GRANTED -> { - // Permission already granted, start recording - try { - audioRecord = AudioRecord( - MediaRecorder.AudioSource.MIC, - sampleRate, - channelConfig, - audioFormat, - bufferSize - ) - - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - Log.e(TAG, "AudioRecord initialization failed") - statusText = "Failed to initialize audio recorder" - return - } - - audioRecord?.startRecording() - isRecording = true - - buttonText = "Recording..." - - val pcmFile = File(getExternalFilesDir(null), "audio_record.pcm") - - recordingThread = Thread { - try { - val os = FileOutputStream(pcmFile) - val buffer = ByteArray(bufferSize) - - while (isRecording) { - val read = audioRecord?.read(buffer, 0, buffer.size) ?: 0 - if (read > 0) { - os.write(buffer, 0, read) - } - } - - os.close() - - runOnUiThread { - writeWavFile(pcmFile) - statusText = "Recording saved" - runParakeet() - } - - } catch (e: IOException) { - Log.e(TAG, "Recording failed", e) - runOnUiThread { - statusText = "Recording failed" - } - } - } - - recordingThread?.start() - - } catch (e: Exception) { - Log.e(TAG, "Failed to start recording", e) - statusText = "Failed to start recording" - } - } - - shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO) -> { - statusText = "Audio recording permission is needed to record audio" - requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) - } - - else -> { - requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) - } - } - } - - private fun stopRecording() { - if (!isRecording) return - - isRecording = false - - try { - audioRecord?.stop() - audioRecord?.release() - } catch (e: Exception) { - Log.e(TAG, "Error stopping recording", e) - } - - audioRecord = null - buttonText = "Processing..." - buttonEnabled = false - - recordingThread?.join() - recordingThread = null - } - - private fun writeWavFile(pcmFile: File) { - try { - val wavFile = File(getExternalFilesDir(null), "audio_record.wav") - val pcmData = pcmFile.readBytes() - - val wavOut = FileOutputStream(wavFile) - - // Write WAV header for 16-bit mono audio at 16 kHz - writeWavHeader(wavOut, pcmData.size.toLong(), sampleRate, 1, 16) - wavOut.write(pcmData) - wavOut.flush() - wavOut.fd.sync() - wavOut.close() - - pcmFile.delete() - - Log.i(TAG, "WAV file saved: ${wavFile.absolutePath}") - - } catch (e: IOException) { - Log.e(TAG, "Failed to write WAV file", e) - } - } - - private fun writeWavHeader( - out: OutputStream, - totalAudioLen: Long, - sampleRate: Int, - channels: Int, - bitsPerSample: Int - ) { - val byteRate = sampleRate * channels * bitsPerSample / 8 - val blockAlign = channels * bitsPerSample / 8 - val totalDataLen = totalAudioLen + 36 - - val header = ByteArray(44) - - // RIFF header - header[0] = 'R'.code.toByte() - header[1] = 'I'.code.toByte() - header[2] = 'F'.code.toByte() - header[3] = 'F'.code.toByte() - - // File size (little-endian) - header[4] = (totalDataLen and 0xff).toByte() - header[5] = ((totalDataLen shr 8) and 0xff).toByte() - header[6] = ((totalDataLen shr 16) and 0xff).toByte() - header[7] = ((totalDataLen shr 24) and 0xff).toByte() - - // WAVE header - header[8] = 'W'.code.toByte() - header[9] = 'A'.code.toByte() - header[10] = 'V'.code.toByte() - header[11] = 'E'.code.toByte() - - // fmt chunk - header[12] = 'f'.code.toByte() - header[13] = 'm'.code.toByte() - header[14] = 't'.code.toByte() - header[15] = ' '.code.toByte() - - // fmt chunk size (16 for PCM) - header[16] = 16 - header[17] = 0 - header[18] = 0 - header[19] = 0 - - // Audio format (1 for PCM) - header[20] = 1 - header[21] = 0 - - // Number of channels - header[22] = channels.toByte() - header[23] = 0 - - // Sample rate (little-endian) - header[24] = (sampleRate and 0xff).toByte() - header[25] = ((sampleRate shr 8) and 0xff).toByte() - header[26] = ((sampleRate shr 16) and 0xff).toByte() - header[27] = ((sampleRate shr 24) and 0xff).toByte() - - // Byte rate (little-endian) - header[28] = (byteRate and 0xff).toByte() - header[29] = ((byteRate shr 8) and 0xff).toByte() - header[30] = ((byteRate shr 16) and 0xff).toByte() - header[31] = ((byteRate shr 24) and 0xff).toByte() - - // Block align - header[32] = blockAlign.toByte() - header[33] = 0 - - // Bits per sample - header[34] = bitsPerSample.toByte() - header[35] = 0 - - // Data chunk header - header[36] = 'd'.code.toByte() - header[37] = 'a'.code.toByte() - header[38] = 't'.code.toByte() - header[39] = 'a'.code.toByte() - - // Data chunk size (little-endian) - header[40] = (totalAudioLen and 0xff).toByte() - header[41] = ((totalAudioLen shr 8) and 0xff).toByte() - header[42] = ((totalAudioLen shr 16) and 0xff).toByte() - header[43] = ((totalAudioLen shr 24) and 0xff).toByte() - - out.write(header, 0, 44) - } - - override fun onDestroy() { - super.onDestroy() - if (isRecording) { - stopRecording() - } - } - - private val requestPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - statusText = "Permission granted. Hold the button to record." - } else { - statusText = "Audio recording permission required" - } - } -} - -@Composable -fun ParakeetScreen( - buttonText: String, - buttonEnabled: Boolean, - statusText: String, - transcriptionResult: String, - modelSettings: ModelSettings, - availableWavFiles: List, - showWavFileDialog: Boolean, - onRecordStart: () -> Unit, - onRecordStop: () -> Unit, - onUseWavFileClick: () -> Unit, - onWavFileSelected: (String) -> Unit, - onWavDialogDismiss: () -> Unit, - onSettingsClick: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top - ) { - Spacer(modifier = Modifier.height(32.dp)) - - Text( - text = "Parakeet Demo", - style = MaterialTheme.typography.headlineMedium - ) - - Spacer(modifier = Modifier.height(24.dp)) - - // Model info card - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Model Configuration", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(8.dp)) - - if (modelSettings.isValid()) { - Text( - text = "Model: ${modelSettings.modelPath.substringAfterLast('/')}", - style = MaterialTheme.typography.bodySmall - ) - Text( - text = "Tokenizer: ${modelSettings.tokenizerPath.substringAfterLast('/')}", - style = MaterialTheme.typography.bodySmall - ) - if (modelSettings.dataPath.isNotBlank()) { - Text( - text = "Data: ${modelSettings.dataPath.substringAfterLast('/')}", - style = MaterialTheme.typography.bodySmall - ) - } - } else { - Text( - text = "No model configured", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - Text( - text = "Please configure models in Settings", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Audio input buttons - val interactionSource = remember { MutableInteractionSource() } - val currentOnRecordStart by rememberUpdatedState(onRecordStart) - val currentOnRecordStop by rememberUpdatedState(onRecordStop) - - LaunchedEffect(interactionSource) { - var isPressed = false - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> { - isPressed = true - currentOnRecordStart() - } - is PressInteraction.Release -> { - if (isPressed) { - isPressed = false - currentOnRecordStop() - } - } - is PressInteraction.Cancel -> { - if (isPressed) { - isPressed = false - currentOnRecordStop() - } - } - } - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = {}, - interactionSource = interactionSource, - enabled = buttonEnabled, - modifier = Modifier.weight(1f) - ) { - Text(text = buttonText) - } - - OutlinedButton( - onClick = onUseWavFileClick, - enabled = buttonEnabled, - modifier = Modifier.weight(1f) - ) { - Text(text = "Use WAV File") - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - // Settings button - OutlinedButton( - onClick = onSettingsClick, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Settings") - } - - Spacer(modifier = Modifier.height(16.dp)) - - if (statusText.isNotEmpty()) { - Text( - text = statusText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.secondary - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - if (transcriptionResult.isNotEmpty()) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.primaryContainer, - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Transcription", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = transcriptionResult, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - } - } - - // WAV file selection dialog - if (showWavFileDialog) { - WavFileSelectionDialog( - files = availableWavFiles, - onDismiss = onWavDialogDismiss, - onSelect = onWavFileSelected - ) - } -} - -@Composable -fun WavFileSelectionDialog( - files: List, - onDismiss: () -> Unit, - onSelect: (String) -> Unit -) { - var selectedFile by remember { mutableStateOf(null) } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Select WAV File") }, - text = { - if (files.isEmpty()) { - Column { - Text("No WAV files found in ${ModelSettings.DEFAULT_DIRECTORY}") - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Use adb to push WAV files:\nadb push audio.wav ${ModelSettings.DEFAULT_DIRECTORY}/", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - files.forEach { filePath -> - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = filePath == selectedFile, - onClick = { selectedFile = filePath } - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text( - text = filePath.substringAfterLast('/'), - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = filePath, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) - } - } - } - }, - confirmButton = { - if (files.isNotEmpty()) { - TextButton( - onClick = { selectedFile?.let { onSelect(it) } }, - enabled = selectedFile != null - ) { - Text("Transcribe") - } - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - } - ) -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelDownloadScreen.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelDownloadScreen.kt deleted file mode 100644 index 9aa328362..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelDownloadScreen.kt +++ /dev/null @@ -1,261 +0,0 @@ -package com.example.parakeetapp - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun ModelDownloadScreen( - downloadViewModel: ModelDownloadViewModel, - onDownloadComplete: () -> Unit, - onSkip: () -> Unit -) { - val status = downloadViewModel.downloadStatus - val progress = downloadViewModel.downloadProgress - val currentFileName = downloadViewModel.currentFileName - val error = downloadViewModel.errorMessage - val selectedIndex = downloadViewModel.selectedPresetIndex - val isDownloading = status == DownloadStatus.DOWNLOADING - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Download Model", - style = MaterialTheme.typography.headlineMedium - ) - - Spacer(modifier = Modifier.height(24.dp)) - - // Preset selection - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Select Model", - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.height(8.dp)) - - ModelDownloadViewModel.MODEL_PRESETS.forEachIndexed { index, preset -> - val alreadyDownloaded = downloadViewModel.isPresetDownloaded(preset) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = index == selectedIndex, - onClick = { - if (!isDownloading) { - downloadViewModel.selectPreset(index) - downloadViewModel.resetStatus() - } - }, - enabled = !isDownloading - ) - Spacer(modifier = Modifier.width(4.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = preset.displayName, - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = preset.modelFile.filename, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (alreadyDownloaded) { - Text( - text = "✓", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - } - if (index < ModelDownloadViewModel.MODEL_PRESETS.size - 1) { - Spacer(modifier = Modifier.height(4.dp)) - } - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Files to download - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Files", - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.height(8.dp)) - - val preset = downloadViewModel.getSelectedPreset() - - // Tokenizer file - FileStatusRow(preset.tokenizerFile.description, preset.tokenizerFile.filename, downloadViewModel) - Spacer(modifier = Modifier.height(4.dp)) - - // Model file - FileStatusRow(preset.modelFile.description, preset.modelFile.filename, downloadViewModel) - - // Download progress - if (isDownloading) { - Spacer(modifier = Modifier.height(12.dp)) - LinearProgressIndicator( - progress = { progress }, - modifier = Modifier.fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Downloading $currentFileName...", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - if (status == DownloadStatus.COMPLETED) { - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = "All files downloaded!", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - - if (error != null) { - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = error, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - when (status) { - DownloadStatus.NOT_STARTED, DownloadStatus.FAILED -> { - Button( - onClick = { downloadViewModel.downloadSelectedPreset() }, - modifier = Modifier.fillMaxWidth() - ) { - Text(if (status == DownloadStatus.FAILED) "Retry Download" else "Download") - } - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedButton( - onClick = onSkip, - modifier = Modifier.fillMaxWidth() - ) { - Text("Back") - } - } - - DownloadStatus.DOWNLOADING -> { - Button( - onClick = {}, - enabled = false, - modifier = Modifier.fillMaxWidth() - ) { - Text("Downloading...") - } - } - - DownloadStatus.COMPLETED -> { - Button( - onClick = onDownloadComplete, - modifier = Modifier.fillMaxWidth() - ) { - Text("Continue") - } - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedButton( - onClick = { - downloadViewModel.resetStatus() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Download Another Model") - } - } - } - } -} - -@Composable -private fun FileStatusRow( - description: String, - filename: String, - downloadViewModel: ModelDownloadViewModel -) { - val status = downloadViewModel.downloadStatus - val currentFileName = downloadViewModel.currentFileName - val modelsDir = downloadViewModel.getModelDir() - val fileExists = java.io.File("$modelsDir/$filename").exists() - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column(modifier = Modifier.weight(1f)) { - Text(text = description, style = MaterialTheme.typography.bodyMedium) - Text( - text = filename, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = when { - fileExists || status == DownloadStatus.COMPLETED -> "✓" - status == DownloadStatus.DOWNLOADING && currentFileName == filename -> "⬇" - else -> "○" - }, - style = MaterialTheme.typography.bodyMedium - ) - } -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelDownloadViewModel.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelDownloadViewModel.kt deleted file mode 100644 index 1b60bfb6e..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelDownloadViewModel.kt +++ /dev/null @@ -1,212 +0,0 @@ -package com.example.parakeetapp - -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream -import java.net.HttpURLConnection -import java.net.URL - -data class ModelFileInfo( - val url: String, - val filename: String, - val description: String -) - -data class ParakeetModelPreset( - val id: String, - val displayName: String, - val modelFile: ModelFileInfo, - val tokenizerFile: ModelFileInfo -) - -enum class DownloadStatus { - NOT_STARTED, - DOWNLOADING, - COMPLETED, - FAILED -} - -class ModelDownloadViewModel : ViewModel() { - - companion object { - private const val TAG = "ModelDownloadViewModel" - private const val MODELS_SUBDIRECTORY = "parakeet" - - private const val PARAKEET_BASE_URL = - "https://huggingface.co/larryliu0820/parakeet-tdt-0.6b-v3-executorch/resolve/main/xnnpack/int4" - - val MODEL_PRESETS = listOf( - ParakeetModelPreset( - id = "parakeet_int4", - displayName = "Parakeet TDT 0.6B (INT4)", - modelFile = ModelFileInfo( - url = "$PARAKEET_BASE_URL/model.pte", - filename = "parakeet_int4.pte", - description = "Parakeet TDT 0.6B INT4 Model" - ), - tokenizerFile = ModelFileInfo( - url = "$PARAKEET_BASE_URL/tokenizer.model", - filename = "tokenizer.model", - description = "Tokenizer" - ) - ) - ) - } - - var downloadStatus by mutableStateOf(DownloadStatus.NOT_STARTED) - private set - - var downloadProgress by mutableFloatStateOf(0f) - private set - - var currentFileIndex by mutableIntStateOf(0) - private set - - var totalFileCount by mutableIntStateOf(0) - private set - - var currentFileName by mutableStateOf("") - private set - - var errorMessage by mutableStateOf(null) - private set - - var selectedPresetIndex by mutableIntStateOf(0) - private set - - private lateinit var modelsDir: String - - fun initialize(filesDir: String) { - modelsDir = filesDir + "/" + MODELS_SUBDIRECTORY - } - - fun getModelDir(): String = modelsDir - - fun selectPreset(index: Int) { - selectedPresetIndex = index - } - - fun getSelectedPreset(): ParakeetModelPreset = MODEL_PRESETS[selectedPresetIndex] - - fun getModelPath(): String = "$modelsDir/${getSelectedPreset().modelFile.filename}" - fun getTokenizerPath(): String = "$modelsDir/${getSelectedPreset().tokenizerFile.filename}" - - fun isPresetDownloaded(preset: ParakeetModelPreset): Boolean { - val modelExists = File("$modelsDir/${preset.modelFile.filename}").exists() - val tokenizerExists = File("$modelsDir/${preset.tokenizerFile.filename}").exists() - return modelExists && tokenizerExists - } - - fun downloadSelectedPreset() { - if (downloadStatus == DownloadStatus.DOWNLOADING) return - - val preset = getSelectedPreset() - - val filesToDownload = mutableListOf() - - // Add tokenizer file if not already present - if (!File("$modelsDir/${preset.tokenizerFile.filename}").exists()) { - filesToDownload.add(preset.tokenizerFile) - } - - // Add model file if not already present - if (!File("$modelsDir/${preset.modelFile.filename}").exists()) { - filesToDownload.add(preset.modelFile) - } - - if (filesToDownload.isEmpty()) { - downloadStatus = DownloadStatus.COMPLETED - return - } - - downloadStatus = DownloadStatus.DOWNLOADING - downloadProgress = 0f - currentFileIndex = 0 - totalFileCount = filesToDownload.size - errorMessage = null - - viewModelScope.launch { - try { - val dir = File(modelsDir) - if (!dir.exists()) { - dir.mkdirs() - } - - for ((index, fileInfo) in filesToDownload.withIndex()) { - currentFileIndex = index - currentFileName = fileInfo.filename - val targetFile = File("$modelsDir/${fileInfo.filename}") - - val success = downloadFile(fileInfo, targetFile) - if (!success) { - downloadStatus = DownloadStatus.FAILED - return@launch - } - downloadProgress = (index + 1).toFloat() / filesToDownload.size - } - - downloadStatus = DownloadStatus.COMPLETED - } catch (e: Exception) { - Log.e(TAG, "Download failed", e) - downloadStatus = DownloadStatus.FAILED - errorMessage = "Download failed: ${e.message}" - } - } - } - - fun resetStatus() { - downloadStatus = DownloadStatus.NOT_STARTED - errorMessage = null - } - - private suspend fun downloadFile( - fileInfo: ModelFileInfo, - targetFile: File - ): Boolean = withContext(Dispatchers.IO) { - try { - Log.i(TAG, "Downloading ${fileInfo.filename} from ${fileInfo.url}") - val url = URL(fileInfo.url) - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.instanceFollowRedirects = true - connection.connectTimeout = 30000 - connection.readTimeout = 30000 - connection.connect() - - if (connection.responseCode != HttpURLConnection.HTTP_OK) { - throw Exception("Server returned HTTP ${connection.responseCode}") - } - - val tempFile = File(targetFile.absolutePath + ".tmp") - connection.inputStream.use { input -> - FileOutputStream(tempFile).use { output -> - val buffer = ByteArray(8192) - var bytesRead: Int - while (input.read(buffer).also { bytesRead = it } != -1) { - output.write(buffer, 0, bytesRead) - } - } - } - tempFile.renameTo(targetFile) - - Log.i(TAG, "Downloaded ${fileInfo.filename} successfully") - true - } catch (e: Exception) { - Log.e(TAG, "Failed to download ${fileInfo.filename}", e) - withContext(Dispatchers.Main) { - errorMessage = "Failed to download ${fileInfo.filename}: ${e.message}" - } - false - } - } -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettings.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettings.kt deleted file mode 100644 index f45b427eb..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettings.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.parakeetapp - -/** - * Data class representing the model file settings for Parakeet inference. - * - * @param modelPath Path to the main Parakeet model (.pte file) - * @param tokenizerPath Path to the tokenizer file (.json, .bin, or .model file) - * @param dataPath Optional path to external data file (.ptd file) - */ -data class ModelSettings( - val modelPath: String = "", - val tokenizerPath: String = "", - val dataPath: String = "" -) { - /** - * Check if the minimum required files are set for inference. - * Model and tokenizer are required. - */ - fun isValid(): Boolean { - return modelPath.isNotBlank() && tokenizerPath.isNotBlank() - } - - companion object { - const val DEFAULT_DIRECTORY = "/data/local/tmp/parakeet" - val MODEL_EXTENSIONS = arrayOf(".pte") - val TOKENIZER_EXTENSIONS = arrayOf(".json", ".bin", ".model") - val DATA_EXTENSIONS = arrayOf(".ptd") - } -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettingsScreen.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettingsScreen.kt deleted file mode 100644 index 6f32a601f..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettingsScreen.kt +++ /dev/null @@ -1,306 +0,0 @@ -package com.example.parakeetapp - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -/** - * Settings screen for selecting model files. - */ -@Composable -fun ModelSettingsScreen( - viewModel: ModelSettingsViewModel, - onBackClick: () -> Unit, - onDownloadClick: () -> Unit -) { - var showModelDialog by remember { mutableStateOf(false) } - var showTokenizerDialog by remember { mutableStateOf(false) } - var showDataDialog by remember { mutableStateOf(false) } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Model Settings", - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(bottom = 24.dp) - ) - - // Model file selection - FileSelectionRow( - label = "Model File (.pte)", - selectedPath = viewModel.modelSettings.modelPath, - required = true, - onClick = { - viewModel.refreshFileLists() - showModelDialog = true - } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Tokenizer file selection - FileSelectionRow( - label = "Tokenizer File", - selectedPath = viewModel.modelSettings.tokenizerPath, - required = true, - onClick = { - viewModel.refreshFileLists() - showTokenizerDialog = true - } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Data file selection (optional) - FileSelectionRow( - label = "Data File (.ptd) - Optional", - selectedPath = viewModel.modelSettings.dataPath, - required = false, - onClick = { - viewModel.refreshFileLists() - showDataDialog = true - }, - onClear = if (viewModel.modelSettings.dataPath.isNotBlank()) { - { viewModel.clearDataFile() } - } else null - ) - - Spacer(modifier = Modifier.height(24.dp)) - - // Download model button - OutlinedButton( - onClick = onDownloadClick, - modifier = Modifier.fillMaxWidth() - ) { - Text("Download Model") - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Status indicator - if (viewModel.isReadyForInference()) { - Text( - text = "✓ Ready for inference", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } else { - Text( - text = "⚠ Model and Tokenizer are required", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error - ) - } - - Spacer(modifier = Modifier.height(24.dp)) - - // Back button - Button( - onClick = onBackClick, - modifier = Modifier.fillMaxWidth() - ) { - Text("OK") - } - - // Info text - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Scanning: ${ModelSettings.DEFAULT_DIRECTORY} and app storage", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // File selection dialogs - if (showModelDialog) { - FileSelectionDialog( - title = "Select Model File", - files = viewModel.availableModels, - currentSelection = viewModel.modelSettings.modelPath, - onDismiss = { showModelDialog = false }, - onSelect = { - viewModel.selectModel(it) - showModelDialog = false - } - ) - } - - if (showTokenizerDialog) { - FileSelectionDialog( - title = "Select Tokenizer File", - files = viewModel.availableTokenizers, - currentSelection = viewModel.modelSettings.tokenizerPath, - onDismiss = { showTokenizerDialog = false }, - onSelect = { - viewModel.selectTokenizer(it) - showTokenizerDialog = false - } - ) - } - - if (showDataDialog) { - FileSelectionDialog( - title = "Select Data File (Optional)", - files = viewModel.availableDataFiles, - currentSelection = viewModel.modelSettings.dataPath, - onDismiss = { showDataDialog = false }, - onSelect = { - viewModel.selectDataFile(it) - showDataDialog = false - }, - allowNone = true - ) - } -} - -/** - * Row displaying file selection information. - */ -@Composable -fun FileSelectionRow( - label: String, - selectedPath: String, - required: Boolean, - onClick: () -> Unit, - onClear: (() -> Unit)? = null -) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.weight(1f) - ) - - Row { - if (onClear != null) { - TextButton(onClick = onClear) { - Text("Clear") - } - } - Button(onClick = onClick) { - Text("Select") - } - } - } - - Spacer(modifier = Modifier.height(4.dp)) - - if (selectedPath.isNotBlank()) { - Text( - text = selectedPath.substringAfterLast('/'), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = selectedPath, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else { - Text( - text = if (required) "Not selected (Required)" else "Not selected (Optional)", - style = MaterialTheme.typography.bodySmall, - color = if (required) MaterialTheme.colorScheme.error - else MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - -/** - * Dialog for selecting a file from a list. - */ -@Composable -fun FileSelectionDialog( - title: String, - files: List, - currentSelection: String, - onDismiss: () -> Unit, - onSelect: (String) -> Unit, - allowNone: Boolean = false -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - if (files.isEmpty()) { - Column { - Text("No files found. Download from the setup screen or use adb push to ${ModelSettings.DEFAULT_DIRECTORY}") - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Use adb to push files:\nadb push ${ModelSettings.DEFAULT_DIRECTORY}/", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - if (allowNone) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onSelect("") }, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = currentSelection.isBlank(), - onClick = { onSelect("") } - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("None", style = MaterialTheme.typography.bodyMedium) - } - Spacer(modifier = Modifier.height(8.dp)) - } - - files.forEach { filePath -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onSelect(filePath) }, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = filePath == currentSelection, - onClick = { onSelect(filePath) } - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text( - text = filePath.substringAfterLast('/'), - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = filePath, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text("Close") - } - } - ) -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettingsViewModel.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettingsViewModel.kt deleted file mode 100644 index f46731d26..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ModelSettingsViewModel.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.example.parakeetapp - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import java.io.File - -/** - * ViewModel for managing model file selection and settings. - * Settings are kept in memory only (not persisted). - */ -class ModelSettingsViewModel : ViewModel() { - - var modelSettings by mutableStateOf(ModelSettings()) - private set - - var availableModels by mutableStateOf>(emptyList()) - private set - - var availableTokenizers by mutableStateOf>(emptyList()) - private set - - var availableDataFiles by mutableStateOf>(emptyList()) - private set - - var availableWavFiles by mutableStateOf>(emptyList()) - private set - - var errorMessage by mutableStateOf(null) - private set - - private var appStorageDirectory: String? = null - - /** - * Initialize the ViewModel by scanning for available files. - */ - fun initialize() { - refreshFileLists() - } - - /** - * Set the app-internal storage directory (filesDir/parakeet) so we can scan it too. - */ - fun setAppStorageDirectory(filesDir: String) { - appStorageDirectory = "$filesDir/parakeet" - refreshFileLists() - } - - /** - * Scan all known directories for available model files. - */ - fun refreshFileLists() { - val directories = buildList { - add(ModelSettings.DEFAULT_DIRECTORY) - appStorageDirectory?.let { add(it) } - } - - availableModels = listLocalFilesFromDirs(directories, ModelSettings.MODEL_EXTENSIONS) - availableTokenizers = listLocalFilesFromDirs(directories, ModelSettings.TOKENIZER_EXTENSIONS) - availableDataFiles = listLocalFilesFromDirs(directories, ModelSettings.DATA_EXTENSIONS) - availableWavFiles = listLocalFilesFromDirs(directories, WAV_EXTENSIONS) - - if (availableModels.isNotEmpty() || availableTokenizers.isNotEmpty()) { - errorMessage = null - } - } - - fun selectModel(path: String) { - modelSettings = modelSettings.copy(modelPath = path) - } - - fun selectTokenizer(path: String) { - modelSettings = modelSettings.copy(tokenizerPath = path) - } - - fun selectDataFile(path: String) { - modelSettings = modelSettings.copy(dataPath = path) - } - - fun clearDataFile() { - selectDataFile("") - } - - fun isReadyForInference(): Boolean { - return modelSettings.isValid() - } - - companion object { - val WAV_EXTENSIONS = arrayOf(".wav") - - /** - * List files in the given directory matching the specified extensions. - */ - fun listLocalFiles(path: String, extensions: Array): List { - val directory = File(path) - if (!directory.exists() || !directory.isDirectory) { - return emptyList() - } - - return directory.listFiles { _, name -> - extensions.any { ext -> name.endsWith(ext, ignoreCase = true) } - }?.filter { it.isFile } - ?.map { it.absolutePath } - ?.sorted() - ?: emptyList() - } - - /** - * List files from multiple directories, deduplicated by absolute path. - */ - fun listLocalFilesFromDirs(dirs: List, extensions: Array): List { - return dirs.flatMap { listLocalFiles(it, extensions) } - .distinct() - .sorted() - } - } -} diff --git a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ui/theme/Theme.kt b/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ui/theme/Theme.kt deleted file mode 100644 index 41a0e03d4..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/java/com/example/parakeetapp/ui/theme/Theme.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.parakeetapp.ui.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -@Composable -fun ParakeetAppTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit -) { - val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } else { - if (darkTheme) { - androidx.compose.material3.darkColorScheme() - } else { - androidx.compose.material3.lightColorScheme() - } - } - - MaterialTheme( - colorScheme = colorScheme, - content = content - ) -} diff --git a/parakeet/android/ParakeetApp/app/src/main/res/drawable/ic_launcher_background.xml b/parakeet/android/ParakeetApp/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cb..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/parakeet/android/ParakeetApp/app/src/main/res/drawable/ic_launcher_foreground.xml b/parakeet/android/ParakeetApp/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d114..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/parakeet/android/ParakeetApp/app/src/main/res/values/strings.xml b/parakeet/android/ParakeetApp/app/src/main/res/values/strings.xml deleted file mode 100644 index 95db29206..000000000 --- a/parakeet/android/ParakeetApp/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Parakeet App - diff --git a/parakeet/android/ParakeetApp/app/src/test/java/com/example/parakeetapp/ModelSettingsTest.kt b/parakeet/android/ParakeetApp/app/src/test/java/com/example/parakeetapp/ModelSettingsTest.kt deleted file mode 100644 index 8dd0f003d..000000000 --- a/parakeet/android/ParakeetApp/app/src/test/java/com/example/parakeetapp/ModelSettingsTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.example.parakeetapp - -import org.junit.Assert.* -import org.junit.Test - -/** - * Unit tests for ModelSettings data class. - */ -class ModelSettingsTest { - - @Test - fun `isValid returns false when model path is empty`() { - val settings = ModelSettings( - modelPath = "", - tokenizerPath = "/data/local/tmp/parakeet/tokenizer.model" - ) - assertFalse(settings.isValid()) - } - - @Test - fun `isValid returns false when tokenizer path is empty`() { - val settings = ModelSettings( - modelPath = "/data/local/tmp/parakeet/model.pte", - tokenizerPath = "" - ) - assertFalse(settings.isValid()) - } - - @Test - fun `isValid returns false when both paths are empty`() { - val settings = ModelSettings() - assertFalse(settings.isValid()) - } - - @Test - fun `isValid returns true when model and tokenizer are set`() { - val settings = ModelSettings( - modelPath = "/data/local/tmp/parakeet/model.pte", - tokenizerPath = "/data/local/tmp/parakeet/tokenizer.model" - ) - assertTrue(settings.isValid()) - } - - @Test - fun `isValid returns true with data path`() { - val settings = ModelSettings( - modelPath = "/data/local/tmp/parakeet/model.pte", - tokenizerPath = "/data/local/tmp/parakeet/tokenizer.model", - dataPath = "/data/local/tmp/parakeet/data.ptd" - ) - assertTrue(settings.isValid()) - } - - @Test - fun `default directory constant is correct`() { - assertEquals("/data/local/tmp/parakeet", ModelSettings.DEFAULT_DIRECTORY) - } - - @Test - fun `model extensions include pte`() { - assertTrue(ModelSettings.MODEL_EXTENSIONS.contains(".pte")) - } - - @Test - fun `tokenizer extensions include json bin and model`() { - assertTrue(ModelSettings.TOKENIZER_EXTENSIONS.contains(".json")) - assertTrue(ModelSettings.TOKENIZER_EXTENSIONS.contains(".bin")) - assertTrue(ModelSettings.TOKENIZER_EXTENSIONS.contains(".model")) - } - - @Test - fun `data extensions include ptd`() { - assertTrue(ModelSettings.DATA_EXTENSIONS.contains(".ptd")) - } - - @Test - fun `copy creates new instance with updated values`() { - val original = ModelSettings( - modelPath = "/path/to/model.pte", - tokenizerPath = "/path/to/tokenizer.model" - ) - val updated = original.copy(dataPath = "/path/to/data.ptd") - - assertEquals("/path/to/model.pte", updated.modelPath) - assertEquals("/path/to/tokenizer.model", updated.tokenizerPath) - assertEquals("/path/to/data.ptd", updated.dataPath) - assertEquals("", original.dataPath) // Original unchanged - } -} diff --git a/parakeet/android/ParakeetApp/app/src/test/java/com/example/parakeetapp/ModelSettingsViewModelTest.kt b/parakeet/android/ParakeetApp/app/src/test/java/com/example/parakeetapp/ModelSettingsViewModelTest.kt deleted file mode 100644 index 192b9ebd1..000000000 --- a/parakeet/android/ParakeetApp/app/src/test/java/com/example/parakeetapp/ModelSettingsViewModelTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.parakeetapp - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import java.io.File - -/** - * Unit tests for ModelSettingsViewModel. - */ -class ModelSettingsViewModelTest { - - @get:Rule - val tempFolder = TemporaryFolder() - - @Test - fun `listLocalFiles returns empty list for non-existent directory`() { - val files = ModelSettingsViewModel.listLocalFiles( - "/non/existent/path", - arrayOf(".pte") - ) - assertTrue(files.isEmpty()) - } - - @Test - fun `listLocalFiles returns empty list for empty directory`() { - val emptyDir = tempFolder.newFolder("empty") - val files = ModelSettingsViewModel.listLocalFiles( - emptyDir.absolutePath, - arrayOf(".pte") - ) - assertTrue(files.isEmpty()) - } - - @Test - fun `listLocalFiles filters by extension`() { - val dir = tempFolder.newFolder("models") - File(dir, "model1.pte").createNewFile() - File(dir, "model2.pte").createNewFile() - File(dir, "tokenizer.json").createNewFile() - File(dir, "readme.txt").createNewFile() - - val pteFiles = ModelSettingsViewModel.listLocalFiles( - dir.absolutePath, - arrayOf(".pte") - ) - - assertEquals(2, pteFiles.size) - assertTrue(pteFiles.all { it.endsWith(".pte") }) - } - - @Test - fun `listLocalFiles supports multiple extensions`() { - val dir = tempFolder.newFolder("tokens") - File(dir, "tokenizer.json").createNewFile() - File(dir, "tokenizer.bin").createNewFile() - File(dir, "tokenizer.model").createNewFile() - File(dir, "model.pte").createNewFile() - - val tokenizerFiles = ModelSettingsViewModel.listLocalFiles( - dir.absolutePath, - arrayOf(".json", ".bin", ".model") - ) - - assertEquals(3, tokenizerFiles.size) - assertTrue(tokenizerFiles.none { it.endsWith(".pte") }) - } - - @Test - fun `listLocalFiles returns absolute paths`() { - val dir = tempFolder.newFolder("abs") - File(dir, "model.pte").createNewFile() - - val files = ModelSettingsViewModel.listLocalFiles( - dir.absolutePath, - arrayOf(".pte") - ) - - assertEquals(1, files.size) - assertTrue(files[0].startsWith("/")) - assertTrue(files[0].contains(dir.name)) - } - - @Test - fun `listLocalFiles returns sorted list`() { - val dir = tempFolder.newFolder("sorted") - File(dir, "zebra.pte").createNewFile() - File(dir, "alpha.pte").createNewFile() - File(dir, "beta.pte").createNewFile() - - val files = ModelSettingsViewModel.listLocalFiles( - dir.absolutePath, - arrayOf(".pte") - ) - - assertEquals(3, files.size) - assertTrue(files[0].endsWith("alpha.pte")) - assertTrue(files[1].endsWith("beta.pte")) - assertTrue(files[2].endsWith("zebra.pte")) - } - - @Test - fun `listLocalFiles ignores directories`() { - val dir = tempFolder.newFolder("mixed") - File(dir, "model.pte").createNewFile() - File(dir, "subdir.pte").mkdirs() // Creates a directory with .pte extension - - val files = ModelSettingsViewModel.listLocalFiles( - dir.absolutePath, - arrayOf(".pte") - ) - - assertEquals(1, files.size) - assertTrue(files[0].endsWith("model.pte")) - } - - @Test - fun `listLocalFiles is case insensitive for extensions`() { - val dir = tempFolder.newFolder("case") - File(dir, "model.pte").createNewFile() - File(dir, "model2.PTE").createNewFile() - File(dir, "model3.Pte").createNewFile() - - val files = ModelSettingsViewModel.listLocalFiles( - dir.absolutePath, - arrayOf(".pte") - ) - - assertEquals(3, files.size) - } -} diff --git a/parakeet/android/ParakeetApp/settings.gradle.kts b/parakeet/android/ParakeetApp/settings.gradle.kts deleted file mode 100644 index da6aa0065..000000000 --- a/parakeet/android/ParakeetApp/settings.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -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() - } -} - -rootProject.name = "Parakeet App" -include(":app") diff --git a/whisper/android/WhisperApp/.gitignore b/whisper/android/WhisperApp/.gitignore deleted file mode 100644 index 87f66f51b..000000000 --- a/whisper/android/WhisperApp/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -.gradle -/local.properties -.idea/ -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/whisper/android/WhisperApp/README.md b/whisper/android/WhisperApp/README.md deleted file mode 100644 index 884d7af21..000000000 --- a/whisper/android/WhisperApp/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Whisper Demo App - -This app demonstrates running the Whisper speech recognition model on Android using ExecuTorch. - -> **Note:** The ExecuTorch `AsrModule` API is not yet released. We will give a snapshot AAR soon™ - -## Export Model Files - -Export the audio preprocessor and model `.pte` files following the instructions at: -https://github.com/pytorch/executorch/tree/main/examples/models/whisper - -This app requires both a model `.pte` and a preprocessor `.pte` file. - -## Run the App - -1. Open WhisperApp in Android Studio -2. Copy the `executorch.aar` library (with audio JNI bindings) into `app/libs` -3. Build and run on device - -## Demo - -https://github.com/user-attachments/assets/eb4c4ae6-b89f-4eb4-a291-549a42c95f54 diff --git a/whisper/android/WhisperApp/app/.gitignore b/whisper/android/WhisperApp/app/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/whisper/android/WhisperApp/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/whisper/android/WhisperApp/app/proguard-rules.pro b/whisper/android/WhisperApp/app/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/whisper/android/WhisperApp/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/whisper/android/WhisperApp/app/src/main/AndroidManifest.xml b/whisper/android/WhisperApp/app/src/main/AndroidManifest.xml deleted file mode 100644 index 323156639..000000000 --- a/whisper/android/WhisperApp/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/whisper/android/WhisperApp/app/src/main/res/drawable/ic_launcher_background.xml b/whisper/android/WhisperApp/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cb..000000000 --- a/whisper/android/WhisperApp/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/whisper/android/WhisperApp/app/src/main/res/drawable/ic_launcher_foreground.xml b/whisper/android/WhisperApp/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d114..000000000 --- a/whisper/android/WhisperApp/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/whisper/android/WhisperApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f3b755bf..000000000 --- a/whisper/android/WhisperApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/whisper/android/WhisperApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f3b755bf..000000000 --- a/whisper/android/WhisperApp/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ec..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1b..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da0..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070f..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f5083..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/whisper/android/WhisperApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37c..000000000 Binary files a/whisper/android/WhisperApp/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/whisper/android/WhisperApp/app/src/main/res/values/strings.xml b/whisper/android/WhisperApp/app/src/main/res/values/strings.xml deleted file mode 100644 index 45afbdd08..000000000 --- a/whisper/android/WhisperApp/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Whisper App - \ No newline at end of file diff --git a/whisper/android/WhisperApp/build.gradle.kts b/whisper/android/WhisperApp/build.gradle.kts deleted file mode 100644 index 952b93066..000000000 --- a/whisper/android/WhisperApp/build.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -// 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 - alias(libs.plugins.kotlin.compose) apply false -} \ No newline at end of file diff --git a/whisper/android/WhisperApp/gradle.properties b/whisper/android/WhisperApp/gradle.properties deleted file mode 100644 index 20e2a0152..000000000 --- a/whisper/android/WhisperApp/gradle.properties +++ /dev/null @@ -1,23 +0,0 @@ -# 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 \ No newline at end of file diff --git a/whisper/android/WhisperApp/gradle/libs.versions.toml b/whisper/android/WhisperApp/gradle/libs.versions.toml deleted file mode 100644 index caafb09e9..000000000 --- a/whisper/android/WhisperApp/gradle/libs.versions.toml +++ /dev/null @@ -1,34 +0,0 @@ -[versions] -agp = "8.9.0" -kotlin = "2.0.21" -coreKtx = "1.16.0" -junit = "4.13.2" -junitVersion = "1.3.0" -espressoCore = "3.7.0" -lifecycleRuntimeKtx = "2.9.2" -activityCompose = "1.10.1" -composeBom = "2024.09.00" -appcompat = "1.7.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-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -androidx-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } -androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } - -[plugins] -android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } - diff --git a/whisper/android/WhisperApp/gradle/wrapper/gradle-wrapper.jar b/whisper/android/WhisperApp/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c02..000000000 Binary files a/whisper/android/WhisperApp/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/whisper/android/WhisperApp/gradle/wrapper/gradle-wrapper.properties b/whisper/android/WhisperApp/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3324d4d66..000000000 --- a/whisper/android/WhisperApp/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Aug 08 17:07:04 PDT 2025 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/whisper/android/WhisperApp/gradlew b/whisper/android/WhisperApp/gradlew deleted file mode 100755 index 4f906e0c8..000000000 --- a/whisper/android/WhisperApp/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/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" "$@" diff --git a/whisper/android/WhisperApp/gradlew.bat b/whisper/android/WhisperApp/gradlew.bat deleted file mode 100644 index ac1b06f93..000000000 --- a/whisper/android/WhisperApp/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@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