Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,6 +2,10 @@ import gradio as gr
|
|
| 2 |
import edge_tts
|
| 3 |
import tempfile
|
| 4 |
import asyncio
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
# Voice Mapping
|
| 7 |
VOICE_MAP = {
|
|
@@ -13,8 +17,37 @@ VOICE_MAP = {
|
|
| 13 |
"English (US) F": "en-US-AriaNeural"
|
| 14 |
}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
async def generate_speech(text, voice, emotion, is_symbol, rate, pitch):
|
| 17 |
-
if not text or not text.strip():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Defaults
|
| 20 |
final_rate = rate if rate and isinstance(rate, str) and len(rate.strip()) > 0 else "+0%"
|
|
@@ -22,25 +55,53 @@ async def generate_speech(text, voice, emotion, is_symbol, rate, pitch):
|
|
| 22 |
|
| 23 |
# Voice Selection
|
| 24 |
selected_voice = "ar-SA-HamedNeural"
|
| 25 |
-
if voice in VOICE_MAP:
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
with gr.Row(visible=False):
|
| 45 |
t = gr.Textbox(label="Text")
|
| 46 |
v = gr.Textbox(label="Voice")
|
|
@@ -52,8 +113,19 @@ with gr.Blocks(title="Natiq Pro API") as demo:
|
|
| 52 |
o = gr.Audio(label="Output", type="filepath")
|
| 53 |
b = gr.Button("Generate", visible=False)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
if __name__ == "__main__":
|
| 59 |
-
demo.queue(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import edge_tts
|
| 3 |
import tempfile
|
| 4 |
import asyncio
|
| 5 |
+
import hashlib
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from functools import lru_cache
|
| 9 |
|
| 10 |
# Voice Mapping
|
| 11 |
VOICE_MAP = {
|
|
|
|
| 17 |
"English (US) F": "en-US-AriaNeural"
|
| 18 |
}
|
| 19 |
|
| 20 |
+
# معالجة 2 طلبات فقط في نفس الوقت (لـ 2 vCPUs)
|
| 21 |
+
TTS_SEMAPHORE = asyncio.Semaphore(2)
|
| 22 |
+
|
| 23 |
+
# Cache directory
|
| 24 |
+
CACHE_DIR = Path("./tts_cache")
|
| 25 |
+
CACHE_DIR.mkdir(exist_ok=True)
|
| 26 |
+
|
| 27 |
+
# تنظيف الكاش القديم عند البداية
|
| 28 |
+
def cleanup_old_cache(max_files=100):
|
| 29 |
+
"""حذف أقدم ملفات الكاش إذا تجاوز العدد الحد المسموح"""
|
| 30 |
+
cache_files = sorted(CACHE_DIR.glob("*.mp3"), key=os.path.getmtime)
|
| 31 |
+
if len(cache_files) > max_files:
|
| 32 |
+
for f in cache_files[:len(cache_files) - max_files]:
|
| 33 |
+
try:
|
| 34 |
+
f.unlink()
|
| 35 |
+
except:
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
cleanup_old_cache(max_files=100)
|
| 39 |
+
|
| 40 |
+
def generate_cache_key(text, voice, rate, pitch):
|
| 41 |
+
"""توليد مفتاح فريد للكاش"""
|
| 42 |
+
content = f"{text[:500]}{voice}{rate}{pitch}" # أول 500 حرف فقط
|
| 43 |
+
return hashlib.md5(content.encode('utf-8')).hexdigest()
|
| 44 |
+
|
| 45 |
async def generate_speech(text, voice, emotion, is_symbol, rate, pitch):
|
| 46 |
+
if not text or not text.strip():
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
# تقليم النص الطويل جداً (حماية من الإساءة)
|
| 50 |
+
text = text[:5000]
|
| 51 |
|
| 52 |
# Defaults
|
| 53 |
final_rate = rate if rate and isinstance(rate, str) and len(rate.strip()) > 0 else "+0%"
|
|
|
|
| 55 |
|
| 56 |
# Voice Selection
|
| 57 |
selected_voice = "ar-SA-HamedNeural"
|
| 58 |
+
if voice in VOICE_MAP:
|
| 59 |
+
selected_voice = VOICE_MAP[voice]
|
| 60 |
+
elif voice in VOICE_MAP.values():
|
| 61 |
+
selected_voice = voice
|
| 62 |
+
|
| 63 |
+
# Check cache
|
| 64 |
+
cache_key = generate_cache_key(text, selected_voice, final_rate, final_pitch)
|
| 65 |
+
cache_path = CACHE_DIR / f"{cache_key}.mp3"
|
| 66 |
+
|
| 67 |
+
if cache_path.exists():
|
| 68 |
+
print(f"✓ Cache hit: {len(text)} chars")
|
| 69 |
+
return str(cache_path)
|
| 70 |
+
|
| 71 |
+
print(f"⚙ Generating: {len(text)} chars | {selected_voice}")
|
| 72 |
+
|
| 73 |
+
# Semaphore للتحكم في التزامن
|
| 74 |
+
async with TTS_SEMAPHORE:
|
| 75 |
+
try:
|
| 76 |
+
# استخدام cache path مباشرة بدلاً من tempfile
|
| 77 |
+
communicate = edge_tts.Communicate(
|
| 78 |
+
text,
|
| 79 |
+
selected_voice,
|
| 80 |
+
rate=final_rate,
|
| 81 |
+
pitch=final_pitch
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# حفظ مباشرة في الكاش
|
| 85 |
+
await asyncio.wait_for(
|
| 86 |
+
communicate.save(str(cache_path)),
|
| 87 |
+
timeout=45.0 # timeout 45 ثانية
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
return str(cache_path)
|
| 91 |
+
|
| 92 |
+
except asyncio.TimeoutError:
|
| 93 |
+
print(f"✗ Timeout for {len(text)} chars")
|
| 94 |
+
if cache_path.exists():
|
| 95 |
+
cache_path.unlink()
|
| 96 |
+
raise gr.Error("Request timeout - try shorter text")
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f"✗ ERROR: {str(e)}")
|
| 99 |
+
if cache_path.exists():
|
| 100 |
+
cache_path.unlink()
|
| 101 |
+
raise gr.Error(f"TTS Error: {str(e)[:100]}")
|
| 102 |
+
|
| 103 |
+
# UI Definition
|
| 104 |
+
with gr.Blocks(title="Natiq Pro API", css=".gradio-container {max-width: 100%}") as demo:
|
| 105 |
with gr.Row(visible=False):
|
| 106 |
t = gr.Textbox(label="Text")
|
| 107 |
v = gr.Textbox(label="Voice")
|
|
|
|
| 113 |
o = gr.Audio(label="Output", type="filepath")
|
| 114 |
b = gr.Button("Generate", visible=False)
|
| 115 |
|
| 116 |
+
b.click(
|
| 117 |
+
generate_speech,
|
| 118 |
+
inputs=[t, v, e, s, r, p],
|
| 119 |
+
outputs=[o],
|
| 120 |
+
api_name="text_to_speech_edge",
|
| 121 |
+
concurrency_limit=3 # 3 طلبات متزامنة كحد أقصى
|
| 122 |
+
)
|
| 123 |
|
| 124 |
if __name__ == "__main__":
|
| 125 |
+
demo.queue(
|
| 126 |
+
default_concurrency_limit=3,
|
| 127 |
+
max_size=30 # قائمة انتظار 30 طلب ف��ط
|
| 128 |
+
).launch(
|
| 129 |
+
max_threads=6, # threads محدودة
|
| 130 |
+
show_error=True
|
| 131 |
+
)
|