Hamed744 commited on
Commit
867ecd1
·
verified ·
1 Parent(s): 082c45f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +299 -248
app.py CHANGED
@@ -10,19 +10,22 @@ import logging
10
 
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
12
 
 
13
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
 
14
  model = None
15
  if GOOGLE_API_KEY:
16
  try:
17
  genai.configure(api_key=GOOGLE_API_KEY)
18
  model = genai.GenerativeModel('gemini-1.5-flash-latest')
19
- logging.info("سرویس Gemini با موفقیت پیکربندی شد.")
20
  except Exception as e:
21
- logging.error(f"خطا در پیکربندی Gemini: {e}", exc_info=True)
 
22
  else:
23
- logging.warning("کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است.")
24
 
25
- # --- دیکشنری کامل صداهای انگلیسی ---
26
  language_dict_persian_keys = {
27
  'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
28
  'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
@@ -57,125 +60,102 @@ language_dict_persian_keys = {
57
  'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
58
  }
59
 
60
- async def translate_text_gemini_async(text, target_language="English"):
61
- if not model: logging.error("Gemini model not loaded."); return "خطا: سرویس ترجمه در دسترس نیست.", None
62
- if not text or not text.strip(): logging.warning("Translate: Empty input text."); return "خطا: متن ورودی خالی است.", None
63
  try:
64
  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}:"
65
- response = await model.generate_content_async(prompt)
66
  translated_text = response.text.strip()
67
  if translated_text.lower().startswith(f"{target_language.lower()}:"):
68
  translated_text = translated_text[len(target_language)+1:].strip()
69
  return "ترجمه موفق", translated_text
70
- except Exception as e: logging.error(f"Gemini translation error: {e}", exc_info=True); return f"خطا در ترجمه: {type(e).__name__}", None
 
 
71
 
72
- async def text_to_speech_edge_tts_async(text_to_speak, tts_voice_key, rate, volume, pitch):
73
- if not text_to_speak or not text_to_speak.strip(): logging.warning("TTS: Empty input text."); return "خطا در TTS: متن ورودی خالی است.", None
74
- voice_id = language_dict_persian_keys.get(tts_voice_key)
75
- if voice_id is None: logging.error(f"TTS: Voice key '{tts_voice_key}' not found."); return f"خطا در TTS: صدای '{tts_voice_key}' یافت نشد.", None
76
  try:
 
 
 
 
77
  rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
78
  communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
79
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
80
- tmp_path = tmp_file.name
81
  await communicate.save(tmp_path)
82
  return "TTS موفق", tmp_path
83
- except Exception as e: logging.error(f"Edge-TTS error: {e}", exc_info=True); return f"خطا در TTS: {type(e).__name__}", None
84
-
85
- _thread_local = threading.local()
86
-
87
- def get_event_loop():
88
- if not hasattr(_thread_local, 'loop') or _thread_local.loop.is_closed():
89
- _thread_local.loop = asyncio.new_event_loop()
90
- logging.info(f"Created or reopened new event loop for thread {threading.get_ident()}")
91
-
92
- try:
93
- current_loop = asyncio.get_event_loop_policy().get_event_loop()
94
- if current_loop is not _thread_local.loop:
95
- asyncio.set_event_loop(_thread_local.loop)
96
- except RuntimeError:
97
- asyncio.set_event_loop(_thread_local.loop)
98
- logging.info(f"Set event loop for thread {threading.get_ident()} as no current loop was found or it was different.")
99
- return _thread_local.loop
100
-
101
-
102
- def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
103
- logging.info(f"Wrapper called. Text: '{persian_text[:30]}...' Voice: {english_tts_voice_key}")
104
- loop = get_event_loop()
105
- audio_file_to_clean = None
106
- status_update = gr.update(value=None, visible=False)
107
 
108
  if not GOOGLE_API_KEY or not model:
109
- msg = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم GOOGLE_API_KEY اطمینان حاصل کنید."
110
- logging.error(msg)
111
- status_update = gr.update(value=f"🚫 {msg}", visible=True)
112
- return msg, None, status_update
113
  if not persian_text or not persian_text.strip():
