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