Hamed744 commited on
Commit
ee3c00f
·
verified ·
1 Parent(s): 1794e08

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -381
app.py CHANGED
@@ -9,7 +9,6 @@ import google.generativeai as genai
9
  import logging
10
 
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
12
- #logging.info(f"Gradio version: {gr.__version__}") # Moved to after import to ensure gr is defined
13
 
14
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
15
  model = None
@@ -103,18 +102,26 @@ def get_event_loop():
103
  def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
104
  logging.info(f"Wrapper called. Text: '{persian_text[:30]}...' Voice: {english_tts_voice_key}")
105
  loop = get_event_loop()
106
-
107
  audio_file_to_clean = None
108
 
 
 
 
109
  if not GOOGLE_API_KEY or not model:
110
  msg = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم GOOGLE_API_KEY اطمینان حاصل کنید."
111
- logging.error(msg); return msg, None, None # Return three values for outputs
 
 
112
  if not persian_text or not persian_text.strip():
113
  msg = "لطفاً متن فارسی را برای ترجمه وارد کنید."
114
- logging.warning(msg); return msg, None, None # Return three values for outputs
 
 
115
  if not english_tts_voice_key or english_tts_voice_key == "لیست خالی":
116
  msg = "لطفاً یک صدای گوینده انتخاب کنید."
117
- logging.warning(msg); return persian_text, None, None # Return three values for outputs
 
 
118
 
119
  try:
120
  translation_status_msg, translated_text = loop.run_until_complete(
@@ -122,22 +129,24 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
122
  )
123
  if "خطا" in translation_status_msg.lower() or not translated_text:
124
  err_msg = f"ترجمه ناموفق: {translation_status_msg} {translated_text or ''}"
125
- logging.error(err_msg); return err_msg, None, gr.update(visible=False) # Return three values
 
 
126
 
127
- # Show the translated text immediately
128
- # gr.update for status_message will be handled in the main thread / by Gradio's event handling
129
-
130
  tts_status_msg, audio_path = loop.run_until_complete(
131
  text_to_speech_edge_tts_async(translated_text, english_tts_voice_key, rate, volume, pitch)
132
  )
133
 
134
  if "خطا" in tts_status_msg.lower() or not audio_path:
135
- err_msg = f"{translated_text}\n(خطای TTS: {tts_status_msg})"
136
- logging.error(err_msg); return err_msg, None, gr.update(value=f"⚠️ خطای TTS: {tts_status_msg}", visible=True) # Return three values
 
 
137
 
138
  audio_file_to_clean = audio_path
139
  logging.info(f"عملیات موفق. متن ترجمه شده: '{translated_text[:30]}...', مسیر صوت: {audio_path}")
140
- return translated_text, audio_path, gr.update(value="✅ ترجمه و تولید صدا موفقیت آمیز بود!", visible=True) # Return three values
 
141
 
142
  except Exception as e:
143
  logging.error(f"خطای غیرمنتظره در wrapper: {e}", exc_info=True)
@@ -147,7 +156,8 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
147
  logging.info(f"Cleaned up temporary audio file: {audio_file_to_clean}")
148
  except OSError as oe:
149
  logging.error(f"Error cleaning up temp file {audio_file_to_clean}: {oe}")
150
- return f"خطای داخلی سرور: {type(e).__name__}", None, gr.update(value=f"🚫 خطای داخلی: {type(e).__name__}", visible=True) # Return three values
 
151
 
152
  # --- تعریف تم و CSS بسیار زیباتر ---
153
  FLY_PRIMARY_COLOR_HEX = "#4F46E5" # Indigo
@@ -159,15 +169,12 @@ FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB" # Gray 50
159
  FLY_WHITE_HEX = "#FFFFFF"
160
  FLY_BORDER_COLOR_HEX = "#D1D5DB" # Gray 300
161
  FLY_INPUT_BG_HEX = "#FFFFFF"
162
- FLY_PANEL_BG_HEX = "#E0E7FF" # Indigo 100 (برای پنل ترجمه)
163
 
164
- # برای استفاده از فونت وزیر در تم Gradio اگر به صورت مستقیم در GoogleFont کار نکند،
165
- # باید از یک فونت جایگزین استاندارد استفاده کرد و سپس با CSS آن را override کرد.
166
- app_theme = gr.themes.Base( # Using Base theme for more CSS control
167
- font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], # Primary font for Gradio's own elements
168
  ).set(
169
  body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
170
- # Button colors will be handled by custom CSS for more advanced styling (gradients, etc.)
171
  )