114
- msg = "لطفاً متن فارسی را برای ترجمه وارد کنید."
115
- logging.warning(msg)
116
- status_update = gr.update(value=f"⚠️ {msg}", visible=True)
117
- return msg, None, status_update
118
- if not english_tts_voice_key or english_tts_voice_key == "لیست خالی":
119
- msg = "لطفاً یک صدای گوینده انتخاب کنید."
120
- logging.warning(msg)
121
- status_update = gr.update(value=f"⚠️ {msg}", visible=True)
122
- return persian_text, None, status_update
123
-
124
- try:
125
- translation_status_msg, translated_text = loop.run_until_complete(
126
- translate_text_gemini_async(persian_text)
127
- )
128
- if "خطا" in translation_status_msg.lower() or not translated_text:
129
- err_msg = f"ترجمه ناموفق: {translation_status_msg} {translated_text or ''}"
130
- logging.error(err_msg)
131
- status_update = gr.update(value=f"🚫 {err_msg}", visible=True)
132
- return err_msg, None, status_update
133
-
134
- tts_status_msg, audio_path = loop.run_until_complete(
135
- text_to_speech_edge_tts_async(translated_text, english_tts_voice_key, rate, volume, pitch)
136
- )
137
-
138
- if "خطا" in tts_status_msg.lower() or not audio_path:
139
- err_msg = f"خطای TTS: {tts_status_msg}"
140
- logging.error(f"Translated: '{translated_text}', TTS Error: {err_msg}")
141
- status_update = gr.update(value=f"⚠️ {err_msg}", visible=True)
142
- return translated_text, None, status_update
143
-
144
- audio_file_to_clean = audio_path
145
- logging.info(f"عملیات موفق. متن ترجمه شده: '{translated_text[:30]}...', مسیر صوت: {audio_path}")
146
- status_update = gr.update(value="✅ ترجمه و تولید صدا موفقیت آمیز بود!", visible=True)
147
- return translated_text, audio_path, status_update
148
 
149
- except Exception as e:
150
- logging.error(f"خطای غیرمنتظره در wrapper: {e}", exc_info=True)
151
- if audio_file_to_clean and os.path.exists(audio_file_to_clean):
152
- try:
153
- os.remove(audio_file_to_clean)
154
- logging.info(f"Cleaned up temporary audio file: {audio_file_to_clean}")
155
- except OSError as oe:
156
- logging.error(f"Error cleaning up temp file {audio_file_to_clean}: {oe}")
157
- status_update = gr.update(value=f"🚫 خطای داخلی سرور: {type(e).__name__}", visible=True)
158
- return f"خطای داخلی سرور: {type(e).__name__}", None, status_update
159
-
160
- # --- تعریف تم و CSS بسیار زیباتر ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  FLY_PRIMARY_COLOR_HEX = "#4F46E5"
162
  FLY_SECONDARY_COLOR_HEX = "#10B981"
163
- FLY_ACCENT_COLOR_HEX = "#F59E0B"
164
- FLY_TEXT_COLOR_HEX = "#111827"
165
  FLY_SUBTLE_TEXT_HEX = "#6B7280"
166
  FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
167
  FLY_WHITE_HEX = "#FFFFFF"
168
  FLY_BORDER_COLOR_HEX = "#D1D5DB"
169
- FLY_INPUT_BG_HEX = "#FFFFFF"
170
- FLY_PANEL_BG_HEX = "#E0E7FF"
171
 
172
- app_theme = gr.themes.Base(
173
  font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
174
  ).set(
175
  body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
176
  )
177
 
178
- # CSS بدون کامنت‌های /* ... */
179
  custom_css = f"""
180
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
181
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
@@ -190,8 +170,8 @@ custom_css = f"""
190
  --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
191
  --fly-bg-white: {FLY_WHITE_HEX};
192
  --fly-border-color: {FLY_BORDER_COLOR_HEX};
193
- --fly-input-bg: {FLY_INPUT_BG_HEX};
194
- --fly-panel-bg: {FLY_PANEL_BG_HEX};
195
 
196
  --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
197
  --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
