diff --git a/kazeia_central/api/app.py b/kazeia_central/api/app.py index a673734..ac33b5d 100644 --- a/kazeia_central/api/app.py +++ b/kazeia_central/api/app.py @@ -267,6 +267,12 @@ def create_app(adb: Adb | None = None, store: Store | None = None, store.audit("voice_unlock", actor=_OPERATOR, target=voice_id, now=_now_ms()) return {"voice_id": voice_id, "locked_profile_id": None} + @app.post("/api/voices/catalog/{voice_id}/reenroll") + def voices_reenroll(voice_id: str): + # régénère le .ovsp (fix §7) depuis le WAV archivé + re-pousse sur ses tablettes + return _guard(lambda: vo.reenroll_voice(adb, store, bridge, voice_id, + now=_now_ms(), actor=_OPERATOR)) + # ---- vue par tablette : voix chargées + chargeables ------------------- @app.get("/api/voices/{serial}") def voices_for_tablet(serial: str): diff --git a/kazeia_central/voice/orchestrator.py b/kazeia_central/voice/orchestrator.py index e6720ef..e1356e4 100644 --- a/kazeia_central/voice/orchestrator.py +++ b/kazeia_central/voice/orchestrator.py @@ -126,6 +126,33 @@ def undeploy_voice(adb: Adb, store: Store, serial: str, voice_id: str, *, return {"voice_id": voice_id, "deployed": False} +def reenroll_voice(adb: Adb, store: Store, bridge: VoiceBridge, voice_id: str, *, + now: int, actor: str | None = None) -> dict[str, Any]: + """Régénère le `.ovsp` depuis le WAV archivé (ex. après le fix §7) et le re-pousse + sur toutes les tablettes où la voix est déployée. `deploy_voice` réutilise le `.ovsp` + stocké — donc sans ré-enrôlement, un artefact pollué reste pollué.""" + if store.voice_wav_bytes(voice_id) is None: + raise ValueError(f"WAV non archivé pour {voice_id} — ré-enrôlement impossible " + f"(ingérer d'abord depuis l'admin)") + info = _enroll_into_store(store, bridge, voice_id, now=now) # écrase ovsp + transcription + store.audit("voice_reenroll", actor=actor, target=voice_id, now=now) + ovsp_bytes = store.voice_ovsp_bytes(voice_id) + redeployed = [] + for serial in store.deployments_of(voice_id): + try: + ov = _patient_omnivoice_dir(adb, serial) + with tempfile.TemporaryDirectory() as td: + local = os.path.join(td, f"{voice_id}.ovsp") + open(local, "wb").write(ovsp_bytes) + transfer.push_ovsp(adb, serial, local, ov, voice_id) + store.add_deployment(voice_id, serial, now=now) + redeployed.append(serial) + except Exception as e: + redeployed.append(f"{serial}:erreur ({e})") + return {"voice_id": voice_id, "reenrolled": True, "ovsp_bytes": info["bytes"], + "ref_text": info.get("ref_text"), "redeployed": redeployed} + + def list_for_tablet(adb: Adb, store: Store, serial: str) -> dict[str, Any]: """Vue d'une tablette : voix **chargées** (sur le device) et voix **chargeables** (catalogue non déployé, filtré par exclusivité).""" diff --git a/kazeia_central/web/index.html b/kazeia_central/web/index.html index 8bd90e0..3a09feb 100644 --- a/kazeia_central/web/index.html +++ b/kazeia_central/web/index.html @@ -290,7 +290,8 @@ async function voiceView() {