172
 
173
  custom_css = f"""
@@ -190,357 +197,71 @@ custom_css = f"""
190
  --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
191
  --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
192
 
193
- --radius-sm: 0.375rem; /* 6px */
194
- --radius-md: 0.5rem; /* 8px */
195
- --radius-lg: 0.75rem; /* 12px */
196
- --radius-xl: 1rem; /* 16px */
197
- --radius-full: 9999px;
198
-
199
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
200
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
201
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
202
  --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
203
-
204
  --transition-fast: all 0.15s ease-in-out;
205
  --transition-normal: all 0.25s ease-in-out;
 
 
206
  }}
207
 
208
- body {{
209
- font-family: var(--font-global);
210
- direction: rtl;
211
- background-color: var(--fly-bg-light);
212
- color: var(--fly-text-primary);
213
- line-height: 1.7;
214
- -webkit-font-smoothing: antialiased;
215
- -moz-osx-font-smoothing: grayscale;
216
- font-size: 16px; /* Base font size */
217
- }}
218
-
219
- .gradio-container {{
220
- max-width: 100% !important;
221
- width: 100% !important;
222
- min-height: 100vh;
223
- margin: 0 auto !important;
224
- padding: 0 !important;
225
- border-radius: 0 !important;
226
- box-shadow: none !important;
227
- background: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); /* Light gradient background for the whole page */
228
- display: flex;
229
- flex-direction: column;
230
- }}
231
-
232
- .app-title-card {{
233
- text-align: center;
234
- padding: 2.5rem 1rem;
235
- margin: 0;
236
- background: linear-gradient(135deg, var(--fly-primary) 0%, var(--fly-secondary) 100%);
237
- color: var(--fly-bg-white);
238
- border-bottom-left-radius: var(--radius-xl);
239
- border-bottom-right-radius: var(--radius-xl);
240
- box-shadow: var(--shadow-lg);
241
- position: relative;
242
- overflow: hidden;
243
- }}
244
- .app-title-card::before {{ /* Decorative element */
245
- content: '';
246
- position: absolute;
247
- top: -50px; right: -50px;
248
- width: 150px; height: 150px;
249
- background: rgba(255,255,255,0.1);
250
- border-radius: var(--radius-full);
251
- opacity: 0.5;
252
- transform: rotate(45deg);
253
- }}
254
- .app-title-card h1 {{
255
- font-size: 2.25em !important; /* Larger title */
256
- font-weight: 800 !important;
257
- margin: 0 0 0.5rem 0;
258
- font-family: var(--font-english);
259
- letter-spacing: -0.5px;
260
- text-shadow: 0 2px 4px rgba(0,0,0,0.1);
261
- }}
262
- .app-title-card p {{
263
- font-size: 1em !important;
264
- margin-top: 0.25rem;
265
- font-weight: 400;
266
- color: rgba(255, 255, 255, 0.85) !important;
267
- }}
268
-
269
- .main-content-area-fly {{
270
- flex-grow: 1;
271
- padding: 1.5rem; /* Add padding around the main content */
272
- max-width: 900px; /* Max width for content on larger screens */
273
- width: 100%;
274
- margin: 0 auto; /* Center the content area */
275
- }}
276
-
277
- .content-panel {{ /* Replaces content-wrapper, more generic name */
278
- background-color: var(--fly-bg-white);
279
- padding: 1.5rem;
280
- border-radius: var(--radius-xl);
281
- box-shadow: var(--shadow-xl);
282
- margin-top: -2rem; /* Overlap with header for a modern effect */
283
- position: relative; /* For stacking context */
284
- z-index: 10;
285
- margin-bottom: 2rem;
286
- }}
287
-
288
- /* --- Styling inputs --- */
289
- .gr-input > label > span.label-text,
290
- .gr-dropdown > label > span.label-text,
291
- .gr-slider > label > span.label-text {{
292
- font-weight: 600 !important;
293
- color: var(--fly-text-primary) !important;
294
- font-size: 0.9em !important;
295
- margin-bottom: 0.5rem !important;
296
- display: block; /* Make label take full width */
297
- }}
298
-
299
- .gr-input > label + div > textarea,
300
- .gr-dropdown > label + div > div > input,
301
- .gr-dropdown > label + div > div > select {{
302
- border-radius: var(--radius-md) !important;
303
- border: 1px solid var(--fly-border-color) !important;
304
- font-size: 1em !important;
305
- background-color: var(--fly-input-bg) !important;
306
- padding: 0.75rem 1rem !important;
307
- line-height: 1.6;
308
- width: 100% !important;
309
- box-sizing: border-box !important;
310
- font-family: var(--font-global) !important;
311
- color: var(--fly-text-primary) !important;
312
- transition: var(--transition-normal);
313
- box-shadow: var(--shadow-sm);
314
- }}
315
- .gr-input > label + div > textarea:focus,
316
- .gr-dropdown > label + div > div > input:focus,
317
- .gr-dropdown > label + div > div > select:focus {{
318
- border-color: var(--fly-primary) !important;
319
- box-shadow: 0 0 0 3px rgba(var(--fly-primary-rgb, 79, 70, 229), 0.3) !important; /* Assuming primary color has RGB equivalent */
320
- outline: none;
321
- transform: translateY(-1px); /* Subtle lift effect */
322
- }}
323
- /* Add this if you define --fly-primary-rgb for the focus shadow */
324
- :root {{ --fly-primary-rgb: 79, 70, 229; }}
325
-
326
-
327
- .gr-dropdown svg.icon {{
328
- right: auto !important;
329
- left: 1rem !important;
330
- color: var(--fly-subtle-text) !important;
331
- }}
332
-
333
- /* Output Textbox styling */
334
- .output-text-container .gr-textbox[label*="ترجمه انگلیسی"] > label + div > textarea,
335
- .output-text-container .gradio-interface .output_text .gr-textbox[data-testid="textbox"] > label + div > textarea {{
336
- background-color: var(--fly-panel-bg) !important;
337
- border: 1px solid var(--fly-primary) !important; /* Accent border */
338
- border-radius: var(--radius-md) !important;
339
- min-height: 120px;
340
- font-family: var(--font-english) !important;
341
- font-size: 1.05em !important;
342
- color: var(--fly-primary) !important;
343
- font-weight: 500;
344
- padding: 0.75rem 1rem !important;
345
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
346
- }}
347
- .output-audio-container .gr-audio {{ /* Style the audio player */
348
- border-radius: var(--radius-md);
349
- box-shadow: var(--shadow-sm);
350
- }}
351
- .output-audio-container .gr-audio audio {{
352
- border-radius: var(--radius-md);
353
- }}
354
-
355
 
