import gradio as gr
import os
import shutil
import tempfile
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import FileResponse
# ── Singletons séparés ────────────────────────────────────────────────────────
_kokoro = None # léger, chargé au premier /api/speak ou Simple TTS
_cloner = None # lourd (840MB), chargé seulement si clone/convert demandé
def get_kokoro():
"""Charge uniquement Kokoro ONNX — sans Kanade."""
global _kokoro
if _kokoro is None:
from core.cloner import KokoClone
print("[KokoOnly] Loading Kokoro TTS (no Kanade)...")
_kokoro = KokoClone.__new__(KokoClone)
# Init manuelle sans Kanade
import torch
_kokoro.device = torch.device("cpu")
_kokoro.kokoro_cache = {}
_kokoro.hf_repo = "PatnaikAshish/kokoclone"
print("[KokoOnly] Ready.")
return _kokoro
def get_cloner():
"""Charge KokoClone complet avec Kanade — seulement si nécessaire."""
global _cloner
if _cloner is None:
from core.cloner import KokoClone
print("[KokoClone] Loading full model (Kokoro + Kanade)...")
_cloner = KokoClone()
print("[KokoClone] Ready.")
return _cloner
# ── Handlers Gradio ───────────────────────────────────────────────────────────
def gradio_speak(text, lang, speed):
if not text or not text.strip():
raise gr.Error("Veuillez entrer du texte.")
output_file = "/tmp/gradio_speak_output.wav"
try:
get_cloner().speak(text=text, lang=lang, output_path=output_file, speed=float(speed))
return output_file
except Exception as e:
raise gr.Error(f"Erreur : {str(e)}")
def gradio_clone(text, lang, ref_audio_path):
if not text or not text.strip():
raise gr.Error("Please enter some text.")
if not ref_audio_path:
raise gr.Error("Please upload or record a reference audio file.")
output_file = "/tmp/gradio_clone_output.wav"
try:
get_cloner().generate(text=text, lang=lang, reference_audio=ref_audio_path, output_path=output_file)
return output_file
except Exception as e:
raise gr.Error(f"An error occurred: {str(e)}")
def gradio_convert(source_audio_path, ref_audio_path):
if not source_audio_path:
raise gr.Error("Please upload a source audio file.")
if not ref_audio_path:
raise gr.Error("Please upload a reference audio file.")
output_file = "/tmp/gradio_convert_output.wav"
try:
get_cloner().convert(source_audio=source_audio_path, reference_audio=ref_audio_path, output_path=output_file)
return output_file
except Exception as e:
raise gr.Error(f"An error occurred: {str(e)}")
# ── Interface Gradio ──────────────────────────────────────────────────────────
with gr.Blocks() as demo:
gr.Markdown(
"""
🎧 KokoClone
Synthèse vocale multilingue · Voice Cloning · Re-voicing
"""
)
with gr.Tabs():
with gr.Tab("🗣️ Simple TTS"):
gr.Markdown("Génère la parole avec une voix Kokoro — **aucun audio de référence requis**.")
with gr.Row():
with gr.Column(scale=1):
speak_text = gr.Textbox(
label="Texte à lire",
lines=5,
placeholder="Hello! How are you today?\nBonjour ! Comment allez-vous ?"
)
speak_lang = gr.Dropdown(
label="Langue / Voix",
choices=[
("🇬🇧 English — af_bella", "en"),
("🇫🇷 Français — ff_siwis", "fr"),
("🇮🇳 Hindi — hf_alpha", "hi"),
("🇪🇸 Español — im_nicola", "es"),
("🇮🇹 Italiano — im_nicola", "it"),
("🇧🇷 Português — pf_dora", "pt"),
],
value="en"
)
speak_speed = gr.Slider(
label="Vitesse",
minimum=0.5,
maximum=1.5,
value=1.0,
step=0.1
)
speak_btn = gr.Button("🗣️ Générer l'audio", variant="primary")
with gr.Column(scale=1):
speak_out = gr.Audio(label="Audio généré", interactive=False, autoplay=True)
gr.Markdown(
"""
**Voix disponibles**
- 🇬🇧 `af_bella` — femme américaine, claire
- 🇫🇷 `ff_siwis` — femme française, fluide
- 🇮🇳 `hf_alpha` — Hindi
- 🇪🇸🇮🇹 `im_nicola` — voix masculine
- 🇧🇷 `pf_dora` — Portugais brésilien
> Vitesse **0.7–0.8** recommandée pour les débutants.
"""
)
speak_btn.click(
fn=gradio_speak,
inputs=[speak_text, speak_lang, speak_speed],
outputs=speak_out
)
with gr.Tab("🎤 Text → Clone"):
gr.Markdown("Synthétise le texte puis applique la voix d'un audio de référence.")
with gr.Row():
with gr.Column(scale=1):
clone_text = gr.Textbox(label="1. Texte à synthétiser", lines=4)
clone_lang = gr.Dropdown(
label="2. Langue",
choices=[
("English", "en"), ("Français", "fr"), ("Hindi", "hi"),
("Japanese", "ja"), ("Chinese", "zh"), ("Español", "es"),
("Italiano", "it"), ("Português", "pt"),
],
value="en"
)
clone_ref = gr.Audio(label="3. Audio de référence", type="filepath")
clone_btn = gr.Button("🚀 Générer avec clonage", variant="primary")
with gr.Column(scale=1):
clone_out = gr.Audio(label="Audio cloné", interactive=False, autoplay=False)
gr.Markdown(
"""
**Conseils**
- Audio de référence propre, sans bruit de fond
- 3 à 10 secondes = résultat optimal
- La langue doit correspondre au texte
"""
)
clone_btn.click(
fn=gradio_clone,
inputs=[clone_text, clone_lang, clone_ref],
outputs=clone_out
)
with gr.Tab("🔁 Audio → Clone"):
gr.Markdown("Re-voice un audio existant pour qu'il sonne comme la voix de référence.")
with gr.Row():
with gr.Column(scale=1):
convert_src = gr.Audio(label="1. Audio source", type="filepath")
convert_ref = gr.Audio(label="2. Voix de référence", type="filepath")
convert_btn = gr.Button("🔁 Convertir la voix", variant="primary")
with gr.Column(scale=1):
convert_out = gr.Audio(label="Audio converti", interactive=False, autoplay=False)
gr.Markdown(
"""
**Comment ça marche**
- **Source** : n'importe quel enregistrement vocal
- **Référence** : court clip du locuteur cible
- KokoClone re-voice la source vers la référence
- Aucune transcription nécessaire
"""
)
convert_btn.click(
fn=gradio_convert,
inputs=[convert_src, convert_ref],
outputs=convert_out
)
# ── FastAPI ───────────────────────────────────────────────────────────────────
app = gr.mount_gradio_app(FastAPI(), demo, path="/")
@app.get("/api/voices")
def list_voices():
return {
"voices": [
{"lang": "en", "voice": "af_bella", "description": "English - American Female"},
{"lang": "fr", "voice": "ff_siwis", "description": "French - Female"},
{"lang": "hi", "voice": "hf_alpha", "description": "Hindi - Female"},
{"lang": "es", "voice": "im_nicola", "description": "Spanish - Male"},
{"lang": "it", "voice": "im_nicola", "description": "Italian - Male"},
{"lang": "pt", "voice": "pf_dora", "description": "Portuguese BR - Female"},
]
}
@app.post("/api/speak")
async def api_speak(
text: str = Form(...),
lang: str = Form(default="en"),
speed: float = Form(default=1.0),
):
out_path = tempfile.mktemp(suffix=".wav")
try:
get_cloner().speak(text=text, lang=lang, output_path=out_path, speed=speed)
return FileResponse(out_path, media_type="audio/wav", filename="speech.wav")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/clone")
async def api_clone(
text: str = Form(...),
lang: str = Form(default="en"),
reference_audio: UploadFile = File(...),
):
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as ref_tmp:
shutil.copyfileobj(reference_audio.file, ref_tmp)
ref_path = ref_tmp.name
out_path = ref_path.replace(".wav", "_out.wav")
try:
get_cloner().generate(text=text, lang=lang, reference_audio=ref_path, output_path=out_path)
return FileResponse(out_path, media_type="audio/wav", filename="cloned.wav")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if os.path.exists(ref_path):
os.unlink(ref_path)
@app.post("/api/convert")
async def api_convert(
source_audio: UploadFile = File(...),
reference_audio: UploadFile = File(...),
):
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as src_tmp:
shutil.copyfileobj(source_audio.file, src_tmp)
src_path = src_tmp.name
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as ref_tmp:
shutil.copyfileobj(reference_audio.file, ref_tmp)
ref_path = ref_tmp.name
out_path = src_path.replace(".wav", "_converted.wav")
try:
get_cloner().convert(source_audio=src_path, reference_audio=ref_path, output_path=out_path)
return FileResponse(out_path, media_type="audio/wav", filename="converted.wav")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if os.path.exists(src_path):
os.unlink(src_path)
if os.path.exists(ref_path):
os.unlink(ref_path)