Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import edge_tts
|
| 3 |
import asyncio
|
|
|
|
|
|
|
|
|
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
EMOTION_PRESETS = {
|
| 6 |
"محايد": {"rate": "+0%", "pitch": "+0Hz"},
|
| 7 |
"سعيد 😊": {"rate": "+15%", "pitch": "+10Hz"},
|
|
@@ -11,10 +17,29 @@ EMOTION_PRESETS = {
|
|
| 11 |
"متحمس 🎉": {"rate": "+30%", "pitch": "+15Hz"}
|
| 12 |
}
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
async def text_to_speech_edge(text, character, emotion, use_advanced, manual_rate, manual_pitch):
|
| 15 |
if not text or text.strip() == "":
|
| 16 |
return None
|
| 17 |
-
|
|
|
|
| 18 |
voice_map = {
|
| 19 |
"رجل (مصري)": "ar-EG-ShakirNeural",
|
| 20 |
"سيدة (مصرية)": "ar-EG-SalmaNeural",
|
|
@@ -22,12 +47,11 @@ async def text_to_speech_edge(text, character, emotion, use_advanced, manual_rat
|
|
| 22 |
"رجل (سعودي)": "ar-SA-HamedNeural",
|
| 23 |
"سيدة (سعودية)": "ar-SA-ZariyahNeural"
|
| 24 |
}
|
| 25 |
-
|
| 26 |
voice = voice_map.get(character, "ar-EG-SalmaNeural")
|
| 27 |
-
|
| 28 |
-
#
|
| 29 |
if use_advanced:
|
| 30 |
-
# تحويل slider (
|
| 31 |
rate_percent = int((manual_rate - 1.0) * 100)
|
| 32 |
rate = f"{rate_percent:+d}%"
|
| 33 |
pitch = manual_pitch
|
|
@@ -36,57 +60,116 @@ async def text_to_speech_edge(text, character, emotion, use_advanced, manual_rat
|
|
| 36 |
rate = preset["rate"]
|
| 37 |
pitch = preset["pitch"]
|
| 38 |
|
| 39 |
-
# صوت الطفل
|
| 40 |
if character == "طفل (محاكاة)":
|
| 41 |
pitch = "+50Hz"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# استخدام parameters مباشرة (بدون SSML)
|
| 46 |
-
communicate = edge_tts.Communicate(
|
| 47 |
-
text=text,
|
| 48 |
-
voice=voice,
|
| 49 |
-
rate=rate,
|
| 50 |
-
pitch=pitch
|
| 51 |
-
)
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return output_file
|
| 55 |
|
|
|
|
|
|
|
|
|
|
| 56 |
EXAMPLES = [
|
| 57 |
-
["
|
| 58 |
-
["أنا
|
| 59 |
-
["
|
| 60 |
]
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
gr.Markdown("# 🎙️ بوت الأصوات ال
|
| 65 |
-
gr.Markdown("###
|
| 66 |
|
| 67 |
with gr.Row():
|
| 68 |
with gr.Column(scale=2):
|
| 69 |
text_input = gr.Textbox(
|
| 70 |
-
label="📝 ا
|
| 71 |
-
placeholder="مثال:
|
| 72 |
-
lines=5
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
| 86 |
|
| 87 |
with gr.Accordion("⚙️ إعدادات متقدمة", open=False):
|
| 88 |
use_advanced = gr.Checkbox(
|
| 89 |
-
label="تفعيل التحكم اليدوي (ي
|
| 90 |
value=False
|
| 91 |
)
|
| 92 |
|
|
@@ -95,45 +178,41 @@ with gr.Blocks(title="🎙️ TTS المصري المتقدم", theme=gr.themes.
|
|
| 95 |
maximum=2.0,
|
| 96 |
value=1.0,
|
| 97 |
step=0.05,
|
| 98 |
-
label="⚡ سرعة الكلام (
|
| 99 |
)
|
| 100 |
|
| 101 |
manual_pitch = gr.Textbox(
|
| 102 |
-
label="🎵
|
| 103 |
-
value="+0Hz"
|
|
|
|
| 104 |
)
|
| 105 |
|
| 106 |
-
generate_btn = gr.Button("🎬 ول
|
| 107 |
|
| 108 |
with gr.Column(scale=1):
|
| 109 |
-
audio_output = gr.Audio(label="🔊 ا
|
| 110 |
|
| 111 |
gr.Markdown("""
|
| 112 |
-
###
|
| 113 |
-
|
| 114 |
-
**الأنماط العاطفية:**
|
| 115 |
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
-
**
|
| 119 |
-
|
| 120 |
-
للتحكم الدقيق في السرعة ودرجة الصوت
|
| 121 |
-
|
| 122 |
-
**اللهجة المصرية:**
|
| 123 |
-
|
| 124 |
-
يدعم العامية المصرية والفصحى والسعودية
|
| 125 |
-
|
| 126 |
-
**نصيحة:**
|
| 127 |
-
|
| 128 |
-
استخدم علامات الترقيم للحصول على تنغيم أفضل
|
| 129 |
""")
|
| 130 |
-
|
|
|
|
| 131 |
gr.Examples(
|
| 132 |
examples=EXAMPLES,
|
| 133 |
inputs=[text_input, voice_selector, emotion_selector, use_advanced, manual_rate, manual_pitch],
|
| 134 |
label="🎯 جرب هذه الأمثلة"
|
| 135 |
)
|
| 136 |
-
|
|
|
|
| 137 |
generate_btn.click(
|
| 138 |
fn=text_to_speech_edge,
|
| 139 |
inputs=[text_input, voice_selector, emotion_selector, use_advanced, manual_rate, manual_pitch],
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import edge_tts
|
| 3 |
import asyncio
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
from pydub import AudioSegment
|
| 7 |
|
| 8 |
+
# ---------------------------------------------------------
|
| 9 |
+
# 1. إعدادات المشاعر (Emotion Presets)
|
| 10 |
+
# ---------------------------------------------------------
|
| 11 |
EMOTION_PRESETS = {
|
| 12 |
"محايد": {"rate": "+0%", "pitch": "+0Hz"},
|
| 13 |
"سعيد 😊": {"rate": "+15%", "pitch": "+10Hz"},
|
|
|
|
| 17 |
"متحمس 🎉": {"rate": "+30%", "pitch": "+15Hz"}
|
| 18 |
}
|
| 19 |
|
| 20 |
+
# ---------------------------------------------------------
|
| 21 |
+
# 2. وظائف المساعدة (Helper Functions)
|
| 22 |
+
# ---------------------------------------------------------
|
| 23 |
+
async def generate_segment(text, voice, rate, pitch, filename):
|
| 24 |
+
"""
|
| 25 |
+
توليد ملف صوتي لجزء نصي واحد باستخدام Edge TTS
|
| 26 |
+
"""
|
| 27 |
+
try:
|
| 28 |
+
communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
|
| 29 |
+
await communicate.save(filename)
|
| 30 |
+
return True
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Error generating segment: {e}")
|
| 33 |
+
return False
|
| 34 |
+
|
| 35 |
+
# ---------------------------------------------------------
|
| 36 |
+
# 3. الوظيفة الرئيسية (Main Logic)
|
| 37 |
+
# ---------------------------------------------------------
|
| 38 |
async def text_to_speech_edge(text, character, emotion, use_advanced, manual_rate, manual_pitch):
|
| 39 |
if not text or text.strip() == "":
|
| 40 |
return None
|
| 41 |
+
|
| 42 |
+
# --- إعداد الصوت ---
|
| 43 |
voice_map = {
|
| 44 |
"رجل (مصري)": "ar-EG-ShakirNeural",
|
| 45 |
"سيدة (مصرية)": "ar-EG-SalmaNeural",
|
|
|
|
| 47 |
"رجل (سعودي)": "ar-SA-HamedNeural",
|
| 48 |
"سيدة (سعودية)": "ar-SA-ZariyahNeural"
|
| 49 |
}
|
|
|
|
| 50 |
voice = voice_map.get(character, "ar-EG-SalmaNeural")
|
| 51 |
+
|
| 52 |
+
# --- إعداد Rate & Pitch ---
|
| 53 |
if use_advanced:
|
| 54 |
+
# تحويل slider (مثلاً 1.5) إلى نسبة مئوية (+50%)
|
| 55 |
rate_percent = int((manual_rate - 1.0) * 100)
|
| 56 |
rate = f"{rate_percent:+d}%"
|
| 57 |
pitch = manual_pitch
|
|
|
|
| 60 |
rate = preset["rate"]
|
| 61 |
pitch = preset["pitch"]
|
| 62 |
|
| 63 |
+
# تعديل خاص لصوت الطفل
|
| 64 |
if character == "طفل (محاكاة)":
|
| 65 |
pitch = "+50Hz"
|
| 66 |
+
|
| 67 |
+
# --- منطق التقسيم والدمج (Stitching Logic) ---
|
| 68 |
+
# هذا النمط يلتقط [pause:N] ويحتفظ به في القائمة
|
| 69 |
+
pattern = r"(\[pause:[\d\.]+\])"
|
| 70 |
+
parts = re.split(pattern, text)
|
| 71 |
|
| 72 |
+
final_audio = AudioSegment.empty()
|
| 73 |
+
temp_files = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
+
print(f"Processing text parts: {parts}")
|
| 76 |
+
|
| 77 |
+
for i, part in enumerate(parts):
|
| 78 |
+
part = part.strip()
|
| 79 |
+
if not part:
|
| 80 |
+
continue
|
| 81 |
+
|
| 82 |
+
# الحالة 1: الجزء هو أمر توقف (مثلاً [pause:2])
|
| 83 |
+
if part.startswith("[pause:") and part.endswith("]"):
|
| 84 |
+
try:
|
| 85 |
+
# استخراج الرقم داخل القوسين
|
| 86 |
+
duration_str = part.split(":")[1].strip("]")
|
| 87 |
+
seconds = float(duration_str)
|
| 88 |
+
|
| 89 |
+
# إنشاء صمت (pydub يستخدم milliseconds)
|
| 90 |
+
silence = AudioSegment.silent(duration=seconds * 1000)
|
| 91 |
+
final_audio += silence
|
| 92 |
+
print(f"Added silence: {seconds}s")
|
| 93 |
+
|
| 94 |
+
except ValueError:
|
| 95 |
+
print(f"⚠️ تجاهل توقف غير صالح: {part}")
|
| 96 |
+
|
| 97 |
+
# الحالة 2: الجزء هو نص عادي
|
| 98 |
+
else:
|
| 99 |
+
temp_filename = f"temp_segment_{i}.mp3"
|
| 100 |
+
success = await generate_segment(part, voice, rate, pitch, temp_filename)
|
| 101 |
+
|
| 102 |
+
if success:
|
| 103 |
+
# قراءة الملف وإضافته للمسار النهائي
|
| 104 |
+
try:
|
| 105 |
+
segment = AudioSegment.from_mp3(temp_filename)
|
| 106 |
+
final_audio += segment
|
| 107 |
+
temp_files.append(temp_filename)
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"Error reading segment {temp_filename}: {e}")
|
| 110 |
+
|
| 111 |
+
# --- الحفظ والتنظيف ---
|
| 112 |
+
output_file = "output_final.mp3"
|
| 113 |
+
try:
|
| 114 |
+
final_audio.export(output_file, format="mp3")
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print(f"Error exporting final audio: {e}")
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
# حذف الملفات المؤقتة
|
| 120 |
+
for f in temp_files:
|
| 121 |
+
if os.path.exists(f):
|
| 122 |
+
try:
|
| 123 |
+
os.remove(f)
|
| 124 |
+
except:
|
| 125 |
+
pass
|
| 126 |
+
|
| 127 |
return output_file
|
| 128 |
|
| 129 |
+
# ---------------------------------------------------------
|
| 130 |
+
# 4. واجهة المستخدم (Gradio UI)
|
| 131 |
+
# ---------------------------------------------------------
|
| 132 |
EXAMPLES = [
|
| 133 |
+
["مرحباً بك. [pause:2] هذا توقف لمدة ثانيتين. [pause:0.5] وهذا نصف ثانية.", "رجل (مصري)", "محايد", False, 1.0, "+0Hz"],
|
| 134 |
+
["أنا أفكر... [pause:3] نعم، وجدتها!", "سيدة (مصرية)", "متحمس 🎉", False, 1.0, "+0Hz"],
|
| 135 |
+
["واحد [pause:1] اثنين [pause:1] ثلاثة [pause:1] انطلاق!", "رجل (سعودي)", "غاضب 😠", False, 1.0, "+0Hz"]
|
| 136 |
]
|
| 137 |
|
| 138 |
+
css = """
|
| 139 |
+
footer {visibility: hidden}
|
| 140 |
+
.gradio-container {direction: rtl}
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
with gr.Blocks(title="🎙️ TTS مع التوقفات الذكية", theme=gr.themes.Soft(), css=css) as demo:
|
| 144 |
|
| 145 |
+
gr.Markdown("# 🎙️ بوت الأصوات + التوقفات الذكية")
|
| 146 |
+
gr.Markdown("### 🇪🇬 يدعم اللهجة المصرية والسعودية مع تحكم دقيق في التوقيت")
|
| 147 |
|
| 148 |
with gr.Row():
|
| 149 |
with gr.Column(scale=2):
|
| 150 |
text_input = gr.Textbox(
|
| 151 |
+
label="📝 النص (استخدم [pause:N] للتوقف)",
|
| 152 |
+
placeholder="مثال: انتظر لحظة [pause:2] شكراً لك.",
|
| 153 |
+
lines=5,
|
| 154 |
+
text_align="right"
|
| 155 |
)
|
| 156 |
|
| 157 |
+
with gr.Row():
|
| 158 |
+
voice_selector = gr.Radio(
|
| 159 |
+
choices=["رجل (مصري)", "سيدة (مصرية)", "طفل (محاكاة)", "رجل (سعودي)", "سيدة (سعودية)"],
|
| 160 |
+
label="🎭 الصوت",
|
| 161 |
+
value="سيدة (مصرية)"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
emotion_selector = gr.Radio(
|
| 165 |
+
choices=list(EMOTION_PRESETS.keys()),
|
| 166 |
+
label="🎨 الحالة الشعورية",
|
| 167 |
+
value="محايد"
|
| 168 |
+
)
|
| 169 |
|
| 170 |
with gr.Accordion("⚙️ إعدادات متقدمة", open=False):
|
| 171 |
use_advanced = gr.Checkbox(
|
| 172 |
+
label="تفعيل التحكم اليدوي (يلغي الحالة الشعورية)",
|
| 173 |
value=False
|
| 174 |
)
|
| 175 |
|
|
|
|
| 178 |
maximum=2.0,
|
| 179 |
value=1.0,
|
| 180 |
step=0.05,
|
| 181 |
+
label="⚡ سرعة الكلام (1.0 = طبيعي)"
|
| 182 |
)
|
| 183 |
|
| 184 |
manual_pitch = gr.Textbox(
|
| 185 |
+
label="🎵 طبقة الصوت (Pitch)",
|
| 186 |
+
value="+0Hz",
|
| 187 |
+
placeholder="+10Hz, -5Hz..."
|
| 188 |
)
|
| 189 |
|
| 190 |
+
generate_btn = gr.Button("🎬 توليد الصوت", variant="primary", size="lg")
|
| 191 |
|
| 192 |
with gr.Column(scale=1):
|
| 193 |
+
audio_output = gr.Audio(label="🔊 الملف الصوتي الناتج", type="filepath")
|
| 194 |
|
| 195 |
gr.Markdown("""
|
| 196 |
+
### ℹ️ دليل التوقفات
|
| 197 |
+
اكتب الرمز `[pause:N]` داخل النص، حيث `N` هو عدد الثواني.
|
|
|
|
| 198 |
|
| 199 |
+
**أمثلة:**
|
| 200 |
+
- `[pause:1]` ⟵ صمت لمدة ثانية واحدة
|
| 201 |
+
- `[pause:0.5]` ⟵ صمت لمدة نصف ثانية
|
| 202 |
+
- `[pause:5]` ⟵ صمت طويل (5 ثواني)
|
| 203 |
|
| 204 |
+
**ملاحظة:**
|
| 205 |
+
يتم معالجة كل جزء صوتي بشكل منفصل ثم دمجهم بدقة.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
""")
|
| 207 |
+
|
| 208 |
+
# أمثلة جاهزة
|
| 209 |
gr.Examples(
|
| 210 |
examples=EXAMPLES,
|
| 211 |
inputs=[text_input, voice_selector, emotion_selector, use_advanced, manual_rate, manual_pitch],
|
| 212 |
label="🎯 جرب هذه الأمثلة"
|
| 213 |
)
|
| 214 |
+
|
| 215 |
+
# تشغيل الزر
|
| 216 |
generate_btn.click(
|
| 217 |
fn=text_to_speech_edge,
|
| 218 |
inputs=[text_input, voice_selector, emotion_selector, use_advanced, manual_rate, manual_pitch],
|