Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ fun LyricsSheet(
isImmersiveTemporarilyDisabled: Boolean,
onSetImmersiveTemporarilyDisabled: (Boolean) -> Unit,
onSaveLyricsToFile: (Song, Lyrics, Boolean) -> Unit,
onTranslateViaAi: () -> Unit,
// BottomToggleRow Params
isShuffleEnabled: Boolean,
repeatMode: Int,
Expand Down Expand Up @@ -1017,6 +1018,7 @@ fun LyricsSheet(
wasResetTriggered = true
resetLyricsForCurrentSong()
},
onTranslateViaAi = onTranslateViaAi,
onToggleSyncControls = {
resetImmersiveTimer()
showSyncControls = !showSyncControls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,7 @@ fun FullPlayerContent(
colorScheme = LocalMaterialTheme.current,
onBackClick = { showLyricsSheet = false },
onSaveLyricsToFile = playerViewModel::saveLyricsToFile,
onTranslateViaAi = { playerViewModel.translateLyricsViaAi() },
onSeekTo = { playerViewModel.seekTo(it) },
onPlayPause = {
playerViewModel.playPause()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ fun LyricsMoreBottomSheet(
isSyncControlsVisible: Boolean,
onSaveLyricsAsLrc: () -> Unit,
onResetImportedLyrics: () -> Unit,
onTranslateViaAi: () -> Unit,
onToggleSyncControls: () -> Unit,
isImmersiveTemporarilyDisabled: Boolean,
onSetImmersiveTemporarilyDisabled: (Boolean) -> Unit,
Expand Down Expand Up @@ -157,6 +158,32 @@ fun LyricsMoreBottomSheet(
)
}

// Translate via AI
if (lyrics != null) {
ListItem(
headlineContent = { Text(stringResource(R.string.ai_translate_via_ai)) },
leadingContent = {
Icon(
imageVector = Icons.Rounded.Translate,
contentDescription = null
)
},
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(itemBackgroundColor)
.clickable {
onDismissRequest()
onTranslateViaAi()
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
headlineColor = contentColor,
leadingIconColor = contentColor
)
)
}

// Reset imported lyrics
val resetShape = if (lyrics != null) {
RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp, bottomStart = 18.dp, bottomEnd = 18.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class AiStateHolder @Inject constructor(
private val dailyMixManager: DailyMixManager,
private val playlistPreferencesRepository: PlaylistPreferencesRepository,
private val dailyMixStateHolder: DailyMixStateHolder,
private val notificationManager: AiNotificationManager
private val notificationManager: AiNotificationManager,
private val aiOrchestrator: com.theveloper.pixelplay.data.ai.AiOrchestrator
) {
// State
// AI State Management: Observables for tracking background generation progress
Expand Down Expand Up @@ -339,6 +340,40 @@ class AiStateHolder @Inject constructor(
}
}

suspend fun translateLyrics(lyricsText: String): Result<String> {
return try {
val targetLanguage = context.resources.configuration.locales[0].displayLanguage
val prompt = """
Translate the provided song lyrics into $targetLanguage.

Keep every timestamp exactly unchanged.

If the lyrics are ALREADY mostly in $targetLanguage, output ONLY the exact phrase "ALREADY_IN_TARGET_LANGUAGE" without any other text.

For each original line, output the original line first, then on the next line output the $targetLanguage translation with the same timestamp.

Do not add any extra text, explanations, numbering, labels, or formatting.
Do not remove, merge, split, or reorder lines.

Output only:
[timestamp] original text
[timestamp] translated text

Lyrics to translate:
$lyricsText
""".trimIndent()

val response = aiOrchestrator.generateContent(
prompt = prompt,
type = AiSystemPromptType.GENERAL,
temperature = 0.1f
)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}

fun onCleared() {
scope = null
allSongsProvider = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5102,6 +5102,57 @@ class PlayerViewModel @Inject constructor(
lyricsStateHolder.importLyricsFromFile(songId, validatedImport, currentSong)
}

fun translateLyricsViaAi() {
val currentSong = stablePlayerState.value.currentSong ?: return
val songId = currentSong.id.toLongOrNull() ?: return
val rawLyrics = currentSong.lyrics
val lyricsObj = stablePlayerState.value.lyrics

if (rawLyrics.isNullOrBlank()) {
sendToast(context.getString(R.string.lyrics_not_found))
return
}

if (lyricsObj?.synced != null) {
val hasValidTranslation = lyricsObj.synced.any { !it.translation.isNullOrBlank() }
if (hasValidTranslation) {
sendToast(context.getString(R.string.ai_lyrics_already_translated))
return
}
}

viewModelScope.launch {
sendToast(context.getString(R.string.ai_lyrics_translating))
val result = aiStateHolder.translateLyrics(rawLyrics)
result.onSuccess { translatedText ->
if (translatedText.trim() == "ALREADY_IN_TARGET_LANGUAGE") {
sendToast(context.getString(R.string.ai_lyrics_already_in_target_language))
return@onSuccess
}

if (translatedText.isNotBlank()) {
val validation = com.theveloper.pixelplay.utils.LyricsImportSecurity.validateImportedLrcContent(translatedText)
if (validation is com.theveloper.pixelplay.utils.LyricsImportValidationResult.Valid) {
lyricsStateHolder.importLyricsFromFile(songId, validation.value, currentSong)
sendToast(context.getString(R.string.ai_lyrics_translation_success))
} else {
val reason = (validation as com.theveloper.pixelplay.utils.LyricsImportValidationResult.Invalid).reason
val errorMsg = com.theveloper.pixelplay.utils.LyricsImportSecurity.messageFor(reason)
sendToast(context.getString(R.string.ai_error_generic, errorMsg))
}
} else {
sendToast(context.getString(R.string.ai_error_generic, "Empty response"))
}
}.onFailure {
if (it.message?.contains("key", ignoreCase = true) == true || it.message?.contains("config", ignoreCase = true) == true) {
sendToast(context.getString(R.string.ai_error_api_key))
} else {
sendToast(context.getString(R.string.ai_error_generic, it.message))
}
}
}
}

/**
* Resetea el estado de la búsqueda de letras a Idle.
*/
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@
<string name="ai_daily_mix_updated">Daily Mix обновлен с помощью ИИ</string>
<string name="ai_no_songs_for_mix">ИИ не удалось найти песни для этого микса</string>

<string name="ai_translate_via_ai">Перевести с ИИ</string>
<string name="ai_lyrics_already_translated">У этого текста уже есть перевод</string>
<string name="ai_lyrics_already_in_target_language">Этот текст уже на этом языке</string>
<string name="ai_lyrics_api_not_configured">API не настроен</string>
<string name="ai_lyrics_translation_success">Текст успешно переведён!</string>
<string name="ai_lyrics_translating">Перевод текста...</string>

<string name="shortcut_shuffle_short">Перемешать</string>
<string name="shortcut_shuffle_long">Перемешать все песни</string>
<string name="shortcut_playlist_short">Плейлист</string>
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/res/values-ru/strings_presentation_batch_g.xml
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@
<string name="presentation_batch_g_presets_cd_pin">Закрепить</string>
<string name="presentation_batch_g_presets_cd_rename">Переименовать</string>
<string name="presentation_batch_g_presets_cd_delete">Удалить</string>
<string name="presentation_batch_g_beta_sheet_version">Бета 0.6.0</string>
<string name="presentation_batch_g_beta_sheet_welcome_title">Добро пожаловать в PixelPlayer 0.6.0-beta</string>
<string name="presentation_batch_g_beta_sheet_version">Бета 0.7.0</string>
<string name="presentation_batch_g_beta_sheet_welcome_title">Добро пожаловать в PixelPlayer 0.7.0-beta</string>
<string name="presentation_batch_g_beta_sheet_welcome_body">Эта бета сфокусирована на стабильности, скорости и кросс-девайс воспроизведении.</string>
<string name="presentation_batch_g_beta_sheet_expect_title">Что нового</string>
<string name="presentation_batch_g_beta_sheet_expect_1">Быстрая работа: плавный запуск, навигация и работа плеера.</string>
Expand Down Expand Up @@ -495,4 +495,4 @@
<string name="presentation_batch_g_beta_sheet_nightly_access">Доступны через артефакты GitHub Actions в репозитории, если опубликованы.</string>
<string name="presentation_batch_g_beta_sheet_nightly_report_title">Отчёт о проблемах в nightly</string>
<string name="presentation_batch_g_beta_sheet_nightly_report_body">При отчёте о проблеме из nightly всегда указывайте, что она в nightly, а не в официальном релизе. Укажите дату сборки, имя или номер запуска workflow или commit SHA — если возможно. Также проверьте, есть ли эта проблема в последнем официальном релизе.</string>
</resources>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@
<string name="ai_daily_mix_updated">Daily Mix updated with AI</string>
<string name="ai_no_songs_for_mix">AI couldn\'t find songs for this mix</string>

<string name="ai_translate_via_ai">Translate via AI</string>
<string name="ai_lyrics_already_translated">These lyrics already have a translation</string>
<string name="ai_lyrics_already_in_target_language">These lyrics are already in this language</string>
<string name="ai_lyrics_api_not_configured">API is not configured</string>
<string name="ai_lyrics_translation_success">Lyrics translated successfully!</string>
<string name="ai_lyrics_translating">Translating lyrics...</string>

<string name="shortcut_shuffle_short">Shuffle</string>
<string name="shortcut_shuffle_long">Shuffle all songs</string>
<string name="shortcut_playlist_short">Playlist</string>
Expand Down
Loading