Update app.py
Browse files
app.py
CHANGED
|
@@ -9,7 +9,7 @@ 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__}")
|
| 13 |
|
| 14 |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
| 15 |
model = None
|
|
@@ -77,10 +77,9 @@ async def text_to_speech_edge_tts_async(text_to_speak, tts_voice_key, rate, volu
|
|
| 77 |
try:
|
| 78 |
rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
|
| 79 |
communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
|
| 80 |
-
# Ensure tmp_path is correctly defined and used
|
| 81 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 82 |
tmp_path = tmp_file.name
|
| 83 |
-
await communicate.save(tmp_path)
|
| 84 |
return "TTS موفق", tmp_path
|
| 85 |
except Exception as e: logging.error(f"Edge-TTS error: {e}", exc_info=True); return f"خطا در TTS: {type(e).__name__}", None
|
| 86 |
|
|
@@ -95,7 +94,7 @@ def get_event_loop():
|
|
| 95 |
current_loop = asyncio.get_event_loop_policy().get_event_loop()
|
| 96 |
if current_loop is not _thread_local.loop:
|
| 97 |
asyncio.set_event_loop(_thread_local.loop)
|
| 98 |
-
except RuntimeError:
|
| 99 |
asyncio.set_event_loop(_thread_local.loop)
|
| 100 |
logging.info(f"Set event loop for thread {threading.get_ident()} as no current loop was found or it was different.")
|
| 101 |
return _thread_local.loop
|
|
@@ -105,7 +104,7 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 105 |
logging.info(f"Wrapper called. Text: '{persian_text[:30]}...' Voice: {english_tts_voice_key}")
|
| 106 |
loop = get_event_loop()
|
| 107 |
|
| 108 |
-
audio_file_to_clean = None
|
| 109 |
|
| 110 |
if not GOOGLE_API_KEY or not model:
|
| 111 |
msg = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم GOOGLE_API_KEY اطمینان حاصل کنید."
|
|
@@ -115,7 +114,6 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 115 |
logging.warning(msg); return msg, None
|
| 116 |
if not english_tts_voice_key or english_tts_voice_key == "لیست خالی":
|
| 117 |
msg = "لطفاً یک صدای گوینده انتخاب کنید."
|
| 118 |
-
# Return Persian text if no voice selected, so user sees their input
|
| 119 |
logging.warning(msg); return persian_text, None
|
| 120 |
|
| 121 |
try:
|
|
@@ -132,15 +130,14 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 132 |
|
| 133 |
if "خطا" in tts_status_msg.lower() or not audio_path:
|
| 134 |
err_msg = f"{translated_text}\n(خطای TTS: {tts_status_msg})"
|
| 135 |
-
logging.error(err_msg); return err_msg, None
|
| 136 |
|
| 137 |
-
audio_file_to_clean = audio_path
|
| 138 |
logging.info(f"عملیات موفق. متن ترجمه شده: '{translated_text[:30]}...', مسیر صوت: {audio_path}")
|
| 139 |
return translated_text, audio_path
|
| 140 |
|
| 141 |
except Exception as e:
|
| 142 |
logging.error(f"خطای غیرمنتظره در wrapper: {e}", exc_info=True)
|
| 143 |
-
# Clean up temp file if an unexpected error occurs after its creation
|
| 144 |
if audio_file_to_clean and os.path.exists(audio_file_to_clean):
|
| 145 |
try:
|
| 146 |
os.remove(audio_file_to_clean)
|
|
@@ -152,14 +149,14 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
|
|
| 152 |
# --- تعریف تم و CSS بهبود یافته ---
|
| 153 |
FLY_PRIMARY_COLOR_HEX = "#2563EB"
|
| 154 |
FLY_SECONDARY_COLOR_HEX = "#059669"
|
| 155 |
-
FLY_ACCENT_COLOR_HEX = "#D97706"
|
| 156 |
FLY_TEXT_COLOR_HEX = "#1F2937"
|
| 157 |
FLY_SUBTLE_TEXT_HEX = "#6B7280"
|
| 158 |
-
FLY_LIGHT_BACKGROUND_HEX = "#F3F4F6"
|
| 159 |
FLY_WHITE_HEX = "#FFFFFF"
|
| 160 |
FLY_BORDER_COLOR_HEX = "#D1D5DB"
|
| 161 |
FLY_INPUT_BG_HEX = "#FFFFFF"
|
| 162 |
-
FLY_PANEL_BG_HEX = "#E0F2FE"
|
| 163 |
FLY_SUCCESS_GREEN_HEX = "#059669"
|
| 164 |
FLY_ERROR_RED_HEX = "#DC2626"
|
| 165 |
|
|
@@ -175,9 +172,6 @@ app_theme = gr.themes.Soft(
|
|
| 175 |
button_primary_text_color=FLY_WHITE_HEX,
|
| 176 |
)
|
| 177 |
|
| 178 |
-
# --- CSS بهبود یافته با دقت در f-string ---
|
| 179 |
-
# IMPORTANT: Using ''' """ ''' to define the multi-line string to avoid f-string parsing issues with CSS's own curly braces.
|
| 180 |
-
# If we were using f-string for CSS, all CSS '{' and '}' would need to be '{{' and '}}'.
|
| 181 |
custom_css = f"""
|
| 182 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap');
|
| 183 |
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
|
|
@@ -254,7 +248,7 @@ body {{
|
|
| 254 |
flex-grow: 1;
|
| 255 |
padding: 0 1rem;
|
| 256 |
}}
|
| 257 |
-
.content-wrapper {{
|
| 258 |
background-color: var(--fly-white);
|
| 259 |
padding: 1.5rem;
|
| 260 |
border-radius: var(--border-radius-lg);
|
|
@@ -271,7 +265,7 @@ body {{
|
|
| 271 |
display: inline-block;
|
| 272 |
}}
|
| 273 |
.gr-input > label + div > textarea,
|
| 274 |
-
.gr-dropdown > label + div > div > input,
|
| 275 |
.gr-dropdown > label + div > div > select {{
|
| 276 |
border-radius: var(--border-radius-md) !important;
|
| 277 |
border: 1px solid var(--fly-border) !important;
|
|
@@ -295,11 +289,9 @@ body {{
|
|
| 295 |
}}
|
| 296 |
.gr-dropdown svg.icon {{
|
| 297 |
right: auto !important;
|
| 298 |
-
left: 10px !important;
|
| 299 |
}}
|
| 300 |
|
| 301 |
-
/* Output Textbox styling */
|
| 302 |
-
/* Targeting both old and new Gradio versions for output textbox */
|
| 303 |
.gr-textbox[label*="ترجمه انگلیسی"] > label + div > textarea,
|
| 304 |
.gradio-interface .output_text .gr-textbox[data-testid="textbox"] > label + div > textarea {{
|
| 305 |
background-color: var(--fly-panel-bg) !important;
|
|
@@ -311,8 +303,7 @@ body {{
|
|
| 311 |
font-weight: 500;
|
| 312 |
}}
|
| 313 |
|
| 314 |
-
|
| 315 |
-
.gr-accordion > button.gr-button {{ /* Accordion Header Button */
|
| 316 |
font-weight: 600 !important;
|
| 317 |
padding: 10px 12px !important;
|
| 318 |
border-radius: var(--border-radius-md) !important;
|
|
@@ -326,7 +317,7 @@ body {{
|
|
| 326 |
.gr-accordion > button.gr-button:hover {{
|
| 327 |
background-color: #D1D5DB !important;
|
| 328 |
}}
|
| 329 |
-
.gr-accordion > div.gr-panel {{
|
| 330 |
border-radius: var(--border-radius-md) !important;
|
| 331 |
border: 1px solid var(--fly-border) !important;
|
| 332 |
background-color: var(--fly-white) !important;
|
|
@@ -341,8 +332,8 @@ body {{
|
|
| 341 |
color: var(--fly-subtle-text);
|
| 342 |
}}
|
| 343 |
|
| 344 |
-
/* Submit Button
|
| 345 |
-
.fly-submit-button .gr-button {{
|
| 346 |
width: 100% !important;
|
| 347 |
padding: 12px 20px !important;
|
| 348 |
font-size: 1em !important;
|
|
@@ -353,12 +344,12 @@ body {{
|
|
| 353 |
letter-spacing: 0.5px;
|
| 354 |
transition: background-color 0.2s ease, transform 0.1s ease;
|
| 355 |
}}
|
| 356 |
-
.fly-submit-button .gr-button:hover {{
|
| 357 |
transform: translateY(-2px);
|
| 358 |
}}
|
| 359 |
|
| 360 |
/* Examples Section */
|
| 361 |
-
div[id*="examples_section"] {{
|
| 362 |
margin-top: 1.5rem;
|
| 363 |
}}
|
| 364 |
div[id*="examples_section"] > .gr-panel {{
|
|
@@ -367,8 +358,8 @@ div[id*="examples_section"] > .gr-panel {{
|
|
| 367 |
padding: 0 !important;
|
| 368 |
box-shadow: none !important;
|
| 369 |
}}
|
| 370 |
-
div[id*="examples_section"] > .gr-form > button.gr-button.gr-button-secondary,
|
| 371 |
-
div[id*="examples_section"] .gr-sample-button
|
| 372 |
background-color: #E0E7FF !important;
|
| 373 |
color: var(--fly-primary) !important;
|
| 374 |
border-radius: var(--border-radius-sm) !important;
|
|
@@ -388,7 +379,6 @@ div[id*="examples_section"] .gr-samples-header {{
|
|
| 388 |
margin-bottom: 0.8rem;
|
| 389 |
}}
|
| 390 |
|
| 391 |
-
|
| 392 |
.custom-hr-fly {{
|
| 393 |
height: 1px;
|
| 394 |
background-color: var(--fly-border);
|
|
@@ -417,7 +407,6 @@ footer, .gradio-footer {{
|
|
| 417 |
display: none !important;
|
| 418 |
}}
|
| 419 |
|
| 420 |
-
/* Desktop improvements */
|
| 421 |
@media (min-width: 768px) {{
|
| 422 |
body {{font-size: 16px;}}
|
| 423 |
.gradio-container {{
|
|
@@ -430,7 +419,7 @@ footer, .gradio-footer {{
|
|
| 430 |
}}
|
| 431 |
.app-header-fly {{
|
| 432 |
border-radius: var(--border-radius-lg);
|
| 433 |
-
margin: -1.5rem -1.5rem 2rem -1.5rem;
|
| 434 |
}}
|
| 435 |
.app-header-fly h1 {{ font-size: 2em !important; }}
|
| 436 |
.app-header-fly p {{ font-size: 1em !important; }}
|
|
@@ -442,13 +431,13 @@ footer, .gradio-footer {{
|
|
| 442 |
gap: 1.5rem;
|
| 443 |
}}
|
| 444 |
.main-content-columns-fly > .gr-column:nth-child(1) {{
|
| 445 |
-
flex: 3;
|
| 446 |
}}
|
| 447 |
.main-content-columns-fly > .gr-column:nth-child(2) {{
|
| 448 |
-
flex: 2;
|
| 449 |
}}
|
| 450 |
|
| 451 |
-
.fly-submit-button .gr-button {{
|
| 452 |
width: auto !important;
|
| 453 |
min-width: 200px;
|
| 454 |
}}
|
|
@@ -462,11 +451,13 @@ footer, .gradio-footer {{
|
|
| 462 |
.result-area-fly {{
|
| 463 |
margin-top: 1rem;
|
| 464 |
}}
|
| 465 |
-
.result-area-fly > .gr-block > .gr-form > .gr-input,
|
| 466 |
-
.result-area-fly >
|
|
|
|
|
|
|
| 467 |
margin-bottom: 0.8rem;
|
| 468 |
}}
|
| 469 |
-
"""
|
| 470 |
|
| 471 |
default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
|
| 472 |
if not language_dict_persian_keys :
|
|
@@ -475,7 +466,6 @@ if not language_dict_persian_keys :
|
|
| 475 |
elif default_english_tts_voice not in language_dict_persian_keys:
|
| 476 |
default_english_tts_voice = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
|
| 477 |
|
| 478 |
-
# --- Gradio UI با ساختار بهبود یافته ---
|
| 479 |
with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فلای | Fly Language Learning") as demo:
|
| 480 |
gr.HTML(f"""<div class="app-header-fly"><h1>Fly Language Learning 🚀</h1><p>ترجمه فارسی به انگلیسی و تلفظ با لهجه دلخواه</p></div>""")
|
| 481 |
|
|
@@ -486,7 +476,7 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
|
|
| 486 |
elif not model: missing_key_msg += "خطا در بارگذاری مدل Gemini."
|
| 487 |
gr.Markdown(f"<div class='api-warning-message-fly'>{missing_key_msg} بخش ترجمه غیرفعال خواهد بود. لطفاً تنظیمات Secrets را بررسی کنید.</div>")
|
| 488 |
|
| 489 |
-
with gr.
|
| 490 |
with gr.Row(elem_classes=["main-content-columns-fly"]):
|
| 491 |
with gr.Column(scale=3):
|
| 492 |
input_text_persian = gr.Textbox(
|
|
@@ -500,12 +490,14 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
|
|
| 500 |
interactive=bool(language_dict_persian_keys and default_english_tts_voice is not None)
|
| 501 |
)
|
| 502 |
with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
|
|
|
|
| 503 |
rate_slider = gr.Slider(minimum=-100, maximum=100, value=0, step=10, label="سرعت گفتار (Rate)")
|
| 504 |
volume_slider = gr.Slider(minimum=-100, maximum=100, value=0, step=10, label="بلندی صدا (Volume)")
|
| 505 |
pitch_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=5, label="زیر و بمی صدا (Pitch)")
|
| 506 |
|
| 507 |
-
|
| 508 |
-
|
|
|
|
| 509 |
|
| 510 |
with gr.Column(scale=2, elem_classes=["result-area-fly"]):
|
| 511 |
output_text_translated = gr.Textbox(
|
|
@@ -520,24 +512,23 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
|
|
| 520 |
num_voices = len(language_dict_persian_keys)
|
| 521 |
voice_keys = list(language_dict_persian_keys.keys())
|
| 522 |
|
| 523 |
-
voice1_idx = 0
|
| 524 |
-
voice2_idx = min(7, num_voices - 1) if num_voices > 1 else 0
|
| 525 |
-
voice3_idx = min(13, num_voices - 1) if num_voices > 1 else 0
|
| 526 |
-
|
| 527 |
-
example_list_data = [
|
| 528 |
-
["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
|
| 529 |
-
["میتوانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx], 5, 0, 0],
|
| 530 |
-
["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", voice_keys[voice3_idx], -10, 10, 0],
|
| 531 |
-
["یک قهوه و یک کیک لطفا.", voice_keys[min(3, num_voices -1) if num_voices > 0 else 0], 0, 0, 0],
|
| 532 |
-
]
|
| 533 |
-
# Filter out examples if voice keys are not valid (e.g. num_voices is 0)
|
| 534 |
valid_example_list = []
|
| 535 |
if num_voices > 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
valid_example_list = example_list_data
|
| 537 |
-
else:
|
| 538 |
logging.warning("No voices available for examples.")
|
| 539 |
|
| 540 |
-
|
| 541 |
if valid_example_list:
|
| 542 |
gr.Examples(
|
| 543 |
examples=valid_example_list,
|
|
@@ -550,14 +541,16 @@ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فل
|
|
| 550 |
|
| 551 |
gr.Markdown("<p class='app-footer-fly'>✨ Fly Language Learning © ۲۰۲۴ - قدرت گرفته از Gemini و EdgeTTS ✨</p>")
|
| 552 |
|
| 553 |
-
|
|
|
|
|
|
|
| 554 |
submit_button.click(
|
| 555 |
fn=translate_and_speak_sync_wrapper,
|
| 556 |
inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
|
| 557 |
outputs=[output_text_translated, output_audio]
|
| 558 |
)
|
| 559 |
else:
|
| 560 |
-
logging.error("Submit button was not initialized correctly.")
|
| 561 |
|
| 562 |
|
| 563 |
if __name__ == "__main__":
|
|
|
|
| 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__}") # This will show the actual version used by HF Spaces
|
| 13 |
|
| 14 |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
| 15 |
model = None
|
|
|
|
| 77 |
try:
|
| 78 |
rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
|
| 79 |
communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
|
|
|
|
| 80 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 81 |
tmp_path = tmp_file.name
|
| 82 |
+
await communicate.save(tmp_path)
|
| 83 |
return "TTS موفق", tmp_path
|
| 84 |
except Exception as e: logging.error(f"Edge-TTS error: {e}", exc_info=True); return f"خطا در TTS: {type(e).__name__}", None
|
| 85 |
|
|
|
|
| 94 |
current_loop = asyncio.get_event_loop_policy().get_event_loop()
|
| 95 |
if current_loop is not _thread_local.loop:
|
| 96 |
asyncio.set_event_loop(_thread_local.loop)
|
| 97 |
+
except RuntimeError:
|
| 98 |
asyncio.set_event_loop(_thread_local.loop)
|
| 99 |
logging.info(f"Set event loop for thread {threading.get_ident()} as no current loop was found or it was different.")
|
| 100 |
return _thread_local.loop
|
|
|
|
| 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 اطمینان حاصل کنید."
|
|
|
|
| 114 |
logging.warning(msg); return msg, None
|
| 115 |
if not english_tts_voice_key or english_tts_voice_key == "لیست خالی":
|
| 116 |
msg = "لطفاً یک صدای گوینده انتخاب کنید."
|
|
|
|
| 117 |
logging.warning(msg); return persian_text, None
|
| 118 |
|
| 119 |
try:
|
|
|
|
| 130 |
|
| 131 |
if "خطا" in tts_status_msg.lower() or not audio_path:
|
| 132 |
err_msg = f"{translated_text}\n(خطای TTS: {tts_status_msg})"
|
| 133 |
+
logging.error(err_msg); return err_msg, None
|
| 134 |
|
| 135 |
+
audio_file_to_clean = audio_path
|
| 136 |
logging.info(f"عملیات موفق. متن ترجمه شده: '{translated_text[:30]}...', مسیر صوت: {audio_path}")
|
| 137 |
return translated_text, audio_path
|
| 138 |
|
| 139 |
except Exception as e:
|
| 140 |
logging.error(f"خطای غیرمنتظره در wrapper: {e}", exc_info=True)
|
|
|
|
| 141 |
if audio_file_to_clean and os.path.exists(audio_file_to_clean):
|
| 142 |
try:
|
| 143 |
os.remove(audio_file_to_clean)
|
|
|
|
| 149 |
# --- تعریف تم و CSS بهبود یافته ---
|
| 150 |
FLY_PRIMARY_COLOR_HEX = "#2563EB"
|
| 151 |
FLY_SECONDARY_COLOR_HEX = "#059669"
|
| 152 |
+
FLY_ACCENT_COLOR_HEX = "#D97706"
|
| 153 |
FLY_TEXT_COLOR_HEX = "#1F2937"
|
| 154 |
FLY_SUBTLE_TEXT_HEX = "#6B7280"
|
| 155 |
+
FLY_LIGHT_BACKGROUND_HEX = "#F3F4F6"
|
| 156 |
FLY_WHITE_HEX = "#FFFFFF"
|
| 157 |
FLY_BORDER_COLOR_HEX = "#D1D5DB"
|
| 158 |
FLY_INPUT_BG_HEX = "#FFFFFF"
|
| 159 |
+
FLY_PANEL_BG_HEX = "#E0F2FE"
|
| 160 |
FLY_SUCCESS_GREEN_HEX = "#059669"
|
| 161 |
FLY_ERROR_RED_HEX = "#DC2626"
|
| 162 |
|
|
|
|
| 172 |
button_primary_text_color=FLY_WHITE_HEX,
|
| 173 |
)
|
| 174 |
|
|
|
|
|
|
|
|
|
|
| 175 |
custom_css = f"""
|
| 176 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap');
|
| 177 |
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
|
|
|
|
| 248 |
flex-grow: 1;
|
| 249 |
padding: 0 1rem;
|
| 250 |
}}
|
| 251 |
+
.content-wrapper {{ /* This class will be applied to gr.Group */
|
| 252 |
background-color: var(--fly-white);
|
| 253 |
padding: 1.5rem;
|
| 254 |
border-radius: var(--border-radius-lg);
|
|
|
|
| 265 |
display: inline-block;
|
| 266 |
}}
|
| 267 |
.gr-input > label + div > textarea,
|
| 268 |
+
.gr-dropdown > label + div > div > input,
|
| 269 |
.gr-dropdown > label + div > div > select {{
|
| 270 |
border-radius: var(--border-radius-md) !important;
|
| 271 |
border: 1px solid var(--fly-border) !important;
|
|
|
|
| 289 |
}}
|
| 290 |
.gr-dropdown svg.icon {{
|
| 291 |
right: auto !important;
|
| 292 |
+
left: 10px !important;
|
| 293 |
}}
|
| 294 |
|
|
|
|
|
|
|
| 295 |
.gr-textbox[label*="ترجمه انگلیسی"] > label + div > textarea,
|
| 296 |
.gradio-interface .output_text .gr-textbox[data-testid="textbox"] > label + div > textarea {{
|
| 297 |
background-color: var(--fly-panel-bg) !important;
|
|
|
|
| 303 |
font-weight: 500;
|
| 304 |
}}
|
| 305 |
|
| 306 |
+
.gr-accordion > button.gr-button {{
|
|
|
|
| 307 |
font-weight: 600 !important;
|
| 308 |
padding: 10px 12px !important;
|
| 309 |
border-radius: var(--border-radius-md) !important;
|
|
|
|
| 317 |
.gr-accordion > button.gr-button:hover {{
|
| 318 |
background-color: #D1D5DB !important;
|
| 319 |
}}
|
| 320 |
+
.gr-accordion > div.gr-panel {{
|
| 321 |
border-radius: var(--border-radius-md) !important;
|
| 322 |
border: 1px solid var(--fly-border) !important;
|
| 323 |
background-color: var(--fly-white) !important;
|
|
|
|
| 332 |
color: var(--fly-subtle-text);
|
| 333 |
}}
|
| 334 |
|
| 335 |
+
/* Submit Button */
|
| 336 |
+
.fly-submit-button-container .gr-button {{ /* Target the container of the button */
|
| 337 |
width: 100% !important;
|
| 338 |
padding: 12px 20px !important;
|
| 339 |
font-size: 1em !important;
|
|
|
|
| 344 |
letter-spacing: 0.5px;
|
| 345 |
transition: background-color 0.2s ease, transform 0.1s ease;
|
| 346 |
}}
|
| 347 |
+
.fly-submit-button-container .gr-button:hover {{
|
| 348 |
transform: translateY(-2px);
|
| 349 |
}}
|
| 350 |
|
| 351 |
/* Examples Section */
|
| 352 |
+
div[id*="examples_section"] {{
|
| 353 |
margin-top: 1.5rem;
|
| 354 |
}}
|
| 355 |
div[id*="examples_section"] > .gr-panel {{
|
|
|
|
| 358 |
padding: 0 !important;
|
| 359 |
box-shadow: none !important;
|
| 360 |
}}
|
| 361 |
+
div[id*="examples_section"] > .gr-form > button.gr-button.gr-button-secondary,
|
| 362 |
+
div[id*="examples_section"] .gr-sample-button {{
|
| 363 |
background-color: #E0E7FF !important;
|
| 364 |
color: var(--fly-primary) !important;
|
| 365 |
border-radius: var(--border-radius-sm) !important;
|
|
|
|
| 379 |
margin-bottom: 0.8rem;
|
| 380 |
}}
|
| 381 |
|
|
|
|
| 382 |
.custom-hr-fly {{
|
| 383 |
height: 1px;
|
| 384 |
background-color: var(--fly-border);
|
|
|
|
| 407 |
display: none !important;
|
| 408 |
}}
|
| 409 |
|
|
|
|
| 410 |
@media (min-width: 768px) {{
|
| 411 |
body {{font-size: 16px;}}
|
| 412 |
.gradio-container {{
|
|
|
|
| 419 |
}}
|
| 420 |
.app-header-fly {{
|
| 421 |
border-radius: var(--border-radius-lg);
|
| 422 |
+
margin: -1.5rem -1.5rem 2rem -1.5rem;
|
| 423 |
}}
|
| 424 |
.app-header-fly h1 {{ font-size: 2em !important; }}
|
| 425 |
.app-header-fly p {{ font-size: 1em !important; }}
|
|
|
|
| 431 |
gap: 1.5rem;
|
| 432 |
}}
|
| 433 |
.main-content-columns-fly > .gr-column:nth-child(1) {{
|
| 434 |
+
flex: 3;
|
| 435 |
}}
|
| 436 |
.main-content-columns-fly > .gr-column:nth-child(2) {{
|
| 437 |
+
flex: 2;
|
| 438 |
}}
|
| 439 |
|
| 440 |
+
.fly-submit-button-container .gr-button {{
|
| 441 |
width: auto !important;
|
| 442 |
min-width: 200px;
|
| 443 |
}}
|
|
|
|
| 451 |
.result-area-fly {{
|
| 452 |
margin-top: 1rem;
|
| 453 |
}}
|
| 454 |
+
.result-area-fly > .gr-block > .gr-form > .gr-input, /* Gradio < 4 */
|
| 455 |
+
.result-area-fly > div[data-testid="block"] > div[data-testid="form"] > .gr-input, /* Gradio 4+ */
|
| 456 |
+
.result-array-fly > .gr-block > .gr-form > .gr-audio, /* Gradio < 4 */
|
| 457 |
+
.result-area-fly > div[data-testid="block"] > div[data-testid="form"] > .gr-audio {{
|
| 458 |
margin-bottom: 0.8rem;
|
| 459 |
}}
|
| 460 |
+
"""
|
| 461 |
|
| 462 |
default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
|
| 463 |
if not language_dict_persian_keys :
|
|
|
|
| 466 |
elif default_english_tts_voice not in language_dict_persian_keys:
|
| 467 |
default_english_tts_voice = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
|
| 468 |
|
|
|
|
| 469 |
with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فلای | Fly Language Learning") as demo:
|
| 470 |
gr.HTML(f"""<div class="app-header-fly"><h1>Fly Language Learning 🚀</h1><p>ترجمه فارسی به انگلیسی و تلفظ با لهجه دلخواه</p></div>""")
|
| 471 |
|
|
|
|
| 476 |
elif not model: missing_key_msg += "خطا در بارگذاری مدل Gemini."
|
| 477 |
gr.Markdown(f"<div class='api-warning-message-fly'>{missing_key_msg} بخش ترجمه غیرفعال خواهد بود. لطفاً تنظیمات Secrets را بررسی کنید.</div>")
|
| 478 |
|
| 479 |
+
with gr.Group(elem_classes=["content-wrapper"]): # CHANGED: gr.Box to gr.Group
|
| 480 |
with gr.Row(elem_classes=["main-content-columns-fly"]):
|
| 481 |
with gr.Column(scale=3):
|
| 482 |
input_text_persian = gr.Textbox(
|
|
|
|
| 490 |
interactive=bool(language_dict_persian_keys and default_english_tts_voice is not None)
|
| 491 |
)
|
| 492 |
with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
|
| 493 |
+
# Using min, max, value for wider compatibility with older Gradio versions if `minimum` etc. are not supported
|
| 494 |
rate_slider = gr.Slider(minimum=-100, maximum=100, value=0, step=10, label="سرعت گفتار (Rate)")
|
| 495 |
volume_slider = gr.Slider(minimum=-100, maximum=100, value=0, step=10, label="بلندی صدا (Volume)")
|
| 496 |
pitch_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=5, label="زیر و بمی صدا (Pitch)")
|
| 497 |
|
| 498 |
+
# To apply class to button container, we wrap it in a Group/Column if direct elem_classes on button's parent isn't enough
|
| 499 |
+
with gr.Group(elem_classes=["fly-submit-button-container"]): # Using Group to contain the button for styling
|
| 500 |
+
submit_button = gr.Button("ترجمه و پخش صدا", variant="primary") # Icon removed for wider compatibility initially
|
| 501 |
|
| 502 |
with gr.Column(scale=2, elem_classes=["result-area-fly"]):
|
| 503 |
output_text_translated = gr.Textbox(
|
|
|
|
| 512 |
num_voices = len(language_dict_persian_keys)
|
| 513 |
voice_keys = list(language_dict_persian_keys.keys())
|
| 514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
valid_example_list = []
|
| 516 |
if num_voices > 0:
|
| 517 |
+
voice1_idx = 0
|
| 518 |
+
voice2_idx = min(7, num_voices - 1)
|
| 519 |
+
voice3_idx = min(13, num_voices - 1)
|
| 520 |
+
voice4_idx = min(3, num_voices - 1)
|
| 521 |
+
|
| 522 |
+
example_list_data = [
|
| 523 |
+
["قیمت این لباس چقدر است؟", voice_keys[voice1_idx], 0, 0, 0],
|
| 524 |
+
["میتوانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx], 5, 0, 0],
|
| 525 |
+
["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", voice_keys[voice3_idx], -10, 10, 0],
|
| 526 |
+
["یک قهوه و یک کیک لطفا.", voice_keys[voice4_idx], 0, 0, 0],
|
| 527 |
+
]
|
| 528 |
valid_example_list = example_list_data
|
| 529 |
+
else:
|
| 530 |
logging.warning("No voices available for examples.")
|
| 531 |
|
|
|
|
| 532 |
if valid_example_list:
|
| 533 |
gr.Examples(
|
| 534 |
examples=valid_example_list,
|
|
|
|
| 541 |
|
| 542 |
gr.Markdown("<p class='app-footer-fly'>✨ Fly Language Learning © ۲۰۲۴ - قدرت گرفته از Gemini و EdgeTTS ✨</p>")
|
| 543 |
|
| 544 |
+
# Ensure submit_button is defined before calling .click
|
| 545 |
+
# This check is good practice but usually not strictly needed if layout is static
|
| 546 |
+
if 'submit_button' in locals() and submit_button is not None:
|
| 547 |
submit_button.click(
|
| 548 |
fn=translate_and_speak_sync_wrapper,
|
| 549 |
inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
|
| 550 |
outputs=[output_text_translated, output_audio]
|
| 551 |
)
|
| 552 |
else:
|
| 553 |
+
logging.error("Submit button was not initialized correctly or is None.")
|
| 554 |
|
| 555 |
|
| 556 |
if __name__ == "__main__":
|