@@ -201,12 +181,9 @@ custom_css = f"""
201
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
202
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
203
  --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
204
- --transition-fast: all 0.15s ease-in-out;
205
- --transition-normal: all 0.25s ease-in-out;
206
- --fly-primary-rgb: 79, 70, 229;
207
- --fly-accent-rgb: 245, 158, 11;
208
  }}
209
 
 
210
  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;}}
211
  .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;}}
212
 
@@ -215,14 +192,19 @@ body {{font-family: var(--font-global); direction: rtl; background-color: var(--
215
  .app-title-card h1 {{font-size: 2.25em !important; font-weight: 800 !important; margin: 0 0 0.5rem 0; font-family: var(--font-english); letter-spacing: -0.5px; text-shadow: 0 2px 4px rgba(0,0,0,0.1);}}
216
  .app-title-card p {{font-size: 1em !important; margin-top: 0.25rem; font-weight: 400; color: rgba(255, 255, 255, 0.85) !important;}}
217
 
218
- .main-content-area-fly {{
 
 
 
 
 
219
  flex-grow: 1;
220
  padding: 0.75rem;
221
  width: 100%;
222
  margin: 0 auto;
223
  box-sizing: border-box;
224
  }}
225
- .content-panel {{
226
  background-color: var(--fly-bg-white);
227
  padding: 1rem;
228
  border-radius: var(--radius-xl);
@@ -235,72 +217,158 @@ body {{font-family: var(--font-global); direction: rtl; background-color: var(--
235
  box-sizing: border-box;
236
  }}
237
 
238
- .gr-input > label > span.label-text, .gr-dropdown > label > span.label-text, .gr-slider > label > span.label-text {{font-weight: 600 !important; color: var(--fly-text-primary) !important; font-size: 0.9em !important; margin-bottom: 0.5rem !important; display: block;}}
239
- .gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select {{border-radius: var(--radius-md) !important; border: 1px solid var(--fly-border-color) !important; font-size: 1em !important; background-color: var(--fly-input-bg) !important; padding: 0.75rem 1rem !important; line-height: 1.6; width: 100% !important; box-sizing: border-box !important; font-family: var(--font-global) !important; color: var(--fly-text-primary) !important; transition: var(--transition-normal); box-shadow: var(--shadow-sm);}}
240
- .gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-dropdown > label + div > div > select:focus {{border-color: var(--fly-primary) !important; box-shadow: 0 0 0 3px rgba(var(--fly-primary-rgb), 0.3) !important; outline: none; transform: translateY(-1px);}}
241
- .gr-dropdown svg.icon {{right: auto !important; left: 1rem !important; color: var(--fly-subtle-text) !important;}}
242
-
243
- .output-text-container .gr-textbox[label*="ترجمه انگلیسی"] > label + div > textarea, .output-text-container .gradio-interface .output_text .gr-textbox[data-testid="textbox"] > label + div > textarea {{background-color: var(--fly-panel-bg) !important; border: 1px solid var(--fly-primary) !important; border-radius: var(--radius-md) !important; min-height: 120px; font-family: var(--font-english) !important; font-size: 1.05em !important; color: var(--fly-primary) !important; font-weight: 500; padding: 0.75rem 1rem !important; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);}}
244
- .output-audio-container .gr-audio {{border-radius: var(--radius-md); box-shadow: var(--shadow-sm);}}
245
- .output-audio-container .gr-audio audio {{border-radius: var(--radius-md);}}
246
-
247
- .gr-accordion > button.gr-button {{font-weight: 600 !important; padding: 0.75rem 1rem !important; border-radius: var(--radius-md) !important; background-color: #EDF2F7 !important; color: var(--fly-text-primary) !important; border: 1px solid var(--fly-border-color) !important; font-size: 0.95em; margin-bottom: 0.5rem; transition: var(--transition-normal); text-align: right;}}
248
- .gr-accordion > button.gr-button:hover {{background-color: #E2E8F0 !important; border-color: var(--fly-primary) !important; color: var(--fly-primary) !important;}}
249
- .gr-accordion > div.gr-panel {{border-radius: var(--radius-md) !important; border: 1px solid var(--fly-border-color) !important; background-color: var(--fly-bg-white) !important; padding: 1.25rem !important; box-shadow: none; margin-top: -0.5rem; position: relative; z-index: 5;}}
250
- .gr-slider {{padding: 0.5rem 0 !important;}}
251
- .gr-slider input[type="range"]::-webkit-slider-thumb {{background: var(--fly-primary) !important; box-shadow: var(--shadow-sm) !important;}}
252
- .gr-slider input[type="range"]::-moz-range-thumb {{background: var(--fly-primary) !important; box-shadow: var(--shadow-sm) !important;}}
253
-
254
- .action-button-container .gr-button.gr-button-primary {{width: 100% !important; padding: 0.85rem 1.5rem !important; font-size: 1.05em !important; font-weight: 700 !important; border-radius: var(--radius-md) !important; margin-top: 1.5rem !important; letter-spacing: 0.5px; color: var(--fly-bg-white) !important; background: linear-gradient(135deg, var(--fly-accent) 0%, #F97316 100%) !important; border: none !important; box-shadow: var(--shadow-md), 0 0 15px rgba(var(--fly-accent-rgb), 0.3); transition: var(--transition-normal), transform 0.1s ease-out; cursor: pointer;}}
255
- .action-button-container .gr-button.gr-button-primary:hover {{transform: translateY(-3px) scale(1.02); box-shadow: var(--shadow-lg), 0 0 25px rgba(var(--fly-accent-rgb), 0.4);}}
256
- .action-button-container .gr-button.gr-button-primary:active {{transform: translateY(-1px) scale(0.98); box-shadow: var(--shadow-sm), 0 0 10px rgba(var(--fly-accent-rgb), 0.2);}}
257
-
258
- div#examples_section {{margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--fly-border-color);}}
259
- div#examples_section > .gr-panel {{background-color: transparent !important; border: none !important; padding: 0 !important; box-shadow: none !important;}}
260
- div#examples_section .gr-samples-header {{font-weight: 700; color: var(--fly-text-primary); font-size: 1.1em; margin-bottom: 1rem; text-align: right;}}
261
- div#examples_section .gr-sample-button, div#examples_section table tbody tr td button.gr-button-secondary {{background-color: var(--fly-input-bg) !important; color: var(--fly-primary) !important; border-radius: var(--radius-md) !important; font-size: 0.9em !important; padding: 0.5rem 1rem !important; border: 1px solid var(--fly-primary) !important; transition: var(--transition-normal); box-shadow: var(--shadow-sm); font-weight: 500;}}
262
- div#examples_section .gr-sample-button:hover, div#examples_section table tbody tr td button.gr-button-secondary:hover {{background-color: var(--fly-primary) !important; color: var(--fly-bg-white) !important; transform: translateY(-2px); box-shadow: var(--shadow-md);}}
263
-
264
- .status-message-fly {{padding: 0.75rem 1rem; margin-top: 1.5rem; border-radius: var(--radius-md); font-weight: 500; text-align: center; font-size: 0.95em; box-shadow: var(--shadow-sm);}}
265
- .status-message-fly.success {{background-color: #D1FAE5; color: #065F46; border: 1px solid #A7F3D0;}}
266
- .status-message-fly.error {{background-color: #FEE2E2; color: #991B1B; border: 1px solid #FECACA;}}
267
-
268
- .custom-hr-fly {{height: 1px; background-color: var(--fly-border-color); margin: 2rem 0; border: none;}}
269
- .api-warning-message-fly {{background-color: #FEF3C7 !important; color: #92400E !important; padding: 0.75rem 1rem !important; border-radius: var(--radius-md) !important; border: 1px solid #FDE68A !important; text-align: center !important; margin: 0 0.5rem 1.5rem 0.5rem !important; font-size: 0.9em !important; font-weight: 500; box-shadow: var(--shadow-sm);}}
270
- .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);}}
271
- footer, .gradio-footer {{display: none !important;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
- @media (min-width: 640px) {{
274
- .main-content-area-fly {{
 
275
  padding: 1.5rem;
276
- max-width: 900px;
277
  }}
278
- .content-panel {{
279
  padding: 1.5rem;
280
  }}
281
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
- @media (min-width: 768px) {{
284
- .main-content-columns-fly {{display: flex; flex-direction: row; gap: 2rem; align-items: flex-start;}}
285
- .main-content-columns-fly > .gr-column:nth-child(1) {{flex: 3;}}
286
- .main-content-columns-fly > .gr-column:nth-child(2) {{flex: 2; position: sticky; top: 1.5rem;}}
287
- .action-button-container .gr-button.gr-button-primary {{width: auto !important; min-width: 220px; align-self: flex-end;}}
288
  .app-title-card h1 {{font-size: 2.75em !important;}}
289
  .app-title-card p {{font-size: 1.1em !important;}}
290
- .content-panel {{ padding: 2rem; }}
291
- }}
292
  """
