Hamed744 commited on
Commit
52386ed
·
verified ·
1 Parent(s): 9379523

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -149
app.py CHANGED
@@ -8,14 +8,13 @@ import os
8
  import google.generativeai as genai
9
 
10
  # --- دریافت کلید API از متغیرهای محیطی ---
11
- # در Hugging Face Spaces، این کلید را در بخش 'Secrets' تنظیم کنید
12
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
13
 
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') # یا 'gemini-pro' برای کیفیت بالاتر (و هزینه بیشتر)
19
  print("سرویس Gemini با موفقیت پیکربندی شد.")
20
  except Exception as e:
21
  print(f"خطا در پیکربندی Gemini: {e}. قابلیت ترجمه غیرفعال خواهد بود.")
@@ -23,8 +22,7 @@ if GOOGLE_API_KEY:
23
  else:
24
  print("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
25
 
26
- # --- دیکشنری صداهای انگلیسی ---
27
- # فقط شامل صداهای انگلیسی برای خواندن متن ترجمه شده
28
  language_dict_persian_keys = {
29
  'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural',
30
  'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
@@ -33,188 +31,310 @@ language_dict_persian_keys = {
33
  'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural',
34
  'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
35
  'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural',
36
- 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
37
  'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural',
38
  'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
39
  'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural',
40
  'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
41
- 'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural',
42
  'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural',
43
  'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
44
  'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural',
45
  'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
46
- 'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural', # لهجه هندی
47
- 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural', # لهجه هندی
48
- 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
49
  'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural',
50
- 'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural',
51
- 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
52
- # می‌توانید صداهای انگلیسی دیگری که توسط edge-tts پشتیبانی می‌شوند را اضافه کنید
53
  }
54
 
55
  # --- تابع ترجمه با Gemini ---
56
  async def translate_text_gemini(text, target_language="English"):
57
  if not model:
58
- return "خطا: سرویس ترجمه به دلیل عدم تنظیم کلید API یا خطای پیکربندی در دسترس نیست.", None
59
  if not text or not text.strip():
60
  return "خطا: متنی برای ترجمه وارد نشده است.", None
61
  try:
62
- # دستور دقیق‌تر برای ترجمه بدون موارد اضافی
63
- prompt = f"Translate the following Persian text to {target_language}. Provide only the translated text itself, without any extra phrases, explanations, or markdown formatting:\n\nPersian: \"{text}\"\n{target_language}:"
64
  response = await model.generate_content_async(prompt)
65
  translated_text = response.text.strip()
66
- return رجمه با موفقیت انجام شد.", translated_text
 
 
 
67
  except Exception as e:
68
- print(f"خطای Gemini در حین ترجمه: {e}")
69
- print(traceback.format_exc())
70
- error_type = type(e).__name__
71
- error_message = str(e)
72
- detailed_error = f"خطا در هنگام ترجمه ({error_type}): {error_message}"
73
- if "API key not valid" in error_message or "PERMISSION_DENIED" in error_message:
74
- detailed_error += " لطفاً کلید API گوگل خود را بررسی کنید."
75
- return detailed_error, None
76
 
77
  # --- توابع تبدیل متن به گفتار ---
78
  async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
79
  try:
80
  if not text_to_speak or not text_to_speak.strip():
81
- return "خطا: متن ترجمه شده برای تبدیل به گفتار خالی است.", None
82
-
83
  voice_id = language_dict_persian_keys.get(tts_voice_key)
84
- if voice_id is None: # این اتفاق نباید بیفتد چون لیست صداها از همین دیکشنری می‌آید
85
- return f"خطا: مدل صدای TTS انتخاب شده ('{tts_voice_key}') در دیکشنری یافت نشد.", None
86
-
87
- # اطمینان از اینکه voice_id معتبر است (باید با en- شروع شود)
88
  if not voice_id.startswith("en-"):
89
- print(f"هشدار جدی: Voice ID '{voice_id}' برای صدای '{tts_voice_key}' با 'en-' شروع نمی‌شود. این یک خطای پیکربندی است.")
90
- return f"خطای پیکربندی: صدای '{tts_voice_key}' به عنوان صدای انگلیسی معتبر نیست.", None
91
-
92
- rate_str = f"{int(rate):+g}%"
93
- volume_str = f"{int(volume):+g}%"
94
- pitch_str = f"{int(pitch):+g}Hz"
95
 
 
96
  communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
97
- # ایجاد فایل موقت با پسوند .mp3 که توسط Gradio.Audio بهتر پشتیبانی می‌شود
98
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
99
  tmp_path = tmp_file.name
100
  await communicate.save(tmp_path)
101
- return "تبدیل متن به گفتار با موفقیت انجام شد.", tmp_path
102
  except edge_tts.exceptions.NoAudioReceived:
103
- error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id}). ممکن است متن با صدای انتخابی سازگار نباشد یا سرویس موقتا در دسترس نباشد."
104
- print(error_msg)
105
- return error_msg, None
106
- except ValueError as ve: # مثلا اگر متن خیلی طولانی باشد
107
- error_msg = f"خطا در پارامترهای ورودی TTS: {ve}"
108
- print(error_msg)
109
- return error_msg, None
110
  except Exception as e:
111
- print(f"خطای غیرمنتظره در Edge-TTS: {e}")
112
- print(traceback.format_exc())
113
- return f"خطای غیرمنتظره در سرور TTS: {type(e).__name__}", None
114
 
115
  _event_loops_by_thread = {}
116
  def _get_or_create_event_loop():
117
  thread_id = threading.get_ident()
118
  if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
119
- loop = asyncio.new_event_loop()
120
- asyncio.set_event_loop(loop)
121
  _event_loops_by_thread[thread_id] = loop
122
  return _event_loops_by_thread[thread_id]
123
 
124
  # --- تابع اصلی: ترجمه و سپس تبدیل به گفتار ---
125
  def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
126
  loop = _get_or_create_event_loop()
127
-
128
- # 1. بر��سی اولیه ورودی و API Key
 
 
129
  if not GOOGLE_API_KEY or not model:
130
- message = "سرویس ترجمه غیرفعال است. "
131
- if not GOOGLE_API_KEY:
132
- message += "کلید GOOGLE_API_KEY در Secrets این Space تنظیم نشده است."
133
- elif not model:
134
- message += "مدل Gemini بارگذاری نشده (احتمالا مشکل در کلید API یا اتصال)."
135
- return message, "ترجمه انجام نشد.", None
136
 
137
  if not persian_text or not persian_text.strip():
138
- return "لطفاً متن فارسی را برای ترجمه وارد کنید.", "ورودی خالی است.", None
 
139
 
140
- # 2. ترجمه متن
141
- translation_status, translated_text = loop.run_until_complete(
142
  translate_text_gemini(persian_text, target_language="English")
143
  )
 
144
 
145
- translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود یا متنی برگردانده نشد."
146
-
147
- if "خطا" in translation_status or not translated_text:
148
- return f"وضعیت ترجمه: {translation_status}", translated_text_output, None
149
 
150
- # 3. تبدیل متن ترجمه شده به گفتار
151
- # `english_tts_voice_key` باید یکی از کلیدهای معتبر در `language_dict_persian_keys` باشد
152
  if english_tts_voice_key not in language_dict_persian_keys:
153
- # این حالت نباید رخ دهد اگر دراپ‌داون درست تنظیم شده باشد
154
- print(f"خطای داخلی: صدای TTS انتخابی '{english_tts_voice_key}' در لیست صداهای موجود نیست.")
155
- # استفاده از اولین صدای موجود در لیست به عنوان جایگزین
156
- if language_dict_persian_keys:
157
  english_tts_voice_key = list(language_dict_persian_keys.keys())[0]
158
- print(f"به صدای '{english_tts_voice_key}' تغییر یافت.")
159
- else: # اگر دیکشنری صداها هم خالی باشد (نباید اتفاق بیفتد)
160
- return f"وضعیت ترجمه: {translation_status} | TTS: خطا - هیچ صدای TTS برای انتخاب موجود نیست.", translated_text_output, None
161
-
162
- tts_status, audio_path = loop.run_until_complete(
163
  text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch)
164
  )
165
 
166
- final_status = f"ترجمه: {translation_status} | TTS: {tts_status}"
167
- return final_status, translated_text_output, audio_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- # --- تعریف تم و CSS (همانند قبل) ---
170
  app_theme = gr.themes.Soft(
171
- primary_hue=gr.themes.colors.blue,
172
- secondary_hue=gr.themes.colors.sky,
173
  neutral_hue=gr.themes.colors.slate,
174
  font=[gr.themes.GoogleFont("Vazirmatn"), "Arial", "sans-serif"],
175
  ).set(
176
- body_background_fill="#f4f7f6",
 
177
  )
178
- custom_css = """
179
- body { font-family: 'Vazirmatn', 'Arial', sans-serif; direction: rtl; }
180
- .gradio-container { max-width: 95% !important; margin: 1rem auto !important; padding: 1rem !important; border-radius: 16px !important; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.07) !important; background-color: #ffffff !important; }
181
- .app-header { text-align: center; padding: 20px 10px; background: #34495e; color: white; border-radius: 12px; margin-bottom: 1.5rem; }
182
- .app-header h1 { color: white !important; font-size: 1.6em !important; font-weight: 600 !important; margin: 5px 0; }
183
- .app-header p { color: #bdc3c7 !important; font-size: 0.9em !important; margin-top: 5px; }
184
- .gr-button.lg.primary { background: #3498db !important; color: white !important; font-weight: 500 !important; border-radius: 8px !important; padding: 12px 15px !important; width: 100% !important; font-size: 1em !important; transition: all 0.2s ease !important; box-shadow: 0 3px 6px rgba(52, 152, 219, 0.25) !important; border: none !important; }
185
- .gr-button.lg.primary:hover { background: #2980b9 !important; transform: translateY(-2px) !important; box-shadow: 0 5px 10px rgba(52, 152, 219, 0.35) !important; }
186
- .gr-input, .gr-dropdown, .gr-textbox, .gr-slider { border-radius: 8px !important; border: 1px solid #ced4da !important; font-size: 0.95em !important; }
187
- .gr-input:focus, .gr-dropdown:focus, .gr-textbox:focus { box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15) !important; border-color: #5dade2 !important; }
188
- .gr-panel { border-radius: 10px !important; border: 1px solid #e9ecef !important; background-color: #f8f9fa !important; padding: 0.75rem !important; }
189
- label > span { font-weight: 500 !important; color: #495057 !important; font-size: 0.9em !important; margin-bottom: 3px !important; display: block; }
190
- 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; }
191
- @media (min-width: 768px) {
192
- .gradio-container { max-width: 800px !important; padding: 1.5rem !important;}
193
- .app-header h1 { font-size: 2em !important; }
194
- .app-header p { font-size: 1em !important; }
195
- .main-content-row { display: flex; flex-direction: row; gap: 1.5rem; }
196
- .main-content-row > .gr-column { flex: 1; margin-bottom: 0; }
197
- .main-content-row > .gr-column:nth-child(1) { flex-basis: 60%; }
198
- .main-content-row > .gr-column:nth-child(2) { flex-basis: 40%; }
199
- .gr-button.lg.primary { width: auto !important; }
200
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  """
202
 