356
- /* Accordion styling */
357
- .gr-accordion > button.gr-button {{
358
- font-weight: 600 !important;
359
- padding: 0.75rem 1rem !important;
360
- border-radius: var(--radius-md) !important;
361
- background-color: #EDF2F7 !important; /* Lighter gray */
362
- color: var(--fly-text-primary) !important;
363
- border: 1px solid var(--fly-border-color) !important;
364
- font-size: 0.95em;
365
- margin-bottom: 0.5rem;
366
- transition: var(--transition-normal);
367
- text-align: right; /* Ensure text is aligned right */
368
- }}
369
- .gr-accordion > button.gr-button:hover {{
370
- background-color: #E2E8F0 !important;
371
- border-color: var(--fly-primary) !important;
372
- color: var(--fly-primary) !important;
373
- }}
374
- .gr-accordion > div.gr-panel {{
375
- border-radius: var(--radius-md) !important;
376
- border: 1px solid var(--fly-border-color) !important;
377
- background-color: var(--fly-bg-white) !important;
378
- padding: 1.25rem !important;
379
- box-shadow: none;
380
- margin-top: -0.5rem; /* Pull up slightly under accordion button */
381
- position: relative;
382
- z-index: 5;
383
- }}
384
- .gr-slider {{
385
- padding: 0.5rem 0 !important;
386
- }}
387
- .gr-slider input[type="range"]::-webkit-slider-thumb {{
388
- background: var(--fly-primary) !important;
389
- box-shadow: var(--shadow-sm) !important;
390
- }}
391
- .gr-slider input[type="range"]::-moz-range-thumb {{
392
- background: var(--fly-primary) !important;
393
- box-shadow: var(--shadow-sm) !important;
394
- }}
395
 
 
 
396
 
