Hamed744 commited on
Commit
119e5ff
·
verified ·
1 Parent(s): 867ecd1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -56
app.py CHANGED
@@ -18,12 +18,12 @@ if GOOGLE_API_KEY:
18
  try:
19
  genai.configure(api_key=GOOGLE_API_KEY)
20
  model = genai.GenerativeModel('gemini-1.5-flash-latest')
21
- logging.info("سرویس Gemini با موفقیت پیکربندی شد.") # Changed from print
22
  except Exception as e:
23
- logging.error(f"خطا در پیکربندی Gemini: {e}. قابلیت ترجمه غیرفعال خواهد بود.") # Changed from print
24
  model = None
25
  else:
26
- logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.") # Changed from print
27
 
28
  # --- دیکشنری کامل‌تر صداهای انگلیسی ---
29
  language_dict_persian_keys = {
@@ -60,26 +60,25 @@ language_dict_persian_keys = {
60
  'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
61
  }
62
 
63
- async def translate_text_gemini(text, target_language="English"): # Renamed from previous
64
  if not model: return "خطا: سرویس ترجمه در دسترس نیست (کلید API یا مدل مشکل دارد).", None
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}:"
68
- response = await model.generate_content_async(prompt) # Used the right model object
69
  translated_text = response.text.strip()
70
  if translated_text.lower().startswith(f"{target_language.lower()}:"):
71
  translated_text = translated_text[len(target_language)+1:].strip()
72
  return "ترجمه موفق", translated_text
73
  except Exception as e:
74
  logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
75
- return f"خطای ترجمه: {type(e).__name__}", None # Corrected type(e).name to type(e).__name__
76
 
77
- async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch): # Renamed from previous
78
  try:
79
  if not text_to_speak or not text_to_speak.strip(): return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
80
  voice_id = language_dict_persian_keys.get(tts_voice_key)
81
  if voice_id is None: return f"خطای TTS: صدای '{tts_voice_key}' یافت نشد.", None
82
- # Removed: if not voice_id.startswith("en-"): return f"خطای پیکربندی TTS: صدای '{tts_voice_key}' انگلیسی نیست.", None
83
  rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
84
  communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
85
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file: tmp_path = tmp_file.name
@@ -88,20 +87,19 @@ async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume,
88
  except edge_tts.exceptions.NoAudioReceived: return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد. (NoAudioReceived)", None
89
  except Exception as e:
90
  logging.error(f"خطای Edge-TTS: {e}\n{traceback.format_exc()}")
91
- return f"خطای TTS: {type(e).__name__}", None # Corrected type(e).name to type(e).__name__
92
 
93
  _event_loops_by_thread = {}
94
- def _get_or_create_event_loop(): # Kept from your provided code
95
  thread_id = threading.get_ident()
96
  if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
97
  loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
98
  _event_loops_by_thread[thread_id] = loop
99
  return _event_loops_by_thread[thread_id]
100
 
101
- def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch): # Kept from your provided code
102
  loop = _get_or_create_event_loop()
103
  error_message_for_ui = ""
104
- audio_file_to_clean = None # For potential cleanup
105
 
106
  if not GOOGLE_API_KEY or not model:
107
  error_message_for_ui = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم صحیح GOOGLE_API_KEY در Secrets اطمینان حاصل کنید."
@@ -111,8 +109,7 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
111
  return error_message_for_ui, None
112
 
113
  translation_status_msg, translated_text = loop.run_until_complete(translate_text_gemini(persian_text, target_language="English"))
114
-
115
- translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود." # Default if translation fails badly
116
 
117
  if "خطا" in translation_status_msg or not translated_text:
118
  error_message_for_ui = f"{translated_text_output}\n({translation_status_msg})"
@@ -130,32 +127,29 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
130
 
131
  if "خطا" in tts_status_msg or not audio_path:
132
  error_message_for_ui = f"{translated_text_output}\n\n({tts_status_msg})"
133
- # No need to clean audio_path here as it's likely None or invalid
134
  return error_message_for_ui, None
135
 
136
- # audio_file_to_clean = audio_path # Gradio handles temp files for gr.Audio output usually
137
  return translated_text_output, audio_path