203
  # --- انتخاب صدای پیش‌فرض انگلیسی ---
204
- default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)' # یک پیش‌فرض مطمئن
205
- if not language_dict_persian_keys: # اگر دیکشنری کلا خالی ��اشد (نباید اتفاق بیفتد)
206
- print("خطای بحرانی: language_dict_persian_keys خالی است! برنامه نمی‌تواند بدون لیست صداها اجرا شود.")
207
- default_english_tts_voice = None # هیچ صدای پیش‌فرضی وجود ندارد
208
  elif default_english_tts_voice not in language_dict_persian_keys:
209
- # اگر صدای پیش‌فرض ما در لیست موجود نبود، اولین مورد لیست را انتخاب کن
210
  default_english_tts_voice = list(language_dict_persian_keys.keys())[0]
211
 
212
 
213
- with gr.Blocks(theme=app_theme, css=custom_css) as demo:
 
214
  gr.HTML(f"""
215
  <div class="app-header">
216
- <h1>مترجم و گوینده هوشمند (فارسی به انگلیسی)</h1>
217
- <p>ویژه آموزش زبان انگلیسی: متن فارسی خود را وارد کنید، به انگلیسی ترجمه شده و با لهجه دلخواه بشنوید</p>
 
218
  </div>
219
  """)
220
 
@@ -222,27 +342,28 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
222
  missing_key_msg = "⚠️ **هشدار: قابلیت ترجمه غیرفعال است.** "
223
  if not GOOGLE_API_KEY:
224
  missing_key_msg += "لطفاً کلید `GOOGLE_API_KEY` را در بخش **Secrets** این Space تنظیم کنید."
225
- elif not model: # یعنی کلید بوده ولی مدل لود نشده
226
- missing_key_msg += "خطا در بارگذاری مدل Gemini. لطفاً لاگ‌ها را برای جزئیات بیشتر بررسی کنید یا از صحت کلید API اطمینان حاصل کنید."
227
- gr.Markdown(f"<div style='background-color: #fff3cd; color: #856404; padding: 10px; border-radius: 5px; border: 1px solid #ffeeba; text-align: center; margin-bottom:1rem;'>{missing_key_msg}</div>")
228
 
229
  with gr.Row(elem_classes="main-content-row"):
230
- with gr.Column(scale=3):
231
  input_text_persian = gr.Textbox(
232
  lines=5,
233
- label="📝 متن فارسی خود را برای ترجمه و خواندن وارد نمایید",
234
- placeholder="اینجا بنویسید...",
235
- value="سلام، حال شما چطور است؟ می‌خواهم در مورد آب و هوا صحبت کنم." # متن نمونه
236
  )
237
- dropdown_label = "🗣️ لهجه و گوینده انگلیسی را انتخاب کنید"
 
238
  if not language_dict_persian_keys:
239
- dropdown_label += " (لیست صداها خالی است!)"
240
 
241
  language_dropdown_tts_english = gr.Dropdown(
242
  choices=list(language_dict_persian_keys.keys()) if language_dict_persian_keys else ["لیست صداها خالی است"],
243
  value=default_english_tts_voice,
244
  label=dropdown_label,
245
- interactive=bool(language_dict_persian_keys) # اگر لیست صداها خالی باشد غیرفعال شود
246
  )
247
  with gr.Accordion("⚙️ تنظیمات پیشرفته صدا (اختیاری)", open=False):
248
  with gr.Row():
@@ -250,50 +371,61 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
250
  volume_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="حجم (%)", scale=1)
251
  pitch_slider = gr.Slider(minimum=-50, maximum=50, step=1, value=0, label="گام (Hz)", scale=2)
252
 
