| | 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 |
| | ) |