138
 
139
-
140
  # --- تعریف تم و CSS ---
141
  FLY_PRIMARY_COLOR_HEX = "#4F46E5"
142
  FLY_SECONDARY_COLOR_HEX = "#10B981"
143
- FLY_ACCENT_COLOR_HEX = "#D97706" # Kept from your "simple" code's button
144
  FLY_TEXT_COLOR_HEX = "#1F2937"
145
  FLY_SUBTLE_TEXT_HEX = "#6B7280"
146
  FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
147
  FLY_WHITE_HEX = "#FFFFFF"
148
  FLY_BORDER_COLOR_HEX = "#D1D5DB"
149
- FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6" # From "simple" code
150
- FLY_PANEL_BG_SIMPLE = "#E0F2FE" # From "simple" code
151
 
152
- app_theme_outer = gr.themes.Base( # For outer shell styling
153
  font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
154
  ).set(
155
  body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
156
  )
157
 
158
- # Combined and adjusted CSS
159
  custom_css = f"""
160
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
161
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
@@ -181,9 +175,10 @@ custom_css = f"""
181
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
182
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
183
  --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
 
 
184
  }}
185
 
186
- /* --- Outer Styling (from "beautiful" code) --- */
187
  body {{font-family: var(--font-global); direction: rtl; background-color: var(--fly-bg-light); color: var(--fly-text-primary); line-height: 1.7; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-size: 16px;}}
188
  .gradio-container {{max-width: 100% !important; width: 100% !important; min-height: 100vh; margin: 0 auto !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; background: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); display: flex; flex-direction: column;}}
189
 
