From 359252bf5e4894575f135a86d3c138d379e2b149 Mon Sep 17 00:00:00 2001 From: Kazeia Team Date: Mon, 15 Jun 2026 07:51:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(voice):=20voix=20pilot=C3=A9e=20par=20prof?= =?UTF-8?q?il=20(app=20admin)=20=E2=80=94=20retrait=20du=20s=C3=A9lecteur?= =?UTF-8?q?=20patient=20=E2=80=94=20v0.1.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La voix n'est plus choisie côté patient ; elle est définie PAR PROFIL dans l'app admin (le champ Profile.voiceId + le dropdown admin existaient déjà ; seul le runtime ne l'appliquait pas). App patient : - ChatActivity : suppression de setupVoiceSelector() + des listes voiceFiles/ voiceNames/voiceColors. activity_chat.xml : suppression de la voiceBar (spinner), l'orbe se contraint désormais sous tvStatus (couleur = défaut service). - KazeiaService : nouvelle source unique = le profil actif. - applyActiveProfileVoice() lit ProfileStore.activeProfile().voiceId (défaut DEFAULT_VOICE_ID="damien" en invité), appelée à l'init ET via un listener ProfileStore → propagation LIVE quand l'admin édite la voix / change de profil. - setVoiceId(id) résout selon le moteur actif : CosyVoice → cosyvoice/.cvps ; Qwen3/lib → ../voix/.wav. setVoice(pathOrId) conservé (intents test) délègue. Validé device (release v0.1.5, SM8750, moteur cosyvoice) : - plus de barre de voix dans l'UI patient (confirmé). - profil voice_id=elodie → service charge 'elodie' (≠ défaut) ; changement live via provider voice_id=damien → ré-appliqué sans redémarrage. - moteur 'lib' charge toujours (pas de régression du stack gelé). Co-Authored-By: Claude Opus 4.8 (1M context) --- kazeia-android/app/build.gradle.kts | 4 +- .../java/com/kazeia/service/KazeiaService.kt | 59 ++++++++++++------- .../main/java/com/kazeia/ui/ChatActivity.kt | 50 ++-------------- .../app/src/main/res/layout/activity_chat.xml | 37 ++---------- 4 files changed, 49 insertions(+), 101 deletions(-) diff --git a/kazeia-android/app/build.gradle.kts b/kazeia-android/app/build.gradle.kts index 60b0219..1431844 100644 --- a/kazeia-android/app/build.gradle.kts +++ b/kazeia-android/app/build.gradle.kts @@ -44,8 +44,8 @@ android { applicationId = "com.kazeia" minSdk = 28 targetSdk = 36 - versionCode = 5 - versionName = "0.1.4" + versionCode = 6 + versionName = "0.1.5" // WebDAV de distribution — injecté depuis local.properties. buildConfigField("String", "WEBDAV_BASE", "\"${localProp("webdav.base")}\"") diff --git a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt index 84ca489..d8b66be 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt @@ -39,6 +39,8 @@ class KazeiaService : Service() { companion object { private const val TAG = "KazeiaService" private const val NOTIFICATION_ID = 1 + // Voix par défaut si le profil actif n'en définit aucune (ou mode invité). + const val DEFAULT_VOICE_ID = "damien" // Diagnostic mode: when true, TTS is bypassed entirely. The chat // bubble updates token-by-token as the LLM emits, so we can see // the raw LLM speed (tok/s) without any TTS coupling. Flip back @@ -874,9 +876,14 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think voiceCommands = com.kazeia.conversation.VoiceCommandProcessor(this@KazeiaService) log("Voice commands loaded: ${voiceCommands.getCommands().size} commands") - // Set default voice for Chatterbox - val defaultVoice = "${KazeiaApplication.MODELS_DIR}/../voix/damien.wav" - setVoice(defaultVoice) + // Voix = celle du PROFIL ACTIF (choisie dans l'app admin). Plus de + // sélecteur côté patient. Re-appliquée quand le profil actif change + // ou que l'admin édite la voix d'un profil (ProfileStore notifie). + applyActiveProfileVoice() + if (!voiceProfileListenerRegistered) { + ProfileStore.get(applicationContext).addListener { applyActiveProfileVoice() } + voiceProfileListenerRegistered = true + } addMessage(ChatMessage( role = ChatMessage.Role.KAZEIA, @@ -950,8 +957,9 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think private val conversationDao: ConversationDao by lazy { ConversationDao(ConversationDb.get(applicationContext)) } - // Nom de la voix courante (basename du .wav), tracké pour le log de tour. - @Volatile private var currentVoiceName: String = "damien" + // Nom de la voix courante (id sans extension), tracké pour le log de tour. + @Volatile private var currentVoiceName: String = DEFAULT_VOICE_ID + @Volatile private var voiceProfileListenerRegistered = false /** * Persiste un tour de conversation pour le profil actif. No-op en mode @@ -990,25 +998,34 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think } } - fun setVoice(voicePath: String) { - // Mémorise le nom de voix (basename sans extension) pour l'historique. - currentVoiceName = voicePath.substringAfterLast('/').substringBeforeLast('.') - val qwen = tts as? com.kazeia.tts.Qwen3TtsEngine - val libTts = tts as? com.kazeia.tts.KazeiaTtsEngine - if (qwen != null) { - // Hot-swap prefix/suffix embeddings — no model reload. Takes - // effect from the NEXT synthesized segment (current in-flight - // one, if any, finishes with the old voice since the arrays - // are already in its closure). - qwen.setVoice(voicePath) - log("Voice set to: $voicePath") - } else if (libTts != null) { - // Engine lib : le natif dérive le x_vector du WAV (speaker encoder). - libTts.setVoice(voicePath) - log("Voice set to: $voicePath (lib)") + /** Applique au moteur TTS la voix du PROFIL ACTIF (ou [DEFAULT_VOICE_ID] en + * mode invité / profil sans voix). Source unique de vérité = l'app admin. */ + private fun applyActiveProfileVoice() { + if (!::tts.isInitialized) return + val vid = ProfileStore.get(applicationContext).activeProfile() + ?.voiceId?.takeIf { it.isNotBlank() } ?: DEFAULT_VOICE_ID + setVoiceId(vid) + } + + /** Applique une voix par IDENTIFIANT (nom sans extension), résolu selon le + * moteur TTS actif : CosyVoice → cosyvoice/.cvps ; Qwen3 / lib → + * ../voix/.wav. Hot-swap : effet au prochain segment synthétisé. */ + fun setVoiceId(voiceId: String) { + currentVoiceName = voiceId + val wavPath = "${KazeiaApplication.MODELS_DIR}/../voix/$voiceId.wav" + when (val e = tts) { + is com.kazeia.tts.CosyVoiceTtsEngine -> { e.setVoice(voiceId); log("Voix (cosyvoice): $voiceId") } + is com.kazeia.tts.Qwen3TtsEngine -> { e.setVoice(wavPath); log("Voix (qwen3): $voiceId") } + is com.kazeia.tts.KazeiaTtsEngine -> { e.setVoice(wavPath); log("Voix (lib): $voiceId") } + else -> log("setVoiceId: moteur TTS inconnu — voix '$voiceId' ignorée") } } + /** Back-compat (intents de test `tts_voice`) : accepte un chemin OU un id et + * délègue à [setVoiceId] (qui résout selon le moteur actif). */ + fun setVoice(voicePathOrId: String) = + setVoiceId(voicePathOrId.substringAfterLast('/').substringBeforeLast('.')) + fun interruptTts() { if (_pipelineState.value is PipelineState.Speaking) { log("TTS interrupted by user") diff --git a/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt b/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt index 549ad18..d170020 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/ui/ChatActivity.kt @@ -90,7 +90,6 @@ class ChatActivity : AppCompatActivity() { setupWindowInsets() setupRecyclerView() setupInputBar() - setupVoiceSelector() setupResourceMonitoring() setupQuitButton() bindToService() @@ -180,30 +179,6 @@ class ChatActivity : AppCompatActivity() { } } - private val voiceFiles = listOf( - "damien.wav", "elodie.wav", "jerome.wav", "richard.wav", - "amir.wav", "didier.wav", "sid.wav", "zelda.wav" - ) - private val voiceNames = listOf( - "Damien", "Elodie", "Jerome", "Richard", - "Amir", "Didier", "Sid", "Zelda" - ) - - /** One color per speaker — derived palette (core + halo + bars) is - * generated inside AudioVisualizerView. Chosen to be calm, - * perceptually distinct, and consistent in saturation so switching - * voices changes *hue* rather than *mood*. */ - private val voiceColors = listOf( - 0xFFBCA4E8.toInt(), // Damien — lavender - 0xFFE8A4CC.toInt(), // Elodie — rose - 0xFF82D5D0.toInt(), // Jerome — aqua - 0xFFE8BFA4.toInt(), // Richard — amber sand - 0xFF95D5A6.toInt(), // Amir — emerald - 0xFF8FA2D4.toInt(), // Didier — indigo - 0xFFE8B89A.toInt(), // Sid — peach - 0xFFA4BEE8.toInt() // Zelda — periwinkle - ) - private fun setupResourceMonitoring() { val graphCpu = findViewById(R.id.graphCpu) val graphGpu = findViewById(R.id.graphGpu) @@ -277,27 +252,10 @@ class ChatActivity : AppCompatActivity() { } } - private fun setupVoiceSelector() { - val spinner = findViewById(R.id.spinnerVoice) ?: return - val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, voiceNames) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - spinner.adapter = adapter - - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: android.view.View?, pos: Int, id: Long) { - val voicePath = "${com.kazeia.KazeiaApplication.MODELS_DIR}/../voix/${voiceFiles[pos]}" - kazeiaService?.setVoice(voicePath) - // Push the matching color to the service so the orb - // view picks it up; the view tweens from the previous - // color so voice changes don't snap visually. - val color = voiceColors[pos.coerceIn(voiceColors.indices)] - kazeiaService?.setVoiceColor(color) - binding.audioViz.setVoiceColor(color) - appendLog("Voix: ${voiceNames[pos]}") - } - override fun onNothingSelected(parent: AdapterView<*>?) {} - } - } + // Sélecteur de voix RETIRÉ (2026-06-15) : la voix n'est plus choisie côté + // patient. Elle est définie par profil dans l'app admin et appliquée par le + // service (KazeiaService.applyActiveProfileVoice). L'orbe garde la couleur + // par défaut du service (flow voiceColor). private fun sendMessage() { val text = binding.etMessage.text?.toString()?.trim() ?: return diff --git a/kazeia-android/app/src/main/res/layout/activity_chat.xml b/kazeia-android/app/src/main/res/layout/activity_chat.xml index 3568651..62e6ee0 100644 --- a/kazeia-android/app/src/main/res/layout/activity_chat.xml +++ b/kazeia-android/app/src/main/res/layout/activity_chat.xml @@ -70,47 +70,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> - - - - - - - - + + word-by-word reveal of the current reply. Color = défaut service. -->