253
- submit_button = gr.Button("📖 ترجمه و 🔊 تولید صدا", variant="primary")
254
-
255
- with gr.Column(scale=2):
256
- output_status = gr.Textbox(label="📊 وضعیت کلی عملیات", interactive=False, lines=2, placeholder="نتیجه اینجا نمایش داده می‌شود...")
257
- output_text_translated = gr.Textbox(label="📜 متن ترجمه شده (انگلیسی)", interactive=False, lines=4, placeholder="متن ترجمه شده اینجا نمایش داده می‌شود...")
258
- output_audio = gr.Audio(type="filepath", label="🎧 فایل صوتی خروجی (انگلیسی)", format="mp3", interactive=False) # مشخص کردن فرمت mp3
259
 
260
- gr.HTML("<hr style='margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid #dee2e6;'>")
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- # تهیه لیست نمونه‌ها با توجه به صداهای موجود
 
263
  example_list = []
264
  if language_dict_persian_keys:
265
- voice1 = default_english_tts_voice
266
- voice2 = list(language_dict_persian_keys.keys())[1 % len(language_dict_persian_keys)] # دومین صدا اگر موجود باشد
267
- voice3 = list(language_dict_persian_keys.keys())[2 % len(language_dict_persian_keys)] # سومین صدا اگر موجود باشد
 
 
 
268
 
269
  example_list = [
270
- ["سلام دنیا، امیدوارم روز خوبی داشته باشید.", voice1, 0, 0, 0],
271
- ["این یک آزمایش برای سیستم ترجمه و تبدیل به گفتار است.", voice2, +5, 0, 0],
272
- ["هوش مصنوعی آینده روشنی دارد و می‌تواند به بشریت کمک زیادی کند.", voice3, -5, 0, 0],
273
  ]
274
 
275
  if example_list:
276
  gr.Examples(
277
  examples=example_list,
278
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
279
- outputs=[output_status, output_text_translated, output_audio],
280
  fn=translate_and_speak_sync_wrapper,
281
- cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
282
- label="💡 چند نمونه برای شروع"
283
  )
284
  else:
285
- gr.Markdown("<p style='text-align:center; color:grey;'>هیچ نمونه‌ای برای نمایش وجود ندارد (احتمالاً به دلیل عدم وجود صدا در تنظیمات).</p>")
286
 
 
287
 
288
- submit_button.click(
289
- fn=translate_and_speak_sync_wrapper,
290
- inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
291
- outputs=[output_status, output_text_translated, output_audio],
292
- api_name="translate_and_speak"
293
- )
294
 
295
  if __name__ == "__main__":
296
  if not language_dict_persian_keys:
297
  print("خطای بحرانی: language_dict_persian_keys خالی است! برنامه نمی‌تواند بدون لیست صداها اجرا شود.")
298
 
299
- demo.launch(server_name="0.0.0.0", server_port=7860, debug=True if os.environ.get("GRADIO_DEBUG") == "True" else False)
 
 
 
 
 
 
8
  import google.generativeai as genai
9
 
10
  # --- دریافت کلید API از متغیرهای محیطی ---
 
11
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
12
 
13
+ model = None
14
  if GOOGLE_API_KEY:
15
  try:
16
  genai.configure(api_key=GOOGLE_API_KEY)
17
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
18
  print("سرویس Gemini با موفقیت پیکربندی شد.")
19
  except Exception as e:
20
  print(f"خطا در پیکربندی Gemini: {e}. قابلیت ترجمه غیرفعال خواهد بود.")
 
22
  else:
23
  print("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
24
 
25
+ # --- دیکشنری صداهای انگلیسی برای برنامه "آموزش زبان فلای" ---
 
26
  language_dict_persian_keys = {
27
  'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural',
28
  'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
 
31
  'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural',
32
  'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
33
  'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural',
34
+ # 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural', # راجر کمی رباتیک است، شاید بهتر باشد حذف شود
35
  'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural',
36
  'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
37
  'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural',
38
  'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
39
+ # 'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural', # توماس هم ممکن است کمی رسمی باشد
40
  'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural',
41
  'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
42
  'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural',
43
  'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
 
 
 
44
  'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural',
45
+ 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
46
+ # لهجه‌های دیگر انگلیسی را در صورت تمایل اضافه کنید
 
47
  }
48
 
49
  # --- تابع ترجمه با Gemini ---
50
  async def translate_text_gemini(text, target_language="English"):
51
  if not model:
52
+ return "خطا: سرویس ترجمه در دسترس نیست (کلید API یا مدل مشکل دارد).", None
53
  if not text or not text.strip():
54
  return "خطا: متنی برای ترجمه وارد نشده است.", None
55
  try:
56
+ 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}:"
 
57
  response = await model.generate_content_async(prompt)
58
  translated_text = response.text.strip()
59
+ # بررسی مجدد برای حذف عبارات ناخواسته احتمالی (بسیار نادر با prompt دقیق)
60
+ if translated_text.lower().startswith(f"{target_language.lower()}:"):
61
+ translated_text = translated_text[len(target_language)+1:].strip()
62
+ return "ترجمه موفق", translated_text # فقط پیام موفقیت و متن
63
  except Exception as e:
64
+ print(f"خطای Gemini: {e}\n{traceback.format_exc()}")
65
+ return f"خطای ترجمه: {type(e).__name__}", None
 
 
 
 
 
 
66
 