397
- /* Submit Button - More engaging */
398
- .action-button-container .gr-button.gr-button-primary {{
399
- width: 100% !important;
400
- padding: 0.85rem 1.5rem !important;
401
- font-size: 1.05em !important;
402
- font-weight: 700 !important;
403
- border-radius: var(--radius-md) !important;
404
- margin-top: 1.5rem !important;
405
- letter-spacing: 0.5px;
406
- color: var(--fly-bg-white) !important;
407
- background: linear-gradient(135deg, var(--fly-accent) 0%, #F97316 100%) !important; /* Amber to Orange gradient */
408
- border: none !important;
409
- box-shadow: var(--shadow-md), 0 0 15px rgba(var(--fly-accent-rgb, 245, 158, 11), 0.3); /* Accent glow */
410
- transition: var(--transition-normal), transform 0.1s ease-out;
411
- cursor: pointer;
412
- }}
413
- /* Add this if you define --fly-accent-rgb for the button glow */
414
- :root {{ --fly-accent-rgb: 245, 158, 11; }}
415
 
416
- .action-button-container .gr-button.gr-button-primary:hover {{
417
- transform: translateY(-3px) scale(1.02);
418
- box-shadow: var(--shadow-lg), 0 0 25px rgba(var(--fly-accent-rgb, 245, 158, 11), 0.4);
419
- }}
420
- .action-button-container .gr-button.gr-button-primary:active {{
421
- transform: translateY(-1px) scale(0.98);
422
- box-shadow: var(--shadow-sm), 0 0 10px rgba(var(--fly-accent-rgb, 245, 158, 11), 0.2);
423
- }}
424
 
 
 
 
 
 
 
425
 
426
- /* Examples Section */
427
- div#examples_section {{ /* Use specific ID */
428
- margin-top: 2rem;
429
- padding-top: 1.5rem;
430
- border-top: 1px solid var(--fly-border-color);
431
- }}
432
- div#examples_section > .gr-panel {{
433
- background-color: transparent !important;
434
- border: none !important;
435
- padding: 0 !important;
436
- box-shadow: none !important;
437
- }}
438
- div#examples_section .gr-samples-header {{ /* Target Gradio 3.x/4.x header */
439
- font-weight: 700;
440
- color: var(--fly-text-primary);
441
- font-size: 1.1em;
442
- margin-bottom: 1rem;
443
- text-align: right;
444
- }}
445
- div#examples_section .gr-sample-button, /* Gradio 4.x */
446
- div#examples_section table tbody tr td button.gr-button-secondary /* Gradio 3.x (more specific) */ {{
447
- background-color: var(--fly-input-bg) !important;
448
- color: var(--fly-primary) !important;
449
- border-radius: var(--radius-md) !important;
450
- font-size: 0.9em !important;
451
- padding: 0.5rem 1rem !important;
452
- border: 1px solid var(--fly-primary) !important;
453
- transition: var(--transition-normal);
454
- box-shadow: var(--shadow-sm);
455
- font-weight: 500;
456
- }}
457
- div#examples_section .gr-sample-button:hover,
458
- div#examples_section table tbody tr td button.gr-button-secondary:hover {{
459
- background-color: var(--fly-primary) !important;
460
- color: var(--fly-bg-white) !important;
461
- transform: translateY(-2px);
462
- box-shadow: var(--shadow-md);
463
- }}
464
 
465
- /* Status Message */
466
- .status-message-fly {{
467
- padding: 0.75rem 1rem;
468
- margin-top: 1.5rem;
469
- border-radius: var(--radius-md);
470
- font-weight: 500;
471
- text-align: center;
472
- font-size: 0.95em;
473
- box-shadow: var(--shadow-sm);
474
- }}
475
- .status-message-fly.success {{
476
- background-color: #D1FAE5; /* Emerald 100 */
477
- color: #065F46; /* Emerald 700 */
478
- border: 1px solid #A7F3D0; /* Emerald 200 */
479
- }}
480
- .status-message-fly.error {{
481
- background-color: #FEE2E2; /* Red 100 */
482
- color: #991B1B; /* Red 700 */
483
- border: 1px solid #FECACA; /* Red 200 */
484
- }}
485
 
 
 
 
486
 
487
- .custom-hr-fly {{
488
- height: 1px;
489
- background-color: var(--fly-border-color);
490
- margin: 2rem 0;
491
- border: none;
492
- }}
493
- .api-warning-message-fly {{
494
- background-color: #FEF3C7 !important; /* Amber 100 */
495
- color: #92400E !important; /* Amber 700 */
496
- padding: 0.75rem 1rem !important;
497
- border-radius: var(--radius-md) !important;
498
- border: 1px solid #FDE68A !important; /* Amber 300 */
499
- text-align: center !important;
500
- margin: 0 0.5rem 1.5rem 0.5rem !important;
501
- font-size: 0.9em !important;
502
- font-weight: 500;
503
- box-shadow: var(--shadow-sm);
504
- }}
505
- .app-footer-fly {{
506
- text-align:center;
507
- font-size:0.85em;
508
- color: var(--fly-text-secondary);
509
- margin-top:2.5rem;
510
- padding: 1rem 0;
511
- background-color: rgba(255,255,255,0.3); /* Subtle background for footer */
512
- backdrop-filter: blur(5px); /* Glassmorphism hint */
513
- border-top: 1px solid var(--fly-border-color);
514
- }}
515
- footer, .gradio-footer {{
516
- display: none !important;
517
- }}
518
 
