Kazeia-central/docs/VOICE_DEPLOYMENT_SPEC.md

127 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 que `PROVIDER_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 `/voices` comme
`ids = basenames(*.wav) basenames(<id>_voice_prefix.bin)` (legacy **Qwen3-TTS**).
Le `state` vaut `ready` si les `.bin` prefix+suffix existent, `recorded` si seul le
WAV est là, sinon `error`. **Le dossier `cosyvoice/*.cvps` n'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 par `voiceId`). Repli gracieux
sur `DEFAULT_VOICE_ID` si le `.cvps` est 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 `.cvps` présent est chargeable par n'importe quel profil.
---
## 1. Adaptation **BLOQUANTE** — `/voices` doit refléter les `.cvps` déployés
**Problème** : Kazeia-central supprime le WAV de la tablette après enrôlement (le WAV
est archivé sur le PC). Une voix qui n'a plus que son `.cvps` n'a **ni `.wav` ni
embedding Qwen3** → l'`ids = wav prefix` actuel **l'omet**. La voix est déployée et
fonctionnelle, mais **invisible** de `/voices` (donc du picker admin et de
Kazeia-central). Incident « zelda » généralisé.
**Demande** : étendre `voicesCursor()` pour **inclure les basenames de
`<modelsDir>/cosyvoice/*.cvps`** dans l'union d'ids, et exposer leur présence.
- Union d'ids = `basenames(*.wav) basenames(prefix.bin) basenames(*.cvps)`.
- Nouvelle colonne **`cvps_exists`** (0/1) à `/voices`.
- `state` : une voix avec `.cvps` présent est **`ready`** (CosyVoice = TTS de prod).
Proposition de cascade : `ready` si `cvps_exists` **ou** (prefix+suffix Qwen3) ;
`recorded` si seul le WAV ; `error` sinon.
- Colonnes inchangées par ailleurs (compat ascendante : ajout additif d'une colonne).
> Sans cette adaptation, le reste du workflow « enrôle → déploie → supprime le WAV »
> casse l'inventaire. C'est le **prérequis n°1**.
---
## 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 (RAPPEL — appliquée côté Kazeia-central)
Pour mémoire, **pas une demande de code app** : la règle est **enforced par
Kazeia-central au moment du 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 les tablettes (parc).
L'app n'a donc **rien à faire** pour cette règle dans le cas **1 patient ↔ 1 tablette**
(modèle de prod implicite, cf. `CLAUDE.md §9` « serial → label patient ») : l'exclusivité
= la localisation du `.cvps`, décidée par le PC.
---
## 4. Adaptation **CONDITIONNELLE** — exclusivité on-device (tablettes multi-patients)
⚠️ **À trancher (Richard / dev)** : si une tablette héberge **plusieurs profils
patients** (observé sur la tablette de dev : `p_test01`, `p_test02`), alors un `.cvps`
exclusif à A, présent sur la tablette parce que A y est, est **techniquement chargeable
par le profil B** (résolution par nom de fichier, aucun contrôle de propriété). La règle
« déploiement uniquement où le profil est » ne suffit alors plus à garantir l'exclusivité
*à l'intérieur* d'une tablette partagée.
Deux options :
- **(a) Mono-patient par tablette (recommandé, défaut supposé)** : aucune adaptation.
L'exclusivité = la localisation du `.cvps`. C'est le modèle « une tablette = un
patient » du parc.
- **(b) Multi-patients par tablette** : l'app doit connaître le **propriétaire** d'une
voix exclusive et empêcher un autre profil de la sélectionner. Nécessite :
- un marqueur de propriété par voix (ex. fichier `cosyvoice/<id>.owner` = profileId,
poussé par Kazeia-central, ou colonne `owner_profile_id` à `/voices`) ;
- côté picker/`setVoiceId` : masquer/refuser une voix exclusive pour un profil ≠
propriétaire (repli sur la voix par défaut).
> Décision attendue : **(a)** suffit si le parc est mono-patient/tablette. Sinon **(b)**
> et on étend cette spec. Kazeia-central est prêt pour les deux (il connaît déjà
> `locked_profile_id` par voix).
---
## 5. Récap — ce qui est demandé au dev app patiente
| # | Adaptation | Criticité | Forme |
| --- | --- | --- | --- |
| 1 | `/voices` inclut les `.cvps` (`cvps_exists`, `state=ready`, ids cvps) | **bloquant** | curseur `voicesCursor()` |
| 2 | `voices_reload` (rescan + `RELOAD_VOICES`, constante `ACTION_RELOAD_VOICES`) | important | méthode `call()` |
| 4 | exclusivité on-device | conditionnel (si multi-patients/tablette) | à décider §4 |
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.*