Skip to content

Commit

Permalink
feat: export progress
Browse files Browse the repository at this point in the history
  • Loading branch information
kkoshin committed Jul 3, 2024
1 parent b82db83 commit 7ddc31a
Show file tree
Hide file tree
Showing 15 changed files with 377 additions and 249 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ class MockTTSProvider : TTSProvider {
),
)

override suspend fun generate(text: String): Result<TTSResult> =
override suspend fun generate(
voiceId: String,
text: String,
): Result<TTSResult> =
runCatching {
withContext(Dispatchers.IO) {
delay(1000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.kkoshin.muse
import android.app.Application
import io.github.kkoshin.muse.dashboard.DashboardViewModel
import io.github.kkoshin.muse.editor.EditorViewModel
import io.github.kkoshin.muse.export.ExportViewModel
import io.github.kkoshin.muse.tts.TTSManager
import io.github.kkoshin.muse.tts.TTSProvider
import io.github.kkoshin.muse.tts.vendor.ElevenLabTTSProvider
Expand All @@ -28,7 +29,8 @@ class App : Application() {
)
}
singleOf(::MuseRepo)
viewModel { EditorViewModel(get(), get()) }
viewModel { EditorViewModel(get()) }
viewModel { ExportViewModel(get(), get()) }
viewModel { DashboardViewModel(get()) }
singleOf(::TTSManager)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ fun MainScreen(navController: NavHostController = rememberNavController()) {
}

composable<EditorArgs> { entry ->
val args = entry.toRoute<EditorArgs>()
EditorScreen(
args = entry.toRoute(),
args = args,
onExportRequest = { voices ->
navController.navigate(
voices.associate { it.voiceId to it.name }.let {
ExportConfigSheetArgs(
voiceIds = it.keys.toList(),
voiceNames = it.values.toList(),
phrases = args.phrases,
)
},
)
Expand Down Expand Up @@ -121,15 +123,19 @@ fun MainScreen(navController: NavHostController = rememberNavController()) {
Modifier.fillMaxHeight(),
voiceIds = args.voiceIds,
voiceNames = args.voiceNames,
onExport = {
navController.navigate(ExportArgs(it))
onExport = { voiceId ->
navController.navigate(ExportArgs(voiceId, args.phrases))
},
)
}

composable<ExportArgs> { entry ->
ExportScreen(args = entry.toRoute(), onExit = {
navController.popBackStack()
ExportScreen(args = entry.toRoute(), onExit = { isSuccess ->
if (isSuccess) {
navController.popBackStack(DashboardArgs, false)
} else {
navController.popBackStack()
}
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ class MuseRepo(
private val appFileHelper = AppFileHelper(context)

private val voicesDir: File by lazy {
appFileHelper.requireCacheDir(false).resolve("voices").also {
it.mkdirs()
}
appFileHelper.requireCacheDir(false).resolve("voices")
}

fun getPcmCache(phrase: String): File = voicesDir.resolve("$phrase.pcm")
private fun getVoiceDir(voiceId: String): File =
voicesDir
.resolve(voiceId)
.also {
it.mkdirs()
}

fun getPcmCache(
voiceId: String,
phrase: String,
): File = getVoiceDir(voiceId).resolve("$phrase.pcm")
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AudioFile
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -63,10 +62,6 @@ fun EditorScreen(
onExportRequest: (List<Voice>) -> Unit,
onPickVoice: () -> Unit,
) {
// LaunchedEffect(Unit) {
// viewModel.refreshQuota()
// }

val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher

var loadingVisible by remember {
Expand Down Expand Up @@ -100,13 +95,6 @@ fun EditorScreen(
},
backgroundColor = MaterialTheme.colors.surface,
title = { Text(text = "Editor") },
actions = {
IconButton(onClick = {
// TODO: edit phrases
}) {
Icon(Icons.Default.EditNote, null)
}
},
)
},
content = { paddingValues ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
package io.github.kkoshin.muse.editor

import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.github.kkoshin.muse.MuseRepo
import io.github.kkoshin.muse.audio.Mp3Decoder
import io.github.kkoshin.muse.tts.CharacterQuota
import io.github.kkoshin.muse.tts.TTSManager
import io.github.kkoshin.muse.tts.Voice
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import logcat.asLog
import logcat.logcat
import okio.buffer
import okio.sink
import org.koin.java.KoinJavaComponent.inject
import java.io.File

class EditorViewModel(
private val ttsManager: TTSManager,
private val repo: MuseRepo,
) : ViewModel() {
private val appContext: Context by inject(Context::class.java)
private val tag = this.javaClass.simpleName

private val _progress: MutableStateFlow<ProgressStatus> =
MutableStateFlow(ProgressStatus.Idle(CharacterQuota.unknown))
val progress: StateFlow<ProgressStatus> = _progress.asStateFlow()

suspend fun fetchAvailableVoices(): Result<List<Voice>> {
val availableVoiceIds =
ttsManager.queryAvailableVoiceIds() ?: return Result.success(emptyList())
Expand All @@ -44,99 +16,4 @@ class EditorViewModel(
list.filter { it.voiceId in availableVoiceIds }
}
}

fun refreshQuota() {
viewModelScope.launch {
ttsManager
.queryQuota()
.onSuccess {
_progress.value = ProgressStatus.Idle(it)
}.onFailure {
logcat(tag) {
it.asLog()
}
}
}
}

/**
* 相同的 phrase 仅需要生成一次,最终返回的时候要按照
*/
fun startTTS(phrases: List<String>) {
_progress.value = ProgressStatus.Processing(0, "${phrases.size} phrases")
viewModelScope.launch {
coroutineScope {
val requests = phrases.toSet().map { phrase ->
async {
// mp3 原始文件暂时没有记录
ttsManager
.getOrGenerate(phrase)
.mapCatching {
saveAsPcm(
phrase,
it,
repo.getPcmCache(phrase),
)
}.onFailure {
logcat(tag) {
it.asLog()
}
_progress.value =
ProgressStatus.Failed(errorMsg = it.message ?: "unknown error")
}
}
}
val result = requests.awaitAll()
if (result.all { it.isSuccess }) {
_progress.value = ProgressStatus.Success(
// 按照 phrases 的顺序返回,前面 tts 这一步是会去重的
pcmList = phrases.map {
repo.getPcmCache(it).toUri()
},
)
}
}
}
}

private suspend fun saveAsPcm(
text: String,
mp3Uri: Uri,
target: File,
) {
val mp3Decoder = Mp3Decoder()
if (target.exists() && target.length() > 0) {
logcat(tag) {
"$text.pcm already exists."
}
}
val output = target.sink().buffer()
runCatching {
output.use {
// 11 labs 生成的音量偏小,这里需要增大音量
mp3Decoder.decodeMp3ToPCM(appContext, output, mp3Uri, volumeBoost = 3.0f)
}
}.onFailure {
target.delete()
}.getOrThrow()
}
}

sealed interface ProgressStatus {
class Idle(
val characterQuota: CharacterQuota,
) : ProgressStatus

class Failed(
val errorMsg: String,
) : ProgressStatus

class Processing(
val value: Int,
val phrase: String,
) : ProgressStatus

class Success(
val pcmList: List<Uri>,
) : ProgressStatus
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import kotlinx.serialization.Serializable

@Serializable
class ExportConfigSheetArgs(
val phrases: List<String>,
val voiceIds: List<String>,
val voiceNames: List<String>,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ class AudioExportPipeline(
pcmInputs.forEachIndexed { index, uri ->
_progress.value = (index / pcmInputs.size.toFloat() * 100).roundToInt()
sink.writeAll(appContext.contentResolver.openInputStream(uri)!!.source())
// 每个词之间加两秒间隔
if (paddingSilence.inWholeSeconds > 0) {
sink.write(getSilence(paddingSilence, audioMetadata))
}
Expand All @@ -85,10 +84,16 @@ class AudioExportPipeline(
_progress.value = -1
}

private suspend fun encodeWavAsMp3(target: Uri, wavParser: WavParser) {
private suspend fun encodeWavAsMp3(
target: Uri,
wavParser: WavParser,
) {
val encoder = Mp3Encoder()
val outputSink =
appContext.contentResolver.openOutputStream(target)!!.sink().buffer()
appContext.contentResolver
.openOutputStream(target)!!
.sink()
.buffer()
outputSink.use {
encoder.encode(
wavParser,
Expand All @@ -97,8 +102,7 @@ class AudioExportPipeline(
.setId3tagArtist("μ's")
.setId3tagYear(
Calendar.getInstance().get(Calendar.YEAR).toString(),
)
.build(),
).build(),
)
}
}
Expand Down
Loading

0 comments on commit 7ddc31a

Please sign in to comment.