| import gradio as gr |
| import edge_tts |
| import tempfile |
| import asyncio |
| import hashlib |
| import os |
| from pathlib import Path |
| from functools import lru_cache |
|
|
| |
| VOICE_MAP = { |
| "رجل (مصري)": "ar-EG-ShakirNeural", |
| "سيدة (مصرية)": "ar-EG-SalmaNeural", |
| "رجل (سعودي)": "ar-SA-HamedNeural", |
| "سيدة (سعودية)": "ar-SA-ZariyahNeural", |
| "English (US) M": "en-US-EricNeural", |
| "English (US) F": "en-US-AriaNeural" |
| } |
|
|
| |
| TTS_SEMAPHORE = asyncio.Semaphore(2) |
|
|
| |
| CACHE_DIR = Path("./tts_cache") |
| CACHE_DIR.mkdir(exist_ok=True) |
|
|
| |
| def cleanup_old_cache(max_files=100): |
| """حذف أقدم ملفات الكاش إذا تجاوز العدد الحد المسموح""" |
| cache_files = sorted(CACHE_DIR.glob("*.mp3"), key=os.path.getmtime) |
| if len(cache_files) > max_files: |
| for f in cache_files[:len(cache_files) - max_files]: |
| try: |
| f.unlink() |
| except: |
| pass |
|
|
| cleanup_old_cache(max_files=100) |
|
|
| def generate_cache_key(text, voice, rate, pitch): |
| """توليد مفتاح فريد للكاش""" |
| content = f"{text[:500]}{voice}{rate}{pitch}" |
| return hashlib.md5(content.encode('utf-8')).hexdigest() |
|
|
| async def generate_speech(text, voice, emotion, is_symbol, rate, pitch): |
| if not text or not text.strip(): |
| return None |
| |
| |
| text = text[:5000] |
| |
| |
| final_rate = rate if rate and isinstance(rate, str) and len(rate.strip()) > 0 else "+0%" |
| final_pitch = pitch if pitch and isinstance(pitch, str) and len(pitch.strip()) > 0 else "+0Hz" |
| |
| |
| selected_voice = "ar-SA-HamedNeural" |
| if voice in VOICE_MAP: |
| selected_voice = VOICE_MAP[voice] |
| elif voice in VOICE_MAP.values(): |
| selected_voice = voice |
| |
| |
| cache_key = generate_cache_key(text, selected_voice, final_rate, final_pitch) |
| cache_path = CACHE_DIR / f"{cache_key}.mp3" |
| |
| if cache_path.exists(): |
| print(f"✓ Cache hit: {len(text)} chars") |
| return str(cache_path) |
| |
| print(f"⚙ Generating: {len(text)} chars | {selected_voice}") |
|
|
| |
| async with TTS_SEMAPHORE: |
| try: |
| |
| communicate = edge_tts.Communicate( |
| text, |
| selected_voice, |
| rate=final_rate, |
| pitch=final_pitch |
| ) |
| |
| |
| await asyncio.wait_for( |
| communicate.save(str(cache_path)), |
| timeout=45.0 |
| ) |
| |
| return str(cache_path) |
| |
| except asyncio.TimeoutError: |
| print(f"✗ Timeout for {len(text)} chars") |
| if cache_path.exists(): |
| cache_path.unlink() |
| raise gr.Error("Request timeout - try shorter text") |
| except Exception as e: |
| print(f"✗ ERROR: {str(e)}") |
| if cache_path.exists(): |
| cache_path.unlink() |
| raise gr.Error(f"TTS Error: {str(e)[:100]}") |
|
|
| |
| with gr.Blocks(title="Natiq Pro API", css=".gradio-container {max-width: 100%}") as demo: |
| with gr.Row(visible=False): |
| t = gr.Textbox(label="Text") |
| v = gr.Textbox(label="Voice") |
| e = gr.Textbox(label="Emotion", value="neutral") |
| s = gr.Checkbox(label="Is Symbol", value=True) |
| r = gr.Textbox(label="Rate", value="+0%") |
| p = gr.Textbox(label="Pitch", value="+0Hz") |
| |
| o = gr.Audio(label="Output", type="filepath") |
| b = gr.Button("Generate", visible=False) |
| |
| b.click( |
| generate_speech, |
| inputs=[t, v, e, s, r, p], |
| outputs=[o], |
| api_name="text_to_speech_edge", |
| concurrency_limit=3 |
| ) |
|
|
| if __name__ == "__main__": |
| demo.queue( |
| default_concurrency_limit=3, |
| max_size=30 |
| ).launch( |
| max_threads=6, |
| show_error=True |
| ) |