import io from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from kokoro_onnx import Kokoro import soundfile as sf MODEL_PATH = "kokoro-v1.0.onnx" VOICES_PATH = "voices-v1.0.bin" VOICES = { "en": ["af_heart", "am_puck"], "fr": ["ff_siwis"], } LANG_CODES = {"en": "en-us", "fr": "fr-fr"} app = FastAPI(title="Speakeo TTS", description="API Kokoro TTS (EN/FR)") app.add_middleware( CORSMiddleware, allow_origins=["*"], # à restreindre au domaine de Speakeo plus tard allow_methods=["*"], allow_headers=["*"], ) _kokoro: Kokoro | None = None def get_kokoro() -> Kokoro: global _kokoro if _kokoro is None: _kokoro = Kokoro(MODEL_PATH, VOICES_PATH) return _kokoro @app.get("/") def root(): return {"status": "ok", "service": "speakeo-tts", "languages": list(VOICES)} @app.get("/voices") def voices(): return VOICES @app.get("/tts") def tts( text: str = Query(..., min_length=1, max_length=2000), lang: str = Query("en", pattern="^(en|fr)$"), voice: str | None = None, speed: float = Query(1.0, ge=0.5, le=2.0), ): if voice is None: voice = VOICES[lang][0] elif voice not in VOICES[lang]: raise HTTPException(400, f"Voix invalide pour '{lang}'. Options : {VOICES[lang]}") samples, sample_rate = get_kokoro().create( text, voice=voice, speed=speed, lang=LANG_CODES[lang] ) buf = io.BytesIO() sf.write(buf, samples, sample_rate, format="WAV") buf.seek(0) return StreamingResponse(buf, media_type="audio/wav")