67
  # --- توابع تبدیل متن به گفتار ---
68
  async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
69
  try:
70
  if not text_to_speak or not text_to_speak.strip():
71
+ return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
 
72
  voice_id = language_dict_persian_keys.get(tts_voice_key)
73
+ if voice_id is None:
74
+ return f"خطای TTS: صدای '{tts_voice_key}' یافت نشد.", None
 
 
75
  if not voice_id.startswith("en-"):
76
+ return f"خطای پیکربندی TTS: صدای '{tts_voice_key}' انگلیسی نیست.", None
 
 
 
 
 
77
 
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 edge_tts.exceptions.NoAudioReceived:
85
+ return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد. (NoAudioReceived)", None
 
 
 
 
 
 
86
  except Exception as e:
87
+ print(f"خطای Edge-TTS: {e}\n{traceback.format_exc()}")
88
+ return f"خطای TTS: {type(e).__name__}", None
 
89
 
90
  _event_loops_by_thread = {}
91
  def _get_or_create_event_loop():
92
  thread_id = threading.get_ident()
93
  if thread_id not in _event_loops_by_thread or _event_loops_by_thread[thread_id].is_closed():
94
+ loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
 
95
  _event_loops_by_thread[thread_id] = loop
96
  return _event_loops_by_thread[thread_id]
97
 
98
  # --- تابع اصلی: ترجمه و سپس تبدیل به گفتار ---
99
  def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
100
  loop = _get_or_create_event_loop()
101
+ translated_text_output = "ترجمه در حال انجام..."
102
+ audio_output_path = None
103
+ error_message_for_ui = ""
104
+
105
  if not GOOGLE_API_KEY or not model:
106
+ error_message_for_ui = "خطا: سرویس ترجمه پیکربندی نشده است. لطفاً از تنظیم صحیح GOOGLE_API_KEY در Secrets اطمینان حاصل کنید."
107
+ return error_message_for_ui, None
 
 
 
 
108
 
109
  if not persian_text or not persian_text.strip():
110
+ error_message_for_ui = "لطفاً متن فارسی را برای ترجمه وارد کنید."
111
+ return error_message_for_ui, None
112
 
113
+ # 1. ترجمه متن
114
+ translation_status_msg, translated_text = loop.run_until_complete(
115
  translate_text_gemini(persian_text, target_language="English")
116
  )
117
+ translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود."
118
 
119
+ if "خطا" in translation_status_msg or not translated_text:
120
+ error_message_for_ui = f"{translated_text_output}\n({translation_status_msg})"
121
+ return error_message_for_ui, None # نمایش خطا در بخش متن ترجمه شده
 
122
 
123
+ # 2. تبدیل متن ترجمه شده به گفتار
 
124
  if english_tts_voice_key not in language_dict_persian_keys:
125
+ if language_dict_persian_keys: # اگر لیست خالی نباشد
 
 
 
126
  english_tts_voice_key = list(language_dict_persian_keys.keys())[0]
127
+ else:
128
+ error_message_for_ui = f"{translated_text_output}\n\n(خطای TTS: هیچ صدایی برای انتخاب موجود نیست.)"
129
+ return error_message_for_ui, None
130
+
131
+ tts_status_msg, audio_path = loop.run_until_complete(
132
  text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch)
133
  )
134
 
135
+ if "خطا" in tts_status_msg or not audio_path:
136
+ # اگر TTS خطا داشت، خطا را به متن ترجمه شده اضافه می‌کنیم
137
+ error_message_for_ui = f"{translated_text_output}\n\n({tts_status_msg})"
138
+ return error_message_for_ui, None
139
+
140
+ audio_output_path = audio_path
141
+ return translated_text_output, audio_output_path
142
+
143
+
144
+ # --- تعریف تم و CSS ---
145
+ FLY_PRIMARY_COLOR = "#4A90E2" # یک آبی آسمانی مدرن
146
+ FLY_SECONDARY_COLOR = "#50E3C2" # یک سبز-آبی تازه
147
+ FLY_ACCENT_COLOR = "#F5A623" # یک نارنجی برای تاکید (مثلا برای دکمه‌ها)
148
+ FLY_TEXT_COLOR = "#4A4A4A"
149
+ FLY_LIGHT_BACKGROUND = "#F7F9FA"
150
+ FLY_WHITE = "#FFFFFF"
151
 
 
152
  app_theme = gr.themes.Soft(
153
+ primary_hue=FLY_PRIMARY_COLOR, #gr.themes.colors.sky,
154
+ secondary_hue=FLY_SECONDARY_COLOR, #gr.themes.colors.teal,
155
  neutral_hue=gr.themes.colors.slate,
156
  font=[gr.themes.GoogleFont("Vazirmatn"), "Arial", "sans-serif"],
157
  ).set(
158
+ body_background_fill=FLY_LIGHT_BACKGROUND,
159
+ # input_background_fill="#E9EFF5" # رنگ پس زمینه ورودی‌ها کمی متفاوت
160
  )
