Voice capture
VoiceCaptureSession uses Android's speech recognizer to turn microphone input into text. It doesn't call January and needs no client or token. To get foods from the text, send the transcript to user.foodAnalysis.analyzeDescription(SearchFoodsByNaturalLanguageRequest(transcript)) (Food analysis).
Add voice input to a field
The SDK's manifest declares RECORD_AUDIO, but your app must request the runtime permission. Create one session per voice-enabled input, close it with the composition, and call every session method on the main thread. startListening() throws a VoiceCaptureException right away when the permission is missing (PERMISSION_DENIED), the device has no speech recognizer (RECOGNIZER_UNAVAILABLE, common on emulators), or a capture is already running (INVALID_STATE). Failures after listening starts arrive on the error flow instead.
import ai.january.partner.voice.VoiceCaptureErrorCode
import ai.january.partner.voice.VoiceCaptureException
import ai.january.partner.voice.VoiceCaptureSession
import ai.january.partner.voice.VoiceCaptureState
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
@Composable
fun VoiceSearchField() {
val context = LocalContext.current
var query by rememberSaveable { mutableStateOf("") }
var voiceError by remember { mutableStateOf<VoiceCaptureErrorCode?>(null) }
val voiceCapture = remember { VoiceCaptureSession(context) }
DisposableEffect(voiceCapture) { onDispose { voiceCapture.close() } }
val state by voiceCapture.state.collectAsState()
val result by voiceCapture.latestResult.collectAsState()
val failure by voiceCapture.error.collectAsState()
fun start() {
voiceError = null
try {
voiceCapture.startListening()
} catch (error: VoiceCaptureException) {
voiceError = error.code
}
}
val permission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) start() else voiceError = VoiceCaptureErrorCode.PERMISSION_DENIED
}
LaunchedEffect(result) {
result?.let {
query = "$query ${it.transcript}".trim()
voiceCapture.clearResult()
}
}
LaunchedEffect(failure) {
failure?.let {
voiceError = it.code
voiceCapture.clearError()
}
}
OutlinedTextField(
value = query,
onValueChange = { query = it },
supportingText = { voiceError?.let { Text("Voice input failed: $it") } },
trailingIcon = {
IconButton(
enabled = state != VoiceCaptureState.PROCESSING,
onClick = {
when {
state == VoiceCaptureState.LISTENING -> voiceCapture.stopListening()
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED -> start()
else -> permission.launch(Manifest.permission.RECORD_AUDIO)
}
},
) { Text(if (state == VoiceCaptureState.LISTENING) "Stop" else "Mic") }
},
)
}Ask for the permission only after the user taps the microphone. After a denial, explain why voice input needs it. If Android no longer shows the prompt, offer to open the app's settings page (Settings.ACTION_APPLICATION_DETAILS_SETTINGS).
Session state
The lifecycle is IDLE → LISTENING → PROCESSING → IDLE. Call stopListening() to end listening and get the final transcript, or cancel() to discard the capture.
audioLevelis aStateFlowfrom 0 to 1 for a level meter.partialTranscriptis aStateFlowwith live text while listening.elapsedDurationMillisis a plain property, not a flow; poll it (for example every 100 ms whileLISTENING) to drive a timer.latestResultholds aVoiceCaptureResultwith the finaltranscriptanddurationMillis. CallclearResult()once you've used it.errorholds the latestVoiceCaptureException. Itscodeis one ofPERMISSION_DENIED,RECOGNIZER_UNAVAILABLE,RECOGNIZER_BUSY,AUDIO,NETWORK,NO_MATCH,INVALID_STATE, orUNKNOWN. CallclearError()once you've shown it.
The constructor also takes locale (default Locale.getDefault()) and endOfSpeechSilence, how long a pause ends the capture (default two seconds; a hint that some recognizers ignore):
Android's speech recognizer owns the microphone stream. The SDK doesn't keep or expose recorded audio.
Last updated
Was this helpful?