293
 
294
  default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
295
- if not language_dict_persian_keys :
296
- logging.warning("Language dictionary is empty!")
297
  default_english_tts_voice = None
298
  elif default_english_tts_voice not in language_dict_persian_keys:
299
- default_english_tts_voice = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
300
 
301
  logging.info(f"Gradio version: {gr.__version__}")
302
 
303
- with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فلای | Fly Language Learning") as demo:
304
  gr.HTML(f"""
305
  <div class="app-title-card">
306
  <h1>🚀 Fly Language Learning</h1>
@@ -308,115 +376,98 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
308
  </div>
309
  """)
310
 
311
- with gr.Column(elem_classes=["main-content-area-fly"]):
312
- if not GOOGLE_API_KEY or not model:
313
- missing_key_msg = "⚠️ **هشدار سرویس ترجمه:** "
314
- if not GOOGLE_API_KEY: missing_key_msg += "کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است."
315
- elif not model: missing_key_msg += "خطا در بارگذاری مدل Gemini."
316
- gr.Markdown(f"<div class='api-warning-message-fly'>{missing_key_msg} بخش ترجمه غیرفعال خواهد بود. لطفاً تنظیمات Secrets را بررسی کنید.</div>")
317
-
318
- with gr.Group(elem_classes=["content-panel"]):
319
- with gr.Row(elem_classes=["main-content-columns-fly"]):
320
- with gr.Column(scale=3, elem_id="input_column"):
 
 
 