161
+
162
+ custom_css = f"""
163
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap');
164
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); /* فونت انگلیسی جذاب */
165
+
166
+ body {{
167
+ font-family: 'Vazirmatn', 'Poppins', Arial, sans-serif;
168
+ direction: rtl;
169
+ background-color: {FLY_LIGHT_BACKGROUND};
170
+ color: {FLY_TEXT_COLOR};
171
+ line-height: 1.7;
172
+ }}
173
+
174
+ .gradio-container {{
175
+ max-width: 800px !important;
176
+ margin: 2.5rem auto !important;
177
+ padding: 2rem 2.5rem !important;
178
+ border-radius: 24px !important;
179
+ box-shadow: 0 12px 35px rgba(0, 0, 0, 0.07), 0 3px 8px rgba(0,0,0,0.03) !important;
180
+ background-color: {FLY_WHITE} !important;
181
+ overflow: hidden; /* برای جلوگیری از سرریز شدن سایه ها یا انیمیشن ها */
182
+ }}
183
+
184
+ .app-header {{
185
+ text-align: center;
186
+ padding: 35px 20px;
187
+ background-image: linear-gradient(120deg, {FLY_PRIMARY_COLOR} 0%, {FLY_SECONDARY_COLOR} 100%);
188
+ color: white;
189
+ border-radius: 18px;
190
+ margin: -2.5rem -2.5rem 2.5rem -2.5rem; /* هدر تمام عرض کانتینر را بگیرد و از پدینگ داخلی بیرون بزند */
191
+ box-shadow: 0 6px 20px rgba(74, 144, 226, 0.3);
192
+ }}
193
+ /* .app-header img.logo {{ اگر لوگو داشتید
194
+ width: 70px; height: auto; margin-bottom: 12px;
195
+ animation: gentleFloat 3s ease-in-out infinite alternate;
196
+ }} */
197
+ .app-header h1 {{ /* نام برنامه "آموزش زبان فلای" */
198
+ color: white !important;
199
+ font-size: 2.2em !important;
200
+ font-weight: 700 !important;
201
+ margin: 0 0 8px 0;
202
+ letter-spacing: 0.5px;
203
+ font-family: 'Poppins', 'Vazirmatn', sans-serif; /* فونت انگلیسی برای عنوان اصلی */
204
+ }}
205
+ .app-header p {{ /* توضیحات زیر عنوان */
206
+ color: rgba(255, 255, 255, 0.85) !important;
207
+ font-size: 1.05em !important;
208
+ margin-top: 5px;
209
+ font-weight: 300;
210
+ }}
211
+
212
+ .gr-button.lg.primary {{ /* دکمه اصلی "ترجمه و تولید صدا" */
213
+ background: {FLY_ACCENT_COLOR} !important; /* رنگ نارنجی تاکیدی */
214
+ /* background-image: linear-gradient(to right, #FF8C00 0%, #F5A623 51%, #FF8C00 100%) !important; */
215
+ margin-top: 1.5rem !important;
216
+ padding: 15px 28px !important;
217
+ text-align: center;
218
+ transition: all 0.35s ease !important;
219
+ background-size: 200% auto !important;
220
+ color: white !important;
221
+ font-weight: 600 !important;
222
+ border-radius: 12px !important;
223
+ border: none !important;
224
+ box-shadow: 0 5px 15px rgba(245, 166, 35, 0.35) !important;
225
+ width: 100% !important;
226
+ font-size: 1.1em !important;
227
+ letter-spacing: 0.3px;
228
+ }}
229
+ .gr-button.lg.primary:hover {{
230
+ /* background-position: right center !important; */
231
+ background: #e0911a !important; /* کمی تیره‌تر در هاور */
232
+ transform: translateY(-3px) scale(1.02) !important;
233
+ box-shadow: 0 8px 20px rgba(245, 166, 35, 0.45) !important;
234
+ }}
235
+
236
+ /* استایل ورودی‌ها، دراپ‌داون و تکست‌باکس‌ها */
237
+ .gr-input, .gr-dropdown, .gr-textbox, .gr-slider {{
238
+ border-radius: 10px !important;
239
+ border: 1.5px solid #DCE1E7 !important; /* حاشیه کمی مشخص‌تر */
240
+ font-size: 1em !important;
241
+ background-color: #FDFEFF !important;
242
+ padding-top: 10px !important; padding-bottom: 10px !important; /* پدینگ داخلی بیشتر */
243
+ transition: border-color 0.25s ease, box-shadow 0.25s ease;
244
+ }}
245
+ .gr-input:focus, .gr-dropdown:focus, .gr-textbox:focus {{
246
+ border-color: {FLY_PRIMARY_COLOR} !important;
247
+ box-shadow: 0 0 0 3.5px rgba(74, 144, 226, 0.18) !important;
248
+ background-color: {FLY_WHITE} !important;
249
+ }}
250
+ .gr-textbox[label="📜 متن ترجمه شده (انگلیسی)"] {{ /* استایل خاص برای باکس متن ترجمه شده */
251
+ background-color: #EDF5FD !important; /* پس‌زمینه آبی بسیار روشن */
252
+ border-color: #C9DFF5 !important;
253
+ min-height: 120px; /* حداقل ارتفاع */
254
+ font-family: 'Poppins', 'Vazirmatn', sans-serif; /* فونت انگلیسی برای متن ترجمه شده */
255
+ font-size: 1.05em !important;
256
+ line-height: 1.6;
257
+ }}
258
+
259
+ /* استایل آکاردئون (تنظیمات پیشرفته) */
260
+ .gr-panel, div[label="⚙️ تنظیمات پیشرفته صدا (اختیاری)"] > .gr-accordion > .gr-panel {{
261
+ border-radius: 12px !important;
262
+ border: 1px solid #E8EDF2 !important;
263
+ background-color: {FLY_LIGHT_BACKGROUND} !important;
264
+ padding: 1rem 1.2rem !important;
265
+ margin-top: 0.8rem;
266
+ box-shadow: 0 2px 5px rgba(0,0,0,0.03);
267
+ }}
268
+ div[label="⚙️ تنظیمات پیشرفته صدا (اختیاری)"] > .gr-accordion > button.gr-button {{ /* دکمه باز/بسته کردن آکاردئون */
269
+ font-weight: 500 !important;
270
+ padding: 10px 12px !important;
271
+ border-radius: 8px !important;
272
+ background-color: #E9EFF5 !important;
273
+ color: {FLY_TEXT_COLOR} !important;
274
+ }}
275
+
276
+ label > span {{ /* استایل عمومی لیبل‌ها */
277
+ font-weight: 600 !important;
278
+ color: #333D47 !important;
279
+ font-size: 0.95em !important;
280
+ margin-bottom: 8px !important;
281
+ }}
282
+ .gr-slider label span {{ font-size: 0.9em !important; color: #555; }}
283
+
284
+ /* استایل بخش نمونه‌ها */
285
+ div[label="💡 چند نمونه برای شروع"] {{
286
+ margin-top: 2rem;
287
+ }}
288
+ div[label="💡 چند نمونه برای شروع"] .gr-button.gr-button-tool {{ /* دکمه‌های کوچک نمونه‌ها */
289
+ background-color: #DDE7F0 !important;
290
+ color: {FLY_PRIMARY_COLOR} !important;
291
+ border-radius: 8px !important;
292
+ font-size: 0.85em !important;
293
+ padding: 6px 10px !important;
294
+ transition: background-color 0.2s ease;
295
+ }}
296
+ div[label="💡 چند نمونه برای شروع"] .gr-button.gr-button-tool:hover {{
297
+ background-color: #C9D9E6 !important;
298
+ }}
299
+
300
+ /* مخفی کردن فوتر و بخش‌های اضافی گرادیو */
301
+ 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 {{
302
+ display: none !important; visibility: hidden !important;
303
+ }}
304
+
305
+ /* انیمیشن برای لوگو (اگر استفاده شود) */
306
+ /* @keyframes gentleFloat {{
307
+ 0% {{ transform: translateY(0px); }}
308
+ 50% {{ transform: translateY(-6px); }}
309
+ 100% {{ transform: translateY(0px); }}
310
+ }} */
311
+
312
+ @media (max-width: 768px) {{
313
+ .gradio-container {{ margin: 1.5rem auto !important; padding: 1.5rem !important; border-radius: 16px !important; }}
314
+ .app-header {{ padding: 25px 15px; border-radius: 12px; margin: -1.5rem -1.5rem 2rem -1.5rem; }}
315
+ .app-header h1 {{ font-size: 1.8em !important; }}
316
+ .app-header p {{ font-size: 0.95em !important; }}
317
+ .main-content-row {{ flex-direction: column; }}
318
+ .gr-button.lg.primary {{ font-size: 1em !important; padding: 14px 20px !important; }}
319
+ }}
320
  """