519
- /* Desktop improvements */
520
  @media (min-width: 768px) {{
521
- .main-content-columns-fly {{
522
- display: flex;
523
- flex-direction: row;
524
- gap: 2rem;
525
- align-items: flex-start; /* Align items to top */
526
- }}
527
- .main-content-columns-fly > .gr-column:nth-child(1) {{
528
- flex: 3;
529
- }}
530
- .main-content-columns-fly > .gr-column:nth-child(2) {{
531
- flex: 2;
532
- position: sticky; /* Make output column sticky on scroll */
533
- top: 1.5rem;
534
- }}
535
-
536
- .action-button-container .gr-button.gr-button-primary {{
537
- width: auto !important;
538
- min-width: 220px;
539
- align-self: flex-end; /* Align button to the right in its container */
540
- }}
541
- .app-title-card h1 {{ font-size: 2.75em !important; }}
542
- .app-title-card p {{ font-size: 1.1em !important; }}
543
- .content-panel {{ padding: 2rem; }}
544
  }}
545
  """
546
 
@@ -588,7 +309,6 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
588
  pitch_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=5, label="زیر و بمی صدا")
589
 
590
  with gr.Column(elem_classes=["action-button-container"]):
591
- # Using variant="primary" for Gradio theming, CSS will override for advanced style
592
  submit_button = gr.Button("✨ ترجمه و پخش جادویی ✨", variant="primary")
593
 
594
  with gr.Column(scale=2, elem_id="output_column", elem_classes=["result-area-fly"]):
@@ -601,21 +321,17 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
601
  with gr.Group(elem_classes=["output-audio-container"]):
602
  output_audio = gr.Audio(label="🎤 بشنوید:", type="filepath", format="mp3", interactive=False)
603
 
604
- status_message = gr.Markdown(visible=False, elem_classes=["status-message-fly"])
605
 
606
 
607
  if language_dict_persian_keys and default_english_tts_voice:
608
- gr.HTML("<hr class='custom-hr-fly'>") # Visual separator
609
  num_voices = len(language_dict_persian_keys)
610
  voice_keys = list(language_dict_persian_keys.keys())
611
 
612
  valid_example_list = []
613
  if num_voices > 0:
614
- voice1_idx = 0
615
- voice2_idx = min(7, num_voices - 1)
616
- voice3_idx = min(13, num_voices - 1)
617
- voice4_idx = min(3, num_voices - 1)
618
-
619
  example_list_data = [
620
  ["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
621
  ["می‌توانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx], 5, 0, 0],
@@ -623,53 +339,56 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
623
  ["یک قهوه و یک کیک لطفا.", voice_keys[voice4_idx], 0, 0, 0],
624
  ]
625
  valid_example_list = example_list_data
626
- else:
627
- logging.warning("No voices available for examples.")
628
 
629
  if valid_example_list:
630
  gr.Examples(
631
  examples=valid_example_list,
632
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
633
- # outputs should match the number of return values from the function
634
  outputs=[output_text_translated, output_audio, status_message],
635
- fn=translate_and_speak_sync_wrapper,
636
  label="🌟 نمونه‌های الهام‌بخش (برای امتحان کلیک کنید):",
637
- elem_id="examples_section" # Ensure this ID is targeted in CSS
638
  )
639
 
640
  gr.Markdown("<p class='app-footer-fly'>ساخته شده با ❤️ و ☕ توسط تیم فلای | Fly Language Learning © ۲۰۲۴</p>")
641
 
642
  if 'submit_button' in locals() and submit_button is not None:
643
- def handle_submit_click(persian_text, english_tts_voice_key, rate, volume, pitch):
644
- translated_text, audio_path, status_update = translate_and_speak_sync_wrapper(
 
645
  persian_text, english_tts_voice_key, rate, volume, pitch
646
  )
647
- # Determine status message class based on content
648
- status_class = "status-message-fly"
649
- if status_update and isinstance(status_update, gr. uomini.Update): # Check if it's a Gradio Update object
650
- current_value = status_update.value
651
- if current_value: # Check if value is not None or empty
 
 
 
 
 
 
652
  if "موفقیت" in current_value or "✅" in current_value:
653
- status_class += " success"
654
  elif "خطا" in current_value or "⚠️" in current_value or "🚫" in current_value:
655
- status_class += " error"
656
 
657
- # Update status_message with new class
658
- updated_status_message = gr.update(value=status_update.value if status_update else None,
659
- visible=bool(status_update and status_update.value),
660
- elem_classes=[status_class]) # Pass as a list
661
 
662
- return translated_text, audio_path, updated_status_message
663
 
664
  submit_button.click(
665
- fn=handle_submit_click, # Use the new handler
666
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
667
- outputs=[output_text_translated, output_audio, status_message] # Match the number of outputs
668
  )
669
  else:
670
  logging.error("Submit button was not initialized correctly or is None.")
671
 
672
-
673
  if __name__ == "__main__":
674
  if not language_dict_persian_keys:
675
  logging.critical("CRITICAL: Voice dictionary (language_dict_persian_keys) is empty!")
 
9
  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
 
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
 
107
+ # Default status update (invisible)
108
+ status_update = gr.update(value=None, visible=False)
109
+
110
  if not GOOGLE_API_KEY or not model:
111
  msg = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم GOOGLE_API_KEY اطمینان حاصل کنید."
112
+ logging.error(msg)
113
+ status_update = gr.update(value=f"🚫 {msg}", visible=True)
114
+ return msg, None, status_update
115
  if not persian_text or not persian_text.strip():
116
  msg = "لطفاً متن فارسی را برای ترجمه وارد کنید."
117
+ logging.warning(msg)
118
+ status_update = gr.update(value=f"⚠️ {msg}", visible=True)
119
+ return msg, None, status_update
120
  if not english_tts_voice_key or english_tts_voice_key == "لیست خالی":
121
  msg = "لطفاً یک صدای گوینده انتخاب کنید."
122
+ logging.warning(msg)
123
+ status_update = gr.update(value=f"⚠️ {msg}", visible=True)
124
+ return persian_text, None, status_update # Return Persian text so user sees their input
125
 
126
  try:
127
  translation_status_msg, translated_text = loop.run_until_complete(
 
129
  )
130
  if "خطا" in translation_status_msg.lower() or not translated_text:
131
  err_msg = f"ترجمه ناموفق: {translation_status_msg} {translated_text or ''}"
132
+ logging.error(err_msg)
133
+ status_update = gr.update(value=f"🚫 {err_msg}", visible=True)
134
+ return err_msg, None, status_update
135
 
 
 
 
136
  tts_status_msg, audio_path = loop.run_until_complete(
137
  text_to_speech_edge_tts_async(translated_text, english_tts_voice_key, rate, volume, pitch)
138
  )
139
 
140
  if "خطا" in tts_status_msg.lower() or not audio_path:
141
+ err_msg = f"خطای TTS: {tts_status_msg}" # Don't repeat translated text here, it's already in output
142
+ logging.error(f"Translated: '{translated_text}', TTS Error: {err_msg}")
143
+ status_update = gr.update(value=f"⚠️ {err_msg}", visible=True)
144
+ return translated_text, None, status_update # Return translated text even if TTS fails
145
 
146
  audio_file_to_clean = audio_path
147
  logging.info(f"عملیات موفق. متن ترجمه شده: '{translated_text[:30]}...', مسیر صوت: {audio_path}")
148
+ status_update = gr.update(value="✅ ترجمه و تولید صدا موفقیت آمیز بود!", visible=True)
149
+ return translated_text, audio_path, status_update
150
 
151
  except Exception as e:
152
  logging.error(f"خطای غیرمنتظره در wrapper: {e}", exc_info=True)
 
156
  logging.info(f"Cleaned up temporary audio file: {audio_file_to_clean}")
157
  except OSError as oe:
158
  logging.error(f"Error cleaning up temp file {audio_file_to_clean}: {oe}")
159
+ status_update = gr.update(value=f"🚫 خطای داخلی سرور: {type(e).__name__}", visible=True)
160
+ return f"خطای داخلی سرور: {type(e).__name__}", None, status_update
161
 
162
  # --- تعریف تم و CSS بسیار زیباتر ---
163
  FLY_PRIMARY_COLOR_HEX = "#4F46E5" # Indigo
 
169
  FLY_WHITE_HEX = "#FFFFFF"
170
  FLY_BORDER_COLOR_HEX = "#D1D5DB" # Gray 300
171
  FLY_INPUT_BG_HEX = "#FFFFFF"
172
+ FLY_PANEL_BG_HEX = "#E0E7FF" # Indigo 100
173
 
174
+ app_theme = gr.themes.Base(
175
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
 
 
176
  ).set(
177
  body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
 
178
  )
179
 
180
  custom_css = f"""
 