321
  input_text_persian = gr.Textbox(
322
- lines=5, label="📝 متن فارسی خود را بنویسید:",
323
- placeholder="مثال: جهان شگفت‌انگیز زبان‌ها را کاوش کنید...",
324
- elem_id="persian_input_textbox"
325
- )
326
- language_dropdown_tts_english = gr.Dropdown(
327
- choices=list(language_dict_persian_keys.keys()) if language_dict_persian_keys else ["لیست خالی"],
328
- value=default_english_tts_voice if default_english_tts_voice else "لیست خالی",
329
- label="🗣️ گوینده و لهجه انگلیسی:",
330
- interactive=bool(language_dict_persian_keys and default_english_tts_voice is not None)
331
  )
332
- with gr.Accordion("🎧 تنظیمات پیشرفته صدا", open=False):
333
- rate_slider = gr.Slider(minimum=-100, maximum=100, value=0, step=10, label="سرعت گفتار")
334
- volume_slider = gr.Slider(minimum=-100, maximum=100, value=0, step=10, label="بلندی صدا")
335
- pitch_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=5, label="زیر و بمی صدا")
336
 
337
- with gr.Column(elem_classes=["action-button-container"]):
338
- submit_button = gr.Button(" ترجمه و پخش جادویی ✨", variant="primary")
339
-
340
- with gr.Column(scale=2, elem_id="output_column", elem_classes=["result-area-fly"]):
341
- with gr.Group(elem_classes=["output-text-container"]):
342
- output_text_translated = gr.Textbox(
343
- label="📜 ترجمه انگلیسی روان:", interactive=False, lines=6,
344
- placeholder="ترجمه در اینجا پدیدار خواهد شد...",
345
- elem_id="english_output_textbox"
346
- )
347
- with gr.Group(elem_classes=["output-audio-container"]):
348
- output_audio = gr.Audio(label="🎤 بشنوید:", type="filepath", format="mp3", interactive=False)
349
 
