Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
66be53c
perf: defer legacy migrations and non-critical loads out of init hot …
VoidX3D Jun 28, 2026
b4689a6
perf: serialize library data loading to avoid I/O contention on slow …
VoidX3D Jun 28, 2026
37ef4d2
perf: derive isLibraryContentEmpty from in-memory state instead of Ro…
VoidX3D Jun 28, 2026
7b9407d
perf: eliminate loading state flapping during sequential library load
VoidX3D Jun 28, 2026
898f756
perf: add distinctUntilChanged to playerUiState combine collectors
VoidX3D Jun 28, 2026
80fb3bf
feat(ai): add LYRICS prompt type for proper logging of translation re…
VoidX3D Jun 28, 2026
d799cd9
feat(settings): split AI settings into AI Provider tab and Generation…
VoidX3D Jun 28, 2026
08e941e
feat(ai): add SearchableProviderSelector with descriptions and live s…
VoidX3D Jun 28, 2026
b10358a
i18n: add generation parameters strings to all 11 locale files
VoidX3D Jun 28, 2026
5b0444c
cleanup: remove unused hasGeminiApiKey, songCountFlow, and repository…
VoidX3D Jun 28, 2026
273f6d9
fix: add missing LYRICS and GENERATION_PARAMETERS branches in when ex…
VoidX3D Jun 28, 2026
61702e2
fix: add missing @OptIn for ExperimentalMaterial3Api on SearchablePro…
VoidX3D Jun 28, 2026
51209dc
fix: use file-level @OptIn for ExperimentalMaterial3Api in SettingsCo…
VoidX3D Jun 28, 2026
c97ddb7
Merge branch 'refs/heads/pr-2497' into review-pr-2497
daedaevibin Jun 28, 2026
031fe1a
Merge branch 'master' into review-pr-2497
daedaevibin Jun 28, 2026
39c0887
fix: address PR review issues
daedaevibin Jun 28, 2026
b19efc3
fix: reset CompletableDeferred gates after trim; consolidate dead LYR…
daedaevibin Jun 28, 2026
901b491
docs: fix comment - categories state tracks artists, not folders
daedaevibin Jun 28, 2026
fb768bf
fix: cancel old jobs before recreating CompletableDeferred gates
daedaevibin Jun 28, 2026
8bd6a2e
fix: restore source language detection in translateLyrics prompt
daedaevibin Jun 28, 2026
450b2b0
fix: track and cancel categories-loading coroutine on re-entry
daedaevibin Jun 28, 2026
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
Comment thread
daedaevibin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class AiHandler @Inject constructor(
AiSystemPromptType.TAGGING -> 0.4f
AiSystemPromptType.PLAYLIST, AiSystemPromptType.DAILY_MIX -> 0.6f
AiSystemPromptType.PERSONA -> 0.85f
AiSystemPromptType.GENERAL -> 0.7f
AiSystemPromptType.GENERAL, AiSystemPromptType.LYRICS -> 0.7f
}
} else temperature
} else params.temperature
Expand All @@ -182,7 +182,7 @@ class AiHandler @Inject constructor(
val basePersona = getBasePersona(userProvider)
val combinedSystemPrompt = promptEngine.buildPrompt(basePersona, type, context)

val hash = (userProvider.name + combinedSystemPrompt + prompt).sha256()
val hash = (type.name + userProvider.name + combinedSystemPrompt + prompt).sha256()
Comment thread
daedaevibin marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Cache hash key uses original provider's prompt but response may come from fallback provider

In AiHandler.generateContent, the cache hash at line 185 is computed using userProvider's system prompt (combinedSystemPrompt), but the actual API call in the provider loop (lines 212-226) builds a finalSystemPrompt using each iteration's provider persona (getBasePersona(provider)). If the user's preferred provider fails and a fallback succeeds, the cached response was generated with a different system prompt than what the cache key represents. This is a pre-existing issue — the PR only adds type.name to the hash (which is a defensive improvement). Worth noting for future cache correctness work.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


cacheDao.getCache(hash)?.let { cached ->
val age = System.currentTimeMillis() - cached.timestamp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum class AiSystemPromptType {
MOOD_ANALYSIS,
PERSONA,
DAILY_MIX,
LYRICS,
GENERAL
}

Expand Down Expand Up @@ -182,6 +183,16 @@ class AiSystemPromptEngine @Inject constructor() {
$dailyMixPersonaPrompt
""".trimIndent()

AiSystemPromptType.LYRICS -> """
<role>Song lyrics translator — you translate lyrics between languages while preserving structure.</role>
<strategy>
- Preserve ALL timestamps and line structure exactly.
- Output each original line followed by its translation on the next line.
- Never add explanations, labels, or formatting beyond the requested format.
- If the source is already in the target language, respond with: ALREADY_IN_TARGET_LANGUAGE
</strategy>
""".trimIndent()
Comment on lines +186 to +194

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: LYRICS prompt type lacks an <output_schema> block unlike all other AI prompt types

Every other AiSystemPromptType in AiSystemPromptEngine.buildPrompt includes an explicit <output_schema> section (e.g., PLAYLIST returns JSON array, METADATA returns JSON object, TAGGING returns comma-separated list). The new LYRICS type at AiSystemPromptEngine.kt:186-194 only has <role> and <strategy> blocks with no <output_schema>. The output format is instead specified in the user-level prompt built in AiStateHolder.translateLyrics (AiStateHolder.kt:299-317). This works but breaks the convention where the system prompt is self-contained regarding output format. The UNIVERSAL_CONSTRAINTS block (which IS applied to LYRICS) says "Output ONLY the expected structure" — but the expected structure is only defined in the user prompt, not the system prompt.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


AiSystemPromptType.GENERAL -> """
<role>PixelPlayer Assistant — a knowledgeable music companion.</role>
<strategy>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ package com.theveloper.pixelplay.data.ai.provider
/**
* Enum representing available AI providers
*/
enum class AiProvider(val displayName: String, val requiresApiKey: Boolean, val hasConfigurableUrl: Boolean = false) {
GEMINI("Google Gemini", requiresApiKey = true),
DEEPSEEK("DeepSeek", requiresApiKey = true),
GROQ("Groq", requiresApiKey = true),
MISTRAL("Mistral", requiresApiKey = true),
NVIDIA("NVIDIA NIM", requiresApiKey = true),
KIMI("Kimi (Moonshot)", requiresApiKey = true),
GLM("Zhipu GLM", requiresApiKey = true),
OPENAI("OpenAI", requiresApiKey = true),
OPENROUTER("OpenRouter", requiresApiKey = true),
OLLAMA("Ollama", requiresApiKey = true),
CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true);
enum class AiProvider(
val displayName: String,
val requiresApiKey: Boolean,
val hasConfigurableUrl: Boolean = false,
val description: String = ""
) {
GEMINI("Google Gemini", requiresApiKey = true, description = "Google's flagship AI models (Gemini 1.5/2.0)"),
DEEPSEEK("DeepSeek", requiresApiKey = true, description = "Open-source reasoning models via DeepSeek API"),
GROQ("Groq", requiresApiKey = true, description = "Ultra-fast inference with Groq LPU hardware"),
MISTRAL("Mistral", requiresApiKey = true, description = "Mistral AI's efficient and powerful models"),
NVIDIA("NVIDIA NIM", requiresApiKey = true, description = "NVIDIA's optimized inference microservices"),
KIMI("Kimi (Moonshot)", requiresApiKey = true, description = "Moonshot AI's long-context Kimi models"),
GLM("Zhipu GLM", requiresApiKey = true, description = "Zhipu AI's bilingual GLM series"),
OPENAI("OpenAI", requiresApiKey = true, description = "GPT-4o, GPT-4, and other OpenAI models"),
OPENROUTER("OpenRouter", requiresApiKey = true, description = "Unified access to 200+ models via OpenRouter"),
OLLAMA("Ollama", requiresApiKey = true, description = "Local models via Ollama (requires running server)"),
CUSTOM("Custom Provider", requiresApiKey = true, hasConfigurableUrl = true, description = "Any OpenAI-compatible API endpoint");
Comment thread
daedaevibin marked this conversation as resolved.

companion object {
fun fromString(value: String): AiProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,6 @@ interface MusicRepository {
storageFilter: com.theveloper.pixelplay.data.model.StorageFilter = com.theveloper.pixelplay.data.model.StorageFilter.ALL
): Flow<Int>

/**
* Returns the count of songs in the library.
* @return Flow emitting the current song count.
*/
fun getSongCountFlow(): Flow<Int>

/**
* Returns the count of cloud songs in the library.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,6 @@ class MusicRepositoryImpl @Inject constructor(
return songRepository.getFavoriteSongCountFlow(storageFilter)
}

override fun getSongCountFlow(): Flow<Int> {
return musicDao.getSongCount().distinctUntilChanged()
}

override fun getCloudSongCountFlow(): Flow<Int> {
return musicDao.getCloudSongCount().distinctUntilChanged()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.material.icons.rounded.LibraryMusic
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.QueueMusic
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.ui.graphics.vector.ImageVector
import com.theveloper.pixelplay.R

Expand Down Expand Up @@ -62,6 +63,12 @@ enum class SettingsCategory(
subtitleRes = R.string.settings_category_ai_subtitle,
iconRes = R.drawable.gemini_ai
),
GENERATION_PARAMETERS(
id = "generation_parameters",
titleRes = R.string.settings_category_generation_parameters_title,
subtitleRes = R.string.settings_category_generation_parameters_subtitle,
icon = Icons.Rounded.Tune
),
BACKUP_RESTORE(
id = "backup_restore",
titleRes = R.string.settings_category_backup_title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ private fun formatPromptType(type: String): String {
"MOOD_ANALYSIS" -> "Analysis"
"PERSONA" -> "Persona"
"DAILY_MIX" -> "Daily Mix"
"LYRICS" -> "Lyrics"
"GENERAL" -> "General"
else -> type.lowercase().replaceFirstChar { it.uppercase() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1002,11 +1002,11 @@ fun LibraryScreen(
}.collectAsStateWithLifecycle(initialValue = LibraryScreenPlayerProjection())
val isLibraryContentEmpty by remember(playerViewModel) {
combine(
playerViewModel.songCountFlow,
playerViewModel.allSongsFlow,
playerViewModel.albumsFlow,
playerViewModel.artistsFlow
) { songCount, albums, artists ->
songCount == 0 && albums.isEmpty() && artists.isEmpty()
) { songs, albums, artists ->
songs.isEmpty() && albums.isEmpty() && artists.isEmpty()
Comment thread
daedaevibin marked this conversation as resolved.
}.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = true)

Expand Down
Loading
Loading