diff --git a/docs/VOICE_DEPLOYMENT_SPEC.md b/docs/VOICE_DEPLOYMENT_SPEC.md index 4d0bb84..5433ce7 100644 --- a/docs/VOICE_DEPLOYMENT_SPEC.md +++ b/docs/VOICE_DEPLOYMENT_SPEC.md @@ -77,42 +77,40 @@ La règle de **localisation** est enforced par Kazeia-central au déploiement 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**. +⚠️ Une tablette héberge **plusieurs profils patients** (confirmé). Mais l'exclusivité au +playback est **déjà garantie** par le binding existant : l'app sélectionne toujours la +voix du **profil actif** (`KazeiaService` : `activeProfile.voiceId ?: DEFAULT_VOICE_ID`). +Un patient B n'utilise donc une voix exclusive à A **que si** `B.voiceId` pointe dessus. +Le point de contrôle est donc l'**assignation de `Profile.voiceId`**, pas un marqueur +parallèle. Voir §4 — l'exclusivité s'appuie sur le mapping voix↔profil existant, sans +nouvel artefact app. --- -## 4. Adaptation **REQUISE** — exclusivité on-device (tablettes multi-patients) +## 4. Exclusivité — via le mapping `Profile.voiceId` existant (pas de nouvel artefact) -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. +L'exclusivité s'appuie sur le binding **voix↔profil déjà présent** (`Profile.voiceId`), +pas sur un marqueur parallèle. Kazeia-central est l'**autorité d'assignation** (il a le +CRUD profils via le provider) : -### 4.1 Marqueur de propriété — sidecar `.owner` -Kazeia-central pousse, **à côté** du `.cvps`, un fichier sidecar : -``` -cosyvoice/.cvps # l'artefact voix (inchangé) -cosyvoice/.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** au `profileId` qu'il contient (UTF-8, - une ligne). Cohérent avec le modèle « tout par fichier + `adb push` » (comme le `.cvps`). +- **Verrouiller** une voix sur un patient = central assigne `Profile.voiceId = ` au + profil propriétaire, **et ne l'assigne à aucun autre profil** (sur la tablette ni + ailleurs). C'est ça l'exclusivité. +- **Déploiement** : central pousse le `.cvps` uniquement sur la/les tablette(s) du + propriétaire (cf. §3). +- **Playback** : déjà exclusif — l'app n'utilise que `activeProfile.voiceId`. -### 4.2 Enforcement côté app -- **`/voices`** : exposer une colonne **`owner_profile_id`** (lue depuis `.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 sur `DEFAULT_VOICE_ID`** (ne jamais faire parler un patient avec la voix - exclusive d'un autre). +### Côté app — rien de nouveau requis pour l'exclusivité +Le playback par profil actif suffit. **Optionnel** (garde-fou de gouvernance, si le +picker admin on-device autorise la (ré)assignation des voix) : l'app peut, à partir de +la liste des profils qu'elle a déjà, signaler qu'une voix est **déjà assignée à un autre +profil** et déconseiller/empêcher de la réassigner — **sans aucun fichier ni colonne +supplémentaire**, juste en croisant `Profile.voiceId`. À implémenter seulement si +l'assignation reste possible on-device ; sinon l'autorité centrale suffit. -> Kazeia-central connaît déjà `locked_profile_id` par voix et poussera/retirera le -> sidecar `.owner` au verrouillage/déverrouillage. L'app n'a qu'à **lire** le sidecar et -> appliquer le filtrage + le garde-fou runtime. +> Kazeia-central garde `locked_profile_id` par voix **en interne** (sa politique) et le +> matérialise par l'assignation `Profile.voiceId` + la localisation du `.cvps`. Aucun +> sidecar `.owner`, aucune ontologie d'exclusivité à dupliquer dans l'app. --- @@ -122,11 +120,12 @@ cosyvoice/.owner # présent SI exclusive → contient le profileId | --- | --- | --- | --- | | 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/.owner`, col. `owner_profile_id`, filtrer picker + repli runtime | **requis** (multi-patients) | `voicesCursor()` + picker + `setVoiceId` | +| 4 | exclusivité = via `Profile.voiceId` existant + autorité d'assignation centrale | **aucun code app** (garde-fou picker optionnel, sans artefact) | central / §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 +**autorité d'assignation** (`Profile.voiceId` du propriétaire, jamais ailleurs pour une +voix exclusive), et la **distribution flotte** (voix globales sur le parc, exclusives chez leur patient) — en cours. ---