Hamed744 commited on
Commit
3ce4ffc
·
verified ·
1 Parent(s): 0a3d1bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -90
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', 'انگلیسی (آمریکا) - گای (مرد)': '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',
37
- 'انگلیسی (آمریکا) - تونی (مرد، جدید)': 'en-US-TonyNeural',
38
- 'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural', 'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
39
- 'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural', 'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
40
- 'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural', 'انگلیسی (بریتانیا) - میا (زن، جدید)': 'en-GB-MiaNeural',
41
- 'انگلیسی (بریتانیا) - نوآ (مرد، جدید)': 'en-GB-NoahNeural', 'انگلیسی (بریتانیا) - الیور (مرد، جدید)': 'en-GB-OliverNeural',
42
- 'انگلیسی (بریتانیا) - اولیویا (زن، جدید)': 'en-GB-OliviaNeural',
43
- 'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural', 'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
44
- 'انگلیسی (استرالیا) - آنت (زن، جدید)': 'en-AU-AnnetteNeural', 'انگلیسی (استرالیا) - کارلی (زن، جدید)': 'en-AU-CarlyNeural',
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',
55
- 'انگلیسی (فیلیپین) - روزا (زن)': 'en-PH-RosaNeural', 'انگلیسی (فیلیپین) - جیمز (مرد)': 'en-PH-JamesNeural',
56
- 'انگلیسی (سنگاپور) - لونا (زن)': 'en-SG-LunaNeural', 'انگلیسی (سنگاپور) - وین (مرد)': 'en-SG-WayneNeural',
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,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
- 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,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
- 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,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 = 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__}")
234
 
@@ -257,18 +295,18 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
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,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(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],
@@ -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