feat: add support for simple speech to text #34

Merged
reya merged 2 commits from feat/speech-to-text into master 2026-07-03 08:38:59 +00:00
2 changed files with 74 additions and 17 deletions
Showing only changes of commit 6fe2ce3e31 - Show all commits

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M280,720L280,240L360,240L360,720L280,720ZM440,880L440,80L520,80L520,880L440,880ZM120,560L120,400L200,400L200,560L120,560ZM600,720L600,240L680,240L680,720L600,720ZM760,560L760,400L840,400L840,560L760,560Z" />
</vector>

View File

@@ -1,6 +1,9 @@
package su.reya.coop.screens package su.reya.coop.screens
import android.app.Activity
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.speech.RecognizerIntent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -66,6 +69,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_add_circle import coop.composeapp.generated.resources.ic_add_circle
import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_audio
import coop.composeapp.generated.resources.ic_cancel import coop.composeapp.generated.resources.ic_cancel
import coop.composeapp.generated.resources.ic_check_circle import coop.composeapp.generated.resources.ic_check_circle
import coop.composeapp.generated.resources.ic_send import coop.composeapp.generated.resources.ic_send
@@ -153,9 +157,19 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
} }
} }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> val fileLauncher =
uri?.let { sendFile(it) } rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
} uri?.let { sendFile(it) }
}
val sttLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
val results = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (!results.isNullOrEmpty()) text = results[0]
}
}
LaunchedEffect(id) { LaunchedEffect(id) {
// Get messages // Get messages
@@ -356,7 +370,24 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
text = "" text = ""
}, },
onUpload = { onUpload = {
launcher.launch("image/*") fileLauncher.launch("image/*")
},
onMicClick = {
val intent =
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak now...")
}
try {
sttLauncher.launch(intent)
} catch (e: Exception) {
scope.launch {
snackbarHostState.showSnackbar("Speech recognition not available")
}
}
} }
) )
} }
@@ -550,7 +581,8 @@ fun ChatInput(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
onUpload: () -> Unit onUpload: () -> Unit,
onMicClick: () -> Unit
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
@@ -576,18 +608,34 @@ fun ChatInput(
}, },
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
FilledTonalIconButton( if (value.isNotEmpty()) {
onClick = onSend, FilledTonalIconButton(
modifier = Modifier.size(56.dp), onClick = onSend,
colors = IconButtonDefaults.filledTonalIconButtonColors( modifier = Modifier.size(56.dp),
containerColor = MaterialTheme.colorScheme.surfaceVariant, colors = IconButtonDefaults.filledTonalIconButtonColors(
contentColor = MaterialTheme.colorScheme.onSurfaceVariant containerColor = MaterialTheme.colorScheme.surfaceVariant,
) contentColor = MaterialTheme.colorScheme.onSurfaceVariant
) { )
Icon( ) {
painter = painterResource(Res.drawable.ic_send), Icon(
contentDescription = "Send" painter = painterResource(Res.drawable.ic_send),
) contentDescription = "Send"
)
}
} else {
FilledTonalIconButton(
onClick = onMicClick,
modifier = Modifier.size(56.dp),
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
) {
Icon(
painter = painterResource(Res.drawable.ic_audio),
contentDescription = "Speech to Text"
)
}
} }
} }
} }