# VOICE_ENROLLMENT_SPEC — conversion WAV → `.cvps` (clonage vocal CosyVoice) > 🛑 **SUPERSEDED (23/06/2026) — CosyVoice retiré, remplacé par OmniVoice.** > Cet algorithme (teacher CosyVoice3-0.5B → 4 tenseurs GGUF `.cvps`) **n'est plus > utilisé**. L'enrôlement actuel = **OmniVoice** : `create_voice_clone_prompt` → binaire > **`.ovsp`** (magic `OVSP` | T_ref | text_len | tokens[8·T_ref] | ref_text), cap réf > **16 s**, ASR de réf via OmniVoice (plus de Whisper), venv **`ov_venv`**. Source de > vérité : **`OMNIVOICE_VOICE_MIGRATION.md`** + `/opt/Kazeia-engine/dist/omnivoice/scripts/make_ovsp.py`. > Le code `kazeia_central/voice/` est migré. Document conservé pour historique. > **Destinataire : dev Kazeia-central.** Comment transformer un échantillon de voix > (WAV) en artefact `.cvps` chargeable par le moteur TTS CosyVoice de l'app patiente. > > **Pourquoi côté PC (Kazeia-central) et pas sur la tablette** : l'enrôlement charge le > **teacher CosyVoice3-0.5B complet en PyTorch (~9,1 Go)** et son *frontend* > d'extraction. La tablette n'embarque que le `.gguf` distillé de **synthèse** (aucun > frontend, aucune entrée JNI d'enrôlement). Porter ça on-device = shipper campplus + > s3tokenizer + torch sur chaque device pour une opération one-shot → mauvais > compromis. L'enrôlement est offline, opérateur, *enroll-once → sync N tablettes* : > c'est le mandat de Kazeia-central (cf. `CLAUDE.md` §6). --- ## 1. Vue d'ensemble du workflow ``` [Tablette] [Kazeia-central / PC] [Flotte] capture WAV (micro, ~15 s) ──adb pull──► enrôlement (cv_make_prompt) ──adb push──► .cvps + pré-transcription Whisper = teacher 0.5B PyTorch dans .../cosyvoice/ (seed REF_TXT, relue opérateur) → 4 tenseurs → GGUF .cvps + maj catalogue ``` 1. La tablette enregistre le clip de consentement (elle le fait déjà) et peut **pré-transcrire avec son Whisper embarqué** pour pré-remplir le texte de référence. 2. Kazeia-central récupère le WAV + le texte (relu/corrigé par l'opérateur), lance l'enrôlement, obtient `.cvps`, le pousse vers les tablettes et met à jour le catalogue voix (source de vérité des voix CosyVoice disponibles). --- ## 2. Référence : `cv_make_prompt.py` (l'algorithme exact) Source : `/opt/Kazeia/cv_make_prompt.py` (et copie `/opt/Kazeia-engine/dist/cosyvoice/`). ```python import sys, numpy as np, torch sys.path.insert(0, "") sys.path.insert(0, "/third_party/Matcha-TTS") from cosyvoice.cli.cosyvoice import CosyVoice3 import gguf REF, REF_TXT, OUT = sys.argv[1], sys.argv[2], sys.argv[3] cv = CosyVoice3("", load_trt=False, fp16=False) # charge ~9,1 Go fe = cv.frontend text = open(REF_TXT).read().strip() if REF_TXT.endswith(".txt") else REF_TXT feat, _ = fe._extract_speech_feat(REF) # mel prompt → [1, T, 80] emb = fe._extract_spk_embedding(REF) # x-vector campplus → [1, dim] tok, _ = fe._extract_speech_token(REF) # speech tokens s3 → [1, n] feat = feat.squeeze(0).cpu().numpy().astype(np.float32) # [T, 80] emb = emb.cpu().numpy().astype(np.float32) # [1, dim] tok = tok.squeeze(0).cpu().numpy().astype(np.int32) # [n] txt = np.frombuffer(text.encode("utf-8"), dtype=np.int8) # [bytes] w = gguf.GGUFWriter(OUT, "cosyvoice-prompt-speech") w.add_tensor("feat", feat) # mel prompt w.add_tensor("embedding", emb) # x-vector w.add_tensor("tokens", tok) # speech tokens w.add_tensor("text", txt) # transcription UTF-8 brute w.write_header_to_file(); w.write_kv_data_to_file(); w.write_tensors_to_file(); w.close() ``` Trois extractions du frontend (c'est *tout* le « clonage ») + écriture GGUF. Pas de crc32, pas de check au chargement côté C++ → le format doit être exact. --- ## 3. Format `.cvps` (contrat avec le loader C++) Conteneur **GGUF**, nom d'architecture **`"cosyvoice-prompt-speech"`**, **4 tenseurs** (noms et dtypes **exacts** — le loader `nativeLoadVoice` ne tolère aucune dérive) : | tenseur | contenu | shape (numpy) | dtype | note layout GGUF | | --- | --- | --- | --- | --- | | `feat` | mel prompt de la voix | `[T, 80]` | `float32` | GGUF inverse → `ne0=80, ne1=T` | | `embedding` | x-vector campplus (empreinte locuteur) | `[1, dim]` | `float32` | `ne0=dim, ne1=1` | | `tokens` | speech tokens s3 | `[n]` | `int32` | 1-D | | `text` | transcription UTF-8 **brute** | `[octets]` | `int8` | 1-D, octets UTF-8 | - `T` ∝ durée du clip ; `dim` fixé par campplus ; `n` = nb de tokens audio. > **Le texte de référence est CONNU, pas à transcrire (décision 2026-06-19).** La > personne lit toujours **la même phrase de consentement** (admin string > `rec_reference_text`, nom interpolé). Donc `text` = cette phrase canonique (nom > rempli), **pas** un résultat Whisper. Conséquence : `transcribe.py`/Whisper **sort > du chemin critique** (fallback legacy seulement, pour des clips au contenu inconnu). > Source de vérité = la string admin (idéalement portée par le `reference_text` du > manifeste voix). Fenêtre d'enrôlement portée à **`CV_TARGET_S=22 s`** pour que la > phrase de consentement (~13 s) + la fin phonétiquement riche entrent en entier. - `text` est conditionnant (CosyVoice = *audio réf + texte réf → texte cible, même voix*) : il **doit correspondre au contenu parlé** du WAV. Une transcription fausse dégrade la similarité. --- ## 4. Entrées & contraintes - **WAV de référence** : mono, PCM ; le frontend gère le ré-échantillonnage (fournir ≥16 kHz propre). **~15 s** = sweet spot (la similarité ECAPA/campplus monte avec la longueur du prompt, mais le **cache K/V on-device ~270 Mo** et la latence du 1ᵉʳ énoncé montent aussi). Éviter <8 s (similarité faible) et >~30 s (coût mémoire). Clip propre, une seule voix, peu de bruit/musique. - **Transcription** : texte exact de ce qui est dit, dans la **langue du clip**. L'enrôlement est **cross-lingual** : un `.cvps` sert pour les 9 langues (fr,en,de,es,it,ru,zh,ja,ko). La tablette peut la pré-générer (Whisper) ; **faire relire/corriger par l'opérateur** avant enrôlement. - **Sortie** : `.cvps`. `voiceId` = slug stable (`[a-z0-9_]`), c'est la clé de cache moteur et le nom de fichier. --- ## 5. Dépendances hôte (poste Kazeia-central) Lourdes mais one-time, sur le poste opérateur (local-first) : | dépendance | détail | emplacement actuel (poste Richard) — **vérifié 2026-06-19** | | --- | --- | --- | | **interpréteur encodeur** | Python 3.10.20, torch 2.3.1+cu121 (CPU, `cuda=False`), `gguf` OK ; `from cosyvoice.cli.cosyvoice import CosyVoice3` résout | **`/opt/Kazeia/cv_venv/bin/python3`** | | **repo CosyVoice** + `third_party/Matcha-TTS` | code `cosyvoice.cli.cosyvoice` | `/opt/Kazeia/cosyvoice-repo` (`requirements.txt` présent) | | **teacher CosyVoice3-0.5B** | ~9,1 Go, modèles frontend (campplus, s3tokenizer, mel) | `/opt/Kazeia/_models_dl/cosyvoice3-0.5b` | | package python `gguf` | écriture du conteneur | dans `cv_venv` | | `cv_make_prompt.py` | le script | `/opt/Kazeia/cv_make_prompt.py` | > **Env de référence confirmé** : `/opt/Kazeia/cv_venv` fait tourner l'encodeur > (imports validés sans charger le teacher). C'est l'env à réutiliser/pointer depuis > Kazeia-central (ou à recréer depuis `cosyvoice-repo/requirements.txt`). > > **Chemins device confirmés** (tablette dev, legacy) : > - WAV source : `/data/local/tmp/kazeia/voix/voix/.wav` > - cible `.cvps` : `/data/local/tmp/kazeia/models/cosyvoice/.cvps` > (présents : `damien.cvps`, `elodie.cvps` — 242 720 o ≈ 15 s, refs de validation). > ⚠️ Les chemins repo/teacher sont **codés en dur** dans `cv_make_prompt.py`. Pour > Kazeia-central : **les rendre configurables** (env/config `CV_REPO`, `CV_TEACHER`) et > documenter le pré-requis d'installation. Le teacher 9,1 Go n'est PAS versionné — le > dev doit le provisionner sur le poste (pas dans le repo, pas dans l'APK). --- ## 6. Intégration dans Kazeia-central (module `voice/`) **Recommandation forte : un worker d'enrôlement persistant, pas un `python` par voix.** Charger le teacher 0.5B coûte plusieurs dizaines de secondes (9,1 Go) — relancer le process à chaque voix est inacceptable pour un enrôlement de flotte. Garder le modèle **chaud** et traiter une file. Forme cible (Python natif à Kazeia-central, d'où le choix de stack) : ``` voice/ enroll.py # importe CosyVoice3 UNE fois ; expose enroll(wav_path, text, voice_id) -> cvps_path worker.py # garde le teacher chaud ; file de jobs d'enrôlement ``` - **Préférer réimplémenter la logique** des 25 lignes de `cv_make_prompt.py` dans `enroll.py` (mêmes 3 extractions + écriture GGUF) plutôt que de shell-out, pour charger `CosyVoice3` une seule fois et enchaîner les voix. - API exposée par Kazeia-central (cf. `CLAUDE.md` §6) : 1. `POST /api/voices/{serial}/capture` → déclenche/récupère le WAV de la tablette (`adb pull` depuis `…/voix/voix/.wav`), pré-transcription Whisper optionnelle. 2. `POST /api/voices/enroll` `{voice_id, wav, transcription}` → worker → `.cvps`. 3. `POST /api/voices/{serial}/deploy` → `adb push` du `.cvps` dans `/cosyvoice/.cvps` + (option) `rag_sync`/reload, ou publication WebDAV + bump catalogue pour propagation OTA à la flotte. --- ## 7. Déploiement & runtime on-device (rappel, pour cohérence) - Destination : `/cosyvoice/.cvps`. Sur la tablette dev (legacy) : `/data/local/tmp/kazeia/models/cosyvoice/` ; en prod : `Android/data/com.kazeia/files/kazeia/models/cosyvoice/`. - Chargement : `CosyVoiceJni.nativeLoadVoice(handle, cvpsPath)`. Synthèse 24 kHz mono. - 1ʳᵉ synthèse d'une voix = lente (remplit le cache K/V prompt ~270 Mo, RTF ~2) ; les suivantes RTF 0,88. Cache par-voix (clé = identité du `.cvps`). - **Repli** : si le `.cvps` demandé est absent, l'app retombe sur la voix par défaut (`damien`) — une voix non enrôlée ne rend plus le TTS muet (fix 613c7ef). Mais l'objectif de Kazeia-central est qu'**aucune voix proposée ne soit non déployée**. --- ## 8. À réconcilier (lié, hors enrôlement strict) L'endpoint `/voices` de l'app patiente reflète l'**ancien inventaire Qwen3** (WAV + `.bin`), pas les `.cvps` CosyVoice réellement présents → l'admin peut sélectionner une voix muette (incident « zelda »). Kazeia-central, en devenant le maître de l'enrôlement et du déploiement des `.cvps`, doit être la **source de vérité des voix CosyVoice disponibles** (ce qui est enrôlé + déployé), et à terme pousser l'app à exposer son inventaire `.cvps` réel. À traiter avec `PROVIDER_RPC_SPEC.md` (méthode `voices_reload` + futur endpoint d'inventaire `.cvps`). --- ## 9. Checklist dev Kazeia-central - [ ] Provisionner sur le poste : repo CosyVoice + teacher 0.5B + `torch` + `gguf`. - [ ] Rendre les chemins configurables (env/config), pas codés en dur. - [ ] `voice/enroll.py` : charger `CosyVoice3` une fois ; `enroll(wav, text, id)→.cvps` (3 extractions + GGUF arch `cosyvoice-prompt-speech`, 4 tenseurs §3, dtypes exacts). - [ ] `voice/worker.py` : teacher chaud + file (pas de cold-load par voix). - [ ] Routes capture / enroll / deploy (§6) + pré-transcription Whisper optionnelle. - [ ] Valider un `.cvps` produit : taille proche de `damien.cvps` (~240 Ko pour ~15 s), push device, synthèse de test non vide, écoute (similarité voix). - [ ] Devenir la source de vérité de l'inventaire voix (§8).