Add Settings dialog and DataStore

This commit is contained in:
2026-07-16 16:37:36 -04:00
parent b73198260d
commit d2eb56ab16
7 changed files with 399 additions and 46 deletions
+1
View File
@@ -41,6 +41,7 @@ google-services.json
*.iws
.idea/workspace.xml
.idea/tasks.xml
.idea/planningMode.xml
.idea/vcs.xml
.idea/dictionaries
.idea/libraries
+7
View File
@@ -4,6 +4,13 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-07-16T13:18:59.424006579Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/cjones/.android/avd/Pixel_9a.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
+2
View File
@@ -42,11 +42,13 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.datastore.preferences)
implementation("com.squareup.okhttp3:okhttp:5.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
@@ -13,29 +13,47 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
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.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.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.first
//import androidx.privacysandbox.tools.core.generator.build
import kotlinx.coroutines.Job
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -53,6 +71,7 @@ import org.json.JSONObject
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
const val TAG = "SpeechClient"
@@ -69,7 +88,11 @@ class MainActivity : ComponentActivity() {
private var buffer: ByteArray? = null
private var isRecording by mutableStateOf(false)
private var transcriptText by mutableStateOf("Transcription will appear here...")
private val client = OkHttpClient()
private var connectionStatus by mutableStateOf("Disconnected")
private lateinit var settingsManager: SettingsManager
private val client = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS)
.build()
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
@@ -82,21 +105,52 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settingsManager = SettingsManager(this)
enableEdgeToEdge()
setContent {
val settings by settingsManager.settingsFlow.collectAsState(
initial = HandshakeSettings(
SettingsManager.DEFAULT_HOST, SettingsManager.DEFAULT_PORT,
SettingsManager.DEFAULT_TASK, SettingsManager.DEFAULT_MODEL,
SettingsManager.DEFAULT_LANGUAGE, SettingsManager.DEFAULT_USE_VAD,
SettingsManager.DEFAULT_AUDIO_FORMAT, SettingsManager.DEFAULT_SEND_LAST_N_SEGMENTS,
SettingsManager.DEFAULT_NO_SPEECH_THRESH, SettingsManager.DEFAULT_CLIP_AUDIO,
SettingsManager.DEFAULT_SAME_OUTPUT_THRESHOLD, SettingsManager.DEFAULT_ENABLE_TRANSLATION,
SettingsManager.DEFAULT_TARGET_LANGUAGE, null,
SettingsManager.DEFAULT_ENABLE_DIARIZATION, SettingsManager.DEFAULT_MAX_SPEAKERS,
SettingsManager.DEFAULT_WORD_TIMESTAMPS
)
)
var showSettings by remember { mutableStateOf(false) }
WhisperClient2Theme {
WhisperApp(transcriptText = transcriptText,
isRecording = isRecording) {
// this block IS the onToggleRecording lambda
if (isRecording)
{
stopRecording()
isRecording = false
}
else
{
startRecording(); isRecording = true
WhisperApp(
transcriptText = transcriptText,
isRecording = isRecording,
connectionStatus = connectionStatus,
onOpenSettings = { showSettings = true },
onToggleRecording = {
if (isRecording) {
stopRecording()
isRecording = false
} else {
startRecording()
isRecording = true
}
}
)
if (showSettings) {
SettingsDialog(
initialSettings = settings,
onDismiss = { showSettings = false },
onSave = { newSettings ->
lifecycleScope.launch {
settingsManager.saveSettings(newSettings)
showSettings = false
}
}
)
}
}
}
@@ -167,37 +221,36 @@ class MainActivity : ComponentActivity() {
}
}
if (audioRecordOK) {
withContext(Dispatchers.Main) {
connectionStatus = "Connecting"
}
// Create the WebSocket instance
val settings = settingsManager.settingsFlow.first()
val request = Request.Builder()
.url("ws://100.81.165.11:9097")
.url("ws://${settings.host}:${settings.port}")
.build()
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// get a new client UUID (unique session ID) for each connection
clientUid = UUID.randomUUID()
// Build the handshake JSON using Android's built-in JSONObject.
// Every field the server expects is here; safe to hardcode the ones
// you don't need to expose in the UI.
val handshake = JSONObject().apply {
put("uid", clientUid.toString())
put("task", "transcribe")
put("model", "OpenVINO/whisper-medium-fp16-ov")
put("language", "en")
put("use_vad", false)
put("audio_format", "int16")
put("send_last_n_segments", 10)
put("no_speech_thresh", 0.45)
put("clip_audio", false)
put("same_output_threshold", 10)
put("enable_translation", false)
put(
"target_language",
"fr"
) // server requires this key even if translation is off
put("hotwords", JSONObject.NULL)
put("enable_diarization", false)
put("max_speakers", 10)
put("word_timestamps", false)
put("task", settings.task)
put("model", settings.model)
put("language", settings.language)
put("use_vad", settings.useVad)
put("audio_format", settings.audioFormat)
put("send_last_n_segments", settings.sendLastNSegments)
put("no_speech_thresh", settings.noSpeechThresh)
put("clip_audio", settings.clipAudio)
put("same_output_threshold", settings.sameOutputThreshold)
put("enable_translation", settings.enableTranslation)
put("target_language", settings.targetLanguage)
put("hotwords", settings.hotwords ?: JSONObject.NULL)
put("enable_diarization", settings.enableDiarization)
put("max_speakers", settings.maxSpeakers)
put("word_timestamps", settings.wordTimestamps)
}
// send() with a String argument sends a WebSocket text frame
webSocket.send(handshake.toString())
@@ -223,12 +276,16 @@ class MainActivity : ComponentActivity() {
"WAIT" -> {
val minutes = message.getDouble("message").roundToInt()
Log.i(TAG, "Server full. Estimated wait: $minutes minutes")
// Optionally show this in the UI
runOnUiThread {
connectionStatus = "Wait ($minutes min)"
}
}
"ERROR" -> {
Log.e(TAG, "Server error: ${message.getString("message")}")
// Stop recording, show error in UI
runOnUiThread {
connectionStatus = "Error"
}
}
"WARNING" -> {
@@ -249,7 +306,7 @@ class MainActivity : ComponentActivity() {
message.getString("backend") // e.g. "faster_whisper"
Log.i(TAG, "Server ready, backend: $backend")
runOnUiThread {
// statusTextView.text = "Connected"
connectionStatus = "Server Ready"
}
startAudioCapture() // <-- safe to start sending audio now
}
@@ -289,12 +346,33 @@ class MainActivity : ComponentActivity() {
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.e(TAG, "WebSocket closing: $code / $reason")
runOnUiThread {
if (code != 1000) {
connectionStatus = "Error"
}
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.i(TAG, "WebSocket closed: $code / $reason")
runOnUiThread {
if (connectionStatus != "Error") {
connectionStatus = "Disconnected"
}
}
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
Log.e(TAG, "WebSocket connection failed: ${t.message}", t)
runOnUiThread {
connectionStatus = "Error"
}
}
}
@@ -306,6 +384,7 @@ class MainActivity : ComponentActivity() {
fun stopRecording() {
connectionStatus = "Disconnected"
stopAudioCapture()
recordingJob?.cancel() // the ?. is Kotlin null-safety — no NPE if job is null
webSocket?.close(1000, "Done")
@@ -358,14 +437,27 @@ fun WhisperApp(text: String)
{ // ... inside your scrollable column ... Text( text = text,
// It is now tied to the 'transcriptText' variable style = MaterialTheme.typography.bodyLarge ) }
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WhisperApp(
transcriptText: String,
isRecording: Boolean = false,
connectionStatus: String = "Disconnected",
onOpenSettings: () -> Unit = {},
onToggleRecording: () -> Unit = {}
) {
Scaffold(
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Whisper Client") },
actions = {
IconButton(onClick = onOpenSettings) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
@@ -393,19 +485,148 @@ fun WhisperApp(
)
}
Button(
onClick = onToggleRecording,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
content = { Text(if (isRecording) "Stop Transcription" else "Start Transcription") }
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = onToggleRecording,
modifier = Modifier.weight(1f),
content = { Text(if (isRecording) "Stop Transcription" else "Start Transcription") }
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = connectionStatus,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = when (connectionStatus) {
"Server Ready" -> Color(0xFF4CAF50) // Green
"Error" -> MaterialTheme.colorScheme.error
"Connecting" -> Color(0xFFFFA000) // Amber
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}
}
}
@Composable
fun SettingsDialog(
initialSettings: HandshakeSettings,
onDismiss: () -> Unit,
onSave: (HandshakeSettings) -> Unit
) {
var host by remember { mutableStateOf(initialSettings.host) }
var port by remember { mutableStateOf(initialSettings.port) }
var task by remember { mutableStateOf(initialSettings.task) }
var model by remember { mutableStateOf(initialSettings.model) }
var language by remember { mutableStateOf(initialSettings.language) }
var useVad by remember { mutableStateOf(initialSettings.useVad) }
var audioFormat by remember { mutableStateOf(initialSettings.audioFormat) }
var sendLastNSegments by remember { mutableStateOf(initialSettings.sendLastNSegments.toString()) }
var noSpeechThresh by remember { mutableStateOf(initialSettings.noSpeechThresh.toString()) }
var clipAudio by remember { mutableStateOf(initialSettings.clipAudio) }
var sameOutputThreshold by remember { mutableStateOf(initialSettings.sameOutputThreshold.toString()) }
var enableTranslation by remember { mutableStateOf(initialSettings.enableTranslation) }
var targetLanguage by remember { mutableStateOf(initialSettings.targetLanguage) }
var hotwords by remember { mutableStateOf(initialSettings.hotwords ?: "") }
var enableDiarization by remember { mutableStateOf(initialSettings.enableDiarization) }
var maxSpeakers by remember { mutableStateOf(initialSettings.maxSpeakers.toString()) }
var wordTimestamps by remember { mutableStateOf(initialSettings.wordTimestamps) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Server & Handshake Settings") },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text("Network", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
OutlinedTextField(value = host, onValueChange = { host = it }, label = { Text("Host / IP") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = port, onValueChange = { port = it }, label = { Text("Port") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text("Whisper Configuration", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
OutlinedTextField(value = task, onValueChange = { task = it }, label = { Text("Task") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = model, onValueChange = { model = it }, label = { Text("Model") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
OutlinedTextField(value = language, onValueChange = { language = it }, label = { Text("Language") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
OutlinedTextField(value = audioFormat, onValueChange = { audioFormat = it }, label = { Text("Audio Format") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
OutlinedTextField(value = hotwords, onValueChange = { hotwords = it }, label = { Text("Hotwords (Optional)") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 8.dp)) {
Text("Use VAD", modifier = Modifier.weight(1f))
Switch(checked = useVad, onCheckedChange = { useVad = it })
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 8.dp)) {
Text("Clip Audio", modifier = Modifier.weight(1f))
Switch(checked = clipAudio, onCheckedChange = { clipAudio = it })
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 8.dp)) {
Text("Word Timestamps", modifier = Modifier.weight(1f))
Switch(checked = wordTimestamps, onCheckedChange = { wordTimestamps = it })
}
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text("Thresholds & Segments", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
OutlinedTextField(value = sendLastNSegments, onValueChange = { sendLastNSegments = it }, label = { Text("Send Last N Segments") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = noSpeechThresh, onValueChange = { noSpeechThresh = it }, label = { Text("No Speech Threshold") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
OutlinedTextField(value = sameOutputThreshold, onValueChange = { sameOutputThreshold = it }, label = { Text("Same Output Threshold") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text("Translation", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Enable Translation", modifier = Modifier.weight(1f))
Switch(checked = enableTranslation, onCheckedChange = { enableTranslation = it })
}
OutlinedTextField(value = targetLanguage, onValueChange = { targetLanguage = it }, label = { Text("Target Language") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text("Diarization", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Enable Diarization", modifier = Modifier.weight(1f))
Switch(checked = enableDiarization, onCheckedChange = { enableDiarization = it })
}
OutlinedTextField(value = maxSpeakers, onValueChange = { maxSpeakers = it }, label = { Text("Max Speakers") }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
}
},
confirmButton = {
TextButton(onClick = {
onSave(
HandshakeSettings(
host = host, port = port, task = task, model = model, language = language,
useVad = useVad, audioFormat = audioFormat,
sendLastNSegments = sendLastNSegments.toIntOrNull() ?: SettingsManager.DEFAULT_SEND_LAST_N_SEGMENTS,
noSpeechThresh = noSpeechThresh.toDoubleOrNull() ?: SettingsManager.DEFAULT_NO_SPEECH_THRESH,
clipAudio = clipAudio,
sameOutputThreshold = sameOutputThreshold.toIntOrNull() ?: SettingsManager.DEFAULT_SAME_OUTPUT_THRESHOLD,
enableTranslation = enableTranslation, targetLanguage = targetLanguage,
hotwords = hotwords.takeIf { it.isNotBlank() },
enableDiarization = enableDiarization,
maxSpeakers = maxSpeakers.toIntOrNull() ?: SettingsManager.DEFAULT_MAX_SPEAKERS,
wordTimestamps = wordTimestamps
)
)
}) {
Text("Save")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
@Preview(showBackground = true)
@Composable
fun WhisperAppPreview() {
WhisperClient2Theme {
WhisperApp("Transcription will appear here...")
WhisperApp(
transcriptText = "Transcription will appear here...",
connectionStatus = "Disconnected"
)
}
}
@@ -0,0 +1,118 @@
package com.objectbrokers.whisperclient2
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
data class HandshakeSettings(
val host: String,
val port: String,
val task: String,
val model: String,
val language: String,
val useVad: Boolean,
val audioFormat: String,
val sendLastNSegments: Int,
val noSpeechThresh: Double,
val clipAudio: Boolean,
val sameOutputThreshold: Int,
val enableTranslation: Boolean,
val targetLanguage: String,
val hotwords: String?,
val enableDiarization: Boolean,
val maxSpeakers: Int,
val wordTimestamps: Boolean
)
class SettingsManager(private val context: Context) {
companion object {
val HOST_KEY = stringPreferencesKey("websocket_host")
val PORT_KEY = stringPreferencesKey("websocket_port")
val TASK_KEY = stringPreferencesKey("task")
val MODEL_KEY = stringPreferencesKey("model")
val LANGUAGE_KEY = stringPreferencesKey("language")
val USE_VAD_KEY = booleanPreferencesKey("use_vad")
val AUDIO_FORMAT_KEY = stringPreferencesKey("audio_format")
val SEND_LAST_N_SEGMENTS_KEY = intPreferencesKey("send_last_n_segments")
val NO_SPEECH_THRESH_KEY = doublePreferencesKey("no_speech_thresh")
val CLIP_AUDIO_KEY = booleanPreferencesKey("clip_audio")
val SAME_OUTPUT_THRESHOLD_KEY = intPreferencesKey("same_output_threshold")
val ENABLE_TRANSLATION_KEY = booleanPreferencesKey("enable_translation")
val TARGET_LANGUAGE_KEY = stringPreferencesKey("target_language")
val HOTWORDS_KEY = stringPreferencesKey("hotwords")
val ENABLE_DIARIZATION_KEY = booleanPreferencesKey("enable_diarization")
val MAX_SPEAKERS_KEY = intPreferencesKey("max_speakers")
val WORD_TIMESTAMPS_KEY = booleanPreferencesKey("word_timestamps")
const val DEFAULT_HOST = "100.81.165.11"
const val DEFAULT_PORT = "9097"
const val DEFAULT_TASK = "transcribe"
const val DEFAULT_MODEL = "OpenVINO/whisper-medium-fp16-ov"
const val DEFAULT_LANGUAGE = "en"
const val DEFAULT_USE_VAD = false
const val DEFAULT_AUDIO_FORMAT = "int16"
const val DEFAULT_SEND_LAST_N_SEGMENTS = 10
const val DEFAULT_NO_SPEECH_THRESH = 0.45
const val DEFAULT_CLIP_AUDIO = false
const val DEFAULT_SAME_OUTPUT_THRESHOLD = 10
const val DEFAULT_ENABLE_TRANSLATION = false
const val DEFAULT_TARGET_LANGUAGE = "fr"
const val DEFAULT_ENABLE_DIARIZATION = false
const val DEFAULT_MAX_SPEAKERS = 10
const val DEFAULT_WORD_TIMESTAMPS = false
}
val settingsFlow: Flow<HandshakeSettings> = context.dataStore.data.map { preferences ->
HandshakeSettings(
host = preferences[HOST_KEY] ?: DEFAULT_HOST,
port = preferences[PORT_KEY] ?: DEFAULT_PORT,
task = preferences[TASK_KEY] ?: DEFAULT_TASK,
model = preferences[MODEL_KEY] ?: DEFAULT_MODEL,
language = preferences[LANGUAGE_KEY] ?: DEFAULT_LANGUAGE,
useVad = preferences[USE_VAD_KEY] ?: DEFAULT_USE_VAD,
audioFormat = preferences[AUDIO_FORMAT_KEY] ?: DEFAULT_AUDIO_FORMAT,
sendLastNSegments = preferences[SEND_LAST_N_SEGMENTS_KEY] ?: DEFAULT_SEND_LAST_N_SEGMENTS,
noSpeechThresh = preferences[NO_SPEECH_THRESH_KEY] ?: DEFAULT_NO_SPEECH_THRESH,
clipAudio = preferences[CLIP_AUDIO_KEY] ?: DEFAULT_CLIP_AUDIO,
sameOutputThreshold = preferences[SAME_OUTPUT_THRESHOLD_KEY] ?: DEFAULT_SAME_OUTPUT_THRESHOLD,
enableTranslation = preferences[ENABLE_TRANSLATION_KEY] ?: DEFAULT_ENABLE_TRANSLATION,
targetLanguage = preferences[TARGET_LANGUAGE_KEY] ?: DEFAULT_TARGET_LANGUAGE,
hotwords = preferences[HOTWORDS_KEY],
enableDiarization = preferences[ENABLE_DIARIZATION_KEY] ?: DEFAULT_ENABLE_DIARIZATION,
maxSpeakers = preferences[MAX_SPEAKERS_KEY] ?: DEFAULT_MAX_SPEAKERS,
wordTimestamps = preferences[WORD_TIMESTAMPS_KEY] ?: DEFAULT_WORD_TIMESTAMPS
)
}
suspend fun saveSettings(settings: HandshakeSettings) {
context.dataStore.edit { preferences ->
preferences[HOST_KEY] = settings.host
preferences[PORT_KEY] = settings.port
preferences[TASK_KEY] = settings.task
preferences[MODEL_KEY] = settings.model
preferences[LANGUAGE_KEY] = settings.language
preferences[USE_VAD_KEY] = settings.useVad
preferences[AUDIO_FORMAT_KEY] = settings.audioFormat
preferences[SEND_LAST_N_SEGMENTS_KEY] = settings.sendLastNSegments
preferences[NO_SPEECH_THRESH_KEY] = settings.noSpeechThresh
preferences[CLIP_AUDIO_KEY] = settings.clipAudio
preferences[SAME_OUTPUT_THRESHOLD_KEY] = settings.sameOutputThreshold
preferences[ENABLE_TRANSLATION_KEY] = settings.enableTranslation
preferences[TARGET_LANGUAGE_KEY] = settings.targetLanguage
if (settings.hotwords != null) {
preferences[HOTWORDS_KEY] = settings.hotwords
} else {
preferences.remove(HOTWORDS_KEY)
}
preferences[ENABLE_DIARIZATION_KEY] = settings.enableDiarization
preferences[MAX_SPEAKERS_KEY] = settings.maxSpeakers
preferences[WORD_TIMESTAMPS_KEY] = settings.wordTimestamps
}
}
}
+5 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "9.2.1"
agp = "9.3.0"
coreKtx = "1.19.0"
junit = "4.13.2"
junitVersion = "1.3.0"
@@ -8,6 +8,7 @@ lifecycleRuntimeKtx = "2.11.0"
activityCompose = "1.13.0"
kotlin = "2.2.10"
composeBom = "2026.02.01"
datastore = "1.2.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,6 +25,9 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-core" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+1 -1
View File
@@ -1,7 +1,7 @@
#Sat Jun 27 14:10:17 EDT 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME