reduce duplication:
New shared controller for state + processing + distorted file cleanup New shared UI section for anonymization header + preset selector
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class VoiceAnonymizationController(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val logTag: String,
|
||||||
|
private val onError: (Throwable) -> Unit,
|
||||||
|
) {
|
||||||
|
var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE)
|
||||||
|
private set
|
||||||
|
var processingPreset: VoicePreset? by mutableStateOf(null)
|
||||||
|
private set
|
||||||
|
var distortedFiles: Map<VoicePreset, AnonymizedResult> by mutableStateOf(emptyMap())
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var processingJob: Job? = null
|
||||||
|
|
||||||
|
fun activeFile(originalFile: File?): File? =
|
||||||
|
if (selectedPreset == VoicePreset.NONE) {
|
||||||
|
originalFile
|
||||||
|
} else {
|
||||||
|
distortedFiles[selectedPreset]?.file
|
||||||
|
}
|
||||||
|
|
||||||
|
fun activeWaveform(originalWaveform: List<Float>?): List<Float>? =
|
||||||
|
if (selectedPreset == VoicePreset.NONE) {
|
||||||
|
originalWaveform
|
||||||
|
} else {
|
||||||
|
distortedFiles[selectedPreset]?.waveform
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectPreset(
|
||||||
|
preset: VoicePreset,
|
||||||
|
originalFile: File?,
|
||||||
|
) {
|
||||||
|
if (processingPreset != null || preset == selectedPreset) return
|
||||||
|
|
||||||
|
if (preset == VoicePreset.NONE) {
|
||||||
|
selectedPreset = preset
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distortedFiles.containsKey(preset)) {
|
||||||
|
selectedPreset = preset
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = originalFile ?: return
|
||||||
|
|
||||||
|
processingJob?.cancel()
|
||||||
|
processingJob =
|
||||||
|
scope.launch {
|
||||||
|
processingPreset = preset
|
||||||
|
try {
|
||||||
|
val anonymizer = VoiceAnonymizer()
|
||||||
|
val result = anonymizer.anonymize(file, preset)
|
||||||
|
|
||||||
|
result
|
||||||
|
.onSuccess { anonymizedResult ->
|
||||||
|
distortedFiles = distortedFiles + (preset to anonymizedResult)
|
||||||
|
selectedPreset = preset
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(logTag, "Failed to anonymize voice", error)
|
||||||
|
onError(error)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processingPreset = null
|
||||||
|
processingJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
cancelProcessing()
|
||||||
|
deleteDistortedFiles()
|
||||||
|
selectedPreset = VoicePreset.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDistortedFiles() {
|
||||||
|
distortedFiles.values.forEach { result ->
|
||||||
|
try {
|
||||||
|
if (result.file.exists()) {
|
||||||
|
result.file.delete()
|
||||||
|
Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distortedFiles = emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelProcessing() {
|
||||||
|
processingJob?.cancel()
|
||||||
|
processingJob = null
|
||||||
|
processingPreset = null
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VoiceAnonymizationSection(
|
||||||
|
selectedPreset: VoicePreset,
|
||||||
|
processingPreset: VoicePreset?,
|
||||||
|
onPresetSelected: (VoicePreset) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.voice_anonymize_title),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.voice_anonymize_description),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
VoicePresetSelector(
|
||||||
|
selectedPreset = selectedPreset,
|
||||||
|
processingPreset = processingPreset,
|
||||||
|
onPresetSelected = onPresetSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -34,11 +34,11 @@ import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter
|
|||||||
import be.tarsos.dsp.io.TarsosDSPAudioFormat
|
import be.tarsos.dsp.io.TarsosDSPAudioFormat
|
||||||
import be.tarsos.dsp.resample.RateTransposer
|
import be.tarsos.dsp.resample.RateTransposer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
data class AnonymizedResult(
|
data class AnonymizedResult(
|
||||||
@@ -147,7 +147,7 @@ class VoiceAnonymizer {
|
|||||||
var inputDone = false
|
var inputDone = false
|
||||||
var outputDone = false
|
var outputDone = false
|
||||||
|
|
||||||
while (!outputDone && coroutineContext.isActive) {
|
while (!outputDone && currentCoroutineContext().isActive) {
|
||||||
if (!inputDone) {
|
if (!inputDone) {
|
||||||
val inputBufferIndex = decoder.dequeueInputBuffer(10000)
|
val inputBufferIndex = decoder.dequeueInputBuffer(10000)
|
||||||
if (inputBufferIndex >= 0) {
|
if (inputBufferIndex >= 0) {
|
||||||
|
|||||||
@@ -20,16 +20,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.actions.uploads
|
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
|
||||||
enum class VoicePreset(
|
enum class VoicePreset(
|
||||||
val pitchFactor: Double,
|
val pitchFactor: Double,
|
||||||
val formantShift: Double,
|
val labelRes: Int,
|
||||||
@StringRes val labelRes: Int,
|
|
||||||
) {
|
) {
|
||||||
NONE(1.0, 0.0, R.string.voice_preset_none),
|
NONE(1.0, R.string.voice_preset_none),
|
||||||
DEEP(1.4, -3.0, R.string.voice_preset_deep),
|
DEEP(1.4, R.string.voice_preset_deep),
|
||||||
HIGH(0.75, 4.0, R.string.voice_preset_high),
|
HIGH(0.75, R.string.voice_preset_high),
|
||||||
NEUTRAL(0.9, -2.0, R.string.voice_preset_neutral),
|
NEUTRAL(0.9, R.string.voice_preset_neutral),
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-30
@@ -25,7 +25,6 @@ import android.net.Uri
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -39,24 +38,20 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -70,8 +65,8 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||||
@@ -101,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.painterRes
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
@@ -385,29 +379,7 @@ private fun NewPostScreenBody(
|
|||||||
|
|
||||||
// Voice anonymization section (only show when not uploading and voice is pending)
|
// Voice anonymization section (only show when not uploading and voice is pending)
|
||||||
if (postViewModel.voiceRecording != null) {
|
if (postViewModel.voiceRecording != null) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
VoiceAnonymizationSection(
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringRes(R.string.voice_anonymize_title),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringRes(R.string.voice_anonymize_description),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = Color.Gray,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
VoicePresetSelector(
|
|
||||||
selectedPreset = postViewModel.selectedPreset,
|
selectedPreset = postViewModel.selectedPreset,
|
||||||
processingPreset = postViewModel.processingPreset,
|
processingPreset = postViewModel.processingPreset,
|
||||||
onPresetSelected = { postViewModel.selectPreset(it) },
|
onPresetSelected = { postViewModel.selectPreset(it) },
|
||||||
|
|||||||
+25
-78
@@ -50,11 +50,10 @@ import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState
|
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState
|
||||||
@@ -131,7 +130,6 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
|||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -199,26 +197,29 @@ open class ShortNotePostViewModel :
|
|||||||
var voiceOrchestrator by mutableStateOf<UploadOrchestrator?>(null)
|
var voiceOrchestrator by mutableStateOf<UploadOrchestrator?>(null)
|
||||||
|
|
||||||
// Voice Anonymization
|
// Voice Anonymization
|
||||||
var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE)
|
private val voiceAnonymization =
|
||||||
var processingPreset: VoicePreset? by mutableStateOf(null)
|
VoiceAnonymizationController(
|
||||||
var distortedFiles: Map<VoicePreset, AnonymizedResult> by mutableStateOf(emptyMap())
|
scope = viewModelScope,
|
||||||
private var processingJob: Job? = null
|
logTag = "ShortNotePostViewModel",
|
||||||
|
onError = { error ->
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
stringRes(Amethyst.instance.appContext, R.string.error),
|
||||||
|
error.message ?: "Voice anonymization failed",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
val activeFile: java.io.File?
|
val activeFile: java.io.File?
|
||||||
get() =
|
get() = voiceAnonymization.activeFile(voiceLocalFile)
|
||||||
if (selectedPreset == VoicePreset.NONE) {
|
|
||||||
voiceLocalFile
|
|
||||||
} else {
|
|
||||||
distortedFiles[selectedPreset]?.file
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeWaveform: List<Float>?
|
val activeWaveform: List<Float>?
|
||||||
get() =
|
get() = voiceAnonymization.activeWaveform(voiceRecording?.amplitudes)
|
||||||
if (selectedPreset == VoicePreset.NONE) {
|
|
||||||
voiceRecording?.amplitudes
|
val selectedPreset: VoicePreset
|
||||||
} else {
|
get() = voiceAnonymization.selectedPreset
|
||||||
distortedFiles[selectedPreset]?.waveform
|
|
||||||
}
|
val processingPreset: VoicePreset?
|
||||||
|
get() = voiceAnonymization.processingPreset
|
||||||
|
|
||||||
// Polls
|
// Polls
|
||||||
var canUsePoll by mutableStateOf(false)
|
var canUsePoll by mutableStateOf(false)
|
||||||
@@ -820,7 +821,7 @@ open class ShortNotePostViewModel :
|
|||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
processingJob?.cancel()
|
voiceAnonymization.clear()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
@@ -828,8 +829,6 @@ open class ShortNotePostViewModel :
|
|||||||
voiceMetadata = null
|
voiceMetadata = null
|
||||||
voiceSelectedServer = null
|
voiceSelectedServer = null
|
||||||
voiceOrchestrator = null
|
voiceOrchestrator = null
|
||||||
selectedPreset = VoicePreset.NONE
|
|
||||||
processingPreset = null
|
|
||||||
pTags = null
|
pTags = null
|
||||||
|
|
||||||
wantsPoll = false
|
wantsPoll = false
|
||||||
@@ -981,13 +980,11 @@ open class ShortNotePostViewModel :
|
|||||||
|
|
||||||
fun selectVoiceRecording(recording: RecordingResult) {
|
fun selectVoiceRecording(recording: RecordingResult) {
|
||||||
// Cancel any ongoing processing and delete existing files
|
// Cancel any ongoing processing and delete existing files
|
||||||
processingJob?.cancel()
|
voiceAnonymization.clear()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = recording
|
voiceRecording = recording
|
||||||
voiceLocalFile = recording.file
|
voiceLocalFile = recording.file
|
||||||
voiceMetadata = null
|
voiceMetadata = null
|
||||||
selectedPreset = VoicePreset.NONE
|
|
||||||
processingPreset = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVoicePreviewMetadata(): AudioMeta? =
|
fun getVoicePreviewMetadata(): AudioMeta? =
|
||||||
@@ -1001,48 +998,11 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectPreset(preset: VoicePreset) {
|
fun selectPreset(preset: VoicePreset) {
|
||||||
if (processingPreset != null || preset == selectedPreset) return
|
voiceAnonymization.selectPreset(preset, voiceLocalFile)
|
||||||
|
|
||||||
if (preset == VoicePreset.NONE) {
|
|
||||||
selectedPreset = preset
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distortedFiles.containsKey(preset)) {
|
|
||||||
selectedPreset = preset
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val originalFile = voiceLocalFile ?: return
|
|
||||||
|
|
||||||
processingJob?.cancel()
|
|
||||||
processingJob =
|
|
||||||
viewModelScope.launch {
|
|
||||||
processingPreset = preset
|
|
||||||
try {
|
|
||||||
val anonymizer = VoiceAnonymizer()
|
|
||||||
val result = anonymizer.anonymize(originalFile, preset)
|
|
||||||
|
|
||||||
result
|
|
||||||
.onSuccess { anonymizedResult ->
|
|
||||||
distortedFiles = distortedFiles + (preset to anonymizedResult)
|
|
||||||
selectedPreset = preset
|
|
||||||
}.onFailure { error ->
|
|
||||||
Log.w("ShortNotePostViewModel", "Failed to anonymize voice", error)
|
|
||||||
accountViewModel.toastManager.toast(
|
|
||||||
stringRes(Amethyst.instance.appContext, R.string.error),
|
|
||||||
error.message ?: "Voice anonymization failed",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processingPreset = null
|
|
||||||
processingJob = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeVoiceMessage() {
|
fun removeVoiceMessage() {
|
||||||
processingJob?.cancel()
|
voiceAnonymization.clear()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
@@ -1050,8 +1010,6 @@ open class ShortNotePostViewModel :
|
|||||||
voiceSelectedServer = null
|
voiceSelectedServer = null
|
||||||
isUploadingVoice = false
|
isUploadingVoice = false
|
||||||
voiceOrchestrator = null
|
voiceOrchestrator = null
|
||||||
selectedPreset = VoicePreset.NONE
|
|
||||||
processingPreset = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteVoiceLocalFile() {
|
private fun deleteVoiceLocalFile() {
|
||||||
@@ -1065,18 +1023,6 @@ open class ShortNotePostViewModel :
|
|||||||
Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e)
|
Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
distortedFiles.values.forEach { result ->
|
|
||||||
try {
|
|
||||||
if (result.file.exists()) {
|
|
||||||
result.file.delete()
|
|
||||||
Log.d("ShortNotePostViewModel", "Deleted distorted file: ${result.file.absolutePath}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("ShortNotePostViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
distortedFiles = emptyMap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun uploadVoiceMessageSync(
|
suspend fun uploadVoiceMessageSync(
|
||||||
@@ -1128,6 +1074,7 @@ open class ShortNotePostViewModel :
|
|||||||
)
|
)
|
||||||
// Delete the local file after successful upload
|
// Delete the local file after successful upload
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
|
voiceAnonymization.deleteDistortedFiles()
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-29
@@ -37,7 +37,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Mic
|
import androidx.compose.material.icons.filled.Mic
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -48,25 +47,21 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
|
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime
|
import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
|
|
||||||
@@ -174,29 +169,7 @@ private fun VoiceReplyScreenBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Voice anonymization section
|
// Voice anonymization section
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
VoiceAnonymizationSection(
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(Size5dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringRes(R.string.voice_anonymize_title),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringRes(R.string.voice_anonymize_description),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = Color.Gray,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
VoicePresetSelector(
|
|
||||||
selectedPreset = viewModel.selectedPreset,
|
selectedPreset = viewModel.selectedPreset,
|
||||||
processingPreset = viewModel.processingPreset,
|
processingPreset = viewModel.processingPreset,
|
||||||
onPresetSelected = { viewModel.selectPreset(it) },
|
onPresetSelected = { viewModel.selectPreset(it) },
|
||||||
|
|||||||
+24
-74
@@ -34,9 +34,8 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
|||||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset
|
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -70,26 +69,29 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null)
|
var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null)
|
||||||
var isUploading: Boolean by mutableStateOf(false)
|
var isUploading: Boolean by mutableStateOf(false)
|
||||||
|
|
||||||
var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE)
|
private val voiceAnonymization =
|
||||||
var processingPreset: VoicePreset? by mutableStateOf(null)
|
VoiceAnonymizationController(
|
||||||
var distortedFiles: Map<VoicePreset, AnonymizedResult> by mutableStateOf(emptyMap())
|
scope = viewModelScope,
|
||||||
private var processingJob: Job? = null
|
logTag = "VoiceReplyViewModel",
|
||||||
|
onError = { error ->
|
||||||
|
accountViewModel.toastManager.toast(
|
||||||
|
stringRes(Amethyst.instance.appContext, R.string.error),
|
||||||
|
error.message ?: "Voice anonymization failed",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
val activeFile: File?
|
val activeFile: File?
|
||||||
get() =
|
get() = voiceAnonymization.activeFile(voiceLocalFile)
|
||||||
if (selectedPreset == VoicePreset.NONE) {
|
|
||||||
voiceLocalFile
|
|
||||||
} else {
|
|
||||||
distortedFiles[selectedPreset]?.file
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeWaveform: List<Float>?
|
val activeWaveform: List<Float>?
|
||||||
get() =
|
get() = voiceAnonymization.activeWaveform(voiceRecording?.amplitudes)
|
||||||
if (selectedPreset == VoicePreset.NONE) {
|
|
||||||
voiceRecording?.amplitudes
|
val selectedPreset: VoicePreset
|
||||||
} else {
|
get() = voiceAnonymization.selectedPreset
|
||||||
distortedFiles[selectedPreset]?.waveform
|
|
||||||
}
|
val processingPreset: VoicePreset?
|
||||||
|
get() = voiceAnonymization.processingPreset
|
||||||
|
|
||||||
private var uploadJob: Job? = null
|
private var uploadJob: Job? = null
|
||||||
|
|
||||||
@@ -137,13 +139,11 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun selectRecording(recording: RecordingResult) {
|
fun selectRecording(recording: RecordingResult) {
|
||||||
cancelUpload()
|
cancelUpload()
|
||||||
processingJob?.cancel()
|
voiceAnonymization.clear()
|
||||||
processingPreset = null
|
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = recording
|
voiceRecording = recording
|
||||||
voiceLocalFile = recording.file
|
voiceLocalFile = recording.file
|
||||||
voiceMetadata = null
|
voiceMetadata = null
|
||||||
selectedPreset = VoicePreset.NONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelUpload() {
|
private fun cancelUpload() {
|
||||||
@@ -162,61 +162,12 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e)
|
Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
distortedFiles.values.forEach { result ->
|
|
||||||
try {
|
|
||||||
if (result.file.exists()) {
|
|
||||||
result.file.delete()
|
|
||||||
Log.d("VoiceReplyViewModel", "Deleted distorted file: ${result.file.absolutePath}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("VoiceReplyViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
distortedFiles = emptyMap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canSend(): Boolean = voiceRecording != null && !isUploading && processingPreset == null
|
fun canSend(): Boolean = voiceRecording != null && !isUploading && processingPreset == null
|
||||||
|
|
||||||
fun selectPreset(preset: VoicePreset) {
|
fun selectPreset(preset: VoicePreset) {
|
||||||
if (processingPreset != null || preset == selectedPreset) return
|
voiceAnonymization.selectPreset(preset, voiceLocalFile)
|
||||||
|
|
||||||
if (preset == VoicePreset.NONE) {
|
|
||||||
selectedPreset = preset
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distortedFiles.containsKey(preset)) {
|
|
||||||
selectedPreset = preset
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val originalFile = voiceLocalFile ?: return
|
|
||||||
|
|
||||||
processingJob?.cancel()
|
|
||||||
processingJob =
|
|
||||||
viewModelScope.launch {
|
|
||||||
processingPreset = preset
|
|
||||||
try {
|
|
||||||
val anonymizer = VoiceAnonymizer()
|
|
||||||
val result = anonymizer.anonymize(originalFile, preset)
|
|
||||||
|
|
||||||
result
|
|
||||||
.onSuccess { anonymizedResult ->
|
|
||||||
distortedFiles = distortedFiles + (preset to anonymizedResult)
|
|
||||||
selectedPreset = preset
|
|
||||||
}.onFailure { error ->
|
|
||||||
Log.w("VoiceReplyViewModel", "Failed to anonymize voice", error)
|
|
||||||
accountViewModel.toastManager.toast(
|
|
||||||
stringRes(Amethyst.instance.appContext, R.string.error),
|
|
||||||
error.message ?: "Voice anonymization failed",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processingPreset = null
|
|
||||||
processingJob = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendVoiceReply(onSuccess: () -> Unit) {
|
fun sendVoiceReply(onSuccess: () -> Unit) {
|
||||||
@@ -326,6 +277,7 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
|
voiceAnonymization.deleteDistortedFiles()
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
voiceMetadata = audioMeta
|
voiceMetadata = audioMeta
|
||||||
@@ -348,7 +300,7 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
cancelUpload()
|
cancelUpload()
|
||||||
processingJob?.cancel()
|
voiceAnonymization.clear()
|
||||||
deleteVoiceLocalFile()
|
deleteVoiceLocalFile()
|
||||||
voiceRecording = null
|
voiceRecording = null
|
||||||
voiceLocalFile = null
|
voiceLocalFile = null
|
||||||
@@ -356,8 +308,6 @@ class VoiceReplyViewModel : ViewModel() {
|
|||||||
voiceSelectedServer = null
|
voiceSelectedServer = null
|
||||||
isUploading = false
|
isUploading = false
|
||||||
voiceOrchestrator = null
|
voiceOrchestrator = null
|
||||||
selectedPreset = VoicePreset.NONE
|
|
||||||
processingPreset = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|||||||
Reference in New Issue
Block a user