197
  --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
198
  --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
199
 
200
+ --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; --radius-full: 9999px;
 
 
 
 
 
201
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
202
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
203
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
204
  --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
 
205
  --transition-fast: all 0.15s ease-in-out;
206
  --transition-normal: all 0.25s ease-in-out;
207
+ --fly-primary-rgb: 79, 70, 229; /* For Indigo #4F46E5 */
208
+ --fly-accent-rgb: 245, 158, 11; /* For Amber #F59E0B */
209
  }}
210
 
211
+ 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;}}
212
+ .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;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
+ .app-title-card {{text-align: center; padding: 2.5rem 1rem; margin: 0; background: linear-gradient(135deg, var(--fly-primary) 0%, var(--fly-secondary) 100%); color: var(--fly-bg-white); border-bottom-left-radius: var(--radius-xl); border-bottom-right-radius: var(--radius-xl); box-shadow: var(--shadow-lg); position: relative; overflow: hidden;}}
215
+ .app-title-card::before {{content: ''; position: absolute; top: -50px; right: -50px; width: 150px; height: 150px; background: rgba(255,255,255,0.1); border-radius: var(--radius-full); opacity: 0.5; transform: rotate(45deg);}}
216
+ .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);}}
217
+ .app-title-card p {{font-size: 1em !important; margin-top: 0.25rem; font-weight: 400; color: rgba(255, 255, 255, 0.85) !important;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
+ .main-content-area-fly {{flex-grow: 1; padding: 1.5rem; max-width: 900px; width: 100%; margin: 0 auto;}}
220
+ .content-panel {{background-color: var(--fly-bg-white); padding: 1.5rem; border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); margin-top: -2rem; position: relative; z-index: 10; margin-bottom: 2rem;}}
221
 
222
+ .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;}}
223
+ .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);}}
224
+ .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);}}
225
+ .gr-dropdown svg.icon {{right: auto !important; left: 1rem !important; color: var(--fly-subtle-text) !important;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ .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);}}
228
+ .output-audio-container .gr-audio {{border-radius: var(--radius-md); box-shadow: var(--shadow-sm);}}
229
+ .output-audio-container .gr-audio audio {{border-radius: var(--radius-md);}}
 
 
 
 
 
