Update app.py
Browse files
app.py
CHANGED
|
@@ -23,13 +23,14 @@ if GOOGLE_API_KEY:
|
|
| 23 |
else:
|
| 24 |
logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
|
| 25 |
|
|
|
|
|
|
|
| 26 |
language_dict_persian_keys = {
|
| 27 |
'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
|
| 28 |
'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
|
| 29 |
'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural', 'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
|
| 30 |
'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural', 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
|
| 31 |
-
|
| 32 |
-
'انگلیسی (آمریکا) - جیسون (مرد)': 'en-US-JasonNeural',
|
| 33 |
'انگلیسی (آمریکا) - براندون (مرد)': 'en-US-BrandonNeural', 'انگلیسی (آمریکا) - الیزابت (زن)': 'en-US-ElizabethNeural',
|
| 34 |
'انگلیسی (آمریکا) - دیویس (مرد، جدید)': 'en-US-DavisNeural', 'انگلیسی (آمریکا) - جین (زن، جدید)': 'en-US-JaneNeural',
|
| 35 |
'انگلیسی (آمریکا) - نانسی (زن، جدید)': 'en-US-NancyNeural', 'انگلیسی (آمریکا) - سارا (زن، جدید)': 'en-US-SaraNeural',
|
|
@@ -44,11 +45,10 @@ language_dict_persian_keys = {
|
|
| 44 |
'انگلیسی (استرالیا) - کن (مرد، جدید)': 'en-AU-KenNeural', 'انگلیسی (استرالیا) - کیمبرلی (زن، جدید)': 'en-AU-KimNeural',
|
| 45 |
'انگلیسی (استرالیا) - تینا (زن، جدید)': 'en-AU-TinaNeural',
|
| 46 |
'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural', 'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
|
| 47 |
-
|
| 48 |
-
'انگلیسی (کانادا) - ژان (مرد، جدید)': 'en-CA-JeanNeural',
|
| 49 |
'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural', 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
|
| 50 |
'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural', 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
|
| 51 |
-
|
| 52 |
'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural', 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
|
| 53 |
'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural', 'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
|
| 54 |
'انگلیسی (نیوزیلند) - هیزل (زن)': 'en-NZ-HazelNeural', 'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
|
|
@@ -57,11 +57,14 @@ language_dict_persian_keys = {
|
|
| 57 |
'انگلیسی (کنیا) - آسیلیا (زن)': 'en-KE-AsiliaNeural', 'انگلیسی (کنیا) - چیلمبا (مرد)': 'en-KE-ChilembaNeural',
|
| 58 |
'انگلیسی (نیجریه) - ازینه (زن)': 'en-NG-EzinneNeural', 'انگلیسی (نیجریه) - آبئو (مرد)': 'en-NG-AbeoNeural',
|
| 59 |
'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
|
| 63 |
async def translate_text_gemini(text, target_language="English"):
|
| 64 |
-
if not model: return "خطا: سرویس ترجمه در دسترس نیست
|
| 65 |
if not text or not text.strip(): return "خطا: متنی برای ترجمه وارد نشده است.", None
|
| 66 |
try:
|
| 67 |
prompt = f"Translate the following Persian text to {target_language}. Provide only the translated English text, naturally and fluently, without any extra phrases, explanations, or markdown formatting. Be concise and accurate.\n\nPersian: \"{text}\"\n{target_language}:"
|
|
@@ -74,9 +77,6 @@ async def translate_text_gemini(text, target_language="English"):
|
|
| 74 |
logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
|
| 75 |
return f"خطای ترجمه: {type(e).__name__}", None
|
| 76 |
|
| 77 |
-
MAX_TTS_RETRIES = 1 # کاهش تعداد تلاش مجدد برای جلوگیری از کندی زیاد
|
| 78 |
-
TTS_RETRY_DELAY = 0.5 # کاهش تاخیر
|
| 79 |
-
|
| 80 |
async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
|
| 81 |
voice_id = language_dict_persian_keys.get(tts_voice_key)
|
| 82 |
if voice_id is None:
|
|
@@ -87,53 +87,53 @@ async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume,
|
|
| 87 |
|
| 88 |
if not text_to_speak or not text_to_speak.strip():
|
| 89 |
return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 96 |
-
tmp_path = tmp_file.name
|
| 97 |
-
|
| 98 |
-
await communicate.save(tmp_path)
|
| 99 |
-
logging.info(f"صدای '{voice_id}' (تلاش {attempt + 1}) با موفقیت در '{tmp_path}' ذخیره شد.")
|
| 100 |
-
return "TTS موفق", tmp_path
|
| 101 |
-
|
| 102 |
-
except edge_tts.exceptions.NoAudioReceived as e_no_audio:
|
| 103 |
-
logging.warning(f"خطای NoAudioReceived برای VoiceID='{voice_id}' (تلاش {attempt + 1}/{MAX_TTS_RETRIES + 1}). متن: '{text_to_speak[:50]}...'. خطا: {e_no_audio}")
|
| 104 |
-
if attempt < MAX_TTS_RETRIES:
|
| 105 |
-
logging.info(f"تلاش مجدد پس از {TTS_RETRY_DELAY} ثانیه...")
|
| 106 |
-
await asyncio.sleep(TTS_RETRY_DELAY)
|
| 107 |
-
else:
|
| 108 |
-
logging.error(f"خطای NoAudioReceived پس از {MAX_TTS_RETRIES + 1} تلاش برای VoiceID='{voice_id}'.")
|
| 109 |
-
return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد (NoAudioReceived). این صدا ممکن است موقتا در دسترس نباشد. لطفاً با صدای دیگری یا کمی بعدتر امتحان کنید.", None
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
_event_loops_by_thread = {}
|
| 118 |
-
def _get_or_create_event_loop():
|
| 119 |
thread_id = threading.get_ident()
|
| 120 |
if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
return _event_loops_by_thread[thread_id]
|
| 124 |
|
|
|
|
| 125 |
def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
|
| 126 |
loop = _get_or_create_event_loop()
|
| 127 |
error_message_for_ui = ""
|
| 128 |
|
| 129 |
if not GOOGLE_API_KEY or not model:
|
| 130 |
-
error_message_for_ui = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً
|
| 131 |
return error_message_for_ui, None
|
| 132 |
if not persian_text or not persian_text.strip():
|
| 133 |
error_message_for_ui = "لطفاً متن فارسی را برای ترجمه وارد کنید."
|
| 134 |
return error_message_for_ui, None
|
| 135 |
|
| 136 |
-
translation_status_msg, translated_text = loop.run_until_complete(translate_text_gemini(persian_text
|
| 137 |
translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود."
|
| 138 |
|
| 139 |
if "خطا" in translation_status_msg or not translated_text:
|
|
@@ -141,11 +141,13 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 141 |
return error_message_for_ui, None
|
| 142 |
|
| 143 |
if english_tts_voice_key not in language_dict_persian_keys:
|
|
|
|
| 144 |
if language_dict_persian_keys:
|
|
|
|
| 145 |
english_tts_voice_key = list(language_dict_persian_keys.keys())[0]
|
| 146 |
-
logging.warning(f"صدای
|
| 147 |
else:
|
| 148 |
-
error_message_for_ui = f"{translated_text_output}\n\n(خطای TTS:
|
| 149 |
return error_message_for_ui, None
|
| 150 |
|
| 151 |
tts_status_msg, audio_path = loop.run_until_complete(text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch))
|
|
@@ -156,6 +158,7 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 156 |
|
| 157 |
return translated_text_output, audio_path
|
| 158 |
|
|
|
|
| 159 |
FLY_PRIMARY_COLOR_HEX = "#4F46E5"
|
| 160 |
FLY_SECONDARY_COLOR_HEX = "#10B981"
|
| 161 |
FLY_ACCENT_COLOR_HEX = "#D97706"
|
|
@@ -224,6 +227,7 @@ if not language_dict_persian_keys:
|
|
| 224 |
logging.critical("خطای بحرانی: language_dict_persian_keys خالی است!")
|
| 225 |
default_english_tts_voice = None
|
| 226 |
elif default_english_tts_voice not in language_dict_persian_keys:
|
|
|
|
| 227 |
default_english_tts_voice = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else "لیست خالی"
|
| 228 |
|
| 229 |
logging.info(f"Gradio version: {gr.__version__}")
|
|
@@ -253,13 +257,18 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
|
|
| 253 |
)
|
| 254 |
|
| 255 |
dropdown_label = "🗣️ انتخاب گوینده و لهجه انگلیسی"
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
language_dropdown_tts_english = gr.Dropdown(
|
| 259 |
-
choices=
|
| 260 |
-
value=
|
| 261 |
label=dropdown_label,
|
| 262 |
-
interactive=bool(language_dict_persian_keys)
|
| 263 |
)
|
| 264 |
with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
|
| 265 |
with gr.Row():
|
|
@@ -288,11 +297,15 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
|
|
| 288 |
gr.HTML("<hr class='custom-hr'>")
|
| 289 |
num_voices = len(language_dict_persian_keys)
|
| 290 |
voice_keys = list(language_dict_persian_keys.keys())
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
example_list = [
|
| 293 |
-
["قیمت این لباس چقدر است؟",
|
| 294 |
-
["میتوانید آدرس را روی نقشه به من نشان دهید؟",
|
| 295 |
-
["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟",
|
| 296 |
]
|
| 297 |
|
| 298 |
gr.Examples(
|
|
@@ -304,7 +317,7 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
|
|
| 304 |
label="💡 نمونههای کاربردی"
|
| 305 |
)
|
| 306 |
else:
|
| 307 |
-
gr.Markdown("<p style='text-align:center; color:var(--fly-text-secondary); margin-top:1rem;'>نمونهای برای نمایش موجود نیست
|
| 308 |
|
| 309 |
gr.Markdown("<p class='app-footer-fly'>Fly Language Learning © 2025</p>")
|
| 310 |
|
|
|
|
| 23 |
else:
|
| 24 |
logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
|
| 25 |
|
| 26 |
+
# !!! مهم: این دیکشنری را با دیکشنری دقیقی که در اسپیس قبلی شما کار میکند جایگزین کنید !!!
|
| 27 |
+
# این یک نسخه کاملتر از پاسخهای قبلی است، اما ممکن است با اسپیس شما متفاوت باشد.
|
| 28 |
language_dict_persian_keys = {
|
| 29 |
'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
|
| 30 |
'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
|
| 31 |
'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural', 'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
|
| 32 |
'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural', 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
|
| 33 |
+
'انگلیسی (آمریکا) - استفانی (زن)': 'en-US-SteffanNeural', 'انگلیسی (آمریکا) - جیسون (مرد)': 'en-US-JasonNeural',
|
|
|
|
| 34 |
'انگلیسی (آمریکا) - براندون (مرد)': 'en-US-BrandonNeural', 'انگلیسی (آمریکا) - الیزابت (زن)': 'en-US-ElizabethNeural',
|
| 35 |
'انگلیسی (آمریکا) - دیویس (مرد، جدید)': 'en-US-DavisNeural', 'انگلیسی (آمریکا) - جین (زن، جدید)': 'en-US-JaneNeural',
|
| 36 |
'انگلیسی (آمریکا) - نانسی (زن، جدید)': 'en-US-NancyNeural', 'انگلیسی (آمریکا) - سارا (زن، جدید)': 'en-US-SaraNeural',
|
|
|
|
| 45 |
'انگلیسی (استرالیا) - کن (مرد، جدید)': 'en-AU-KenNeural', 'انگلیسی (استرالیا) - کیمبرلی (زن، جدید)': 'en-AU-KimNeural',
|
| 46 |
'انگلیسی (استرالیا) - تینا (زن، جدید)': 'en-AU-TinaNeural',
|
| 47 |
'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural', 'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
|
| 48 |
+
'انگلیسی (کانادا) - کوبی (مرد، جدید)': 'en-CA-CobiNeural', 'انگلیسی (کانادا) - ژان (مرد، جدید)': 'en-CA-JeanNeural',
|
|
|
|
| 49 |
'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural', 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
|
| 50 |
'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural', 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
|
| 51 |
+
'انگلیسی (هند) - میتالی (زن، جدید)': 'en-IN-MithaliNeural',
|
| 52 |
'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural', 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
|
| 53 |
'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural', 'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
|
| 54 |
'انگلیسی (نیوزیلند) - هیزل (زن)': 'en-NZ-HazelNeural', 'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
|
|
|
|
| 57 |
'انگلیسی (کنیا) - آسیلیا (زن)': 'en-KE-AsiliaNeural', 'انگلیسی (کنیا) - چیلمبا (مرد)': 'en-KE-ChilembaNeural',
|
| 58 |
'انگلیسی (نیجریه) - ازینه (زن)': 'en-NG-EzinneNeural', 'انگلیسی (نیجریه) - آبئو (مرد)': 'en-NG-AbeoNeural',
|
| 59 |
'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
|
| 60 |
+
# ممکن است صداهای فارسی یا زبانهای دیگر هم در لیست شما باشند
|
| 61 |
+
'فارسی (ایران) - دلآرا (زن)': 'fa-IR-DilaraNeural',
|
| 62 |
+
'فارسی (ایران) - فرید (مرد)': 'fa-IR-FaridNeural',
|
| 63 |
}
|
| 64 |
|
| 65 |
|
| 66 |
async def translate_text_gemini(text, target_language="English"):
|
| 67 |
+
if not model: return "خطا: سرویس ترجمه در دسترس نیست.", None
|
| 68 |
if not text or not text.strip(): return "خطا: متنی برای ترجمه وارد نشده است.", None
|
| 69 |
try:
|
| 70 |
prompt = f"Translate the following Persian text to {target_language}. Provide only the translated English text, naturally and fluently, without any extra phrases, explanations, or markdown formatting. Be concise and accurate.\n\nPersian: \"{text}\"\n{target_language}:"
|
|
|
|
| 77 |
logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
|
| 78 |
return f"خطای ترجمه: {type(e).__name__}", None
|
| 79 |
|
|
|
|
|
|
|
|
|
|
| 80 |
async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
|
| 81 |
voice_id = language_dict_persian_keys.get(tts_voice_key)
|
| 82 |
if voice_id is None:
|
|
|
|
| 87 |
|
| 88 |
if not text_to_speak or not text_to_speak.strip():
|
| 89 |
return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
|
| 90 |
+
try:
|
| 91 |
+
rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
|
| 92 |
+
communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
|
| 93 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 94 |
+
tmp_path = tmp_file.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
await communicate.save(tmp_path)
|
| 97 |
+
logging.info(f"صدای '{voice_id}' با موفقیت در '{tmp_path}' ذخیره شد.")
|
| 98 |
+
return "TTS موفق", tmp_path
|
| 99 |
+
|
| 100 |
+
except edge_tts.exceptions.NoAudioReceived as e_no_audio:
|
| 101 |
+
logging.error(f"خطای NoAudioReceived برای VoiceID='{voice_id}', VoiceKey='{tts_voice_key}'. متن: '{text_to_speak[:50]}...'. خطا: {e_no_audio}")
|
| 102 |
+
return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد (NoAudioReceived). ممکن است این صدا موقتا در دسترس نباشد یا مشکلی در سرویس TTS وجود داشته باشد.", None
|
| 103 |
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logging.error(f"خطای Edge-TTS برای VoiceID='{voice_id}', VoiceKey='{tts_voice_key}': {e}\n{traceback.format_exc()}")
|
| 106 |
+
return f"خطای TTS ({type(e).__name__}): مشکلی در تولید صدا برای '{tts_voice_key}' پیش آمد.", None
|
| 107 |
+
|
| 108 |
|
| 109 |
_event_loops_by_thread = {}
|
| 110 |
+
def _get_or_create_event_loop(): # تابع از کد نمونه شما
|
| 111 |
thread_id = threading.get_ident()
|
| 112 |
if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
|
| 113 |
+
new_loop = asyncio.new_event_loop()
|
| 114 |
+
asyncio.set_event_loop(new_loop)
|
| 115 |
+
_event_loops_by_thread[thread_id] = new_loop
|
| 116 |
+
logging.info(f"Created new event loop {new_loop} for thread {thread_id}")
|
| 117 |
+
else:
|
| 118 |
+
# Ensure the existing loop is set as the current loop for this thread
|
| 119 |
+
# This handles cases where Gradio might switch contexts
|
| 120 |
+
asyncio.set_event_loop(_event_loops_by_thread[thread_id])
|
| 121 |
+
logging.debug(f"Reusing event loop {_event_loops_by_thread[thread_id]} for thread {thread_id}")
|
| 122 |
return _event_loops_by_thread[thread_id]
|
| 123 |
|
| 124 |
+
|
| 125 |
def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
|
| 126 |
loop = _get_or_create_event_loop()
|
| 127 |
error_message_for_ui = ""
|
| 128 |
|
| 129 |
if not GOOGLE_API_KEY or not model:
|
| 130 |
+
error_message_for_ui = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً GOOGLE_API_KEY را بررسی کنید."
|
| 131 |
return error_message_for_ui, None
|
| 132 |
if not persian_text or not persian_text.strip():
|
| 133 |
error_message_for_ui = "لطفاً متن فارسی را برای ترجمه وارد کنید."
|
| 134 |
return error_message_for_ui, None
|
| 135 |
|
| 136 |
+
translation_status_msg, translated_text = loop.run_until_complete(translate_text_gemini(persian_text))
|
| 137 |
translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود."
|
| 138 |
|
| 139 |
if "خطا" in translation_status_msg or not translated_text:
|
|
|
|
| 141 |
return error_message_for_ui, None
|
| 142 |
|
| 143 |
if english_tts_voice_key not in language_dict_persian_keys:
|
| 144 |
+
# اگر دیکشنری صداها خالی است یا کلید معتبر نیست، از اولین صدای موجود استفاده کن (اگر وجود داشته باشد)
|
| 145 |
if language_dict_persian_keys:
|
| 146 |
+
original_choice = english_tts_voice_key
|
| 147 |
english_tts_voice_key = list(language_dict_persian_keys.keys())[0]
|
| 148 |
+
logging.warning(f"صدای '{original_choice}' نامعتبر بود یا یافت نشد. به صدای پیشفرض '{english_tts_voice_key}' تغییر یافت.")
|
| 149 |
else:
|
| 150 |
+
error_message_for_ui = f"{translated_text_output}\n\n(خطای TTS: لیست صداها خالی است.)"
|
| 151 |
return error_message_for_ui, None
|
| 152 |
|
| 153 |
tts_status_msg, audio_path = loop.run_until_complete(text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch))
|
|
|
|
| 158 |
|
| 159 |
return translated_text_output, audio_path
|
| 160 |
|
| 161 |
+
|
| 162 |
FLY_PRIMARY_COLOR_HEX = "#4F46E5"
|
| 163 |
FLY_SECONDARY_COLOR_HEX = "#10B981"
|
| 164 |
FLY_ACCENT_COLOR_HEX = "#D97706"
|
|
|
|
| 227 |
logging.critical("خطای بحرانی: language_dict_persian_keys خالی است!")
|
| 228 |
default_english_tts_voice = None
|
| 229 |
elif default_english_tts_voice not in language_dict_persian_keys:
|
| 230 |
+
# اگر صدای پیشفرض انگلیسی در لیست نبود، اولین مورد لیست کلی را انتخاب کن (میتواند فارسی باشد)
|
| 231 |
default_english_tts_voice = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else "لیست خالی"
|
| 232 |
|
| 233 |
logging.info(f"Gradio version: {gr.__version__}")
|
|
|
|
| 257 |
)
|
| 258 |
|
| 259 |
dropdown_label = "🗣️ انتخاب گوینده و لهجه انگلیسی"
|
| 260 |
+
# اطمینان از اینکه گزینه "لیست خالی" فقط زمانی نمایش داده می شود که دیکشنری واقعا خالی است
|
| 261 |
+
dropdown_choices = list(language_dict_persian_keys.keys()) if language_dict_persian_keys else ["لیست صداها خالی است"]
|
| 262 |
+
dropdown_value = default_english_tts_voice
|
| 263 |
+
if not language_dict_persian_keys:
|
| 264 |
+
dropdown_label += " (لیست خالی!)"
|
| 265 |
+
dropdown_value = "لیست صداها خالی است"
|
| 266 |
|
| 267 |
language_dropdown_tts_english = gr.Dropdown(
|
| 268 |
+
choices=dropdown_choices,
|
| 269 |
+
value=dropdown_value,
|
| 270 |
label=dropdown_label,
|
| 271 |
+
interactive=bool(language_dict_persian_keys) # interactive فقط اگر صدا وجود دارد
|
| 272 |
)
|
| 273 |
with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
|
| 274 |
with gr.Row():
|
|
|
|
| 297 |
gr.HTML("<hr class='custom-hr'>")
|
| 298 |
num_voices = len(language_dict_persian_keys)
|
| 299 |
voice_keys = list(language_dict_persian_keys.keys())
|
| 300 |
+
# انتخاب صداها برای نمونهها با دقت بیشتر برای جلوگیری از خطا
|
| 301 |
+
voice1_idx = 0
|
| 302 |
+
voice2_idx = min(7, num_voices - 1) if num_voices > 1 else 0
|
| 303 |
+
voice3_idx = min(13, num_voices - 1) if num_voices > 1 else 0
|
| 304 |
+
|
| 305 |
example_list = [
|
| 306 |
+
["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
|
| 307 |
+
["میتوانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx], 0, 0, 0],
|
| 308 |
+
["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", voice_keys[voice3_idx], -10, 0, 0],
|
| 309 |
]
|
| 310 |
|
| 311 |
gr.Examples(
|
|
|
|
| 317 |
label="💡 نمونههای کاربردی"
|
| 318 |
)
|
| 319 |
else:
|
| 320 |
+
gr.Markdown("<p style='text-align:center; color:var(--fly-text-secondary); margin-top:1rem;'>نمونهای برای نمایش موجود نیست.</p>")
|
| 321 |
|
| 322 |
gr.Markdown("<p class='app-footer-fly'>Fly Language Learning © 2025</p>")
|
| 323 |
|