Update app.py
Browse files
app.py
CHANGED
|
@@ -23,48 +23,82 @@ if GOOGLE_API_KEY:
|
|
| 23 |
else:
|
| 24 |
logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
# این یک نسخه کاملتر از پاسخهای قبلی است، اما ممکن است با اسپیس شما متفاوت باشد.
|
| 28 |
language_dict_persian_keys = {
|
| 29 |
-
'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural',
|
| 30 |
-
'انگلیسی (آمریکا) -
|
| 31 |
-
'انگلیسی (آمریکا) -
|
| 32 |
-
'انگلیسی (آمریکا) -
|
| 33 |
-
'انگلیسی (آمریکا) -
|
| 34 |
-
'انگلیسی (آمریکا) -
|
| 35 |
-
'انگلیسی (آمریکا) -
|
| 36 |
-
'انگلیسی (آمریکا) -
|
| 37 |
-
'انگلیسی (آمریکا) -
|
| 38 |
-
'انگلیسی (
|
| 39 |
-
'انگلیسی (
|
| 40 |
-
'انگلیسی (
|
| 41 |
-
'انگلیسی (
|
| 42 |
-
'انگلیسی (
|
| 43 |
-
'انگلیسی (
|
| 44 |
-
'انگلیسی (
|
| 45 |
-
|
| 46 |
-
'انگلیسی (
|
| 47 |
-
'انگلیسی (
|
| 48 |
-
'انگلیسی (
|
| 49 |
-
'انگلیسی (
|
| 50 |
-
'انگلیسی (
|
| 51 |
-
'انگلیسی (
|
| 52 |
-
'انگلیسی (
|
| 53 |
-
'انگلیسی (
|
| 54 |
-
'انگلیسی (
|
| 55 |
-
|
| 56 |
-
'انگلیسی (
|
| 57 |
-
'انگلیسی (
|
| 58 |
-
'انگلیسی (
|
| 59 |
-
'انگلیسی (تا
|
| 60 |
-
#
|
| 61 |
-
'
|
| 62 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,6 +111,9 @@ async def translate_text_gemini(text, target_language="English"):
|
|
| 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,53 +124,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 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
|
| 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 |
-
|
| 114 |
-
|
| 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,13 +178,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 |
-
# اگر دیکشنری صداها خالی است یا کلید معتبر نیست، از اولین صدای موجود استفاده کن (اگر وجود داشته باشد)
|
| 145 |
if language_dict_persian_keys:
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
logging.warning(f"صدای '{
|
|
|
|
| 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,7 +195,6 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 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"
|
|
@@ -222,13 +258,15 @@ footer,.gradio-footer,.flagging-container,.flex.row.gap-2.absolute.bottom-2.righ
|
|
| 222 |
@media (min-width:768px) {{.main-content-area {{max-width:780px;}} .content-panel-simple {{padding:2rem;}} .content-panel-simple .main-content-row {{display:flex !important;flex-direction:row !important;gap:1.5rem !important;}} .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{flex-basis:60%;}} .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{flex-basis:40%;}} .content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{width:auto !important;align-self:flex-start;}} .app-title-card h1 {{font-size:2.75em !important;}} .app-title-card p {{font-size:1.1em !important;}} }}
|
| 223 |
"""
|
| 224 |
|
| 225 |
-
default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
|
|
|
|
| 226 |
if not language_dict_persian_keys:
|
| 227 |
-
logging.critical("خطای بحرانی: language_dict_persian_keys خالی است!")
|
| 228 |
-
default_english_tts_voice =
|
| 229 |
elif default_english_tts_voice not in language_dict_persian_keys:
|
| 230 |
-
|
| 231 |
-
|
|
|
|
| 232 |
|
| 233 |
logging.info(f"Gradio version: {gr.__version__}")
|
| 234 |
|
|
@@ -257,18 +295,18 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
|
|
| 257 |
)
|
| 258 |
|
| 259 |
dropdown_label = "🗣️ انتخاب گوینده و لهجه انگلیسی"
|
| 260 |
-
# اطمینان از اینکه
|
| 261 |
-
|
| 262 |
-
|
| 263 |
if not language_dict_persian_keys:
|
| 264 |
-
|
| 265 |
-
|
| 266 |
|
| 267 |
language_dropdown_tts_english = gr.Dropdown(
|
| 268 |
-
choices=
|
| 269 |
-
value=
|
| 270 |
label=dropdown_label,
|
| 271 |
-
interactive=bool(language_dict_persian_keys)
|
| 272 |
)
|
| 273 |
with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
|
| 274 |
with gr.Row():
|
|
@@ -297,10 +335,10 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
|
|
| 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(
|
| 303 |
-
voice3_idx = min(
|
| 304 |
|
| 305 |
example_list = [
|
| 306 |
["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
|
|
@@ -317,7 +355,7 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
|
|
| 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 |
|
|
|
|
| 23 |
else:
|
| 24 |
logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
|
| 25 |
|
| 26 |
+
# --- دیکشنری صداهای انگلیسی با حذف موارد مشخص شده ---
|
|
|
|
| 27 |
language_dict_persian_keys = {
|
| 28 |
+
'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural',
|
| 29 |
+
'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
|
| 30 |
+
'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural',
|
| 31 |
+
'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
|
| 32 |
+
'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural',
|
| 33 |
+
'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
|
| 34 |
+
'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural',
|
| 35 |
+
'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
|
| 36 |
+
# 'انگلیسی (آمریکا) - جیسون (مرد)': 'en-US-JasonNeural', # حذف شد
|
| 37 |
+
# 'انگلیسی (آمریکا) - براندون (مرد)': 'en-US-BrandonNeural', # حذف شد
|
| 38 |
+
# 'انگلیسی (آمریکا) - الیزابت (زن)': 'en-US-ElizabethNeural', # حذف شد
|
| 39 |
+
# 'انگلیسی (آمریکا) - دیویس (مرد، جدید)': 'en-US-DavisNeural', # حذف شد
|
| 40 |
+
# 'انگلیسی (آمریکا) - جین (زن، جدید)': 'en-US-JaneNeural', # حذف شد
|
| 41 |
+
# 'انگلیسی (آمریکا) - نانسی (زن، جدید)': 'en-US-NancyNeural', # حذف شد
|
| 42 |
+
# 'انگلیسی (آمریکا) - سارا (زن، جدید)': 'en-US-SaraNeural', # حذف شد
|
| 43 |
+
# 'انگلیسی (آمریکا) - تونی (مرد، جدید)': 'en-US-TonyNeural', # حذف شد (تکراری هم بود)
|
| 44 |
+
|
| 45 |
+
'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural',
|
| 46 |
+
'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
|
| 47 |
+
'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural',
|
| 48 |
+
'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
|
| 49 |
+
'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural',
|
| 50 |
+
'انگلیسی (بریتانیا) - میا (زن، جدید)': 'en-GB-MiaNeural',
|
| 51 |
+
# 'انگلیسی (بریتانیا) - نوآ (مرد، جدید)': 'en-GB-NoahNeural', # حذف شد
|
| 52 |
+
# 'انگلیسی (بریتانیا) - الیور (مرد، جدید)': 'en-GB-OliverNeural', # حذف شد
|
| 53 |
+
# 'انگلیسی (بریتانیا) - اولیویا (زن، جدید)': 'en-GB-OliviaNeural', # حذف شد
|
| 54 |
+
|
| 55 |
+
'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural',
|
| 56 |
+
'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
|
| 57 |
+
# 'انگلیسی (استرالیا) - آنت (زن، جدید)': 'en-AU-AnnetteNeural', # حذف شد
|
| 58 |
+
# 'انگلیسی (استرالیا) - کارلی (زن، جدید)': 'en-AU-CarlyNeural', # حذف شد
|
| 59 |
+
# 'انگلیسی (استرالیا) - کن (مرد، جدید)': 'en-AU-KenNeural', # حذف شد
|
| 60 |
+
# 'انگلیسی (استرالیا) - کیمبرلی (زن، جدید)': 'en-AU-KimNeural', # حذف شد
|
| 61 |
+
# 'انگلیسی (استرالیا) - تینا (زن، جدید)': 'en-AU-TinaNeural', # حذف شد
|
| 62 |
+
|
| 63 |
+
'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural',
|
| 64 |
+
'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
|
| 65 |
+
# 'انگلیسی (کانادا) - کوبی (مرد، جدید)': 'en-CA-CobiNeural', # حذف شد
|
| 66 |
+
# 'انگلیسی (کانادا) - ژان (مرد، جدید)': 'en-CA-JeanNeural', # حذف شد
|
| 67 |
+
|
| 68 |
+
'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural',
|
| 69 |
+
'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
|
| 70 |
+
|
| 71 |
+
'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural',
|
| 72 |
+
'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
|
| 73 |
+
# 'انگلیسی (هند) - میتالی (زن، جدید)': 'en-IN-MithaliNeural', # حذف شد
|
| 74 |
+
|
| 75 |
+
'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural',
|
| 76 |
+
'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
|
| 77 |
+
|
| 78 |
+
'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural',
|
| 79 |
+
'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
|
| 80 |
+
|
| 81 |
+
# 'انگلیسی (نیوزیلند) - هیزل (زن)': 'en-NZ-HazelNeural', # حذف شد
|
| 82 |
+
'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
|
| 83 |
+
|
| 84 |
+
'انگلیسی (فیلیپین) - روزا (زن)': 'en-PH-RosaNeural',
|
| 85 |
+
'انگلیسی (فیلیپین) - جیمز (مرد)': 'en-PH-JamesNeural',
|
| 86 |
+
|
| 87 |
+
'انگلیسی (سنگاپور) - لونا (زن)': 'en-SG-LunaNeural',
|
| 88 |
+
'انگلیسی (سنگاپور) - وین (مرد)': 'en-SG-WayneNeural',
|
| 89 |
+
|
| 90 |
+
'انگلیسی (کنیا) - آسیلیا (زن)': 'en-KE-AsiliaNeural',
|
| 91 |
+
'انگلیسی (کنیا) - چیلمبا (مرد)': 'en-KE-ChilembaNeural',
|
| 92 |
+
|
| 93 |
+
'انگلیسی (نیجریه) - ازینه (زن)': 'en-NG-EzinneNeural',
|
| 94 |
+
'انگلیسی (نیجریه) - آبئو (مرد)': 'en-NG-AbeoNeural',
|
| 95 |
+
|
| 96 |
+
'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural',
|
| 97 |
+
'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
|
| 98 |
}
|
| 99 |
|
|
|
|
| 100 |
async def translate_text_gemini(text, target_language="English"):
|
| 101 |
+
if not model: return "خطا: سرویس ترجمه در دسترس نیست (کلید API یا مدل مشکل دارد).", None
|
| 102 |
if not text or not text.strip(): return "خطا: متنی برای ترجمه وارد نشده است.", None
|
| 103 |
try:
|
| 104 |
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}:"
|
|
|
|
| 111 |
logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
|
| 112 |
return f"خطای ترجمه: {type(e).__name__}", None
|
| 113 |
|
| 114 |
+
MAX_TTS_RETRIES = 1
|
| 115 |
+
TTS_RETRY_DELAY = 0.5
|
| 116 |
+
|
| 117 |
async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
|
| 118 |
voice_id = language_dict_persian_keys.get(tts_voice_key)
|
| 119 |
if voice_id is None:
|
|
|
|
| 124 |
|
| 125 |
if not text_to_speak or not text_to_speak.strip():
|
| 126 |
return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
|
| 127 |
+
|
| 128 |
+
for attempt in range(MAX_TTS_RETRIES + 1):
|
| 129 |
+
try:
|
| 130 |
+
rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
|
| 131 |
+
communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
|
| 132 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 133 |
+
tmp_path = tmp_file.name
|
| 134 |
+
|
| 135 |
+
await communicate.save(tmp_path)
|
| 136 |
+
logging.info(f"صدای '{voice_id}' (تلاش {attempt + 1}) با موفقیت در '{tmp_path}' ذخیره شد.")
|
| 137 |
+
return "TTS موفق", tmp_path
|
| 138 |
|
| 139 |
+
except edge_tts.exceptions.NoAudioReceived as e_no_audio:
|
| 140 |
+
logging.warning(f"خطای NoAudioReceived برای VoiceID='{voice_id}' (تلاش {attempt + 1}/{MAX_TTS_RETRIES + 1}). متن: '{text_to_speak[:50]}...'. خطا: {e_no_audio}")
|
| 141 |
+
if attempt < MAX_TTS_RETRIES:
|
| 142 |
+
logging.info(f"تلاش مجدد پس از {TTS_RETRY_DELAY} ثانیه...")
|
| 143 |
+
await asyncio.sleep(TTS_RETRY_DELAY)
|
| 144 |
+
else:
|
| 145 |
+
logging.error(f"خطای NoAudioReceived پس از {MAX_TTS_RETRIES + 1} تلاش برای VoiceID='{voice_id}'.")
|
| 146 |
+
return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد (NoAudioReceived). این صدا ممکن است موقتا در دسترس نباشد. لطفاً با صدای دیگری یا کمی بعدتر امتحان کنید.", None
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logging.error(f"خطای Edge-TTS (تلاش {attempt + 1}) برای VoiceID='{voice_id}', VoiceKey='{tts_voice_key}': {e}\n{traceback.format_exc()}")
|
| 150 |
+
return f"خطای TTS ({type(e).__name__}): مشکلی در تولید صدا برای '{tts_voice_key}' پیش آمد.", None
|
| 151 |
|
| 152 |
+
return "خطای TTS: تلاشها برای تولید صدا ناموفق بود.", None
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
_event_loops_by_thread = {}
|
| 155 |
+
def _get_or_create_event_loop():
|
| 156 |
thread_id = threading.get_ident()
|
| 157 |
if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
|
| 158 |
+
loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
|
| 159 |
+
_event_loops_by_thread[thread_id] = loop
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
return _event_loops_by_thread[thread_id]
|
| 161 |
|
|
|
|
| 162 |
def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
|
| 163 |
loop = _get_or_create_event_loop()
|
| 164 |
error_message_for_ui = ""
|
| 165 |
|
| 166 |
if not GOOGLE_API_KEY or not model:
|
| 167 |
+
error_message_for_ui = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم صحیح GOOGLE_API_KEY در Secrets اطمینان حاصل کنید."
|
| 168 |
return error_message_for_ui, None
|
| 169 |
if not persian_text or not persian_text.strip():
|
| 170 |
error_message_for_ui = "لطفاً متن فارسی را برای ترجمه وارد کنید."
|
| 171 |
return error_message_for_ui, None
|
| 172 |
|
| 173 |
+
translation_status_msg, translated_text = loop.run_until_complete(translate_text_gemini(persian_text, target_language="English"))
|
| 174 |
translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود."
|
| 175 |
|
| 176 |
if "خطا" in translation_status_msg or not translated_text:
|
|
|
|
| 178 |
return error_message_for_ui, None
|
| 179 |
|
| 180 |
if english_tts_voice_key not in language_dict_persian_keys:
|
|
|
|
| 181 |
if language_dict_persian_keys:
|
| 182 |
+
# اگر صدای انتخاب شده در لیست جدید نیست، به اولین صدای موجود تغییر بده
|
| 183 |
+
default_fallback_voice = list(language_dict_persian_keys.keys())[0]
|
| 184 |
+
logging.warning(f"صدای انتخابی '{english_tts_voice_key}' نامعتبر یا حذف شده، به صدای پیشفرض '{default_fallback_voice}' تغییر یافت.")
|
| 185 |
+
english_tts_voice_key = default_fallback_voice
|
| 186 |
else:
|
| 187 |
+
error_message_for_ui = f"{translated_text_output}\n\n(خطای TTS: هیچ صدایی برای انتخاب موجود نیست.)"
|
| 188 |
return error_message_for_ui, None
|
| 189 |
|
| 190 |
tts_status_msg, audio_path = loop.run_until_complete(text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch))
|
|
|
|
| 195 |
|
| 196 |
return translated_text_output, audio_path
|
| 197 |
|
|
|
|
| 198 |
FLY_PRIMARY_COLOR_HEX = "#4F46E5"
|
| 199 |
FLY_SECONDARY_COLOR_HEX = "#10B981"
|
| 200 |
FLY_ACCENT_COLOR_HEX = "#D97706"
|
|
|
|
| 258 |
@media (min-width:768px) {{.main-content-area {{max-width:780px;}} .content-panel-simple {{padding:2rem;}} .content-panel-simple .main-content-row {{display:flex !important;flex-direction:row !important;gap:1.5rem !important;}} .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{flex-basis:60%;}} .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{flex-basis:40%;}} .content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{width:auto !important;align-self:flex-start;}} .app-title-card h1 {{font-size:2.75em !important;}} .app-title-card p {{font-size:1.1em !important;}} }}
|
| 259 |
"""
|
| 260 |
|
| 261 |
+
default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)' # مقدار پیشفرض اولیه
|
| 262 |
+
# اگر لیست خالی نیست و صدای پیشفرض در لیست جدید نیست، اولین صدای موجود را انتخاب کن
|
| 263 |
if not language_dict_persian_keys:
|
| 264 |
+
logging.critical("خطای بحرانی: language_dict_persian_keys خالی است! هیچ صدایی در دسترس نیست.")
|
| 265 |
+
default_english_tts_voice = "لیست صداها خالی است"
|
| 266 |
elif default_english_tts_voice not in language_dict_persian_keys:
|
| 267 |
+
default_english_tts_voice = list(language_dict_persian_keys.keys())[0]
|
| 268 |
+
logging.warning(f"صدای پیشفرض اولیه '{'انگلیسی (آمریکا) - جنی (زن)'}' در لیست جدید یافت نشد، به '{default_english_tts_voice}' تغییر یافت.")
|
| 269 |
+
|
| 270 |
|
| 271 |
logging.info(f"Gradio version: {gr.__version__}")
|
| 272 |
|
|
|
|
| 295 |
)
|
| 296 |
|
| 297 |
dropdown_label = "🗣️ انتخاب گوینده و لهجه انگلیسی"
|
| 298 |
+
# اطمینان از اینکه لیست خالی بودن به درستی بررسی میشود
|
| 299 |
+
current_voice_list = list(language_dict_persian_keys.keys()) if language_dict_persian_keys else ["لیست صداها خالی است"]
|
| 300 |
+
current_default_voice = default_english_tts_voice
|
| 301 |
if not language_dict_persian_keys:
|
| 302 |
+
dropdown_label += " (لیست خالی!)"
|
| 303 |
+
current_default_voice = "لیست صداها خالی است"
|
| 304 |
|
| 305 |
language_dropdown_tts_english = gr.Dropdown(
|
| 306 |
+
choices=current_voice_list,
|
| 307 |
+
value=current_default_voice,
|
| 308 |
label=dropdown_label,
|
| 309 |
+
interactive=bool(language_dict_persian_keys)
|
| 310 |
)
|
| 311 |
with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
|
| 312 |
with gr.Row():
|
|
|
|
| 335 |
gr.HTML("<hr class='custom-hr'>")
|
| 336 |
num_voices = len(language_dict_persian_keys)
|
| 337 |
voice_keys = list(language_dict_persian_keys.keys())
|
| 338 |
+
# اطمینان از اینکه اندیسها معتبر هستند حتی اگر لیست کوچک باشد
|
| 339 |
voice1_idx = 0
|
| 340 |
+
voice2_idx = min(1, num_voices - 1) if num_voices > 1 else 0
|
| 341 |
+
voice3_idx = min(2, num_voices - 1) if num_voices > 2 else 0
|
| 342 |
|
| 343 |
example_list = [
|
| 344 |
["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
|
|
|
|
| 355 |
label="💡 نمونههای کاربردی"
|
| 356 |
)
|
| 357 |
else:
|
| 358 |
+
gr.Markdown("<p style='text-align:center; color:var(--fly-text-secondary); margin-top:1rem;'>نمونهای برای نمایش موجود نیست (لیست صداها خالی است یا صدای پیشفرض معتبر نیست).</p>")
|
| 359 |
|
| 360 |
gr.Markdown("<p class='app-footer-fly'>Fly Language Learning © 2025</p>")
|
| 361 |
|