Hamed744 commited on
Commit
d9e5c15
·
verified ·
1 Parent(s): 5df57bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -145
app.py CHANGED
@@ -5,9 +5,12 @@ import asyncio
5
  import traceback
6
  import threading
7
  import os
8
- import uuid # برای تولید ID منحصر به فرد
9
 
10
  # --- دیکشنری زبان‌ها و صداها با کلیدهای فارسی (نمونه) ---
 
 
 
 
11
  language_dict_persian_keys = {
12
  'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
13
  'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
@@ -18,7 +21,7 @@ language_dict_persian_keys = {
18
  'انگلیسی - میشل (زن)': 'en-US-MichelleNeural',
19
  'انگلیسی - راجر (مرد)': 'en-US-RogerNeural',
20
  'اسپانیایی (مکزیک) - دالیا (زن)': 'es-MX-DaliaNeural',
21
- 'اسپانیایی (مکزیک) - خورخه (مرد)': 'es-MX-JorgeNeural',
22
  'کره‌ای - سان-هی (زن)': 'ko-KR-SunHiNeural',
23
  'کره‌ای - این‌جون (مرد)': 'ko-KR-InJoonNeural',
24
  'تایلندی - پرموادی (زن)': 'th-TH-PremwadeeNeural',
@@ -40,13 +43,13 @@ language_dict_persian_keys = {
40
  'هلندی - کولت (زن)': 'nl-NL-ColetteNeural',
41
  'هلندی - فنا (زن)': 'nl-NL-FennaNeural',
42
  'هلندی - مارتن (مرد)': 'nl-NL-MaartenNeural',
43
- 'مالایی - عثمان (مرد)': 'ms-MY-OsmanNeural',
44
  'مالایی - یاسمین (زن)': 'ms-MY-YasminNeural',
45
  'نروژی - پرنیل (زن)': 'nb-NO-PernilleNeural',
46
  'نروژی - فین (مرد)': 'nb-NO-FinnNeural',
47
  'سوئدی - سوفی (زن)': 'sv-SE-SofieNeural',
48
  'سوئدی - ماتیاس (مرد)': 'sv-SE-MattiasNeural',
49
- 'عربی (عربستان) - حامد (مرد)': 'ar-SA-HamedNeural',
50
  'عربی (عربستان) - زاریا (زن)': 'ar-SA-ZariyahNeural',
51
  'یونانی - آتنا (زن)': 'el-GR-AthinaNeural',
52
  'یونانی - نستوراس (مرد)': 'el-GR-NestorasNeural',
@@ -54,9 +57,9 @@ language_dict_persian_keys = {
54
  'آلمانی - آمالا (زن)': 'de-DE-AmalaNeural',
55
  'آلمانی - کنراد (مرد)': 'de-DE-ConradNeural',
56
  'آلمانی - کیلیان (مرد)': 'de-DE-KillianNeural',
57
- 'آفریقایی - آدری (زن)': 'af-ZA-AdriNeural',
58
  'آفریقایی - ویلم (مرد)': 'af-ZA-WillemNeural',
59
- 'اتیوپیایی - آمه‌ها (مرد)': 'am-ET-AmehaNeural',
60
  'اتیوپیایی - مکدس (زن)': 'am-ET-MekdesNeural',
61
  'عربی (امارات) - فاطمه (زن)': 'ar-AE-FatimaNeural',
62
  'عربی (امارات) - حمدان (مرد)': 'ar-AE-HamdanNeural',
@@ -66,9 +69,9 @@ language_dict_persian_keys = {
66
  'عربی (مصر) - سلما (زن)': 'ar-EG-SalmaNeural',
67
  'عربی (مصر) - شاکر (مرد)': 'ar-EG-ShakirNeural',
68
  'عربی (عراق) - باسل (مرد)': 'ar-IQ-BasselNeural',
69
- 'عربی (عراق) - رعنا (زن)': 'ar-IQ-RanaNeural',
70
  'عربی (اردن) - سانا (زن)': 'ar-JO-SanaNeural',
71
- 'عربی (اردن) - تایم (مرد)': 'ar-JO-TaimNeural',
72
  'عربی (کویت) - فهد (مرد)': 'ar-KW-FahedNeural',
73
  'عربی (کویت) - نورا (زن)': 'ar-KW-NouraNeural',
74
  'عربی (لبنان) - لیلا (زن)': 'ar-LB-LaylaNeural',
@@ -79,11 +82,11 @@ language_dict_persian_keys = {
79
  'عربی (مراکش) - مونا (زن)': 'ar-MA-MounaNeural',
80
  'عربی (عمان) - عبدالله (مرد)': 'ar-OM-AbdullahNeural',
81
  'عربی (عمان) - عایشه (زن)': 'ar-OM-AyshaNeural',
82
- 'عربی (قطر) - امل (زن)': 'ar-QA-AmalNeural',
83
  'عربی (قطر) - معاذ (مرد)': 'ar-QA-MoazNeural',
84
  'عربی (سوریه) - امانی (زن)': 'ar-SY-AmanyNeural',
85
  'عربی (سوریه) - لیث (مرد)': 'ar-SY-LaithNeural',
86
- 'عربی (تونس) - هادی (مرد)': 'ar-TN-HediNeural',
87
  'عربی (تونس) - ریم (زن)': 'ar-TN-ReemNeural',
88
  'عربی (��من) - مریم (زن)': 'ar-YE-MaryamNeural',
89
  'عربی (یمن) - صالح (مرد)': 'ar-YE-SalehNeural',
@@ -94,14 +97,14 @@ language_dict_persian_keys = {
94
  'بنگالی (بنگلادش) - نابانیتا (زن)': 'bn-BD-NabanitaNeural',
95
  'بنگالی (بنگلادش) - پرادیپ (مرد)': 'bn-BD-PradeepNeural',
96
  'بنگالی (هند) - باشکار (مرد)': 'bn-IN-BashkarNeural',
97
- 'بنگالی (هند) - تانیشا (زن)': 'bn-IN-TanishaaNeural',
98
- 'بوسنیایی - گوران (مرد)': 'bs-BA-GoranNeural',
99
  'بوسنیایی - وسنا (زن)': 'bs-BA-VesnaNeural',
100
- 'کاتالان (اسپانیا) - جوآنا (زن)': 'ca-ES-JoanaNeural',
101
  'کاتالان (اسپانیا) - انریک (مرد)': 'ca-ES-EnricNeural',
102
- 'چکی - آنتونین (مرد)': 'cs-CZ-AntoninNeural',
103
  'چکی - ولاستا (زن)': 'cs-CZ-VlastaNeural',
104
- 'ولزی (بریتانیا) - آلد (مرد)': 'cy-GB-AledNeural',
105
  'ولزی (بریتانیا) - نیا (زن)': 'cy-GB-NiaNeural',
106
  'دانمارکی - کریستل (زن)': 'da-DK-ChristelNeural',
107
  'دانمارکی - یپه (مرد)': 'da-DK-JeppeNeural',
@@ -172,13 +175,13 @@ language_dict_persian_keys = {
172
  'اسپانیایی (پاراگوئه) - تانیا (زن)': 'es-PY-TaniaNeural',
173
  'اسپانیایی (السالوادور) - لورنا (زن)': 'es-SV-LorenaNeural',
174
  'اسپانیایی (السالوادور) - رودریگو (مرد)': 'es-SV-RodrigoNeural',
175
- 'اسپانیایی (آمریکا) - آلونسو (مرد)': 'es-US-AlonsoNeural',
176
  'اسپانیایی (آمریکا) - پالوما (زن)': 'es-US-PalomaNeural',
177
  'اسپانیایی (اروگوئه) - ماتئو (مرد)': 'es-UY-MateoNeural',
178
  'اسپانیایی (اروگوئه) - والنتینا (زن)': 'es-UY-ValentinaNeural',
179
  'اسپانیایی (ونزوئلا) - پائولا (زن)': 'es-VE-PaolaNeural',
180
  'اسپانیایی (ونزوئلا) - سباستین (مرد)': 'es-VE-SebastianNeural',
181
- 'استونیایی - آنو (زن)': 'et-EE-AnuNeural',
182
  'استونیایی - کرت (مرد)': 'et-EE-KertNeural',
183
  'فارسی (ایران) - دل‌آرا (زن)': 'fa-IR-DilaraNeural',
184
  'فارسی (ایران) - فرید (مرد)': 'fa-IR-FaridNeural',
@@ -193,7 +196,7 @@ language_dict_persian_keys = {
193
  'فرانسوی (سوئیس) - فابریس (مرد)': 'fr-CH-FabriceNeural',
194
  'ایرلندی - کلم (مرد)': 'ga-IE-ColmNeural',
195
  'ایرلندی - اورلا (زن)': 'ga-IE-OrlaNeural',
196
- 'گالیسی (اسپانیا) - روی (مرد)': 'gl-ES-RoiNeural',
197
  'گالیسی (اسپانیا) - سابلا (زن)': 'gl-ES-SabelaNeural',
198
  'گجراتی (هند) - دوانی (زن)': 'gu-IN-DhwaniNeural',
199
  'گجراتی (هند) - نیرانجان (مرد)': 'gu-IN-NiranjanNeural',
@@ -201,7 +204,7 @@ language_dict_persian_keys = {
201
  'عبری (اسرائیل) - هیلا (زن)': 'he-IL-HilaNeural',
202
  'هندی (هند) - مادور (مرد)': 'hi-IN-MadhurNeural',
203
  'هندی (هند) - سوارا (زن)': 'hi-IN-SwaraNeural',
204
- 'کروات - گابریلا (زن)': 'hr-HR-GabrijelaNeural',
205
  'کروات - سرچکو (مرد)': 'hr-HR-SreckoNeural',
206
  'مجاری - نوئمی (زن)': 'hu-HU-NoemiNeural',
207
  'مجاری - تاماش (مرد)': 'hu-HU-TamasNeural',
@@ -209,15 +212,15 @@ language_dict_persian_keys = {
209
  'ارمنی - هایک (مرد)': 'hy-AM-HaykNeural',
210
  'ایسلندی - گودرون (زن)': 'is-IS-GudrunNeural',
211
  'ایسلندی - گونار (مرد)': 'is-IS-GunnarNeural',
212
- 'جاوه‌ای (اندونزی) - دیماس (مرد)': 'jv-ID-DimasNeural',
213
  'جاوه‌ای (اندونزی) - سیتی (زن)': 'jv-ID-SitiNeural',
214
  'گرجی - اکا (زن)': 'ka-GE-EkaNeural',
215
  'گرجی - گیورگی (مرد)': 'ka-GE-GiorgiNeural',
216
  'قزاقی - آیگول (زن)': 'kk-KZ-AigulNeural',
217
  'قزاقی - دولت (مرد)': 'kk-KZ-DauletNeural',
218
- 'خمر (کامبوج) - پیست (مرد)': 'km-KH-PisethNeural',
219
  'خمر (کامبوج) - سری‌مم (زن)': 'km-KH-SreymomNeural',
220
- 'کانادایی (هند) - گاگان (مرد)': 'kn-IN-GaganNeural',
221
  'کانادایی (هند) - ساپنا (زن)': 'kn-IN-SapnaNeural',
222
  'لائوسی - چانتاونگ (مرد)': 'lo-LA-ChanthavongNeural',
223
  'لائوسی - کئومانی (زن)': 'lo-LA-KeomanyNeural',
@@ -233,7 +236,7 @@ language_dict_persian_keys = {
233
  'مغولی - یسوی (زن)': 'mn-MN-YesuiNeural',
234
  'مراتی (هند) - آروهی (زن)': 'mr-IN-AarohiNeural',
235
  'مراتی (هند) - مانوهار (مرد)': 'mr-IN-ManoharNeural',
236
- 'مالتی (مالت) - گریس (زن)': 'mt-MT-GraceNeural',
237
  'مالتی (مالت) - جوزف (مرد)': 'mt-MT-JosephNeural',
238
  'برمه‌ای (میانمار) - نیلار (زن)': 'my-MM-NilarNeural',
239
  'برمه‌ای (میانمار) - تیها (مرد)': 'my-MM-ThihaNeural',
@@ -251,7 +254,7 @@ language_dict_persian_keys = {
251
  'رومانیایی - امیل (مرد)': 'ro-RO-EmilNeural',
252
  'روسی - دیمیتری (مرد)': 'ru-RU-DmitryNeural',
253
  'روسی - سوتلانا (زن)': 'ru-RU-SvetlanaNeural',
254
- 'سینهالی (سریلانکا) - دینوکا (مرد)': 'si-LK-DinukaNeural',
255
  'سینهالی (سریلانکا) - تیلینی (زن)': 'si-LK-ThiliniNeural',
256
  'اسلواک - لوکاش (مرد)': 'sk-SK-LukasNeural',
257
  'اسلواک - ویکتوریا (زن)': 'sk-SK-ViktoriaNeural',
@@ -263,7 +266,7 @@ language_dict_persian_keys = {
263
  'آلبانیایی - ایلیر (مرد)': 'sq-AL-IlirNeural',
264
  'صربی - نیکولا (مرد)': 'sr-RS-NikolaNeural',
265
  'صربی - سوفی (زن)': 'sr-RS-SophieNeural',
266
- 'سوندانی (اندونزی) - جاجانگ (مرد)': 'su-ID-JajangNeural',
267
  'سوندانی (اندونزی) - توتی (زن)': 'su-ID-TutiNeural',
268
  'سواحیلی (کنیا) - رفیقی (مرد)': 'sw-KE-RafikiNeural',
269
  'سواحیلی (کنیا) - زوری (زن)': 'sw-KE-ZuriNeural',
@@ -299,26 +302,26 @@ language_dict_persian_keys = {
299
  'زولو (آفریقای جنوبی) - تمبا (مرد)': 'zu-ZA-ThembaNeural',
300
  }
301
 
302
- # --- توابع تبدیل متن به گفتار ---
303
- async def text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch):
304
  try:
305
  if not text: return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
 
306
  voice_id = language_dict_persian_keys.get(language_code_persian)
307
  if voice_id is None: return f"خطا: مدل صدای انتخاب شده ('{language_code_persian}') یافت نشد.", None
308
  rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
309
  communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
310
- with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
311
- tmp_path = tmp_file.name
312
  await communicate.save(tmp_path)
313
  return "تبدیل با موفقیت انجام شد.", tmp_path
314
  except edge_tts.exceptions.NoAudioReceived:
315
  error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
316
  return error_msg, None
317
  except ValueError as ve:
318
- error_msg = f"خطا در پارامترهای ورودی: {ve}"
319
  return error_msg, None
320
  except Exception as e:
321
- return f"خطای غیرمنتظره در سرور: {type(e).__name__} - {e}", None
322
 
323
  _event_loops_by_thread = {}
324
  def _get_or_create_event_loop():
@@ -328,35 +331,17 @@ def _get_or_create_event_loop():
328
  return _event_loops_by_thread[thread_id]
329
 
330
  def text_to_speech_edge_sync_wrapper(text, language_code_persian, rate, volume, pitch):
331
- status_msg, audio_path = None, None
332
  try:
333
- loop = _get_or_create_event_loop()
334
- asyncio.set_event_loop(loop)
335
- status_msg, audio_path = loop.run_until_complete(
336
- text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch)
337
- )
338
  except RuntimeError as e:
339
  if "no current event loop" in str(e).lower() or "cannot be called from a running event loop" in str(e).lower():
340
- new_loop = asyncio.new_event_loop()
341
- asyncio.set_event_loop(new_loop)
342
- try:
343
- status_msg, audio_path = new_loop.run_until_complete(
344
- text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch)
345
- )
346
- finally:
347
- new_loop.close()
348
- else:
349
- status_msg = f"خطای اجرایی: {e}"
350
- except Exception as e:
351
- status_msg = f"خطای غیرمنتظره در wrapper: {type(e).__name__} - {e}"
352
-
353
- if audio_path:
354
- return status_msg, audio_path, gr.update(visible=True), audio_path
355
- else:
356
- if status_msg is None:
357
- status_msg = "خطا در تولید صدا."
358
- return status_msg, None, gr.update(visible=False), None
359
-
360
 
361
  # --- تعریف تم و CSS ---
362
  app_theme = gr.themes.Soft(
@@ -379,8 +364,10 @@ body { font-family: 'Vazirmatn', 'Arial', sans-serif; direction: rtl; }
379
  text-align: center; padding: 20px 10px; background: #34495e; color: white;
380
  border-radius: 12px; margin-bottom: 1.5rem;
381
  }
382
- .app-header img.logo { display: none; }
383
-
 
 
384
  .app-header h1 {
385
  color: white !important; font-size: 1.6em !important; font-weight: 600 !important;
386
  margin: 5px 0;
@@ -425,43 +412,6 @@ div[data-testid="flag"] { display: none !important; }
425
  button[title="Flag"], button[aria-label="Flag"] {display: none !important; }
426
  .footer-utils { display: none !important; visibility: hidden !important; }
427
 
428
- .download-button-custom {
429
- background: #2ecc71 !important;
430
- color: white !important;
431
- font-weight: 500 !important;
432
- border-radius: 8px !important;
433
- padding: 10px 15px !important;
434
- width: 100% !important;
435
- font-size: 0.95em !important;
436
- transition: all 0.2s ease !important;
437
- box-shadow: 0 3px 6px rgba(46, 204, 113, 0.25) !important;
438
- border: none !important;
439
- margin-top: 10px !important;
440
- text-align: center !important;
441
- }
442
- .download-button-custom:hover {
443
- background: #27ae60 !important;
444
- transform: translateY(-2px) !important;
445
- box-shadow: 0 5px 10px rgba(46, 204, 113, 0.35) !important;
446
- }
447
-
448
- /* برای اینکه فضای خالی توسط HTML نامرئی اشغال نشود */
449
- /* یک div با این کلاس دور gr.HTML قرار می‌گیرد */
450
- .hidden-html-container > div {
451
- display: none !important; /* مخفی کردن محتوای داخلی پیش‌فرض gr.HTML */
452
- }
453
- .hidden-html-container { /* خود کانتینر اصلی gr.HTML */
454
- line-height: 0 !important;
455
- font-size: 0 !important;
456
- margin: 0 !important;
457
- padding: 0 !important;
458
- height: 0 !important;
459
- overflow: hidden !important;
460
- border: none !important; /* حذف هرگونه بوردر احتمالی */
461
- background: transparent !important; /* شفاف کردن پس‌زمینه */
462
- }
463
-
464
-
465
  @keyframes float_soft {
466
  0% { transform: translatey(0px) scale(1); }
467
  50% { transform: translatey(-5px) scale(1.05); }
@@ -476,15 +426,18 @@ button[title="Flag"], button[aria-label="Flag"] {display: none !important; }
476
  .main-content-row > .gr-column:nth-child(1) { flex-basis: 60%; }
477
  .main-content-row > .gr-column:nth-child(2) { flex-basis: 40%; }
478
  .gr-button.lg.primary { width: auto !important; }
479
- .download-button-custom { width: auto !important; }
480
  }
481
  """
482
 
483
  # انتخاب صدای پیش فرض فارسی
484
  default_voice_key_persian = 'فارسی (ایران) - فرید (مرد)'
485
  if default_voice_key_persian not in language_dict_persian_keys:
 
486
  default_voice_key_persian = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
487
 
 
 
 
488
  with gr.Blocks(theme=app_theme, css=custom_css) as demo:
489
  with gr.Row():
490
  gr.HTML(f"""
@@ -502,7 +455,7 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
502
  placeholder="اینجا بنویسید...",
503
  value=""
504
  )
505
- language_dropdown = gr.Dropdown(
506
  choices=list(language_dict_persian_keys.keys()),
507
  value=default_voice_key_persian,
508
  label="🗣️ زبان و گوینده را انتخاب کنید"
@@ -518,12 +471,6 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
518
  with gr.Column(scale=2):
519
  output_text_status = gr.Textbox(label="📊 وضعیت عملیات", interactive=False, lines=1, placeholder="نتیجه اینجا نمایش داده می‌شود...")
520
  output_audio = gr.Audio(type="filepath", label="🎧 فایل صوتی خروجی", interactive=False)
521
-
522
- audio_file_path_state = gr.State(value=None)
523
- download_button = gr.Button("📥 دانلود صدا", visible=False, elem_classes="download-button-custom")
524
- # کامپوننت HTML برای اجرای اسکریپت، با محتوای اولیه یک کامنت HTML و کلاس برای مخفی‌سازی بصری
525
- download_trigger_html = gr.HTML(value="<!-- script placeholder -->", visible=True, elem_classes="hidden-html-container")
526
-
527
 
528
  gr.HTML("<hr style='margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid #dee2e6;'>")
529
 
@@ -533,8 +480,8 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
533
  ["This is a test of the speech synthesis system.", 'انگلیسی - جنی (زن)', +5, 0, 0],
534
  ["تجربه کاربری در این سامانه بسیار روان است.", 'فارسی (ایران) - فرید (مرد)', -5, 0, 0],
535
  ],
536
- inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
537
- outputs=[output_text_status, output_audio, download_button, audio_file_path_state],
538
  fn=text_to_speech_edge_sync_wrapper,
539
  cache_examples=False,
540
  label="💡 چند نمونه برای شروع"
@@ -542,44 +489,8 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
542
 
543
  submit_button.click(
544
  fn=text_to_speech_edge_sync_wrapper,
545
- inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
546
- outputs=[output_text_status, output_audio, download_button, audio_file_path_state],
547
- )
548
-
549
- def trigger_download_action(filepath_from_state):
550
- if filepath_from_state:
551
- # اطمینان از اسلش صحیح برای URL و حذف هرگونه بک‌اسلش
552
- normalized_filepath = filepath_from_state.replace("\\", "/")
553
- # Gradio فایل‌ها را از طریق '/file=' سرویس‌دهی می‌کند.
554
- file_url = f"/file={normalized_filepath}"
555
-
556
- # استفاده از یک ID منحصر به فرد برای تگ اسکریپت تا در هر بار کلیک،
557
- # مرورگر آن را به عنوان یک تغییر جدید در DOM تشخیص دهد و اجرا کند.
558
- script_id = f"script-{uuid.uuid4()}"
559
-
560
- # اسکریپت برای باز کردن لینک در تب جدید
561
- # استفاده از setTimeout با تاخیر 0 برای اطمینان از اجرا پس از به‌روزرسانی DOM
562
- script_content = f"""
563
- <script id="{script_id}">
564
- setTimeout(function() {{
565
- console.log('Gradio TTS: Attempting to open URL: {file_url}');
566
- var newWindow = window.open('{file_url}', '_blank');
567
- if (!newWindow || newWindow.closed || typeof newWindow.closed=='undefined') {{
568
- console.error('Gradio TTS: Failed to open new window. Pop-up blocker might be active.');
569
- // در اینجا می‌توانید یک پیام به کاربر نمایش دهید که پاپ‌آپ بلاکر را غیرفعال کند
570
- // alert('پنجره جدید باز نشد. لطفاً مسدودکننده پنجره را غیرفعال کنید و دوباره تلاش نمایید.');
571
- }}
572
- }}, 0);
573
- </script>
574
- """
575
- return script_content
576
- # اگر مسیری وجود نداشت، یک اسکریپت خالی یا کامنت برگردان
577
- return "<!-- No file to open -->"
578
-
579
- download_button.click(
580
- fn=trigger_download_action,
581
- inputs=[audio_file_path_state],
582
- outputs=[download_trigger_html]
583
  )
584
 
585
- demo.launch(debug=True)
 
5
  import traceback
6
  import threading
7
  import os
 
8
 
9
  # --- دیکشنری زبان‌ها و صداها با کلیدهای فارسی (نمونه) ---
10
+ # توجه: این فقط یک نمونه کوچک است. شما باید کل دیکشنری را به این شکل فارسی کنید.
11
+ # برای سادگی، من فقط چند مورد اول را تغییر می دهم و بقیه را انگلیسی نگه می دارم.
12
+ # شما باید برای هر کلید، نام زبان و جنسیت را به فارسی ترجمه کنید.
13
+
14
  language_dict_persian_keys = {
15
  'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
16
  'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
 
21
  'انگلیسی - میشل (زن)': 'en-US-MichelleNeural',
22
  'انگلیسی - راجر (مرد)': 'en-US-RogerNeural',
23
  'اسپانیایی (مکزیک) - دالیا (زن)': 'es-MX-DaliaNeural',
24
+ 'اسپانیایی (مکزیک) - خورخه (مرد)': 'es-MX-JorgeNeural', # نام خورخه ممکن است دقیق نباشد
25
  'کره‌ای - سان-هی (زن)': 'ko-KR-SunHiNeural',
26
  'کره‌ای - این‌جون (مرد)': 'ko-KR-InJoonNeural',
27
  'تایلندی - پرموادی (زن)': 'th-TH-PremwadeeNeural',
 
43
  'هلندی - کولت (زن)': 'nl-NL-ColetteNeural',
44
  'هلندی - فنا (زن)': 'nl-NL-FennaNeural',
45
  'هلندی - مارتن (مرد)': 'nl-NL-MaartenNeural',
46
+ 'مالایی - عثمان (مرد)': 'ms-MY-OsmanNeural', # "Malese" به "مالایی"
47
  'مالایی - یاسمین (زن)': 'ms-MY-YasminNeural',
48
  'نروژی - پرنیل (زن)': 'nb-NO-PernilleNeural',
49
  'نروژی - فین (مرد)': 'nb-NO-FinnNeural',
50
  'سوئدی - سوفی (زن)': 'sv-SE-SofieNeural',
51
  'سوئدی - ماتیاس (مرد)': 'sv-SE-MattiasNeural',
52
+ 'عربی (عربستان) - حامد (مرد)': 'ar-SA-HamedNeural', # "عربی" به "عربی (عربستان)"
53
  'عربی (عربستان) - زاریا (زن)': 'ar-SA-ZariyahNeural',
54
  'یونانی - آتنا (زن)': 'el-GR-AthinaNeural',
55
  'یونانی - نستوراس (مرد)': 'el-GR-NestorasNeural',
 
57
  'آلمانی - آمالا (زن)': 'de-DE-AmalaNeural',
58
  'آلمانی - کنراد (مرد)': 'de-DE-ConradNeural',
59
  'آلمانی - کیلیان (مرد)': 'de-DE-KillianNeural',
60
+ 'آفریقایی - آدری (زن)': 'af-ZA-AdriNeural', # "Afrikaans" به "آفریقایی"
61
  'آفریقایی - ویلم (مرد)': 'af-ZA-WillemNeural',
62
+ 'اتیوپیایی - آمه‌ها (مرد)': 'am-ET-AmehaNeural', # "Ethiopian" به "اتیوپیایی"
63
  'اتیوپیایی - مکدس (زن)': 'am-ET-MekdesNeural',
64
  'عربی (امارات) - فاطمه (زن)': 'ar-AE-FatimaNeural',
65
  'عربی (امارات) - حمدان (مرد)': 'ar-AE-HamdanNeural',
 
69
  'عربی (مصر) - سلما (زن)': 'ar-EG-SalmaNeural',
70
  'عربی (مصر) - شاکر (مرد)': 'ar-EG-ShakirNeural',
71
  'عربی (عراق) - باسل (مرد)': 'ar-IQ-BasselNeural',
72
+ 'عربی (عراق) - رعنا (زن)': 'ar-IQ-RanaNeural', # "Rana" به "رعنا"
73
  'عربی (اردن) - سانا (زن)': 'ar-JO-SanaNeural',
74
+ 'عربی (اردن) - تایم (مرد)': 'ar-JO-TaimNeural', # "Taim"
75
  'عربی (کویت) - فهد (مرد)': 'ar-KW-FahedNeural',
76
  'عربی (کویت) - نورا (زن)': 'ar-KW-NouraNeural',
77
  'عربی (لبنان) - لیلا (زن)': 'ar-LB-LaylaNeural',
 
82
  'عربی (مراکش) - مونا (زن)': 'ar-MA-MounaNeural',
83
  'عربی (عمان) - عبدالله (مرد)': 'ar-OM-AbdullahNeural',
84
  'عربی (عمان) - عایشه (زن)': 'ar-OM-AyshaNeural',
85
+ 'عربی (قطر) - امل (زن)': 'ar-QA-AmalNeural', # "Amal"
86
  'عربی (قطر) - معاذ (مرد)': 'ar-QA-MoazNeural',
87
  'عربی (سوریه) - امانی (زن)': 'ar-SY-AmanyNeural',
88
  'عربی (سوریه) - لیث (مرد)': 'ar-SY-LaithNeural',
89
+ 'عربی (تونس) - هادی (مرد)': 'ar-TN-HediNeural', # "Hedi"
90
  'عربی (تونس) - ریم (زن)': 'ar-TN-ReemNeural',
91
  'عربی (��من) - مریم (زن)': 'ar-YE-MaryamNeural',
92
  'عربی (یمن) - صالح (مرد)': 'ar-YE-SalehNeural',
 
97
  'بنگالی (بنگلادش) - نابانیتا (زن)': 'bn-BD-NabanitaNeural',
98
  'بنگالی (بنگلادش) - پرادیپ (مرد)': 'bn-BD-PradeepNeural',
99
  'بنگالی (هند) - باشکار (مرد)': 'bn-IN-BashkarNeural',
100
+ 'بنگالی (هند) - تانیشا (زن)': 'bn-IN-TanishaaNeural', # "Tanishaa"
101
+ 'بوسنیایی - گوران (مرد)': 'bs-BA-GoranNeural', # "Bosnian" به "بوسنیایی"
102
  'بوسنیایی - وسنا (زن)': 'bs-BA-VesnaNeural',
103
+ 'کاتالان (اسپانیا) - جوآنا (زن)': 'ca-ES-JoanaNeural', # "Catalan"
104
  'کاتالان (اسپانیا) - انریک (مرد)': 'ca-ES-EnricNeural',
105
+ 'چکی - آنتونین (مرد)': 'cs-CZ-AntoninNeural', # "Czech" به "چکی"
106
  'چکی - ولاستا (زن)': 'cs-CZ-VlastaNeural',
107
+ 'ولزی (بریتانیا) - آلد (مرد)': 'cy-GB-AledNeural', # "Welsh"
108
  'ولزی (بریتانیا) - نیا (زن)': 'cy-GB-NiaNeural',
109
  'دانمارکی - کریستل (زن)': 'da-DK-ChristelNeural',
110
  'دانمارکی - یپه (مرد)': 'da-DK-JeppeNeural',
 
175
  'اسپانیایی (پاراگوئه) - تانیا (زن)': 'es-PY-TaniaNeural',
176
  'اسپانیایی (السالوادور) - لورنا (زن)': 'es-SV-LorenaNeural',
177
  'اسپانیایی (السالوادور) - رودریگو (مرد)': 'es-SV-RodrigoNeural',
178
+ 'اسپانیایی (آمریکا) - آلونسو (مرد)': 'es-US-AlonsoNeural', # "United States" به "آمریکا"
179
  'اسپانیایی (آمریکا) - پالوما (زن)': 'es-US-PalomaNeural',
180
  'اسپانیایی (اروگوئه) - ماتئو (مرد)': 'es-UY-MateoNeural',
181
  'اسپانیایی (اروگوئه) - والنتینا (زن)': 'es-UY-ValentinaNeural',
182
  'اسپانیایی (ونزوئلا) - پائولا (زن)': 'es-VE-PaolaNeural',
183
  'اسپانیایی (ونزوئلا) - سباستین (مرد)': 'es-VE-SebastianNeural',
184
+ 'استونیایی - آنو (زن)': 'et-EE-AnuNeural', # "Estonian"
185
  'استونیایی - کرت (مرد)': 'et-EE-KertNeural',
186
  'فارسی (ایران) - دل‌آرا (زن)': 'fa-IR-DilaraNeural',
187
  'فارسی (ایران) - فرید (مرد)': 'fa-IR-FaridNeural',
 
196
  'فرانسوی (سوئیس) - فابریس (مرد)': 'fr-CH-FabriceNeural',
197
  'ایرلندی - کلم (مرد)': 'ga-IE-ColmNeural',
198
  'ایرلندی - اورلا (زن)': 'ga-IE-OrlaNeural',
199
+ 'گالیسی (اسپانیا) - روی (مرد)': 'gl-ES-RoiNeural', # "Galician"
200
  'گالیسی (اسپانیا) - سابلا (زن)': 'gl-ES-SabelaNeural',
201
  'گجراتی (هند) - دوانی (زن)': 'gu-IN-DhwaniNeural',
202
  'گجراتی (هند) - نیرانجان (مرد)': 'gu-IN-NiranjanNeural',
 
204
  'عبری (اسرائیل) - هیلا (زن)': 'he-IL-HilaNeural',
205
  'هندی (هند) - مادور (مرد)': 'hi-IN-MadhurNeural',
206
  'هندی (هند) - سوارا (زن)': 'hi-IN-SwaraNeural',
207
+ 'کروات - گابریلا (زن)': 'hr-HR-GabrijelaNeural', # "Croatian"
208
  'کروات - سرچکو (مرد)': 'hr-HR-SreckoNeural',
209
  'مجاری - نوئمی (زن)': 'hu-HU-NoemiNeural',
210
  'مجاری - تاماش (مرد)': 'hu-HU-TamasNeural',
 
212
  'ارمنی - هایک (مرد)': 'hy-AM-HaykNeural',
213
  'ایسلندی - گودرون (زن)': 'is-IS-GudrunNeural',
214
  'ایسلندی - گونار (مرد)': 'is-IS-GunnarNeural',
215
+ 'جاوه‌ای (اندونزی) - دیماس (مرد)': 'jv-ID-DimasNeural', # "Javanese"
216
  'جاوه‌ای (اندونزی) - سیتی (زن)': 'jv-ID-SitiNeural',
217
  'گرجی - اکا (زن)': 'ka-GE-EkaNeural',
218
  'گرجی - گیورگی (مرد)': 'ka-GE-GiorgiNeural',
219
  'قزاقی - آیگول (زن)': 'kk-KZ-AigulNeural',
220
  'قزاقی - دولت (مرد)': 'kk-KZ-DauletNeural',
221
+ 'خمر (کامبوج) - پیست (مرد)': 'km-KH-PisethNeural', # "Khmer"
222
  'خمر (کامبوج) - سری‌مم (زن)': 'km-KH-SreymomNeural',
223
+ 'کانادایی (هند) - گاگان (مرد)': 'kn-IN-GaganNeural', # "Kannada"
224
  'کانادایی (هند) - ساپنا (زن)': 'kn-IN-SapnaNeural',
225
  'لائوسی - چانتاونگ (مرد)': 'lo-LA-ChanthavongNeural',
226
  'لائوسی - کئومانی (زن)': 'lo-LA-KeomanyNeural',
 
236
  'مغولی - یسوی (زن)': 'mn-MN-YesuiNeural',
237
  'مراتی (هند) - آروهی (زن)': 'mr-IN-AarohiNeural',
238
  'مراتی (هند) - مانوهار (مرد)': 'mr-IN-ManoharNeural',
239
+ 'مالتی (مالت) - گریس (زن)': 'mt-MT-GraceNeural', # "Maltese"
240
  'مالتی (مالت) - جوزف (مرد)': 'mt-MT-JosephNeural',
241
  'برمه‌ای (میانمار) - نیلار (زن)': 'my-MM-NilarNeural',
242
  'برمه‌ای (میانمار) - تیها (مرد)': 'my-MM-ThihaNeural',
 
254
  'رومانیایی - امیل (مرد)': 'ro-RO-EmilNeural',
255
  'روسی - دیمیتری (مرد)': 'ru-RU-DmitryNeural',
256
  'روسی - سوتلانا (زن)': 'ru-RU-SvetlanaNeural',
257
+ 'سینهالی (سریلانکا) - دینوکا (مرد)': 'si-LK-DinukaNeural', # "Sinhala"
258
  'سینهالی (سریلانکا) - تیلینی (زن)': 'si-LK-ThiliniNeural',
259
  'اسلواک - لوکاش (مرد)': 'sk-SK-LukasNeural',
260
  'اسلواک - ویکتوریا (زن)': 'sk-SK-ViktoriaNeural',
 
266
  'آلبانیایی - ایلیر (مرد)': 'sq-AL-IlirNeural',
267
  'صربی - نیکولا (مرد)': 'sr-RS-NikolaNeural',
268
  'صربی - سوفی (زن)': 'sr-RS-SophieNeural',
269
+ 'سوندانی (اندونزی) - جاجانگ (مرد)': 'su-ID-JajangNeural', # "Sundanese"
270
  'سوندانی (اندونزی) - توتی (زن)': 'su-ID-TutiNeural',
271
  'سواحیلی (کنیا) - رفیقی (مرد)': 'sw-KE-RafikiNeural',
272
  'سواحیلی (کنیا) - زوری (زن)': 'sw-KE-ZuriNeural',
 
302
  'زولو (آفریقای جنوبی) - تمبا (مرد)': 'zu-ZA-ThembaNeural',
303
  }
304
 
305
+ # --- توابع تبدیل متن به گفتار و wrapper (همانند قبل) ---
306
+ async def text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch): # language_code_persian نام پارامتر تغییر کرد
307
  try:
308
  if not text: return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
309
+ # دریافت voice_id از دیکشنری با کلیدهای فارسی
310
  voice_id = language_dict_persian_keys.get(language_code_persian)
311
  if voice_id is None: return f"خطا: مدل صدای انتخاب شده ('{language_code_persian}') یافت نشد.", None
312
  rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
313
  communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
314
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file: tmp_path = tmp_file.name
 
315
  await communicate.save(tmp_path)
316
  return "تبدیل با موفقیت انجام شد.", tmp_path
317
  except edge_tts.exceptions.NoAudioReceived:
318
  error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
319
  return error_msg, None
320
  except ValueError as ve:
321
+ error_msg = f"خطا در پارامترهای ورودی: {ve}" # حذف اشاره به edge-tts
322
  return error_msg, None
323
  except Exception as e:
324
+ return f"خطای غیرمنتظره در سرور: {type(e).__name__}", None
325
 
326
  _event_loops_by_thread = {}
327
  def _get_or_create_event_loop():
 
331
  return _event_loops_by_thread[thread_id]
332
 
333
  def text_to_speech_edge_sync_wrapper(text, language_code_persian, rate, volume, pitch):
 
334
  try:
335
+ loop = _get_or_create_event_loop(); asyncio.set_event_loop(loop)
336
+ result = loop.run_until_complete(text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch))
 
 
 
337
  except RuntimeError as e:
338
  if "no current event loop" in str(e).lower() or "cannot be called from a running event loop" in str(e).lower():
339
+ new_loop = asyncio.new_event_loop(); asyncio.set_event_loop(new_loop)
340
+ try: result = new_loop.run_until_complete(text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch))
341
+ finally: new_loop.close()
342
+ else: return f"خطای اجرایی: {e}", None
343
+ except Exception as e: return f"خطای غیرمنتظره: {type(e).__name__}", None
344
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  # --- تعریف تم و CSS ---
347
  app_theme = gr.themes.Soft(
 
364
  text-align: center; padding: 20px 10px; background: #34495e; color: white;
365
  border-radius: 12px; margin-bottom: 1.5rem;
366
  }
367
+ .app-header img.logo {
368
+ width: 50px; height: auto; margin-bottom: 5px;
369
+ animation: float_soft 4s ease-in-out infinite alternate;
370
+ }
371
  .app-header h1 {
372
  color: white !important; font-size: 1.6em !important; font-weight: 600 !important;
373
  margin: 5px 0;
 
412
  button[title="Flag"], button[aria-label="Flag"] {display: none !important; }
413
  .footer-utils { display: none !important; visibility: hidden !important; }
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  @keyframes float_soft {
416
  0% { transform: translatey(0px) scale(1); }
417
  50% { transform: translatey(-5px) scale(1.05); }
 
426
  .main-content-row > .gr-column:nth-child(1) { flex-basis: 60%; }
427
  .main-content-row > .gr-column:nth-child(2) { flex-basis: 40%; }
428
  .gr-button.lg.primary { width: auto !important; }
 
429
  }
430
  """
431
 
432
  # انتخاب صدای پیش فرض فارسی
433
  default_voice_key_persian = 'فارسی (ایران) - فرید (مرد)'
434
  if default_voice_key_persian not in language_dict_persian_keys:
435
+ # اگر به هر دلیلی صدای پیش فرض فارسی ما در لیست نبود، اولین مورد لیست را انتخاب کن
436
  default_voice_key_persian = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
437
 
438
+ # LOGO_URL دیگر استفاده نمی‌شود چون لوگو حذف شده است، اما برای حفظ سایر بخش‌های کد دست نخورده، آن را نگه می‌داریم
439
+ LOGO_URL = "https://www.gstatic.com/lamda/images/gemini/google_bard_logo_150_v2_dark_color_1x.png"
440
+
441
  with gr.Blocks(theme=app_theme, css=custom_css) as demo:
442
  with gr.Row():
443
  gr.HTML(f"""
 
455
  placeholder="اینجا بنویسید...",
456
  value=""
457
  )
458
+ language_dropdown = gr.Dropdown( # نام متغیر برای وضوح بیشتر
459
  choices=list(language_dict_persian_keys.keys()),
460
  value=default_voice_key_persian,
461
  label="🗣️ زبان و گوینده را انتخاب کنید"
 
471
  with gr.Column(scale=2):
472
  output_text_status = gr.Textbox(label="📊 وضعیت عملیات", interactive=False, lines=1, placeholder="نتیجه اینجا نمایش داده می‌شود...")
473
  output_audio = gr.Audio(type="filepath", label="🎧 فایل صوتی خروجی", interactive=False)
 
 
 
 
 
 
474
 
475
  gr.HTML("<hr style='margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid #dee2e6;'>")
476
 
 
480
  ["This is a test of the speech synthesis system.", 'انگلیسی - جنی (زن)', +5, 0, 0],
481
  ["تجربه کاربری در این سامانه بسیار روان است.", 'فارسی (ایران) - فرید (مرد)', -5, 0, 0],
482
  ],
483
+ inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider], # استفاده از نام متغیرهای جدید
484
+ outputs=[output_text_status, output_audio],
485
  fn=text_to_speech_edge_sync_wrapper,
486
  cache_examples=False,
487
  label="💡 چند نمونه برای شروع"
 
489
 
490
  submit_button.click(
491
  fn=text_to_speech_edge_sync_wrapper,
492
+ inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider], # استفاده از نام متغیرهای جدید
493
+ outputs=[output_text_status, output_audio],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  )
495
 
496
+ demo.launch()