deploy_voice réutilise le .ovsp stocké → une voix enrôlée avec l'ancien cap garde son
artefact pollué. reenroll_voice : ré-enrôle depuis le WAV archivé (enroll §7) + re-pousse
sur toutes les tablettes déployées. API POST /voices/catalog/{id}/reenroll, bouton UI
(si WAV archivé). tests +2. 42/42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le cap dur à 16s coupait la réf en plein mot ; l'auto-ASR transcrivait le bout tronqué
(ex. 'présenter') et OmniVoice le recrachait en tête de chaque génération.
- prepare_wav : couper sur une frontière de silence (librosa.effects.split), jamais à
cap brut → clip finit en silence, mots entiers.
- enroll : tronquer ref_text à la dernière phrase complète (. ! ?).
Validé sur le consentement réel de richard : 24.4s→15.7s, ref_text finit '…des oiseaux.'
(plus de mot parasite). NB : voix enrôlées avec l'ancien cap = à ré-enrôler.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refonte : une voix n'est plus liée à sa tablette d'enregistrement. Catalogue central
clé voice_id, .ovsp stocké chiffré → déployable sur N tablettes sans ré-enrôler.
- store : voices PK voice_id (+ origin_serial, ovsp_enc) + table voice_deployments
(many-to-many) + migration de l'ancien schéma (serial,voice_id)→global. Méthodes
globales + add/remove_deployment, voices_on, deployments_of, store/voice_ovsp_bytes.
- orchestrator : catalog(), deploy_voice/undeploy_voice (scope-conscient), list_for_tablet
(chargées + chargeables), ingest_voice/sync_admin au catalogue global. Suppr. legacy
/voices-based (list_voices/prepare/enroll_deploy/sync_device).
- API : GET /voices/catalog, POST /voices/catalog/{vid}/lock|unlock, GET /voices/{serial}
(chargées+chargeables), POST /voices/{serial}/deploy|undeploy.
- UI : carte Catalogue flotte (visible sans tablette) + par tablette chargées/chargeables
avec charger/retirer.
- tests réécrits (40/40). Validé live : catalog [], /voices/{serial} chargées+chargeables.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le pipeline est déjà paramétré par langue (context.language → STT/TTS OmniVoice) mais
figé sur 'fr' ; Profile n'a pas de champ langue. Spec : ajouter Profile.language
(déf. fr), l'exposer/accepter dans /profiles, et le câbler dans KazeiaService
(activeProfile.language au lieu du 'fr' codé en dur, ~l.1100/1351). Fiche clinique =
central-only, hors app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comble le gap : les voix fraîches + leur manifeste (scope/owner/reference_text) vivent
dans Android/data/com.kazeia.admin/files/voix/ (VoiceStorage.kt), pas dans /voices.
- admin_ingest.py : list_manifests (ls + pull .json), delete_admin_voice. Schéma fondé
sur VoiceStorage.save().
- orchestrator : list_admin_voices, ingest_voice (pull→archive→scope/owner/consentement
→verrou si exclusive→enroll ASR→deploy selon scope), sync_admin. pending = archivée,
ni enrôlée ni déployée. exclusive owner absent = verrou_profil_absent.
- _patient_omnivoice_dir robuste : env → wav_path legacy → sonde MODELS_DIR device
(post-migration /voices.wav_path est VIDE pour une voix déployée).
- store : colonnes scope/owner_name/consent_text_enc (consentement chiffré, PII) +
migration + set_voice_manifest. consent_text déchiffré dans voice_record/voices.
- autosync → sync_admin (le déclencheur auto ingère depuis l'admin).
- API : GET /voices/{serial}/admin, POST /voices/{serial}/admin/sync.
- enroll = ASR du 16s (migration §6) ; reference_text gardé comme trace de consentement.
- tests +8. 46/46. Validé live : manifeste synthétique → .ovsp déployé sur tablette.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- VOICE_ENROLLMENT_SPEC : le texte de référence est CONNU (phrase de consentement
admin), pas à transcrire → Whisper sort du chemin critique (fallback legacy).
Fenêtre CV_TARGET_S 15→22s pour que consentement + fin riche entrent dans le .cvps.
- VOICE_DEPLOYMENT_SPEC §4 : exclusivité DURE (consentement/RGPD), capturée
record-time (scope+owner au manifeste). Gap acté : ingérer depuis le stockage
ADMIN (pas le provider /voices). Phase 2 = garde-fou on-device. Récap des états.
- enroll.py : CV_TARGET_S=22.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'exclusivité s'appuie sur le mapping voix↔profil EXISTANT, pas un artefact parallèle :
l'app joue toujours activeProfile.voiceId (playback déjà exclusif par profil). Central =
autorité d'assignation (verrouiller = assigner Profile.voiceId au propriétaire, jamais
ailleurs + déploiement ciblé). Aucun code app requis (garde-fou picker optionnel sans fichier).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Décidé : une tablette héberge plusieurs profils patients. La localisation du .cvps ne
suffit pas → mécanisme sidecar cosyvoice/<id>.owner (profileId, absent=généraliste)
poussé par Kazeia-central ; app lit le sidecar, expose owner_profile_id à /voices,
filtre le picker, repli runtime sur DEFAULT_VOICE_ID si profil non-propriétaire.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recadrage suite échange : pas un blocage fonctionnel ; c'est l'inventaire voulu
(distinguer voix à convertir vs prêtes, garder les prêtes visibles après suppression WAV).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec livrée au dev kazeia-android (stack figée §11) :
- BLOQUANT : /voices doit inclure cosyvoice/*.cvps (cvps_exists, state=ready, ids∪cvps)
— sinon une voix dont le WAV est supprimé (workflow voulu) DISPARAÎT de l'inventaire.
- voices_reload (rescan + ACTION_RELOAD_VOICES) après push direct.
- conditionnel : exclusivité on-device si tablettes multi-patients (sinon la règle de
déploiement côté central suffit en mono-patient/tablette).
Fondé sur le code réel (voicesCursor blind aux .cvps, résolution par nom de fichier,
pas de notion de propriété).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- store : colonne locked_profile_id (+ migration ALTER idempotente), lock_voice/
unlock_voice. Verrou = assignation exclusive à un profil ; NULL = voix généraliste.
- orchestrator : list_voices expose locked_profile_id + used_by_profiles (croise
profile.voice_id) ; sync_device enrôle+déploie les voix en attente, REFUSE de
déployer une voix verrouillée sur une tablette sans le profil propriétaire.
- autosync : VoiceAutoSync — watcher de connexions armable depuis l'UI (désarmé par
défaut), statut par tablette, ne fait rien si store verrouillé (PII), delete opt-in.
- API : lock/unlock, POST /{serial}/sync, GET/POST /voices/autosync (déclarées AVANT
/{serial} pour éviter la capture "autosync" comme serial).
- tests : +8 (verrou, sync verrou-conscient, autosync). 39/39.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- worker.py : process persistant sous cv_venv (teacher + Whisper chauds), protocole
JSON-lines. Canal stdout propre (fd1→stderr) pour ne pas corrompre l'IPC avec le
bruit torch/librosa.
- bridge.py : pont côté API py3.14 (sans torch) — lance/pilote le worker en
subprocess, jobs sérialisés, démarrage paresseux.
- transcribe.py : Whisper sur le segment PRÉPARÉ (texte == segment enrôlé), défaut
"small" ("base" transcrit mal le FR, validé).
- Validé live : 1 worker chaud sert ping+transcribe(fr)+enroll, réutilisé (même PID),
protocole non corrompu (bruit libs → stderr).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- enroll.py : réimplémentation configurable de cv_make_prompt (env CV_REPO/CV_TEACHER),
teacher chargé une fois (singleton), + préparation WAV obligatoire (mono/16k/~15s)
car le tokenizer s3 plante >30s et la similarité chute <8s.
- Sortie GGUF arch cosyvoice-prompt-speech, 4 tenseurs aux dtypes exacts.
- Validé live : richard.wav (111s stéréo) → richard.cvps structurellement identique à
damien.cvps (feat F32[80,750], embedding F32[192,1], tokens I32[375], text I8).
- Hygiène stdout (museler numba/librosa) en prévision de l'IPC worker.
NB : tourne sous cv_venv (torch), PAS sous l'API py3.14 → worker subprocess à venir.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- API : /api/lock-status, /api/unlock, /api/lock, PUT /api/devices/{serial}/label,
/api/audit ; /api/devices enrichi du label patient quand le store est déverrouillé.
- db.py thread-safe (check_same_thread=False + RLock) pour le threadpool FastAPI ;
ajout lock(), device_labels().
- UI : barre de déverrouillage opérateur, affichage 👤 label, édition ✎ du nom.
- Sécurité : labels chiffrés (jamais en clair en base ni dans l'audit), opérations
journalisées, store re-verrouillable.
- tests : +5 intégration API↔store (dont non-fuite PII dans l'audit). 19/19.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>