350
- status_message = gr.Markdown(visible=False, elem_classes=["status-message-fly"])
351
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- if language_dict_persian_keys and default_english_tts_voice:
354
- gr.HTML("<hr class='custom-hr-fly'>")
 
355
  num_voices = len(language_dict_persian_keys)
356
  voice_keys = list(language_dict_persian_keys.keys())
357
-
358
- valid_example_list = []
359
- if num_voices > 0:
360
- voice1_idx, voice2_idx, voice3_idx, voice4_idx = 0, min(7, num_voices - 1), min(13, num_voices - 1), min(3, num_voices - 1)
361
- example_list_data = [
362
- ["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
363
- ["می‌توانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx], 5, 0, 0],
364
- ["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", voice_keys[voice3_idx], -10, 10, 0],
365
- ["یک قهوه و یک کیک لطفا.", voice_keys[voice4_idx], 0, 0, 0],
366
  ]
367
- valid_example_list = example_list_data
368
- else: logging.warning("No voices available for examples.")
369
-
370
- if valid_example_list:
371
- gr.Examples(
372
- examples=valid_example_list,
373
- inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
374
- outputs=[output_text_translated, output_audio, status_message],
375
- fn=translate_and_speak_sync_wrapper,
376
- label="🌟 نمونه‌های الهام‌بخش (برای امتحان کلیک کنید):",
377
- elem_id="examples_section"
378
- )
379
-
380
- gr.Markdown("<p class='app-footer-fly'>ساخته شده با ❤️ و ☕ توسط تیم فلای | Fly Language Learning © ۲۰۲۴</p>")
381
-
382
- if 'submit_button' in locals() and submit_button is not None:
383
- def handle_submit_click_ui_update(persian_text, english_tts_voice_key, rate, volume, pitch):
384
- translated_text, audio_path, status_update_dict = translate_and_speak_sync_wrapper(
385
- persian_text, english_tts_voice_key, rate, volume, pitch
386
- )
387
-
388
- current_value = None
389
- is_visible = False
390
- status_class_list = ["status-message-fly"]
391
-
392
- if status_update_dict and isinstance(status_update_dict, dict):
393
- current_value = status_update_dict.get("value")
394
- is_visible = status_update_dict.get("visible", False)
395
 
396
- if current_value:
397
- if "موفقیت" in current_value or "✅" in current_value:
398
- status_class_list.append("success")
399
- elif "خطا" in current_value or "⚠️" in current_value or "🚫" in current_value:
400
- status_class_list.append("error")
401
-
402
- updated_status_message_ui = gr.update(value=current_value,
403
- visible=is_visible,
404
- elem_classes=status_class_list)
405
-
406
- return translated_text, audio_path, updated_status_message_ui
 
407
 
 
408
  submit_button.click(
409
- fn=handle_submit_click_ui_update,
410
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
411
- outputs=[output_text_translated, output_audio, status_message]
412
  )
413
  else:
414
  logging.error("Submit button was not initialized correctly or is None.")
415
 
416
  if __name__ == "__main__":
417
- if not language_dict_persian_keys:
418
- logging.critical("CRITICAL: Voice dictionary (language_dict_persian_keys) is empty!")
419
- elif not default_english_tts_voice:
420
- logging.warning("WARNING: No default English TTS voice could be set.")
421
-
422
- demo.launch(server_name="0.0.0.0", server_port=7860, debug=True, show_error=True)
 
 
 
 
10
 
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
12
 
13
+ # --- دریافت کلید API از متغیرهای محیطی ---
14
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
15
+
16
  model = None
17
  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 = {
30
  'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
31
  'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
 
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
 
86
  await communicate.save(tmp_path)
87
  return "TTS موفق", tmp_path
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 اطمینان حاصل کنید."
108
+ return error_message_for_ui, None
 
 
109
  if not persian_text or not persian_text.strip():
110
+ error_message_for_ui = "لطفاً متن فارسی را برای ترجمه وارد کنید."
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})"
119
+ return error_message_for_ui, None
120
+
121
+ if english_tts_voice_key not in language_dict_persian_keys:
122
+ if language_dict_persian_keys:
123
+ english_tts_voice_key = list(language_dict_persian_keys.keys())[0]
124
+ logging.warning(f"صدای انتخابی نامعتبر، به صدای پیشفرض {english_tts_voice_key} تغییر یافت.")
125
+ else:
126
+ error_message_for_ui = f"{translated_text_output}\n\n(خطای TTS: هیچ صدایی برای انتخاب موجود نیست.)"
127
+ return error_message_for_ui, None
128
+
129
+ tts_status_msg, audio_path = loop.run_until_complete(text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch))
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');
 