230
 
231
+ .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;}}
232
+ .gr-accordion > button.gr-button:hover {{background-color: #E2E8F0 !important; border-color: var(--fly-primary) !important; color: var(--fly-primary) !important;}}
233
+ .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;}}
234
+ .gr-slider {{padding: 0.5rem 0 !important;}}
235
+ .gr-slider input[type="range"]::-webkit-slider-thumb {{background: var(--fly-primary) !important; box-shadow: var(--shadow-sm) !important;}}
236
+ .gr-slider input[type="range"]::-moz-range-thumb {{background: var(--fly-primary) !important; box-shadow: var(--shadow-sm) !important;}}
237
 
238
+ .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;}}
239
+ .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);}}
240
+ .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);}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
+ div#examples_section {{margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--fly-border-color);}}
243
+ div#examples_section > .gr-panel {{background-color: transparent !important; border: none !important; padding: 0 !important; box-shadow: none !important;}}
244
+ div#examples_section .gr-samples-header {{font-weight: 700; color: var(--fly-text-primary); font-size: 1.1em; margin-bottom: 1rem; text-align: right;}}
245
+ 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;}}
246
+ 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);}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
+ .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);}}
249
+ .status-message-fly.success {{background-color: #D1FAE5; color: #065F46; border: 1px solid #A7F3D0;}}
250
+ .status-message-fly.error {{background-color: #FEE2E2; color: #991B1B; border: 1px solid #FECACA;}}
251
 
252
+ .custom-hr-fly {{height: 1px; background-color: var(--fly-border-color); margin: 2rem 0; border: none;}}
253
+ .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);}}
254
+ .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);}}
255
+ footer, .gradio-footer {{display: none !important;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
 
257
  @media (min-width: 768px) {{
258
+ .main-content-columns-fly {{display: flex; flex-direction: row; gap: 2rem; align-items: flex-start;}}
259
+ .main-content-columns-fly > .gr-column:nth-child(1) {{flex: 3;}}
260
+ .main-content-columns-fly > .gr-column:nth-child(2) {{flex: 2; position: sticky; top: 1.5rem;}}
261
+ .action-button-container .gr-button.gr-button-primary {{width: auto !important; min-width: 220px; align-self: flex-end;}}
262
+ .app-title-card h1 {{font-size: 2.75em !important;}}
263
+ .app-title-card p {{font-size: 1.1em !important;}}
264
+ .content-panel {{padding: 2rem;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  }}
266
  """
267
 
 
309
  pitch_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=5, label="زیر و بمی صدا")
310
 
311
  with gr.Column(elem_classes=["action-button-container"]):
 
312
  submit_button = gr.Button("✨ ترجمه و پخش جادویی ✨", variant="primary")
313
 
314
  with gr.Column(scale=2, elem_id="output_column", elem_classes=["result-area-fly"]):
 
321
  with gr.Group(elem_classes=["output-audio-container"]):
322
  output_audio = gr.Audio(label="🎤 بشنوید:", type="filepath", format="mp3", interactive=False)
323
 
324
+ status_message = gr.Markdown(visible=False, elem_classes=["status-message-fly"]) # Base class only
325
 
326
 
327
  if language_dict_persian_keys and default_english_tts_voice:
328
+ gr.HTML("<hr class='custom-hr-fly'>")
329
  num_voices = len(language_dict_persian_keys)
330
  voice_keys = list(language_dict_persian_keys.keys())
331
 
332
  valid_example_list = []
333
  if num_voices > 0:
334
+ voice1_idx, voice2_idx, voice3_idx, voice4_idx = 0, min(7, num_voices - 1), min(13, num_voices - 1), min(3, num_voices - 1)
 
 
 
 
335
  example_list_data = [
336
  ["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
337
  ["می‌توانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx], 5, 0, 0],
 
339
  ["یک قهوه و یک کیک لطفا.", voice_keys[voice4_idx], 0, 0, 0],
340
  ]
341
  valid_example_list = example_list_data
342
+ else: logging.warning("No voices available for examples.")
 
343
 
344
  if valid_example_list:
345
  gr.Examples(
346
  examples=valid_example_list,
347
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
 
348
  outputs=[output_text_translated, output_audio, status_message],
349
+ fn=translate_and_speak_sync_wrapper, # Direct call, status will be handled by its return
350
  label="🌟 نمونه‌های الهام‌بخش (برای امتحان کلیک کنید):",
351
+ elem_id="examples_section"
352
  )
353
 
354
  gr.Markdown("<p class='app-footer-fly'>ساخته شده با ❤️ و ☕ توسط تیم فلای | Fly Language Learning © ۲۰۲۴</p>")
355
 
356
  if 'submit_button' in locals() and submit_button is not None:
357
+ def handle_submit_click_ui_update(persian_text, english_tts_voice_key, rate, volume, pitch):
358
+ # Call the core logic function
359
+ translated_text, audio_path, status_update_dict = translate_and_speak_sync_wrapper(
360
  persian_text, english_tts_voice_key, rate, volume, pitch
361
  )
362
+
363
+ # Prepare the status message UI update based on the dictionary returned
364
+ current_value = None
365
+ is_visible = False
366
+ status_class_list = ["status-message-fly"] # Base class
367
+
368
+ if status_update_dict and isinstance(status_update_dict, dict):
369
+ current_value = status_update_dict.get("value")
370
+ is_visible = status_update_dict.get("visible", False) # Default to False if not specified
371
+
372
+ if current_value: # Only add specific class if there's a message
373
  if "موفقیت" in current_value or "✅" in current_value:
374
+ status_class_list.append("success")
375
  elif "خطا" in current_value or "⚠️" in current_value or "🚫" in current_value:
376
+ status_class_list.append("error")
377
 
378
+ updated_status_message_ui = gr.update(value=current_value,
379
+ visible=is_visible,
380
+ elem_classes=status_class_list)
 
381
 
382
+ return translated_text, audio_path, updated_status_message_ui
383
 
384
  submit_button.click(
385
+ fn=handle_submit_click_ui_update,
386
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
387
+ outputs=[output_text_translated, output_audio, status_message]
388
  )
389
  else:
390
  logging.error("Submit button was not initialized correctly or is None.")
391
 
 
392
  if __name__ == "__main__":
393
  if not language_dict_persian_keys:
394
  logging.critical("CRITICAL: Voice dictionary (language_dict_persian_keys) is empty!")