Skip to content

Commit 77f91a2

Browse files
authored
Merge pull request #304 from celskeggs/cskeggs-patch-5
2 parents 78fe432 + 4e0ea3b commit 77f91a2

File tree

11 files changed

+96
-9
lines changed

11 files changed

+96
-9
lines changed

app/src/main/kotlin/org/stypox/dicio/di/SttInputDeviceWrapper.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import dagger.Provides
99
import dagger.hilt.InstallIn
1010
import dagger.hilt.android.qualifiers.ApplicationContext
1111
import dagger.hilt.components.SingletonComponent
12+
import javax.inject.Singleton
1213
import kotlinx.coroutines.CoroutineScope
1314
import kotlinx.coroutines.Dispatchers
1415
import kotlinx.coroutines.Job
@@ -24,15 +25,15 @@ import org.stypox.dicio.io.input.SttState
2425
import org.stypox.dicio.io.input.external_popup.ExternalPopupInputDevice
2526
import org.stypox.dicio.io.input.vosk.VoskInputDevice
2627
import org.stypox.dicio.settings.datastore.InputDevice
27-
import org.stypox.dicio.settings.datastore.InputDevice.INPUT_DEVICE_NOTHING
2828
import org.stypox.dicio.settings.datastore.InputDevice.INPUT_DEVICE_EXTERNAL_POPUP
29+
import org.stypox.dicio.settings.datastore.InputDevice.INPUT_DEVICE_NOTHING
2930
import org.stypox.dicio.settings.datastore.InputDevice.INPUT_DEVICE_UNSET
3031
import org.stypox.dicio.settings.datastore.InputDevice.INPUT_DEVICE_VOSK
3132
import org.stypox.dicio.settings.datastore.InputDevice.UNRECOGNIZED
3233
import org.stypox.dicio.settings.datastore.SttPlaySound
3334
import org.stypox.dicio.settings.datastore.UserSettings
3435
import org.stypox.dicio.util.distinctUntilChangedBlockingFirst
35-
import javax.inject.Singleton
36+
import org.stypox.dicio.util.toStateFlowDistinctBlockingFirst
3637

3738