321
 
322
  # --- انتخاب صدای پیش‌فرض انگلیسی ---
323
+ default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
324
+ if not language_dict_persian_keys:
325
+ print("خطای بحرانی: language_dict_persian_keys خالی است!")
326
+ default_english_tts_voice = None
327
  elif default_english_tts_voice not in language_dict_persian_keys:
 
328
  default_english_tts_voice = list(language_dict_persian_keys.keys())[0]
329
 
330
 
331
+ with gr.Blocks(theme=app_theme, css=custom_css, title="آموزش زبان فلای") as demo:
332
+ # لوگو را اگر دارید، می‌توانید اینجا با gr.Image یا در HTML قرار دهید
333
  gr.HTML(f"""
334
  <div class="app-header">
335
+ <!-- <img src="URL_LOGO_شما_اگر_دارید" alt="لوگو فلای" class="logo"> -->
336
+ <h1>Fly Language Learning</h1>
337
+ <p>متن فارسی خود را وارد کنید، به انگلیسی ترجمه شده و با لهجه دلخواه بشنوید</p>
338
  </div>
339
  """)
340
 
 
342
  missing_key_msg = "⚠️ **هشدار: قابلیت ترجمه غیرفعال است.** "
343
  if not GOOGLE_API_KEY:
344
  missing_key_msg += "لطفاً کلید `GOOGLE_API_KEY` را در بخش **Secrets** این Space تنظیم کنید."