@@ -195,16 +190,14 @@ body {{font-family: var(--font-global); direction: rtl; background-color: var(--
195
  .app-footer-fly {{text-align:center; font-size:0.85em; color: var(--fly-text-secondary); margin-top:2.5rem; padding: 1rem 0; background-color: rgba(255,255,255,0.3); backdrop-filter: blur(5px); border-top: 1px solid var(--fly-border-color);}}
196
  footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.right-2.gr-compact.gr-box.gr-text-gray-500, div[data-testid="flag"], button[title="Flag"], button[aria-label="Flag"], .footer-utils {{ display: none !important; visibility: hidden !important; }}
197
 
198
-
199
- /* --- Inner Content Area (from "beautiful" code, adapted) --- */
200
- .main-content-area {{ /* This class will be on the gr.Column wrapping the inner content */
201
  flex-grow: 1;
202
  padding: 0.75rem;
203
  width: 100%;
204
  margin: 0 auto;
205
  box-sizing: border-box;
206
  }}
207
- .content-panel-simple {{ /* This class will be on the gr.Group wrapping the Row */
208
  background-color: var(--fly-bg-white);
209
  padding: 1rem;
210
  border-radius: var(--radius-xl);
@@ -217,9 +210,8 @@ footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.r
217
  box-sizing: border-box;
218
  }}
219
 
220
- /* --- Inner Elements Styling (from "simple" code, with adjustments) --- */
221
- .content-panel-simple .gr-button.lg.primary, /* Target button inside the simple panel */
222
- .content-panel-simple button[variant="primary"] /* More generic for Gradio 4+ */
223
  {{
224
  background: var(--fly-accent) !important;
225
  margin-top: 1rem !important;
@@ -229,7 +221,7 @@ footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.r
229
  font-weight: 600 !important;
230
  border-radius: 10px !important;
231
  border: none !important;
232
- box-shadow: 0 3px 8px -1px rgba(var(--fly-accent-rgb), 0.3) !important; /* Use RGB for accent color */
233
  width: 100% !important;
234
  font-size: 1em !important;
235
  display: flex;
@@ -269,7 +261,7 @@ footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.r
269
  cursor: pointer;
270
  }}
271
 
272
- .content-panel-simple .gr-textbox[label*="ترجمه شده"] > label + div > textarea {{ /* Target output textbox */
273
  background-color: var(--fly-panel-bg-simple) !important;
274
  border-color: #A5D5FE !important;
275
  min-height: 100px;
@@ -295,12 +287,12 @@ footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.r
295
  color: var(--fly-text-primary) !important;
296
  border: 1px solid #D1D5DB !important;
297
  }}
298
- .content-panel-simple label > span.label-text {{ /* Targeting Gradio 4 label span */
299
  font-weight: 500 !important;
300
  color: #4B5563 !important;
301
  font-size: 0.88em !important;
302
  margin-bottom: 6px !important;
303
- display: inline-block; /* Keep labels inline */
304
  }}
305
  .content-panel-simple .gr-slider label span {{ font-size: 0.82em !important; color: var(--fly-text-secondary); }}
306
 
@@ -325,25 +317,23 @@ footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.r
325
  font-size: 0.85em !important;
326
  }}
327
 
328
-
329
- /* --- Responsive adjustments (from "beautiful" code, adapted) --- */
330
- @media (min-width: 640px) {
331
  .main-content-area {{
332
  padding: 1.5rem;
333
  max-width: 700px;
334
  }}
335
- .content-panel-simple {{ /* Apply to the simple panel as well */
336
  padding: 1.5rem;
337
  }}
338
  .app-title-card h1 {{ font-size: 2.5em !important; }}
339
  .app-title-card p {{ font-size: 1.05em !important; }}
340
- }
341
 
342
- @media (min-width: 768px) {
343
  .main-content-area {{ max-width: 780px; }}
344
- .content-panel-simple {{ padding: 2rem; }} /* Wider padding for desktop */
345
 
346
- .content-panel-simple .main-content-row {{ /* Class from your simple layout */
347
  display: flex !important;
348
  flex-direction: row !important;
349
  gap: 1.5rem !important;
@@ -356,7 +346,7 @@ footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.r
356
 
357
  .app-title-card h1 {{font-size: 2.75em !important;}}
358
  .app-title-card p {{font-size: 1.1em !important;}}
359
- }
360
  """
361
 
362
  default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
@@ -376,9 +366,7 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
376
  </div>
377
  """)
378
 
379
- # main-content-area برای کنترل عرض کلی در دسکتاپ و پدینگ موبایل
380
  with gr.Column(elem_classes=["main-content-area"]):
381
- # content-panel-simple برای اعمال استایل کارت مانند و همپوشانی با هدر
382
  with gr.Group(elem_classes=["content-panel-simple"]):
383
  if not GOOGLE_API_KEY or not model:
384
  missing_key_msg = "⚠️ هشدار: قابلیت ترجمه غیرفعال است. "
@@ -386,7 +374,6 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
386
  elif not model: missing_key_msg += "خطا در بارگذاری مدل Gemini. لطفاً لاگ‌ها یا صحت کلید API را بررسی کنید."
387
  gr.Markdown(f"<div class='api-warning-message'>{missing_key_msg}</div>")
388
 
389
- # main-content-row از کد "ساده" شما برای چیدمان دو ستونی داخلی
390
  with gr.Row(elem_classes=["main-content-row"]):
391
  with gr.Column(scale=3, min_width=300):
392
  input_text_persian = gr.Textbox(
@@ -406,12 +393,12 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
406
  interactive=bool(language_dict_persian_keys)
407
  )
408
  with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
409
- with gr.Row(): # Ensure sliders are in a row if desired
410
  rate_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="سرعت (%)", scale=1)
411
  volume_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="حجم (%)", scale=1)
412
- pitch_slider = gr.Slider(minimum=-50, maximum=50, step=1, value=0, label="گام (Hz)") # Removed scale=2 as it might conflict
413
 
414
- submit_button = gr.Button("🚀 ترجمه و تلفظ", variant="primary", elem_classes=["lg"]) # Added elem_classes for simple CSS button
415
 
416
  with gr.Column(scale=2, min_width=280):
417
  output_text_translated = gr.Textbox(
@@ -427,9 +414,8 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
427
  interactive=False
428
  )
429
 
430
- # Examples section inside content-panel-simple
431
  if language_dict_persian_keys and default_english_tts_voice and default_english_tts_voice != "لیست صداها خالی است":
432
- gr.HTML("<hr class='custom-hr'>") # Use the class from simple CSS
433
  num_voices = len(language_dict_persian_keys)
434
  voice_keys = list(language_dict_persian_keys.keys())