3839
interface SttInputDeviceWrapper {
@@ -58,6 +59,7 @@ class SttInputDeviceWrapperImpl(
5859

5960
private var inputDeviceSetting: InputDevice
6061
private var sttPlaySoundSetting: SttPlaySound
62+
private val silencesBeforeStop: StateFlow<Int>
6163
private var sttInputDevice: SttInputDevice?
6264

6365
// null means that the user has not enabled any STT input device
@@ -75,6 +77,8 @@ class SttInputDeviceWrapperImpl(
7577

7678
inputDeviceSetting = firstSettings.first
7779
sttPlaySoundSetting = firstSettings.second
80+
silencesBeforeStop = dataStore.data.map(SttInputDevice::getSttSilenceDurationOrDefault)
81+
.toStateFlowDistinctBlockingFirst(scope)
7882
sttInputDevice = buildInputDevice(inputDeviceSetting)
7983
scope.launch {
8084
restartUiStateJob()
@@ -102,7 +106,7 @@ class SttInputDeviceWrapperImpl(
102106
return when (setting) {
103107
UNRECOGNIZED,
104108
INPUT_DEVICE_UNSET,
105-
INPUT_DEVICE_VOSK -> VoskInputDevice(appContext, okHttpClient, localeManager)
109+
INPUT_DEVICE_VOSK -> VoskInputDevice(appContext, okHttpClient, localeManager, silencesBeforeStop)
106110
INPUT_DEVICE_EXTERNAL_POPUP ->
107111
ExternalPopupInputDevice(appContext, activityForResultManager, localeManager)
108112
INPUT_DEVICE_NOTHING -> null

app/src/main/kotlin/org/stypox/dicio/io/input/SttInputDevice.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.stypox.dicio.io.input
22

33
import kotlinx.coroutines.flow.StateFlow
4+
import org.stypox.dicio.settings.datastore.UserSettings
45

56
interface SttInputDevice {
67
val uiState: StateFlow<SttState>
@@ -12,4 +13,12 @@ interface SttInputDevice {
1213
fun onClick(eventListener: (InputEvent) -> Unit)
1314

1415
suspend fun destroy()
16+
17+
companion object {
18+
const val DEFAULT_STT_SILENCE_DURATION = 2
19+
fun getSttSilenceDurationOrDefault(settings: UserSettings): Int {
20+
// unfortunately there is no way to tell protobuf to use "2" as the default value
21+
return settings.sttSilenceDuration.takeIf { it > 0 } ?: DEFAULT_STT_SILENCE_DURATION
22+
}
23+
}
1524
}

app/src/main/kotlin/org/stypox/dicio/io/input/vosk/VoskInputDevice.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import org.stypox.dicio.io.input.vosk.VoskState.NotDownloaded
4949
import org.stypox.dicio.io.input.vosk.VoskState.NotInitialized
5050
import org.stypox.dicio.io.input.vosk.VoskState.NotLoaded
5151
import org.stypox.dicio.io.input.vosk.VoskState.Unzipping
52+
import org.stypox.dicio.settings.datastore.UserSettings
5253
import org.stypox.dicio.ui.util.Progress
5354
import org.stypox.dicio.util.FileToDownload
5455
import org.stypox.dicio.util.LocaleUtils
@@ -68,6 +69,7 @@ class VoskInputDevice(
6869
@ApplicationContext appContext: Context,
6970
private val okHttpClient: OkHttpClient,
7071
localeManager: LocaleManager,
72+
private val silencesBeforeStop: StateFlow<Int>
7173
) : SttInputDevice {
7274

7375
private val _state: MutableStateFlow<VoskState>
@@ -84,7 +86,6 @@ class VoskInputDevice(
8486
private val modelDirectory: File get() = File(filesDir, "vosk-model")
8587
private val modelExistFileCheck: File get() = File(modelDirectory, "ivector")
8688

87-
8889
init {
8990
// Run blocking, because the locale is always available right away since LocaleManager also
9091
// initializes in a blocking way. Moreover, if VoskInputDevice were not initialized straight
@@ -420,7 +421,7 @@ class VoskInputDevice(
420421
eventListener: (InputEvent) -> Unit,
421422
) {
422423
_state.value = Listening(speechService, eventListener)
423-
speechService.startListening(VoskListener(this, eventListener, speechService))
424+
speechService.startListening(VoskListener(this, eventListener, silencesBeforeStop.value, speechService))
424425
}
425426

426427
/**

app/src/main/kotlin/org/stypox/dicio/io/input/vosk/VoskListener.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ import org.vosk.android.SpeechService
3131
* This class is an implementation of [RecognitionListener] that listens to events generated by a
3232
* Vosk [SpeechService], pushes them to [eventListener], and handles the listening state via
3333
* [VoskInputDevice].
34+
*
35+
* @param silencesBeforeStop how many times Vosk should report a "silence" before giving up trying
36+
* to get speech; must be `>= 1`, and setting this to 1 is equivalent to disabling this feature
3437
*/
3538
internal class VoskListener(
3639
private val voskInputDevice: VoskInputDevice,
3740
private val eventListener: (InputEvent) -> Unit,
41+
private var silencesBeforeStop: Int,
3842
private val speechService: SpeechService,
3943
) : RecognitionListener {
4044

@@ -67,10 +71,7 @@ internal class VoskListener(
6771
*/
6872
@Suppress("ktlint:Style:NestedBlockDepth")
6973
override fun onResult(s: String) {
70-
Log.d(TAG, "onResult called with s = $s")
71-
72-
// we only want to listen one sentence at a time
73-
voskInputDevice.stopListening(speechService, eventListener, false)
74+
Log.d(TAG, "onResult called with s = $s; silencesBeforeStop=$silencesBeforeStop")
7475

7576
// collect all alternative user inputs generated by the STT model
7677
val inputs = try {
@@ -79,9 +80,18 @@ internal class VoskListener(
7980
} catch (e: JSONException) {
8081
Log.e(TAG, "Can't obtain result from $s", e)
8182
eventListener(InputEvent.Error(e))
83+
voskInputDevice.stopListening(speechService, eventListener, false)
84+
return
85+
}
86+
87+
if (inputs.isEmpty() && silencesBeforeStop > 1) {
88+
silencesBeforeStop -= 1
8289
return
8390
}
8491

92+
// we only want to listen one sentence at a time
93+
voskInputDevice.stopListening(speechService, eventListener, false)
94+
8595
// emit the final event
8696
if (inputs.isEmpty()) {
8797
eventListener(InputEvent.None)

app/src/main/kotlin/org/stypox/dicio/settings/Definitions.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Cloud
99
import androidx.compose.material.icons.filled.ColorLens
1010
import androidx.compose.material.icons.filled.DarkMode
1111
import androidx.compose.material.icons.filled.Hearing
12+
import androidx.compose.material.icons.filled.HourglassEmpty
1213
import androidx.compose.material.icons.filled.InvertColors
1314
import androidx.compose.material.icons.filled.KeyboardAlt
1415
import androidx.compose.material.icons.filled.Language
@@ -30,6 +31,7 @@ import org.stypox.dicio.settings.datastore.SttPlaySound
3031
import org.stypox.dicio.settings.datastore.Theme
3132
import org.stypox.dicio.settings.datastore.WakeDevice
3233
import org.stypox.dicio.settings.ui.BooleanSetting
34+
import org.stypox.dicio.settings.ui.IntSetting
3335
import org.stypox.dicio.settings.ui.ListSetting
3436

3537

@@ -171,6 +173,15 @@ fun speechOutputDevice() = ListSetting(
171173
),
172174
)
173175

176+
@Composable
177+
fun sttSilenceDuration() = IntSetting(
178+
title = stringResource(R.string.pref_stt_silence_duration_title),
179+
icon = Icons.Default.HourglassEmpty,
180+
description = @Composable { stringResource(R.string.pref_stt_silence_duration_description, it) },
181+
minimum = 1,
182+
maximum = 7,
183+
)
184+
174185
@Composable
175186
fun sttAutoFinish() = BooleanSetting(
176187
title = stringResource(R.string.pref_stt_auto_finish_title),

app/src/main/kotlin/org/stypox/dicio/settings/MainSettingsScreen.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview
3232
import androidx.compose.ui.unit.dp
3333
import androidx.hilt.navigation.compose.hiltViewModel
3434
import org.stypox.dicio.R
35+
import org.stypox.dicio.io.input.SttInputDevice
3536
import org.stypox.dicio.settings.datastore.InputDevice
3637
import org.stypox.dicio.settings.datastore.Language
3738
import org.stypox.dicio.settings.datastore.SpeechOutputDevice
@@ -187,6 +188,12 @@ private fun MainSettingsScreen(
187188
viewModel::setSttPlaySound
188189
)
189190
}
191+
item {
192+
sttSilenceDuration().Render(
193+
SttInputDevice.getSttSilenceDurationOrDefault(settings),
194+
viewModel::setSttSilenceDuration
195+
)
196+
}
190197
item {
191198
sttAutoFinish().Render(
192199
settings.autoFinishSttPopup,

app/src/main/kotlin/org/stypox/dicio/settings/MainSettingsViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ class MainSettingsViewModel @Inject constructor(
7171
updateData { it.setSpeechOutputDevice(value) }
7272
fun setSttPlaySound(value: SttPlaySound) =
7373
updateData { it.setSttPlaySound(value) }
74+
fun setSttSilenceDuration(value: Int) =
75+
updateData { it.setSttSilenceDuration(value) }
7476
fun setAutoFinishSttPopup(value: Boolean) =
7577
updateData { it.setAutoFinishSttPopup(value) }
7678
}

app/src/main/kotlin/org/stypox/dicio/settings/ui/Setting.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import androidx.compose.material.icons.filled.BookmarkAdded
1515
import androidx.compose.material.icons.filled.BookmarkRemove
1616
import androidx.compose.material.icons.filled.Pets
1717
import androidx.compose.material3.Card
18+
import androidx.compose.material3.ExperimentalMaterial3Api
1819
import androidx.compose.material3.Icon
1920
import androidx.compose.material3.MaterialTheme
2021
import androidx.compose.material3.RadioButton
22+
import androidx.compose.material3.Slider
2123
import androidx.compose.material3.Switch
2224
import androidx.compose.material3.Text
2325
import androidx.compose.material3.TextButton
@@ -44,6 +46,7 @@ import androidx.compose.ui.unit.dp
4446
import androidx.compose.ui.window.Dialog
4547
import kotlinx.coroutines.job
4648
import org.stypox.dicio.R
49+
import kotlin.math.roundToInt
4750

4851
interface SettingWithValue<T> {
4952
@Composable
@@ -79,6 +82,38 @@ class BooleanSetting(
7982
}
8083
}
8184

85+
class IntSetting(
86+
private val title: String,
87+
private val minimum: Int,
88+
private val maximum: Int,
89+
private val icon: ImageVector? = null,
90+
private val description: (@Composable (Int) -> String)? = null,
91+
) : SettingWithValue<Int> {
92+
init {
93+
assert(maximum > minimum)
94+
}
95+
96+
@Composable
97+
override fun Render(
98+
value: Int,
99+
onValueChange: (Int) -> Unit,
100+
) {
101+
SettingsItem(
102+
title = title,
103+
icon = icon,
104+
description = description?.invoke(value),
105+
innerContent = {
106+
Slider(
107+
value = value.toFloat(),
108+
onValueChange = { onValueChange(it.roundToInt()) },
109+
steps = maximum - minimum - 1,
110+
valueRange = minimum.toFloat()..maximum.toFloat()
111+
)
112+
},
113+
)
114+
}
115+
}
116+
82117
class ListSetting<T>(
83118
private val title: String,
84119
private val icon: ImageVector? = null,

app/src/main/kotlin/org/stypox/dicio/settings/ui/SettingsItem.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fun SettingsItem(
3737
icon: ImageVector? = null,
3838
description: String? = null,
3939
content: (@Composable () -> Unit)? = null,
40+
innerContent: (@Composable () -> Unit)? = null,
4041
) {
4142
Row(
4243
verticalAlignment = Alignment.CenterVertically,
@@ -70,6 +71,10 @@ fun SettingsItem(
7071
style = MaterialTheme.typography.bodyMedium,
7172
)
7273
}
74+
if (innerContent != null) {
75+
Spacer(modifier = Modifier.height(3.dp))
76+
innerContent()
77+
}
7378
}
7479
if (content != null) {
7580
Spacer(modifier = Modifier.width(16.dp))

app/src/main/proto/user_settings.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ message UserSettings {
2020
map<string, bool> enabled_skills = 7;
2121
WakeDevice wake_device = 8;
2222
SttPlaySound stt_play_sound = 9;
23+
int32 stt_silence_duration = 10;
2324
}

0 commit comments

Comments
 (0)