Hamed744 commited on
Commit
8a69473
·
verified ·
1 Parent(s): fe7c760

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -211
app.py CHANGED
@@ -10,9 +10,7 @@ import logging
10
 
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
12
 
13
- # --- دریافت کلید API از متغیرهای محیطی ---
14
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
15
-
16
  model = None
17
  if GOOGLE_API_KEY:
18
  try:
@@ -25,14 +23,13 @@ if GOOGLE_API_KEY:
25
  else:
26
  logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
27
 
28
- # --- دیکشنری کامل‌تر صداهای انگلیسی ---
29
- # !!! توجه: این لیست باید با خروجی `edge-tts --list-voices` در محیط اجرایی شما بررسی و به‌روز شود.
30
  language_dict_persian_keys = {
31
  'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
32
  'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
33
  'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural', 'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
34
  'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural', 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
35
- 'انگلیسی (آمریکا) - استفانی (زن)': 'en-US-SteffanNeural', 'انگلیسی مریکا) - جیسون (مرد)': 'en-US-JasonNeural',
 
36
  'انگلیسی (آمریکا) - براندون (مرد)': 'en-US-BrandonNeural', 'انگلیسی (آمریکا) - الیزابت (زن)': 'en-US-ElizabethNeural',
37
  'انگلیسی (آمریکا) - دیویس (مرد، جدید)': 'en-US-DavisNeural', 'انگلیسی (آمریکا) - جین (زن، جدید)': 'en-US-JaneNeural',
38
  'انگلیسی (آمریکا) - نانسی (زن، جدید)': 'en-US-NancyNeural', 'انگلیسی (آمریکا) - سارا (زن، جدید)': 'en-US-SaraNeural',
@@ -47,10 +44,11 @@ language_dict_persian_keys = {
47
  'انگلیسی (استرالیا) - کن (مرد، جدید)': 'en-AU-KenNeural', 'انگلیسی (استرالیا) - کیمبرلی (زن، جدید)': 'en-AU-KimNeural',
48
  'انگلیسی (استرالیا) - تینا (زن، جدید)': 'en-AU-TinaNeural',
49
  'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural', 'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
50
- 'انگلیسی (کانادا) - کوبی (مرد، جدید)': 'en-CA-CobiNeural', 'انگلیسی (کانادا) - ژان (مرد، جدید)': 'en-CA-JeanNeural', # یکی از موارد مشکل‌دار
 
51
  'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural', 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
52
  'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural', 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
53
- 'انگلیسی (هند) - میتالی (زن، جدید)': 'en-IN-MithaliNeural', # یکی از موارد مشکل‌دار
54
  'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural', 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
55
  'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural', 'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
56
  'انگلیسی (نیوزیلند) - هیزل (زن)': 'en-NZ-HazelNeural', 'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
@@ -61,6 +59,7 @@ language_dict_persian_keys = {
61
  'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
62
  }
63
 
 
64
  async def translate_text_gemini(text, target_language="English"):
65
  if not model: return "خطا: سرویس ترجمه در دسترس نیست (کلید API یا مدل مشکل دارد).", None
66
  if not text or not text.strip(): return "خطا: متنی برای ترجمه وارد نشده است.", None
@@ -75,6 +74,9 @@ async def translate_text_gemini(text, target_language="English"):
75
  logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
76
  return f"خطای ترجمه: {type(e).__name__}", None
77
 
 
 
 
78
  async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
79
  voice_id = language_dict_persian_keys.get(tts_voice_key)
80
  if voice_id is None:
@@ -83,21 +85,34 @@ async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume,
83
 
84
  logging.info(f"تلاش برای تولید صدا با: VoiceKey='{tts_voice_key}', VoiceID='{voice_id}', Text='{text_to_speak[:30]}...'")
85
 
86
- try:
87
- if not text_to_speak or not text_to_speak.strip(): return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
90
- communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
91
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file: tmp_path = tmp_file.name
92
- await communicate.save(tmp_path)
93
- logging.info(f"صدای '{voice_id}' با موفقیت در '{tmp_path}' ذخیره شد.")
94
- return "TTS موفق", tmp_path
95
- except edge_tts.exceptions.NoAudioReceived:
96
- logging.error(f"خطای NoAudioReceived برای VoiceID='{voice_id}', VoiceKey='{tts_voice_key}'. متن: '{text_to_speak[:50]}...'")
97
- return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد (NoAudioReceived). ممکن است این صدا موقتا در دسترس نباشد.", None
98
- except Exception as e:
99
- logging.error(f"خطای Edge-TTS برای VoiceID='{voice_id}', VoiceKey='{tts_voice_key}': {e}\n{traceback.format_exc()}")
100
- return f"خطای TTS ({type(e).__name__}): مشکلی در تولید صدا برای '{tts_voice_key}' پیش آمد.", None
 
 
101
 
102
  _event_loops_by_thread = {}
103
  def _get_or_create_event_loop():
@@ -141,7 +156,6 @@ def translate_and_speak_sync_wrapper(persian_text, english_tts_voice_key, rate,
141
 
142
  return translated_text_output, audio_path
143
 
144
- # --- تعریف تم و CSS ---
145
  FLY_PRIMARY_COLOR_HEX = "#4F46E5"
146
  FLY_SECONDARY_COLOR_HEX = "#10B981"
147
  FLY_ACCENT_COLOR_HEX = "#D97706"
@@ -163,199 +177,46 @@ custom_css = f"""
163
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
164
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
165
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
166
-
167
  :root {{
168
- --fly-primary: {FLY_PRIMARY_COLOR_HEX};
169
- --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
170
- --fly-accent: {FLY_ACCENT_COLOR_HEX};
171
- --fly-text-primary: {FLY_TEXT_COLOR_HEX};
172
- --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX};
173
- --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
174
- --fly-bg-white: {FLY_WHITE_HEX};
175
- --fly-border-color: {FLY_BORDER_COLOR_HEX};
176
- --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE};
177
- --fly-panel-bg-simple: {FLY_PANEL_BG_SIMPLE};
178
-
179
  --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
180
  --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
181
-
182
  --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; --radius-full: 9999px;
183
- --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
184
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
185
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
186
- --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
187
- --fly-primary-rgb: 79, 70, 229;
188
- --fly-accent-rgb: 217, 119, 6;
189
- }}
190
-
191
- 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;}}
192
- .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;}}
193
-
194
- .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;}}
195
- .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);}}
196
- .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);}}
197
- .app-title-card p {{font-size: 1em !important; margin-top: 0.25rem; font-weight: 400; color: rgba(255, 255, 255, 0.85) !important;}}
198
-
199
- .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);}}
200
- 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; }}
201
-
202
- .main-content-area {{
203
- flex-grow: 1;
204
- padding: 0.75rem;
205
- width: 100%;
206
- margin: 0 auto;
207
- box-sizing: border-box;
208
- }}
209
- .content-panel-simple {{
210
- background-color: var(--fly-bg-white);
211
- padding: 1rem;
212
- border-radius: var(--radius-xl);
213
- box-shadow: var(--shadow-xl);
214
- margin-top: -2rem;
215
- position: relative;
216
- z-index: 10;
217
- margin-bottom: 2rem;
218
- width: 100%;
219
- box-sizing: border-box;
220
- }}
221
-
222
- .content-panel-simple .gr-button.lg.primary,
223
- .content-panel-simple button[variant="primary"]
224
- {{
225
- background: var(--fly-accent) !important;
226
- margin-top: 1rem !important;
227
- padding: 12px 20px !important;
228
- transition: all 0.25s ease-in-out !important;
229
- color: white !important;
230
- font-weight: 600 !important;
231
- border-radius: 10px !important;
232
- border: none !important;
233
- box-shadow: 0 3px 8px -1px rgba(var(--fly-accent-rgb), 0.3) !important;
234
- width: 100% !important;
235
- font-size: 1em !important;
236
- display: flex;
237
- align-items: center;
238
- justify-content: center;
239
- }}
240
- .content-panel-simple .gr-button.lg.primary:hover,
241
- .content-panel-simple button[variant="primary"]:hover {{
242
- background: #B45309 !important;
243
- transform: translateY(-1px) !important;
244
- box-shadow: 0 5px 10px -1px rgba(var(--fly-accent-rgb), 0.4) !important;
245
- }}
246
-
247
- .content-panel-simple .gr-input > label + div > textarea,
248
- .content-panel-simple .gr-dropdown > label + div > div > input,
249
- .content-panel-simple .gr-dropdown > label + div > div > select,
250
- .content-panel-simple .gr-textbox > label + div > textarea {{
251
- border-radius: 8px !important;
252
- border: 1.5px solid var(--fly-border-color) !important;
253
- font-size: 0.95em !important;
254
- background-color: var(--fly-input-bg-simple) !important;
255
- padding: 10px 12px !important;
256
- color: var(--fly-text-primary) !important;
257
- }}
258
- .content-panel-simple .gr-input > label + div > textarea:focus,
259
- .content-panel-simple .gr-dropdown > label + div > div > input:focus,
260
- .content-panel-simple .gr-dropdown > label + div > div > select:focus,
261
- .content-panel-simple .gr-textbox > label + div > textarea:focus {{
262
- border-color: var(--fly-primary) !important;
263
- box-shadow: 0 0 0 3px rgba(var(--fly-primary-rgb), 0.12) !important;
264
- background-color: var(--fly-bg-white) !important;
265
- }}
266
-
267
- .content-panel-simple .gr-dropdown select {{
268
- font-family: var(--font-global) !important;
269
- width: 100%;
270
- cursor: pointer;
271
- }}
272
-
273
- .content-panel-simple .gr-textbox[label*="ترجمه شده"] > label + div > textarea {{
274
- background-color: var(--fly-panel-bg-simple) !important;
275
- border-color: #A5D5FE !important;
276
- min-height: 100px;
277
- font-family: var(--font-english);
278
- font-size: 1em !important;
279
- line-height: 1.5;
280
- padding: 10px !important;
281
- }}
282
- .content-panel-simple .gr-panel,
283
- .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > .gr-panel {{
284
- border-radius: 8px !important;
285
- border: 1px solid var(--fly-border-color) !important;
286
- background-color: var(--fly-input-bg-simple) !important;
287
- padding: 0.8rem 1rem !important;
288
- margin-top: 0.6rem;
289
- box-shadow: none;
290
- }}
291
- .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > button.gr-button {{
292
- font-weight: 500 !important;
293
- padding: 8px 10px !important;
294
- border-radius: 6px !important;
295
- background-color: #E5E7EB !important;
296
- color: var(--fly-text-primary) !important;
297
- border: 1px solid #D1D5DB !important;
298
- }}
299
- .content-panel-simple label > span.label-text {{
300
- font-weight: 500 !important;
301
- color: #4B5563 !important;
302
- font-size: 0.88em !important;
303
- margin-bottom: 6px !important;
304
- display: inline-block;
305
- }}
306
- .content-panel-simple .gr-slider label span {{ font-size: 0.82em !important; color: var(--fly-text-secondary); }}
307
-
308
- .content-panel-simple div[label*="نمونه"] {{ margin-top: 1.5rem; }}
309
- .content-panel-simple div[label*="نمونه"] .gr-button.gr-button-tool,
310
- .content-panel-simple div[label*="نمونه"] .gr-sample-button {{
311
- background-color: #E0E7FF !important;
312
- color: var(--fly-primary) !important;
313
- border-radius: 6px !important;
314
- font-size: 0.78em !important;
315
- padding: 4px 8px !important;
316
- }}
317
- .content-panel-simple .custom-hr {{ height: 1px; background-color: var(--fly-border-color); margin: 1.5rem 0; border: none; }}
318
- .api-warning-message {{
319
- background-color: #FFFBEB !important;
320
- color: #92400E !important;
321
- padding: 10px 12px !important;
322
- border-radius: 8px !important;
323
- border: 1px solid #FDE68A !important;
324
- text-align: center !important;
325
- margin: 0 0.2rem 1rem 0.2rem !important;
326
- font-size: 0.85em !important;
327
- }}
328
-
329
- @media (min-width: 640px) {{
330
- .main-content-area {{
331
- padding: 1.5rem;
332
- max-width: 700px;
333
- }}
334
- .content-panel-simple {{
335
- padding: 1.5rem;
336
- }}
337
- .app-title-card h1 {{ font-size: 2.5em !important; }}
338
- .app-title-card p {{ font-size: 1.05em !important; }}
339
- }}
340
-
341
- @media (min-width: 768px) {{
342
- .main-content-area {{ max-width: 780px; }}
343
- .content-panel-simple {{ padding: 2rem; }}
344
-
345
- .content-panel-simple .main-content-row {{
346
- display: flex !important;
347
- flex-direction: row !important;
348
- gap: 1.5rem !important;
349
- }}
350
- .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{ flex-basis: 60%; }}
351
- .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{ flex-basis: 40%; }}
352
-
353
- .content-panel-simple .gr-button.lg.primary,
354
- .content-panel-simple button[variant="primary"] {{ width: auto !important; align-self: flex-start; }}
355
-
356
- .app-title-card h1 {{font-size: 2.75em !important;}}
357
- .app-title-card p {{font-size: 1.1em !important;}}
358
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  """
360
 
361
  default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
@@ -420,7 +281,7 @@ with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آموزش زبان
420
  label="🎧 فایل صوتی",
421
  format="mp3",
422
  interactive=False,
423
- autoplay=True # <--- پخش خودکار فعال شد
424
  )
425
 
426
  if language_dict_persian_keys and default_english_tts_voice and default_english_tts_voice != "لیست صداها خالی است":
 
10
 
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
12
 
 
13
  GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
 
14
  model = None
15
  if GOOGLE_API_KEY:
16
  try:
 
23
  else:
24
  logging.warning("هشدار: کلید API گوگل (GOOGLE_API_KEY) تنظیم نشده است. قابلیت ترجمه غیرفعال خواهد بود.")
25
 
 
 
26
  language_dict_persian_keys = {
27
  'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
28
  'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
29
  'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural', 'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
30
  'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural', 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
31
+ # 'انگلیسی (آمریکا) - استفانی (زن)': 'en-US-SteffanNeural', # ممکن است منسوخ شده باشد
32
+ 'انگلیسی (آمریکا) - جیسون (مرد)': 'en-US-JasonNeural',
33
  'انگلیسی (آمریکا) - براندون (مرد)': 'en-US-BrandonNeural', 'انگلیسی (آمریکا) - الیزابت (زن)': 'en-US-ElizabethNeural',
34
  'انگلیسی (آمریکا) - دیویس (مرد، جدید)': 'en-US-DavisNeural', 'انگلیسی (آمریکا) - جین (زن، جدید)': 'en-US-JaneNeural',
35
  'انگلیسی (آمریکا) - نانسی (زن، جدید)': 'en-US-NancyNeural', 'انگلیسی (آمریکا) - سارا (زن، جدید)': 'en-US-SaraNeural',
 
44
  'انگلیسی (استرالیا) - کن (مرد، جدید)': 'en-AU-KenNeural', 'انگلیسی (استرالیا) - کیمبرلی (زن، جدید)': 'en-AU-KimNeural',
45
  'انگلیسی (استرالیا) - تینا (زن، جدید)': 'en-AU-TinaNeural',
46
  'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural', 'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
47
+ # 'انگلیسی (کانادا) - کوبی (مرد، جدید)': 'en-CA-CobiNeural', # بررسی شود
48
+ 'انگلیسی (کانادا) - ژان (مرد، جدید)': 'en-CA-JeanNeural',
49
  'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural', 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
50
  'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural', 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
51
+ # 'انگلیسی (هند) - میتالی (زن، جدید)': 'en-IN-MithaliNeural', # بررسی شود
52
  'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural', 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
53
  'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural', 'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
54
  'انگلیسی (نیوزیلند) - هیزل (زن)': 'en-NZ-HazelNeural', 'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
 
59
  'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural', 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
60
  }
61
 
62
+
63
  async def translate_text_gemini(text, target_language="English"):
64
  if not model: return "خطا: سرویس ترجمه در دسترس نیست (کلید API یا مدل مشکل دارد).", None
65
  if not text or not text.strip(): return "خطا: متنی برای ترجمه وارد نشده است.", None
 
74
  logging.error(f"خطای Gemini: {e}\n{traceback.format_exc()}")
75
  return f"خطای ترجمه: {type(e).__name__}", None
76
 
77
+ MAX_TTS_RETRIES = 1 # کاهش تعداد تلاش مجدد برای جلوگیری از کندی زیاد
78
+ TTS_RETRY_DELAY = 0.5 # کاهش تاخیر
79
+
80
  async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
81
  voice_id = language_dict_persian_keys.get(tts_voice_key)
82
  if voice_id is None:
 
85
 
86
  logging.info(f"تلاش برای تولید صدا با: VoiceKey='{tts_voice_key}', VoiceID='{voice_id}', Text='{text_to_speak[:30]}...'")
87
 
88
+ if not text_to_speak or not text_to_speak.strip():
89
+ return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
90
+
91
+ for attempt in range(MAX_TTS_RETRIES + 1):
92
+ try:
93
+ rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
94
+ communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
95
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
96
+ tmp_path = tmp_file.name
97
+
98
+ await communicate.save(tmp_path)
99
+ logging.info(f"صدای '{voice_id}' (تلاش {attempt + 1}) با موفقیت در '{tmp_path}' ذخیره شد.")
100
+ return "TTS موفق", tmp_path
101
 
102
+ except edge_tts.exceptions.NoAudioReceived as e_no_audio:
103
+ logging.warning(f"خطای NoAudioReceived برای VoiceID='{voice_id}' (تلاش {attempt + 1}/{MAX_TTS_RETRIES + 1}). متن: '{text_to_speak[:50]}...'. خطا: {e_no_audio}")
104
+ if attempt < MAX_TTS_RETRIES:
105
+ logging.info(f"تلاش مجدد پس از {TTS_RETRY_DELAY} ثانیه...")
106
+ await asyncio.sleep(TTS_RETRY_DELAY)
107
+ else:
108
+ logging.error(f"خطای NoAudioReceived پس از {MAX_TTS_RETRIES + 1} تلاش برای VoiceID='{voice_id}'.")
109
+ return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد (NoAudioReceived). این صدا ممکن است موقتا در دسترس نباشد. لطفاً با صدای دیگری یا کمی بعدتر امتحان کنید.", None
110
+
111
+ except Exception as e:
112
+ logging.error(f"خطای Edge-TTS (تلاش {attempt + 1}) برای VoiceID='{voice_id}', VoiceKey='{tts_voice_key}': {e}\n{traceback.format_exc()}")
113
+ return f"خطای TTS ({type(e).__name__}): مشکلی در تولید صدا برای '{tts_voice_key}' پیش آمد.", None
114
+
115
+ return "خطای TTS: تلاش‌ها برای تولید صدا ناموفق بود.", None # Should not be reached
116
 
117
  _event_loops_by_thread = {}
118
  def _get_or_create_event_loop():
 
156
 
157
  return translated_text_output, audio_path
158
 
 
159
  FLY_PRIMARY_COLOR_HEX = "#4F46E5"
160
  FLY_SECONDARY_COLOR_HEX = "#10B981"
161
  FLY_ACCENT_COLOR_HEX = "#D97706"
 
177
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
178
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
179
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
 
180
  :root {{
181
+ --fly-primary: {FLY_PRIMARY_COLOR_HEX}; --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
182
+ --fly-accent: {FLY_ACCENT_COLOR_HEX}; --fly-text-primary: {FLY_TEXT_COLOR_HEX};
183
+ --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX}; --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
184
+ --fly-bg-white: {FLY_WHITE_HEX}; --fly-border-color: {FLY_BORDER_COLOR_HEX};
185
+ --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE}; --fly-panel-bg-simple: {FLY_PANEL_BG_SIMPLE};
 
 
 
 
 
 
186
  --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
187
  --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
 
188
  --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; --radius-full: 9999px;
189
+ --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05); --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1);
190
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);
191
+ --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1);
192
+ --fly-primary-rgb: 79,70,229; --fly-accent-rgb: 217,119,6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }}
194
+ 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;}}
195
+ .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;}}
196
+ .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;}}
197
+ .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);}}
198
+ .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);}}
199
+ .app-title-card p {{font-size:1em !important;margin-top:0.25rem;font-weight:400;color:rgba(255,255,255,0.85) !important;}}
200
+ .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);}}
201
+ 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;}}
202
+ .main-content-area {{flex-grow:1;padding:0.75rem;width:100%;margin:0 auto;box-sizing:border-box;}}
203
+ .content-panel-simple {{background-color:var(--fly-bg-white);padding:1rem;border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);margin-top:-2rem;position:relative;z-index:10;margin-bottom:2rem;width:100%;box-sizing:border-box;}}
204
+ .content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{background:var(--fly-accent) !important;margin-top:1rem !important;padding:12px 20px !important;transition:all 0.25s ease-in-out !important;color:white !important;font-weight:600 !important;border-radius:10px !important;border:none !important;box-shadow:0 3px 8px -1px rgba(var(--fly-accent-rgb),0.3) !important;width:100% !important;font-size:1em !important;display:flex;align-items:center;justify-content:center;}}
205
+ .content-panel-simple .gr-button.lg.primary:hover,.content-panel-simple button[variant="primary"]:hover {{background:#B45309 !important;transform:translateY(-1px) !important;box-shadow:0 5px 10px -1px rgba(var(--fly-accent-rgb),0.4) !important;}}
206
+ .content-panel-simple .gr-input > label + div > textarea,.content-panel-simple .gr-dropdown > label + div > div > input,.content-panel-simple .gr-dropdown > label + div > div > select,.content-panel-simple .gr-textbox > label + div > textarea {{border-radius:8px !important;border:1.5px solid var(--fly-border-color) !important;font-size:0.95em !important;background-color:var(--fly-input-bg-simple) !important;padding:10px 12px !important;color:var(--fly-text-primary) !important;}}
207
+ .content-panel-simple .gr-input > label + div > textarea:focus,.content-panel-simple .gr-dropdown > label + div > div > input:focus,.content-panel-simple .gr-dropdown > label + div > div > select:focus,.content-panel-simple .gr-textbox > label + div > textarea:focus {{border-color:var(--fly-primary) !important;box-shadow:0 0 0 3px rgba(var(--fly-primary-rgb),0.12) !important;background-color:var(--fly-bg-white) !important;}}
208
+ .content-panel-simple .gr-dropdown select {{font-family:var(--font-global) !important;width:100%;cursor:pointer;}}
209
+ .content-panel-simple .gr-textbox[label*="ترجمه شده"] > label + div > textarea {{background-color:var(--fly-panel-bg-simple) !important;border-color:#A5D5FE !important;min-height:100px;font-family:var(--font-english);font-size:1em !important;line-height:1.5;padding:10px !important;}}
210
+ .content-panel-simple .gr-panel,.content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > .gr-panel {{border-radius:8px !important;border:1px solid var(--fly-border-color) !important;background-color:var(--fly-input-bg-simple) !important;padding:0.8rem 1rem !important;margin-top:0.6rem;box-shadow:none;}}
211
+ .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > button.gr-button {{font-weight:500 !important;padding:8px 10px !important;border-radius:6px !important;background-color:#E5E7EB !important;color:var(--fly-text-primary) !important;border:1px solid #D1D5DB !important;}}
212
+ .content-panel-simple label > span.label-text {{font-weight:500 !important;color:#4B5563 !important;font-size:0.88em !important;margin-bottom:6px !important;display:inline-block;}}
213
+ .content-panel-simple .gr-slider label span {{font-size:0.82em !important;color:var(--fly-text-secondary);}}
214
+ .content-panel-simple div[label*="نمونه"] {{margin-top:1.5rem;}}
215
+ .content-panel-simple div[label*="نمونه"] .gr-button.gr-button-tool,.content-panel-simple div[label*="نمونه"] .gr-sample-button {{background-color:#E0E7FF !important;color:var(--fly-primary) !important;border-radius:6px !important;font-size:0.78em !important;padding:4px 8px !important;}}
216
+ .content-panel-simple .custom-hr {{height:1px;background-color:var(--fly-border-color);margin:1.5rem 0;border:none;}}
217
+ .api-warning-message {{background-color:#FFFBEB !important;color:#92400E !important;padding:10px 12px !important;border-radius:8px !important;border:1px solid #FDE68A !important;text-align:center !important;margin:0 0.2rem 1rem 0.2rem !important;font-size:0.85em !important;}}
218
+ @media (min-width:640px) {{.main-content-area {{padding:1.5rem;max-width:700px;}} .content-panel-simple {{padding:1.5rem;}} .app-title-card h1 {{font-size:2.5em !important;}} .app-title-card p {{font-size:1.05em !important;}} }}
219
+ @media (min-width:768px) {{.main-content-area {{max-width:780px;}} .content-panel-simple {{padding:2rem;}} .content-panel-simple .main-content-row {{display:flex !important;flex-direction:row !important;gap:1.5rem !important;}} .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{flex-basis:60%;}} .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{flex-basis:40%;}} .content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{width:auto !important;align-self:flex-start;}} .app-title-card h1 {{font-size:2.75em !important;}} .app-title-card p {{font-size:1.1em !important;}} }}
220
  """
221
 
222
  default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
 
281
  label="🎧 فایل صوتی",
282
  format="mp3",
283
  interactive=False,
284
+ autoplay=True
285
  )
286
 
287
  if language_dict_persian_keys and default_english_tts_voice and default_english_tts_voice != "لیست صداها خالی است":