435
  voice1, voice2, voice3 = voice_keys[0], voice_keys[min(7, num_voices-1)], voice_keys[min(13, num_voices-1)]
@@ -445,18 +431,18 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
445
  outputs=[output_text_translated, output_audio],
446
  fn=translate_and_speak_sync_wrapper,
447
  cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
448
- label="💡 نمونه‌های کاربردی" # Label from simple code
449
  )
450
  else:
451
  gr.Markdown("<p style='text-align:center; color:var(--fly-text-secondary); margin-top:1rem;'>نمونه‌ای برای نمایش موجود نیست (لیست صداها خالی است یا صدای پیش‌فرض معتبر نیست).</p>")
452
 
453
- gr.Markdown("<p class='app-footer-fly'>Fly Language Learning © 2024</p>") # Using beautiful footer class
454
 
455
  if 'submit_button' in locals() and submit_button is not None:
456
  submit_button.click(
457
- fn=translate_and_speak_sync_wrapper, # Direct call, no complex UI update for status
458
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
459
- outputs=[output_text_translated, output_audio] # Only two outputs now
460
  )
461
  else:
462
  logging.error("Submit button was not initialized correctly or is None.")
@@ -469,5 +455,5 @@ if __name__ == "__main__":
469
  server_name="0.0.0.0",
470
  server_port=7860,
471
  debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true",
472
- show_error=True # Good for debugging
473
  )
 
18
  try:
19
  genai.configure(api_key=GOOGLE_API_KEY)
20
  model = genai.GenerativeModel('gemini-1.5-flash-latest')
21
+ logging.info("سرویس Gemini با موفقیت پیکربندی شد.")
22
  except Exception as e:
23
+ logging.error(f"خطا در پیکربندی Gemini: {e}. قابلیت ترجمه غیرفعال خواهد بود.")
24
  model = None
25
  else:
26
+ logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
27
 
28
  # --- دیکشنری کامل‌تر صداهای انگلیسی ---
29
  language_dict_persian_keys = {
 
60
  'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
61
  }
62
 
63
+ async def translate_text_gemini(text, target_language="English"):
64
  if not model: return "خطا: سرویس ترجمه در دسترس نیست (کلید API یا مدل مشکل دارد).", None
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}:"
68
+ response = await model.generate_content_async(prompt)
69
  translated_text = response.text.strip()
70
  if translated_text.lower().startswith(f"{target_language.lower()}:"):
71
  translated_text = translated_text[len(target_language)+1:].strip()
72
  return "ترجمه موفق", translated_text
73
  except Exception as e:
74
  logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
75
+ return f"خطای ترجمه: {type(e).__name__}", None
76
 
77
+ async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
78
  try:
79
  if not text_to_speak or not text_to_speak.strip(): return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
80
  voice_id = language_dict_persian_keys.get(tts_voice_key)
81
  if voice_id is None: return f"خطای TTS: صدای '{tts_voice_key}' یافت نشد.", None
 
82
  rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
83
  communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
84
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file: tmp_path = tmp_file.name
 
87
  except edge_tts.exceptions.NoAudioReceived: return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد. (NoAudioReceived)", None
88
  except Exception as e:
89
  logging.error(f"خطای Edge-TTS: {e}\n{traceback.format_exc()}")
90
+ return f"خطای TTS: {type(e).__name__}", None
91
 
92
  _event_loops_by_thread = {}
93
+ def _get_or_create_event_loop():
94
  thread_id = threading.get_ident()
95
  if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
96
  loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
97
  _event_loops_by_thread[thread_id] = loop
98
  return _event_loops_by_thread[thread_id]
99
 
100
+ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
101
  loop = _get_or_create_event_loop()
102
  error_message_for_ui = ""
 
103
 
104
  if not GOOGLE_API_KEY or not model:
105
  error_message_for_ui = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم صحیح GOOGLE_API_KEY در Secrets اطمینان حاصل کنید."
 
109
  return error_message_for_ui, None
110
 
111
  translation_status_msg, translated_text = loop.run_until_complete(translate_text_gemini(persian_text, target_language="English"))
112
+ translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود."
 
113
 
114
  if "خطا" in translation_status_msg or not translated_text:
115
  error_message_for_ui = f"{translated_text_output}\n({translation_status_msg})"
 
127
 
128
  if "خطا" in tts_status_msg or not audio_path:
129
  error_message_for_ui = f"{translated_text_output}\n\n({tts_status_msg})"
 
130
  return error_message_for_ui, None
131
 
 
132
  return translated_text_output, audio_path
133
 
 
134
  # --- تعریف تم و CSS ---
135
  FLY_PRIMARY_COLOR_HEX = "#4F46E5"
136
  FLY_SECONDARY_COLOR_HEX = "#10B981"
137
+ FLY_ACCENT_COLOR_HEX = "#D97706"
138
  FLY_TEXT_COLOR_HEX = "#1F2937"
139
  FLY_SUBTLE_TEXT_HEX = "#6B7280"
140
  FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
141
  FLY_WHITE_HEX = "#FFFFFF"
142
  FLY_BORDER_COLOR_HEX = "#D1D5DB"
143
+ FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6"
144
+ FLY_PANEL_BG_SIMPLE = "#E0F2FE"
145
 
146
+ app_theme_outer = gr.themes.Base(
147
  font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
148
  ).set(
149
  body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
150
  )
151
 
152
+ # CSS با حذف کامل کامنت های /* ... */
153
  custom_css = f"""
154
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
155
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
 
175
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
176
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
177
  --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
178
+ --fly-primary-rgb: 79, 70, 229;
179
+ --fly-accent-rgb: 217, 119, 6;
180
  }}
181
 
 
182
  body {{font-family: var(--font-global); direction: rtl; background-color: var(--fly-bg-light); color: var(--fly-text-primary); line-height: 1.7; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-size: 16px;}}
183
  .gradio-container {{max-width: 100% !important; width: 100% !important; min-height: 100vh; margin: 0 auto !important; padding: 0 !important; border-radius: 0 !important; box-shadow: none !important; background: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); display: flex; flex-direction: column;}}
184
 
 
190
  .app-footer-fly {{text-align:center; font-size:0.85em; color: var(--fly-text-secondary); margin-top:2.5rem; padding: 1rem 0; background-color: rgba(255,255,255,0.3); backdrop-filter: blur(5px); border-top: 1px solid var(--fly-border-color);}}
191
  footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.right-2.gr-compact.gr-box.gr-text-gray-500, div[data-testid="flag"], button[title="Flag"], button[aria-label="Flag"], .footer-utils {{ display: none !important; visibility: hidden !important; }}
192
 
193
+ .main-content-area {{
 
 
194
  flex-grow: 1;
195
  padding: 0.75rem;
196
  width: 100%;
197
  margin: 0 auto;
198
  box-sizing: border-box;
199
  }}
200
+ .content-panel-simple {{
201
  background-color: var(--fly-bg-white);
202
  padding: 1rem;
203
  border-radius: var(--radius-xl);
 
210
  box-sizing: border-box;
211
  }}
212
 
213
+ .content-panel-simple .gr-button.lg.primary,
214
+ .content-panel-simple button[variant="primary"]
 
215
  {{
216
  background: var(--fly-accent) !important;
217
  margin-top: 1rem !important;
 
221
  font-weight: 600 !important;
222
  border-radius: 10px !important;
223
  border: none !important;
224
+ box-shadow: 0 3px 8px -1px rgba(var(--fly-accent-rgb), 0.3) !important;
225
  width: 100% !important;
226
  font-size: 1em !important;
227
  display: flex;
 
261
  cursor: pointer;
262
  }}
263
 
264
+ .content-panel-simple .gr-textbox[label*="ترجمه شده"] > label + div > textarea {{
265
  background-color: var(--fly-panel-bg-simple) !important;
266
  border-color: #A5D5FE !important;
267
  min-height: 100px;
 
287
  color: var(--fly-text-primary) !important;
288
  border: 1px solid #D1D5DB !important;
289
  }}
290
+ .content-panel-simple label > span.label-text {{
291
  font-weight: 500 !important;
292
  color: #4B5563 !important;
293
  font-size: 0.88em !important;
294
  margin-bottom: 6px !important;
295
+ display: inline-block;
296
  }}
297
  .content-panel-simple .gr-slider label span {{ font-size: 0.82em !important; color: var(--fly-text-secondary); }}
298
 
 
317
  font-size: 0.85em !important;
318
  }}
319
 
320
+ @media (min-width: 640px) {{
 
 
321
  .main-content-area {{
322
  padding: 1.5rem;
323
  max-width: 700px;
324
  }}
325
+ .content-panel-simple {{
326
  padding: 1.5rem;
327
  }}
328
  .app-title-card h1 {{ font-size: 2.5em !important; }}
329
  .app-title-card p {{ font-size: 1.05em !important; }}
330
+ }}
331
 
332
+ @media (min-width: 768px) {{
333
  .main-content-area {{ max-width: 780px; }}
334
+ .content-panel-simple {{ padding: 2rem; }}
335
 
336
+ .content-panel-simple .main-content-row {{
337
  display: flex !important;
338
  flex-direction: row !important;
339
  gap: 1.5rem !important;
 
346
 
347
  .app-title-card h1 {{font-size: 2.75em !important;}}
348
  .app-title-card p {{font-size: 1.1em !important;}}
349
+ }}
350
  """