170
  --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
171
  --fly-bg-white: {FLY_WHITE_HEX};
172
  --fly-border-color: {FLY_BORDER_COLOR_HEX};
173
+ --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE};
174
+ --fly-panel-bg-simple: {FLY_PANEL_BG_SIMPLE};
175
 
176
  --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
177
  --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
 
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
 
 
192
  .app-title-card h1 {{font-size: 2.25em !important; font-weight: 800 !important; margin: 0 0 0.5rem 0; font-family: var(--font-english); letter-spacing: -0.5px; text-shadow: 0 2px 4px rgba(0,0,0,0.1);}}
193
  .app-title-card p {{font-size: 1em !important; margin-top: 0.25rem; font-weight: 400; color: rgba(255, 255, 255, 0.85) !important;}}
194
 
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
  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;
226
+ padding: 12px 20px !important;
227
+ transition: all 0.25s ease-in-out !important;
228
+ color: white !important;
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;
236
+ align-items: center;
237
+ justify-content: center;
238
+ }}
239
+ .content-panel-simple .gr-button.lg.primary:hover,
240
+ .content-panel-simple button[variant="primary"]:hover {{
241
+ background: #B45309 !important;
242
+ transform: translateY(-1px) !important;
243
+ box-shadow: 0 5px 10px -1px rgba(var(--fly-accent-rgb), 0.4) !important;
244
+ }}
245
+
246
+ .content-panel-simple .gr-input > label + div > textarea,
247
+ .content-panel-simple .gr-dropdown > label + div > div > input,
248
+ .content-panel-simple .gr-dropdown > label + div > div > select,
249
+ .content-panel-simple .gr-textbox > label + div > textarea {{
250
+ border-radius: 8px !important;
251
+ border: 1.5px solid var(--fly-border-color) !important;
252
+ font-size: 0.95em !important;
253
+ background-color: var(--fly-input-bg-simple) !important;
254
+ padding: 10px 12px !important;
255
+ color: var(--fly-text-primary) !important;
256
+ }}
257
+ .content-panel-simple .gr-input > label + div > textarea:focus,
258
+ .content-panel-simple .gr-dropdown > label + div > div > input:focus,
259
+ .content-panel-simple .gr-dropdown > label + div > div > select:focus,
260
+ .content-panel-simple .gr-textbox > label + div > textarea:focus {{
261
+ border-color: var(--fly-primary) !important;
262
+ box-shadow: 0 0 0 3px rgba(var(--fly-primary-rgb), 0.12) !important;
263
+ background-color: var(--fly-bg-white) !important;
264
+ }}
265
+
266
+ .content-panel-simple .gr-dropdown select {{
267
+ font-family: var(--font-global) !important;
268
+ width: 100%;
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;
276
+ font-family: var(--font-english);
277
+ font-size: 1em !important;
278
+ line-height: 1.5;
279
+ padding: 10px !important;
280
+ }}
281
+ .content-panel-simple .gr-panel,
282
+ .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > .gr-panel {{
283
+ border-radius: 8px !important;
284
+ border: 1px solid var(--fly-border-color) !important;
285
+ background-color: var(--fly-input-bg-simple) !important;
286
+ padding: 0.8rem 1rem !important;
287
+ margin-top: 0.6rem;
288
+ box-shadow: none;
289
+ }}
290
+ .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > button.gr-button {{
291
+ font-weight: 500 !important;
292
+ padding: 8px 10px !important;
293
+ border-radius: 6px !important;
294
+ background-color: #E5E7EB !important;
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
+
307
+ .content-panel-simple div[label*="نمونه"] {{ margin-top: 1.5rem; }}
308
+ .content-panel-simple div[label*="نمونه"] .gr-button.gr-button-tool,
309
+ .content-panel-simple div[label*="نمونه"] .gr-sample-button {{
310
+ background-color: #E0E7FF !important;
311
+ color: var(--fly-primary) !important;
312
+ border-radius: 6px !important;
313
+ font-size: 0.78em !important;
314
+ padding: 4px 8px !important;
315
+ }}
316
+ .content-panel-simple .custom-hr {{ height: 1px; background-color: var(--fly-border-color); margin: 1.5rem 0; border: none; }}
317
+ .api-warning-message {{
318
+ background-color: #FFFBEB !important;
319
+ color: #92400E !important;
320
+ padding: 10px 12px !important;
321
+ border-radius: 8px !important;
322
+ border: 1px solid #FDE68A !important;
323
+ text-align: center !important;
324
+ margin: 0 0.2rem 1rem 0.2rem !important;
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;
350
+ }}
351
+ .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{ flex-basis: 60%; }}
352
+ .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{ flex-basis: 40%; }}
353
+
354
+ .content-panel-simple .gr-button.lg.primary,
355
+ .content-panel-simple button[variant="primary"] {{ width: auto !important; align-self: flex-start; }}
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 = 'انگلیسی (آمریکا) - جنی (زن)'
363
+ if not language_dict_persian_keys:
364
+ logging.critical("خطای بحرانی: language_dict_persian_keys خالی است!")
365
  default_english_tts_voice = None
