7.3 KiB
VOICE_DEPLOYMENT_SPEC — adaptations app patiente pour le déploiement des voix .cvps
Destinataire : dev de l'app patiente
com.kazeia. Auteur : Kazeia-central. Même démarche quePROVIDER_RPC_SPEC.md/VOICE_ENROLLMENT_SPEC.md: la stack patiente est figée (FROZEN.md), donc ces changements sont décrits ici et livrés, pas réécrits sauvagement.Contexte : Kazeia-central devient le maître de l'enrôlement et du déploiement des voix CosyVoice (
.cvps). Il enrôle un WAV →.cvps, le pousse sur la tablette, et supprime le WAV d'origine (archivé chiffré côté PC). Le workflow exige trois adaptations côté app, dont une bloquante (sans elle, une voix déployée disparaît de l'inventaire).
0. État observé (code actuel, vérifié)
KazeiaTelemetryProvider.voicesCursor()construit l'inventaire/voicescommeids = basenames(*.wav) ∪ basenames(<id>_voice_prefix.bin)(legacy Qwen3-TTS). Lestatevautreadysi les.binprefix+suffix existent,recordedsi seul le WAV est là, sinonerror. Le dossiercosyvoice/*.cvpsn'est jamais lu.- TTS de prod = CosyVoice uniquement (
CosyVoiceTtsEngine). Résolution voix purement par nom :cvpsPath(voiceId) = "<modelsDir>/cosyvoice/<voiceId>.cvps",ensureVoice()charge le.cvpsà la demande (cache parvoiceId). Repli gracieux surDEFAULT_VOICE_IDsi le.cvpsest absent. Profile.voiceId(nullable) = la voix d'un profil ;KazeiaService.setVoiceId()→CosyVoiceTtsEngine.setVoice(). Aucune notion d'« exclusivité/propriété » d'une voix par un profil : un.cvpsprésent est chargeable par n'importe quel profil.
1. Adaptation — /voices scanne WAV et .cvps (inventaire à-convertir / prête)
Intention : /voices doit refléter le cycle de vie d'une voix en scannant les
deux dossiers, chacun pour un état :
- WAV présent, pas de
.cvps→ voix « à convertir » (enregistrée, en attente d'enrôlement par Kazeia-central). .cvpsprésent → voix « prête » (enrôlée + déployée).
Conséquence voulue : après conversion+déploiement puis suppression du WAV (le WAV est
archivé côté PC), la voix reste affichée « prête » grâce au scan .cvps — elle ne
disparaît pas de l'inventaire. (Aujourd'hui voicesCursor() ne scanne que
WAV ∪ embeddings Qwen3 → une voix .cvps-seule serait omise.)
Demande : étendre voicesCursor() —
- Union d'ids =
basenames(*.wav) ∪ basenames(prefix.bin) ∪ basenames(cosyvoice/*.cvps). - Nouvelle colonne
cvps_exists(0/1). state(cascade) :readysicvps_exists(ou prefix+suffix Qwen3 legacy) ;recorded(= « à convertir ») si seul le WAV ;errorsinon.- Ajout additif (colonne en plus) → compat ascendante.
Pas un prérequis fonctionnel (la synthèse charge le
.cvpspar son nom, sans cette liste), mais c'est l'inventaire voulu : distinguer voix à convertir vs prêtes, et garder les voix prêtes visibles une fois le WAV supprimé.
2. Adaptation — voices_reload (rescan après push direct)
Kazeia-central pousse le .cvps par adb push direct (hors WebDAV). Le .cvps est
utilisable immédiatement (chargé à la demande par ensureVoice() au prochain usage
de la voix), mais l'inventaire /voices et le picker admin ne se rafraîchissent pas
tant qu'aucun évènement ne le déclenche.
Demande : méthode call() voices_reload (déjà listée nice-to-have dans
PROVIDER_RPC_SPEC §9) — re-scanne le dossier voix et diffuse RELOAD_VOICES
(constante ACTION_RELOAD_VOICES à créer au passage ; aujourd'hui la string est en dur,
cf. provider ~l.656). Permet à Kazeia-central de finaliser un déploiement par un refresh
propre, sans aller-retour WebDAV ni redémarrage.
3. Politique de déploiement (appliquée côté Kazeia-central)
La règle de localisation est enforced par Kazeia-central au déploiement —
- une voix verrouillée (exclusive à un profil patient) n'est poussée que sur la/les tablette(s) où ce profil est déployé ;
- seules les voix globales/non-exclusives sont déployées sur le parc.
⚠️ Mais une tablette héberge plusieurs profils patients (confirmé). La localisation
ne suffit donc pas : un .cvps exclusif à A, présent sur la tablette partagée
(parce que A y est), serait techniquement sélectionnable par B. → l'exclusivité doit
aussi être enforced on-device : c'est l'objet du §4, qui devient requis.
4. Adaptation REQUISE — exclusivité on-device (tablettes multi-patients)
Décidé : une tablette héberge plusieurs profils patients. Une voix exclusive ne doit
donc être ni sélectionnable, ni audible par un profil qui n'en est pas propriétaire,
même si son .cvps est physiquement sur la tablette.
4.1 Marqueur de propriété — sidecar .owner
Kazeia-central pousse, à côté du .cvps, un fichier sidecar :
cosyvoice/<voiceId>.cvps # l'artefact voix (inchangé)
cosyvoice/<voiceId>.owner # présent SI exclusive → contient le profileId propriétaire
- Absence de
.owner⇒ voix généraliste : sélectionnable par tout profil. - Présence de
.owner⇒ voix exclusive auprofileIdqu'il contient (UTF-8, une ligne). Cohérent avec le modèle « tout par fichier +adb push» (comme le.cvps).
4.2 Enforcement côté app
/voices: exposer une colonneowner_profile_id(lue depuis<id>.owner, vide si généraliste) — pour que le picker filtre.- Picker / assignation
Profile.voiceId: ne pas proposer/permettre l'assignation d'une voix exclusive à un profil ≠ propriétaire. - Runtime (
KazeiaService.setVoiceId/CosyVoiceTtsEngine.ensureVoice) : garde-fou défensif — si le profil actif n'est pas propriétaire d'une voix exclusive demandée, repli surDEFAULT_VOICE_ID(ne jamais faire parler un patient avec la voix exclusive d'un autre).
Kazeia-central connaît déjà
locked_profile_idpar voix et poussera/retirera le sidecar.ownerau verrouillage/déverrouillage. L'app n'a qu'à lire le sidecar et appliquer le filtrage + le garde-fou runtime.
5. Récap — ce qui est demandé au dev app patiente
| # | Adaptation | Criticité | Forme |
|---|---|---|---|
| 1 | /voices scanne WAV (« à convertir ») et .cvps (« prête »), col. cvps_exists |
inventaire voulu | curseur voicesCursor() |
| 2 | voices_reload (rescan + RELOAD_VOICES, constante ACTION_RELOAD_VOICES) |
important | méthode call() |
| 4 | exclusivité on-device : lire cosyvoice/<id>.owner, col. owner_profile_id, filtrer picker + repli runtime |
requis (multi-patients) | voicesCursor() + picker + setVoiceId |
Côté Kazeia-central (déjà fait / à faire, pas côté app) : enrôlement, archive chiffrée du WAV, suppression du WAV device, politique de déploiement verrou-conscient, et la distribution flotte (pousser les voix globales sur le parc, les exclusives chez leur patient) — en cours.
Source de vérité : KazeiaTelemetryProvider.voicesCursor(), CosyVoiceTtsEngine.kt,
Profile.kt, KazeiaService.setVoiceId() (kazeia-android). Vérifié le 2026-06-19.