351
 
352
  default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
 
366
  </div>
367
  """)
368
 
 
369
  with gr.Column(elem_classes=["main-content-area"]):
 
370
  with gr.Group(elem_classes=["content-panel-simple"]):
371
  if not GOOGLE_API_KEY or not model:
372
  missing_key_msg = "⚠️ هشدار: قابلیت ترجمه غیرفعال است. "
 
374
  elif not model: missing_key_msg += "خطا در بارگذاری مدل Gemini. لطفاً لاگ‌ها یا صحت کلید API را بررسی کنید."
375
  gr.Markdown(f"<div class='api-warning-message'>{missing_key_msg}</div>")
376
 
 
377
  with gr.Row(elem_classes=["main-content-row"]):
378
  with gr.Column(scale=3, min_width=300):
379
  input_text_persian = gr.Textbox(
 
393
  interactive=bool(language_dict_persian_keys)
394
  )
395
  with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
396
+ with gr.Row():
397
  rate_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="سرعت (%)", scale=1)
398
  volume_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="حجم (%)", scale=1)
399
+ pitch_slider = gr.Slider(minimum=-50, maximum=50, step=1, value=0, label="گام (Hz)")
400
 
401
+ submit_button = gr.Button("🚀 ترجمه و تلفظ", variant="primary", elem_classes=["lg"])
402
 
403
  with gr.Column(scale=2, min_width=280):
404
  output_text_translated = gr.Textbox(
 
414
  interactive=False
415
  )
416
 
 
417
  if language_dict_persian_keys and default_english_tts_voice and default_english_tts_voice != "لیست صداها خالی است":
418
+ gr.HTML("<hr class='custom-hr'>")
419
  num_voices = len(language_dict_persian_keys)
420
  voice_keys = list(language_dict_persian_keys.keys())
421
  voice1, voice2, voice3 = voice_keys[0], voice_keys[min(7, num_voices-1)], voice_keys[min(13, num_voices-1)]
 
431
  outputs=[output_text_translated, output_audio],
432
  fn=translate_and_speak_sync_wrapper,
433
  cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
434
+ label="💡 نمونه‌های کاربردی"
435
  )
436
  else:
437
  gr.Markdown("<p style='text-align:center; color:var(--fly-text-secondary); margin-top:1rem;'>نمونه‌ای برای نمایش موجود نیست (لیست صداها خالی است یا صدای پیش‌فرض معتبر نیست).</p>")
438
 
439
+ gr.Markdown("<p class='app-footer-fly'>Fly Language Learning © 2024</p>")
440
 
441
  if 'submit_button' in locals() and submit_button is not None:
442
  submit_button.click(
443
+ fn=translate_and_speak_sync_wrapper,
444
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
445
+ outputs=[output_text_translated, output_audio]
446
  )
447
  else:
448
  logging.error("Submit button was not initialized correctly or is None.")
 
455
  server_name="0.0.0.0",
456
  server_port=7860,
457
  debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true",
458
+ show_error=True
459
  )