366
  elif default_english_tts_voice not in language_dict_persian_keys:
367
+ default_english_tts_voice = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else "لیست خالی"
368
 
369
  logging.info(f"Gradio version: {gr.__version__}")
370
 
371
+ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان فلای") as demo:
372
  gr.HTML(f"""
373
  <div class="app-title-card">
374
  <h1>🚀 Fly Language Learning</h1>
 
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 = "⚠️ هشدار: قابلیت ترجمه غیرفعال است. "
385
+ if not GOOGLE_API_KEY: missing_key_msg += "لطفاً کلید GOOGLE_API_KEY را در بخش Secrets این Space تنظیم کنید."
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(
393
+ lines=4,
394
+ label="📝 متن فارسی برای ترجمه",
395
+ placeholder="مثال: سلام، فردا هوا چطور است؟",
396
+ value="سلام، امروز می‌خواهیم در مورد رنگ‌ها صحبت کنیم."
 
 
 
 
 
397
  )
 
 
 
 
398
 
399
+ dropdown_label = "🗣️ انتخاب گوینده و لهجه انگلیسی"
400
+ if not language_dict_persian_keys: dropdown_label += " (لیست خالی!)"
 
 
 
 
 
 
 
 
 
 
401
 
402
+ language_dropdown_tts_english = gr.Dropdown(
403
+ choices=list(language_dict_persian_keys.keys()) if language_dict_persian_keys else ["لیست صداها خالی است"],
404
+ value=default_english_tts_voice if default_english_tts_voice else "لیست صداها خالی است",
405
+ label=dropdown_label,
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(
418
+ label="📜 متن ترجمه شده (انگلیسی)",
419
+ interactive=False,
420
+ lines=6,
421
+ placeholder="متن انگلیسی ترجمه شده یا پیام‌های خطا..."
422
+ )
423
+ output_audio = gr.Audio(
424
+ type="filepath",
425
+ label="🎧 فایل صوتی",
426
+ format="mp3",
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)]
436
+ example_list = [
437
+ ["قیمت این لباس چقدر است؟", voice1, 0, 0, 0],
438
+ ["می‌توانید آدرس را روی نقشه به من نشان دهید؟", voice2, 0, 0, 0],
439
+ ["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", voice3, -10, 0, 0],
 
 
 
 
440
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
+ gr.Examples(
443
+ examples=example_list,
444
+ inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
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.")
463
 
464
  if __name__ == "__main__":
465
+ if not language_dict_persian_keys:
466
+ logging.critical("خطای بحرانی: language_dict_persian_keys خالی است!")
467
+
468
+ demo.launch(
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
+ )