345
+ elif not model:
346
+ missing_key_msg += "خطا در بارگذاری مدل Gemini. لطفاً لاگ‌ها یا صحت کلید API را بررسی کنید."
347
+ gr.Markdown(f"<div style='background-color: #FFF3CD; color: #856404; padding: 12px 15px; border-radius: 10px; border: 1px solid #FFEEBA; text-align: center; margin-bottom:1.5rem; font-size:0.95em;'>{missing_key_msg}</div>")
348
 
349
  with gr.Row(elem_classes="main-content-row"):
350
+ with gr.Column(scale=3, min_width=350):
351
  input_text_persian = gr.Textbox(
352
  lines=5,
353
+ label="📝 متن فارسی برای ترجمه و گفتار",
354
+ placeholder="مثال: سلام، چطوری؟ می‌خوام در مورد سفر به لندن صحبت کنیم...",
355
+ value="سلام، حال شما چطور است؟ امروز می‌خواهیم چند عبارت کاربردی در مورد خرید کردن یاد بگیریم."
356
  )
357
+
358
+ dropdown_label = "🗣️ انتخاب لهجه و گوینده انگلیسی"
359
  if not language_dict_persian_keys:
360
+ dropdown_label += " (لیست صداها بارگذاری نشده!)"
361
 
362
  language_dropdown_tts_english = gr.Dropdown(
363
  choices=list(language_dict_persian_keys.keys()) if language_dict_persian_keys else ["لیست صداها خالی است"],
364
  value=default_english_tts_voice,
365
  label=dropdown_label,
366
+ interactive=bool(language_dict_persian_keys)
367
  )
368
  with gr.Accordion("⚙️ تنظیمات پیشرفته صدا (اختیاری)", open=False):
369
  with gr.Row():
 
371
  volume_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="حجم (%)", scale=1)
372
  pitch_slider = gr.Slider(minimum=-50, maximum=50, step=1, value=0, label="گام (Hz)", scale=2)
373
 
374
+ submit_button = gr.Button("🚀 ترجمه و پخش صدا", variant="primary")
 
 
 
 
 
375
 
376
+ with gr.Column(scale=2, min_width=300):
377
+ output_text_translated = gr.Textbox(
378
+ label="📜 متن ترجمه شده (انگلیسی)",
379
+ interactive=False,
380
+ lines=7, # خطوط بیشتر برای نمایش بهتر متن و خطاهای احتمالی
381
+ placeholder="متن انگلیسی ترجمه شده و یا پیام‌های خطا اینجا نمایش داده می‌شود..."
382
+ )
383
+ output_audio = gr.Audio(
384
+ type="filepath",
385
+ label="🎧 فایل صوتی (انگلیسی)",
386
+ format="mp3",
387
+ interactive=False
388
+ )
389
 
390
+ gr.HTML("<div style='height: 1px; background-color: #E5E7EB; margin-top: 2rem; margin-bottom: 1.5rem;'></div>") # جداکننده خطی
391
+
392
  example_list = []
393
  if language_dict_persian_keys:
394
+ # انتخاب صداها برای نمونه‌ها با دقت بیشتر برای جلوگیری از خطا
395
+ num_voices = len(language_dict_persian_keys)
396
+ voice_keys = list(language_dict_persian_keys.keys())
397
+ voice1 = voice_keys[0]
398
+ voice2 = voice_keys[min(1, num_voices-1)]
399
+ voice3 = voice_keys[min(2, num_voices-1)]
400
 
401
  example_list = [
402
+ ["چطور میتونم به نزدیکترین ایستگاه اتوبوس برم؟", voice1, 0, 0, 0],
403
+ ["هزینه این بلیط چقدر است؟", voice2, +5, 0, 0],
404
+ ["می‌توانید این جمله را کمی آهسته‌تر تکرار کنید؟", voice3, -10, 0, 0],
405
  ]
406
 
407
  if example_list:
408
  gr.Examples(
409
  examples=example_list,
410
  inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
411
+ outputs=[output_text_translated, output_audio],
412
  fn=translate_and_speak_sync_wrapper,
413
+ cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true", # برای توسعه True و برای پابلیش False
414
+ label="💡 چند نمونه برای شروع سریع"
415
  )
416
  else:
417
+ gr.Markdown("<p style='text-align:center; color:#777; margin-top:1rem;'>نمونه‌ای برای نمایش وجود ندارد (بررسی تنظیمات صدا).</p>")
418
 
419
+ gr.Markdown("<p style='text-align:center; font-size:0.85em; color:#999; margin-top:2.5rem;'>ساخته شده با ❤️ برای آموزش زبان فلای</p>")
420
 
 
 
 
 
 
 
421
 
422
  if __name__ == "__main__":
423
  if not language_dict_persian_keys:
424
  print("خطای بحرانی: language_dict_persian_keys خالی است! برنامه نمی‌تواند بدون لیست صداها اجرا شود.")
425
 
426
+ demo.launch(
427
+ server_name="0.0.0.0",
428
+ server_port=7860,
429
+ debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true",
430
+ # share=True # اگر می‌خواهید لینک عمومی موقت برای تست داشته باشید (در هاگینگ فیس لازم نیست)
431
+ )