import os import uuid import unicodedata import re import pathlib from fastapi import FastAPI, HTTPException, Query from fastapi.responses import FileResponse, HTMLResponse from pydantic import BaseModel import edge_tts app = FastAPI(title="Edge TTS API", description="Microsoft Edge Neural TTS API") os.makedirs("tts_outputs", exist_ok=True) VOICES = { "ar": {"male": "ar-EG-ShakirNeural", "female": "ar-EG-SalmaNeural"}, "ar-sa": {"female": "ar-SA-ZariyahNeural"}, "en": {"male": "en-US-GuyNeural", "female": "en-US-AvaNeural"}, "en-gb": {"female": "en-GB-SoniaNeural"}, "fr": {"male": "fr-FR-HenriNeural", "female": "fr-FR-DeniseNeural"}, "de": {"male": "de-DE-ConradNeural", "female": "de-DE-KatjaNeural"}, "es": {"male": "es-ES-AlvaroNeural", "female": "es-ES-ElviraNeural"}, "it": {"male": "it-IT-DiegoNeural", "female": "it-IT-ElsaNeural"}, "tr": {"male": "tr-TR-AhmetNeural", "female": "tr-TR-EmelNeural"}, "ru": {"male": "ru-RU-DmitryNeural", "female": "ru-RU-SvetlanaNeural"}, "hi": {"male": "hi-IN-MadhurNeural", "female": "hi-IN-SwaraNeural"} } SUPPORTED_LANGUAGES = list(VOICES.keys()) def clean_text(text: str) -> str: text = unicodedata.normalize("NFC", text) text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text) text = re.sub(r'\s+', ' ', text) return text.strip() class TTSRequest(BaseModel): text: str language: str = "en" gender: str = "female" rate: str = "+0%" @app.get("/", response_class=HTMLResponse) def get_app(): html_path = pathlib.Path("index.html") if html_path.exists(): return html_path.read_text(encoding="utf-8") return "

TTS API is running

" @app.post("/tts") async def synthesize(req: TTSRequest): text = clean_text(req.text) if not text: raise HTTPException(status_code=400, detail="Text cannot be empty.") if req.language not in VOICES: raise HTTPException(status_code=400, detail=f"Unsupported language. Choose from: {SUPPORTED_LANGUAGES}") if req.gender not in ["male", "female"]: raise HTTPException(status_code=400, detail="Gender must be 'male' or 'female'.") lang_voices = VOICES[req.language] gender = req.gender if req.gender in lang_voices else list(lang_voices.keys())[0] voice = lang_voices[gender] output_path = os.path.join("tts_outputs", f"{uuid.uuid4()}.mp3") try: communicate = edge_tts.Communicate(text, voice, rate=req.rate) await communicate.save(output_path) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) return FileResponse(path=output_path, media_type="audio/mpeg", filename="output.mp3", headers={"X-Voice": voice}) @app.post("/tts-text") async def synthesize_text( text: str = Query(..., description="النص المراد تحويله"), language: str = Query("en", description="ar, en, fr, es, de, it, tr, ru, hi"), gender: str = Query("female", description="male or female"), rate: str = Query("+0%", description="السرعة: -50%, -25%, +0%, +25%, +50%") ): text = clean_text(text) if not text: raise HTTPException(status_code=400, detail="Text cannot be empty.") if language not in VOICES: raise HTTPException(status_code=400, detail=f"Unsupported language. Choose from: {SUPPORTED_LANGUAGES}") if gender not in ["male", "female"]: raise HTTPException(status_code=400, detail="Gender must be 'male' or 'female'.") lang_voices = VOICES[language] gender = gender if gender in lang_voices else list(lang_voices.keys())[0] voice = lang_voices[gender] output_path = os.path.join("tts_outputs", f"{uuid.uuid4()}.mp3") try: communicate = edge_tts.Communicate(text, voice, rate=rate) await communicate.save(output_path) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) return FileResponse(path=output_path, media_type="audio/mpeg", filename="output.mp3", headers={"X-Voice": voice}) @app.get("/voices") def get_voices(): return VOICES