Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions app/src/main/kotlin/org/stypox/dicio/di/SttInputDeviceWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.stypox.dicio.R
Expand Down Expand Up @@ -56,8 +55,7 @@ class SttInputDeviceWrapperImpl(
) : SttInputDeviceWrapper {
private val scope = CoroutineScope(Dispatchers.Default)

private var inputDeviceSetting: InputDevice
private var sttPlaySoundSetting: SttPlaySound
private var settings: UserSettings
private var sttInputDevice: SttInputDevice?

// null means that the user has not enabled any STT input device
Expand All @@ -69,30 +67,27 @@ class SttInputDeviceWrapperImpl(
init {
// Run blocking, because the data store is always available right away since LocaleManager
// also initializes in a blocking way from the same data store.
val (firstSettings, nextSettingsFlow) = dataStore.data
.map { Pair(it.inputDevice, it.sttPlaySound) }
.distinctUntilChangedBlockingFirst()
val (firstSettings, nextSettingsFlow) = dataStore.data.distinctUntilChangedBlockingFirst()

inputDeviceSetting = firstSettings.first
sttPlaySoundSetting = firstSettings.second
sttInputDevice = buildInputDevice(inputDeviceSetting)
settings = firstSettings
sttInputDevice = buildInputDevice(settings.inputDevice)
scope.launch {
restartUiStateJob()
}

scope.launch {
nextSettingsFlow.collect { (inputDevice, sttPlaySound) ->
sttPlaySoundSetting = sttPlaySound
if (inputDeviceSetting != inputDevice) {
changeInputDeviceTo(inputDevice)
nextSettingsFlow.collect { newSettings ->
val oldSettings = settings
settings = newSettings
if (oldSettings.inputDevice != newSettings.inputDevice) {
changeInputDeviceTo(newSettings.inputDevice)
}
}
}
}

private suspend fun changeInputDeviceTo(setting: InputDevice) {
val prevSttInputDevice = sttInputDevice
inputDeviceSetting = setting
sttInputDevice = buildInputDevice(setting)
prevSttInputDevice?.destroy()
restartUiStateJob()
Expand All @@ -102,7 +97,7 @@ class SttInputDeviceWrapperImpl(
return when (setting) {
UNRECOGNIZED,
INPUT_DEVICE_UNSET,
INPUT_DEVICE_VOSK -> VoskInputDevice(appContext, okHttpClient, localeManager)
INPUT_DEVICE_VOSK -> VoskInputDevice(appContext, okHttpClient, localeManager, settings)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user changes the settings after this has been built, the old settings instance will remain. It's probably better to maintain the previous code for the rest of the class and instead pass dataStore here.

INPUT_DEVICE_EXTERNAL_POPUP ->
ExternalPopupInputDevice(appContext, activityForResultManager, localeManager)
INPUT_DEVICE_NOTHING -> null
Expand Down Expand Up @@ -130,7 +125,7 @@ class SttInputDeviceWrapperImpl(
private fun playSound(resid: Int) {
val attributes = AudioAttributes.Builder()
.setUsage(
when (sttPlaySoundSetting) {
when (settings.sttPlaySound!!) {
SttPlaySound.UNRECOGNIZED,
SttPlaySound.STT_PLAY_SOUND_UNSET,
SttPlaySound.STT_PLAY_SOUND_NOTIFICATION -> AudioAttributes.USAGE_NOTIFICATION
Expand Down Expand Up @@ -169,7 +164,7 @@ class SttInputDeviceWrapperImpl(
}

override fun reinitializeToReleaseResources() {
scope.launch { changeInputDeviceTo(inputDeviceSetting) }
scope.launch { changeInputDeviceTo(settings.inputDevice) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import org.stypox.dicio.io.input.vosk.VoskState.NotDownloaded
import org.stypox.dicio.io.input.vosk.VoskState.NotInitialized
import org.stypox.dicio.io.input.vosk.VoskState.NotLoaded
import org.stypox.dicio.io.input.vosk.VoskState.Unzipping
import org.stypox.dicio.settings.datastore.UserSettings
import org.stypox.dicio.ui.util.Progress
import org.stypox.dicio.util.FileToDownload
import org.stypox.dicio.util.LocaleUtils
Expand All @@ -69,6 +70,7 @@ class VoskInputDevice(
@ApplicationContext appContext: Context,
private val okHttpClient: OkHttpClient,
localeManager: LocaleManager,
private val settings: UserSettings,
) : SttInputDevice {

private val _state: MutableStateFlow<VoskState>
Expand All @@ -85,7 +87,6 @@ class VoskInputDevice(
private val modelDirectory: File get() = File(filesDir, "vosk-model")
private val modelExistFileCheck: File get() = File(modelDirectory, "ivector")


init {
// Run blocking, because the locale is always available right away since LocaleManager also
// initializes in a blocking way. Moreover, if VoskInputDevice were not initialized straight
Expand Down Expand Up @@ -429,7 +430,7 @@ class VoskInputDevice(
eventListener: (InputEvent) -> Unit,
) {
_state.value = Listening(speechService, eventListener)
speechService.startListening(VoskListener(this, eventListener, speechService))
speechService.startListening(VoskListener(this, eventListener, settings.sttSilenceDuration, speechService))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to set settings.sttSilenceDuration to something like 2 by default (i.e. when the setting has never been changed by the user yet)?

}

/**
Expand Down
13 changes: 10 additions & 3 deletions app/src/main/kotlin/org/stypox/dicio/io/input/vosk/VoskListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.vosk.android.SpeechService
internal class VoskListener(
private val voskInputDevice: VoskInputDevice,
private val eventListener: (InputEvent) -> Unit,
private var silencesBeforeStop: Int,
private val speechService: SpeechService,
) : RecognitionListener {

Expand Down Expand Up @@ -69,19 +70,25 @@ internal class VoskListener(
override fun onResult(s: String) {
Log.d(TAG, "onResult called with s = $s")

// we only want to listen one sentence at a time
voskInputDevice.stopListening(speechService, eventListener, false)

// collect all alternative user inputs generated by the STT model
val inputs = try {
val jsonResult = JSONObject(s)
utterancesFromJson(jsonResult)
} catch (e: JSONException) {
Log.e(TAG, "Can't obtain result from $s", e)
eventListener(InputEvent.Error(e))
voskInputDevice.stopListening(speechService, eventListener, false)
return
}

if (inputs.isEmpty() && silencesBeforeStop > 0) {
silencesBeforeStop -= 1
return
}

// we only want to listen one sentence at a time
voskInputDevice.stopListening(speechService, eventListener, false)

// emit the final event
if (inputs.isEmpty()) {
eventListener(InputEvent.None)
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/kotlin/org/stypox/dicio/settings/Definitions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.stypox.dicio.settings.datastore.SttPlaySound
import org.stypox.dicio.settings.datastore.Theme
import org.stypox.dicio.settings.datastore.WakeDevice
import org.stypox.dicio.settings.ui.BooleanSetting
import org.stypox.dicio.settings.ui.IntSetting
import org.stypox.dicio.settings.ui.ListSetting


Expand Down Expand Up @@ -168,6 +169,15 @@ fun speechOutputDevice() = ListSetting(
),
)

@Composable
fun sttSilenceDuration() = IntSetting(
title = stringResource(R.string.pref_stt_silence_duration_title),
icon = Icons.AutoMirrored.Filled.Send,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use a different icon here, like hourglass_empty

description = @Composable { stringResource(R.string.pref_stt_silence_duration_description, it) },
minimum = 1,
maximum = 7,
)

@Composable
fun sttAutoFinish() = BooleanSetting(
title = stringResource(R.string.pref_stt_auto_finish_title),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ private fun MainSettingsScreen(
viewModel::setSttPlaySound
)
}
item {
sttSilenceDuration().Render(
settings.sttSilenceDuration + 1,
) { viewModel.setSttSilenceDuration(it - 1) }
}
item {
sttAutoFinish().Render(
settings.autoFinishSttPopup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class MainSettingsViewModel @Inject constructor(
updateData { it.setSpeechOutputDevice(value) }
fun setSttPlaySound(value: SttPlaySound) =
updateData { it.setSttPlaySound(value) }
fun setSttSilenceDuration(value: Int) =
updateData { it.setSttSilenceDuration(value) }
fun setAutoFinishSttPopup(value: Boolean) =
updateData { it.setAutoFinishSttPopup(value) }
}
35 changes: 35 additions & 0 deletions app/src/main/kotlin/org/stypox/dicio/settings/ui/Setting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import androidx.compose.material.icons.filled.BookmarkAdded
import androidx.compose.material.icons.filled.BookmarkRemove
import androidx.compose.material.icons.filled.Pets
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
Expand All @@ -44,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.job
import org.stypox.dicio.R
import kotlin.math.roundToInt

interface SettingWithValue<T> {
@Composable
Expand Down Expand Up @@ -79,6 +82,38 @@ class BooleanSetting(
}
}

class IntSetting(
private val title: String,
private val minimum: Int,
private val maximum: Int,
private val icon: ImageVector? = null,
private val description: (@Composable (Int) -> String)? = null,
) : SettingWithValue<Int> {
init {
assert(maximum > minimum)
}

@Composable
override fun Render(
value: Int,
onValueChange: (Int) -> Unit,
) {
SettingsItem(
title = title,
icon = icon,
description = description?.invoke(value),
innerContent = {
Slider(
value = value.toFloat(),
onValueChange = { onValueChange(it.roundToInt()) },
steps = maximum - minimum - 1,
valueRange = minimum.toFloat()..maximum.toFloat()
)
},
)
}
}

class ListSetting<T>(
private val title: String,
private val icon: ImageVector? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fun SettingsItem(
icon: ImageVector? = null,
description: String? = null,
content: (@Composable () -> Unit)? = null,
innerContent: (@Composable () -> Unit)? = null,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
Expand Down Expand Up @@ -67,6 +68,10 @@ fun SettingsItem(
style = MaterialTheme.typography.bodyMedium,
)
}
if (innerContent != null) {
Spacer(modifier = Modifier.height(3.dp))
innerContent()
}
}
if (content != null) {
Spacer(modifier = Modifier.width(16.dp))
Expand Down
1 change: 1 addition & 0 deletions app/src/main/proto/user_settings.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ message UserSettings {
map<string, bool> enabled_skills = 7;
WakeDevice wake_device = 8;
SttPlaySound stt_play_sound = 9;
int32 stt_silence_duration = 10;
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
<string name="pref_search_engine_duckduckgo" translatable="false">DuckDuckGo</string>
<string name="pref_weather_default_city">Default city</string>
<string name="pref_weather_default_city_using_ip_info">Set the city to use for weather when you do not explicitly say one. The current behaviour is to get the location from IP info.</string>
<string name="pref_stt_silence_duration_title">Duration of silence</string>
<string name="pref_stt_silence_duration_description">Wait %1$d times for speech before giving up</string>
<string name="pref_stt_auto_finish_title">Directly send result of speech to text popup</string>
<string name="pref_stt_auto_finish_summary_on">Automatically send speech result to requesting app when listening finishes</string>
<string name="pref_stt_auto_finish_summary_off">Wait for manual confirmation before sending speech result to requesting app</string>
Expand Down