suprimedev commited on
Commit
7389150
·
verified ·
1 Parent(s): d9d499b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -203
app.py CHANGED
@@ -1,12 +1,11 @@
1
- import gradio as gr
2
  import edge_tts
3
- import tempfile
4
  import asyncio
5
- import traceback
6
  import os
 
7
 
8
  # --- دیکشنری زبان‌ها و صداها با کلیدهای فارسی (نمونه) ---
9
- # ... (بدون تغییر) ...
10
  language_dict_persian_keys = {
11
  'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
12
  'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
@@ -17,7 +16,7 @@ language_dict_persian_keys = {
17
  'انگلیسی - میشل (زن)': 'en-US-MichelleNeural',
18
  'انگلیسی - راجر (مرد)': 'en-US-RogerNeural',
19
  'اسپانیایی (مکزیک) - دالیا (زن)': 'es-MX-DaliaNeural',
20
- 'اسپانیایی (مکزیک) - خورخه (مرد)': 'es-MX-JorgeNeural', # نام خورخه ممکن است دقیق نباشد
21
  'کره‌ای - سان-هی (زن)': 'ko-KR-SunHiNeural',
22
  'کره‌ای - این‌جون (مرد)': 'ko-KR-InJoonNeural',
23
  'تایلندی - پرموادی (زن)': 'th-TH-PremwadeeNeural',
@@ -39,13 +38,13 @@ language_dict_persian_keys = {
39
  'هلندی - کولت (زن)': 'nl-NL-ColetteNeural',
40
  'هلندی - فنا (زن)': 'nl-NL-FennaNeural',
41
  'هلندی - مارتن (مرد)': 'nl-NL-MaartenNeural',
42
- 'مالایی - عثمان (مرد)': 'ms-MY-OsmanNeural', # "Malese" به "مالایی"
43
  'مالایی - یاسمین (زن)': 'ms-MY-YasminNeural',
44
  'نروژی - پرنیل (زن)': 'nb-NO-PernilleNeural',
45
  'نروژی - فین (مرد)': 'nb-NO-FinnNeural',
46
  'سوئدی - سوفی (زن)': 'sv-SE-SofieNeural',
47
  'سوئدی - ماتیاس (مرد)': 'sv-SE-MattiasNeural',
48
- 'عربی (عربستان) - حامد (مرد)': 'ar-SA-HamedNeural', # "عربی" به "عربی (عربستان)"
49
  'عربی (عربستان) - زاریا (زن)': 'ar-SA-ZariyahNeural',
50
  'یونانی - آتنا (زن)': 'el-GR-AthinaNeural',
51
  'یونانی - نستوراس (مرد)': 'el-GR-NestorasNeural',
@@ -53,9 +52,9 @@ language_dict_persian_keys = {
53
  'آلمانی - آمالا (زن)': 'de-DE-AmalaNeural',
54
  'آلمانی - کنراد (مرد)': 'de-DE-ConradNeural',
55
  'آلمانی - کیلیان (مرد)': 'de-DE-KillianNeural',
56
- 'آفریقایی - آدری (زن)': 'af-ZA-AdriNeural', # "Afrikaans" به "آفریقایی"
57
  'آفریقایی - ویلم (مرد)': 'af-ZA-WillemNeural',
58
- 'اتیوپیایی - آمه‌ها (مرد)': 'am-ET-AmehaNeural', # "Ethiopian" به "اتیوپیایی"
59
  'اتیوپیایی - مکدس (زن)': 'am-ET-MekdesNeural',
60
  'عربی (امارات) - فاطمه (زن)': 'ar-AE-FatimaNeural',
61
  'عربی (امارات) - حمدان (مرد)': 'ar-AE-HamdanNeural',
@@ -65,9 +64,9 @@ language_dict_persian_keys = {
65
  'عربی (مصر) - سلما (زن)': 'ar-EG-SalmaNeural',
66
  'عربی (مصر) - شاکر (مرد)': 'ar-EG-ShakirNeural',
67
  'عربی (عراق) - باسل (مرد)': 'ar-IQ-BasselNeural',
68
- 'عربی (عراق) - رعنا (زن)': 'ar-IQ-RanaNeural', # "Rana" به "رعنا"
69
  'عربی (اردن) - سانا (زن)': 'ar-JO-SanaNeural',
70
- 'عربی (اردن) - تایم (مرد)': 'ar-JO-TaimNeural', # "Taim"
71
  'عربی (کویت) - فهد (مرد)': 'ar-KW-FahedNeural',
72
  'عربی (کویت) - نورا (زن)': 'ar-KW-NouraNeural',
73
  'عربی (لبنان) - لیلا (زن)': 'ar-LB-LaylaNeural',
@@ -78,11 +77,11 @@ language_dict_persian_keys = {
78
  'عربی (مراکش) - مونا (زن)': 'ar-MA-MounaNeural',
79
  'عربی (عمان) - عبدالله (مرد)': 'ar-OM-AbdullahNeural',
80
  'عربی (عمان) - عایشه (زن)': 'ar-OM-AyshaNeural',
81
- 'عربی (قطر) - امل (زن)': 'ar-QA-AmalNeural', # "Amal"
82
  'عربی (قطر) - معاذ (مرد)': 'ar-QA-MoazNeural',
83
  'عربی (سوریه) - امانی (زن)': 'ar-SY-AmanyNeural',
84
  'عربی (سوریه) - لیث (مرد)': 'ar-SY-LaithNeural',
85
- 'عربی (تونس) - هادی (مرد)': 'ar-TN-HediNeural', # "Hedi"
86
  'عربی (تونس) - ریم (زن)': 'ar-TN-ReemNeural',
87
  'عربی (یمن) - مریم (زن)': 'ar-YE-MaryamNeural',
88
  'عربی (یمن) - صالح (مرد)': 'ar-YE-SalehNeural',
@@ -93,14 +92,14 @@ language_dict_persian_keys = {
93
  'بنگالی (بنگلادش) - نابانیتا (زن)': 'bn-BD-NabanitaNeural',
94
  'بنگالی (بنگلادش) - پرادیپ (مرد)': 'bn-BD-PradeepNeural',
95
  'بنگالی (هند) - باشکار (مرد)': 'bn-IN-BashkarNeural',
96
- 'بنگالی (هند) - تانیشا (زن)': 'bn-IN-TanishaaNeural', # "Tanishaa"
97
- 'بوسنیایی - گوران (مرد)': 'bs-BA-GoranNeural', # "Bosnian" به "بوسنیایی"
98
  'بوسنیایی - وسنا (زن)': 'bs-BA-VesnaNeural',
99
- 'کاتالان (اسپانیا) - جوآنا (زن)': 'ca-ES-JoanaNeural', # "Catalan"
100
  'کاتالان (اسپانیا) - انریک (مرد)': 'ca-ES-EnricNeural',
101
- 'چکی - آنتونین (مرد)': 'cs-CZ-AntoninNeural', # "Czech" به "چکی"
102
  'چکی - ولاستا (زن)': 'cs-CZ-VlastaNeural',
103
- 'ولزی (بریتانیا) - آلد (مرد)': 'cy-GB-AledNeural', # "Welsh"
104
  'ولزی (بریتانیا) - نیا (زن)': 'cy-GB-NiaNeural',
105
  'دانمارکی - کریستل (زن)': 'da-DK-ChristelNeural',
106
  'دانمارکی - یپه (مرد)': 'da-DK-JeppeNeural',
@@ -171,13 +170,13 @@ language_dict_persian_keys = {
171
  'اسپانیایی (پاراگوئه) - تانیا (زن)': 'es-PY-TaniaNeural',
172
  'اسپانیایی (السالوادور) - لورنا (زن)': 'es-SV-LorenaNeural',
173
  'اسپانیایی (السالوادور) - رودریگو (مرد)': 'es-SV-RodrigoNeural',
174
- 'اسپانیایی (آمریکا) - آلونسو (مرد)': 'es-US-AlonsoNeural', # "United States" به "آمریکا"
175
  'اسپانیایی (آمریکا) - پالوما (زن)': 'es-US-PalomaNeural',
176
  'اسپانیایی (اروگوئه) - ماتئو (مرد)': 'es-UY-MateoNeural',
177
  'اسپانیایی (اروگوئه) - والنتینا (زن)': 'es-UY-ValentinaNeural',
178
  'اسپانیایی (ونزوئلا) - پائولا (زن)': 'es-VE-PaolaNeural',
179
  'اسپانیایی (ونزوئلا) - سباستین (مرد)': 'es-VE-SebastianNeural',
180
- 'استونیایی - آنو (زن)': 'et-EE-AnuNeural', # "Estonian"
181
  'استونیایی - کرت (مرد)': 'et-EE-KertNeural',
182
  'فارسی (ایران) - دل‌آرا (زن)': 'fa-IR-DilaraNeural',
183
  'فارسی (ایران) - فرید (مرد)': 'fa-IR-FaridNeural',
@@ -192,7 +191,7 @@ language_dict_persian_keys = {
192
  'فرانسوی (سوئیس) - فابریس (مرد)': 'fr-CH-FabriceNeural',
193
  'ایرلندی - کلم (مرد)': 'ga-IE-ColmNeural',
194
  'ایرلندی - اورلا (زن)': 'ga-IE-OrlaNeural',
195
- 'گالیسی (اسپانیا) - روی (مرد)': 'gl-ES-RoiNeural', # "Galician"
196
  'گالیسی (اسپانیا) - سابلا (زن)': 'gl-ES-SabelaNeural',
197
  'گجراتی (هند) - دوانی (زن)': 'gu-IN-DhwaniNeural',
198
  'گجراتی (هند) - نیرانجان (مرد)': 'gu-IN-NiranjanNeural',
@@ -200,7 +199,7 @@ language_dict_persian_keys = {
200
  'عبری (اسرائیل) - هیلا (زن)': 'he-IL-HilaNeural',
201
  'هندی (هند) - مادور (مرد)': 'hi-IN-MadhurNeural',
202
  'هندی (هند) - سوارا (زن)': 'hi-IN-SwaraNeural',
203
- 'کروات - گابریلا (زن)': 'hr-HR-GabrijelaNeural', # "Croatian"
204
  'کروات - سرچکو (مرد)': 'hr-HR-SreckoNeural',
205
  'مجاری - نوئمی (زن)': 'hu-HU-NoemiNeural',
206
  'مجاری - تاماش (مرد)': 'hu-HU-TamasNeural',
@@ -208,15 +207,15 @@ language_dict_persian_keys = {
208
  'ارمنی - هایک (مرد)': 'hy-AM-HaykNeural',
209
  'ایسلندی - گودرون (زن)': 'is-IS-GudrunNeural',
210
  'ایسلندی - گونار (مرد)': 'is-IS-GunnarNeural',
211
- 'جاوه‌ای (اندونزی) - دیماس (مرد)': 'jv-ID-DimasNeural', # "Javanese"
212
  'جاوه‌ای (اندونزی) - سیتی (زن)': 'jv-ID-SitiNeural',
213
  'گرجی - اکا (زن)': 'ka-GE-EkaNeural',
214
  'گرجی - گیورگی (مرد)': 'ka-GE-GiorgiNeural',
215
  'قزاقی - آیگول (زن)': 'kk-KZ-AigulNeural',
216
  'قزاقی - دولت (مرد)': 'kk-KZ-DauletNeural',
217
- 'خمر (کامبوج) - پیست (مرد)': 'km-KH-PisethNeural', # "Khmer"
218
  'خمر (کامبوج) - سری‌مم (زن)': 'km-KH-SreymomNeural',
219
- 'کانادایی (هند) - گاگان (مرد)': 'kn-IN-GaganNeural', # "Kannada"
220
  'کانادایی (هند) - ساپنا (زن)': 'kn-IN-SapnaNeural',
221
  'لائوسی - چانتاونگ (مرد)': 'lo-LA-ChanthavongNeural',
222
  'لائوسی - کئومانی (زن)': 'lo-LA-KeomanyNeural',
@@ -232,7 +231,7 @@ language_dict_persian_keys = {
232
  'مغولی - یسوی (زن)': 'mn-MN-YesuiNeural',
233
  'مراتی (هند) - آروهی (زن)': 'mr-IN-AarohiNeural',
234
  'مراتی (هند) - مانوهار (مرد)': 'mr-IN-ManoharNeural',
235
- 'مالتی (مالت) - گریس (زن)': 'mt-MT-GraceNeural', # "Maltese"
236
  'مالتی (مالت) - جوزف (مرد)': 'mt-MT-JosephNeural',
237
  'برمه‌ای (میانمار) - نیلار (زن)': 'my-MM-NilarNeural',
238
  'برمه‌ای (میانمار) - تیها (مرد)': 'my-MM-ThihaNeural',
@@ -250,7 +249,7 @@ language_dict_persian_keys = {
250
  'رومانیایی - امیل (مرد)': 'ro-RO-EmilNeural',
251
  'روسی - دیمیتری (مرد)': 'ru-RU-DmitryNeural',
252
  'روسی - سوتلانا (زن)': 'ru-RU-SvetlanaNeural',
253
- 'سینهالی (سریلانکا) - دینوکا (مرد)': 'si-LK-DinukaNeural', # "Sinhala"
254
  'سینهالی (سریلانکا) - تیلینی (زن)': 'si-LK-ThiliniNeural',
255
  'اسلواک - لوکاش (مرد)': 'sk-SK-LukasNeural',
256
  'اسلواک - ویکتوریا (زن)': 'sk-SK-ViktoriaNeural',
@@ -262,7 +261,7 @@ language_dict_persian_keys = {
262
  'آلبانیایی - ایلیر (مرد)': 'sq-AL-IlirNeural',
263
  'صربی - نیکولا (مرد)': 'sr-RS-NikolaNeural',
264
  'صربی - سوفی (زن)': 'sr-RS-SophieNeural',
265
- 'سوندانی (اندونزی) - جاجانگ (مرد)': 'su-ID-JajangNeural', # "Sundanese"
266
  'سوندانی (اندونزی) - توتی (زن)': 'su-ID-TutiNeural',
267
  'سواحیلی (کنیا) - رفیقی (مرد)': 'sw-KE-RafikiNeural',
268
  'سواحیلی (کنیا) - زوری (زن)': 'sw-KE-ZuriNeural',
@@ -299,207 +298,133 @@ language_dict_persian_keys = {
299
  }
300
 
301
 
302
- # --- توابع تبدیل متن به گفتار (نسخه Async که مستقیماً در Gradio استفاده می‌شود) ---
303
- async def text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch):
304
  """
305
  تابع ناهمزمان برای تبدیل متن به گفتار با استفاده از Edge TTS.
306
- این تابع مستقیماً توسط Gradio فراخوانی می‌شود، نیازی به wrapper همزمان نیست.
307
- خروجی: (پیام وضعیت, مسیر فایل صوتی یا None)
 
 
 
 
 
 
 
 
 
 
308
  """
309
- temp_path = None
310
  try:
311
  if not text:
312
  return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
313
 
314
  voice_id = language_dict_persian_keys.get(language_code_persian)
315
  if voice_id is None:
316
- return f"خطا: مدل صدای انتخاب شده ('{language_code_persian}') یافت نشد.", None
317
 
318
- rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
319
- communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
320
-
321
- # Gradio خودش یک پوشه 'temp' محلی برای فایل‌های موقت دارد.
322
- # استفاده از tempfile.gettempdir() برای اطمینان از ایجاد فایل در مکانی قابل دسترسی توسط سیستم.
323
- # Gradio فایل‌ها را از این مکان به درستی مدیریت می‌کند.
324
- with tempfile.NamedTemporaryFile(delete=False, suffix=".wav", dir=tempfile.gettempdir()) as tmp_file:
325
- temp_path = tmp_file.name
326
-
327
- await communicate.save(temp_path)
328
 
329
- # مهم: Gradio پس از استفاده از فایل (که به output_audio منتقل می‌شود)، آن را خودش پاک می‌کند.
330
- # نیازی به os.remove در اینجا نیست مگر اینکه بخواهید فایل را بلافاصله پس از اولین استفاده (و قبل از اینکه Gradio آن را به کلاینت بفرستد) پاک کنید.
331
- # اما برای نمایش در Gradio، باید فایل تا زمانی که Gradio آن را Serving کند وجود داشته باشد.
332
 
333
- return "تبدیل با موفقیت انجام شد.", temp_path
 
 
 
334
 
335
  except edge_tts.exceptions.NoAudioReceived:
336
- error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
337
- if temp_path and os.path.exists(temp_path):
338
- os.remove(temp_path) # در صورت خطا، فایل موقت را پاک می‌کنیم
339
- return error_msg, None
340
  except ValueError as ve:
341
- error_msg = f"خطا در پارامترهای ورودی: {ve}"
342
- if temp_path and os.path.exists(temp_path):
343
- os.remove(temp_path)
344
- return error_msg, None
345
  except Exception as e:
346
- error_msg = f"خطای غیرمنتظره در سرور: {type(e).__name__} - {e}"
347
- traceback.print_exc() # برای مشاهده traceback در کنسول (در محیط Hugging Face در logs دیده می‌شود)
348
- if temp_path and os.path.exists(temp_path):
349
- os.remove(temp_path)
350
- return error_msg, None
351
 
352
- # --- تعریف تم و CSS (بدون تغییر) ---
353
- app_theme = gr.themes.Soft(
354
- primary_hue=gr.themes.colors.blue,
355
- secondary_hue=gr.themes.colors.sky,
356
- neutral_hue=gr.themes.colors.slate,
357
- font=[gr.themes.GoogleFont("Vazirmatn"), "Arial", "sans-serif"],
358
- ).set(
359
- body_background_fill="#f4f7f6",
360
- )
361
-
362
- custom_css = """
363
- body { font-family: 'Vazirmatn', 'Arial', sans-serif; direction: rtl; }
364
- .gradio-container {
365
- max-width: 95% !important; margin: 1rem auto !important; padding: 1rem !important;
366
- border-radius: 16px !important; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.07) !important;
367
- background-color: #ffffff !important;
368
- }
369
- .app-header {
370
- text-align: center; padding: 20px 10px; background: #34495e; color: white;
371
- border-radius: 12px; margin-bottom: 1.5rem;
372
- }
373
- .app-header img.logo {
374
- width: 50px; height: auto; margin-bottom: 5px;
375
- animation: float_soft 4s ease-in-out infinite alternate;
376
- }
377
- .app-header h1 {
378
- color: white !important; font-size: 1.6em !important; font-weight: 600 !important;
379
- margin: 5px 0;
380
- }
381
- .app-header p { color: #bdc3c7 !important; font-size: 0.9em !important; margin-top: 5px; }
382
- .main-content-row > .gr-column { margin-bottom: 1rem; }
383
- .gr-button.lg.primary {
384
- background: #3498db !important; color: white !important; font-weight: 500 !important;
385
- border-radius: 8px !important; padding: 12px 15px !important; width: 100% !important;
386
- font-size: 1em !important; transition: all 0.2s ease !important;
387
- box-shadow: 0 3px 6px rgba(52, 152, 219, 0.25) !important; border: none !important;
388
- }
389
- .gr-button.lg.primary:hover {
390
- background: #2980b9 !important; transform: translateY(-2px) !important;
391
- box-shadow: 0 5px 10px rgba(52, 152, 219, 0.35) !important;
392
- }
393
- .gr-input, .gr-dropdown, .gr-textbox, .gr-slider {
394
- border-radius: 8px !important; border: 1px solid #ced4da !important;
395
- font-size: 0.95em !important;
396
- }
397
- .gr-input:focus, .gr-dropdown:focus, .gr-textbox:focus {
398
- box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15) !important;
399
- border-color: #5dade2 !important;
400
- }
401
- .gr-panel {
402
- border-radius: 10px !important; border: 1px solid #e9ecef !important;
403
- background-color: #f8f9fa !important; padding: 0.75rem !important;
404
- }
405
- label > span {
406
- font-weight: 500 !important; color: #495057 !important; font-size: 0.9em !important;
407
- margin-bottom: 3px !important; display: block;
408
- }
409
- .gr-examples table { font-size: 0.85em; }
410
- .gr-examples th, .gr-examples td { padding: 6px 8px !important; }
411
- footer { display: none !important; visibility: hidden !important; }
412
- .gradio-footer { display: none !important; visibility: hidden !important; }
413
- .flagging-container { display: none !important; visibility: hidden !important; }
414
- .flex.row.gap-2.absolute.bottom-2.right-2.gr-compact.gr-box.gr-text-gray-500 { display: none !important; visibility: hidden !important; }
415
- div[data-testid="flag"] { display: none !important; }
416
- button[title="Flag"], button[aria-label="Flag"] {display: none !important; }
417
- .footer-utils { display: none !important; visibility: hidden !important; }
418
- @keyframes float_soft {
419
- 0% { transform: translatey(0px) scale(1); }
420
- 50% { transform: translatey(-5px) scale(1.05); }
421
- 100% { transform: translatey(0px) scale(1); }
422
- }
423
- @media (min-width: 768px) {
424
- .gradio-container { max-width: 800px !important; padding: 1.5rem !important;}
425
- .app-header h1 { font-size: 2em !important; }
426
- .app-header p { font-size: 1em !important; }
427
- .main-content-row { display: flex; flex-direction: row; gap: 1.5rem; }
428
- .main-content-row > .gr-column { flex: 1; margin-bottom: 0; }
429
- .main-content-row > .gr-column:nth-child(1) { flex-basis: 60%; }
430
- .main-content-row > .gr-column:nth-child(2) { flex-basis: 40%; }
431
- .gr-button.lg.primary { width: auto !important; }
432
- }
433
- """
434
 
435
- # انتخاب صدای پیش فرض فارسی
436
- default_voice_key_persian = 'فارسی (ایران) - فرید (مرد)'
437
- if default_voice_key_persian not in language_dict_persian_keys:
438
- default_voice_key_persian = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
439
 
440
- LOGO_URL = "https://www.gstatic.com/lamda/images/gemini/google_bard_logo_150_v2_dark_color_1x.png" # بدون تغییر
 
 
 
 
 
441
 
442
- with gr.Blocks(theme=app_theme, css=custom_css) as demo:
443
- with gr.Row():
444
- gr.HTML(f"""
445
- <div class="app-header">
446
- <h1>مبدل هوشمند متن به گفتار</h1>
447
- <p>با کیفیت صدای طبیعی و روان، متن خود را زنده کنید</p>
448
- </div>
449
- """)
450
 
451
- with gr.Row(elem_classes="main-content-row"):
452
- with gr.Column(scale=3):
453
- input_text = gr.Textbox(
454
- lines=5,
455
- label="📝 متن خود را برای تبدیل وارد نمایید",
456
- placeholder="اینجا بنویسید...",
457
- value=""
458
- )
459
- language_dropdown = gr.Dropdown(
460
- choices=list(language_dict_persian_keys.keys()),
461
- value=default_voice_key_persian,
462
- label="🗣️ زبان و گوینده را انتخاب کنید"
463
- )
464
- with gr.Accordion("⚙️ تنظیمات پیشرفته صدا (اختیاری)", open=False):
465
- with gr.Row():
466
- rate_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="سرعت (%)", scale=1)
467
- volume_slider = gr.Slider(minimum=-100, maximum=100, step=1, value=0, label="حجم (%)", scale=1)
468
- pitch_slider = gr.Slider(minimum=-50, maximum=50, step=1, value=0, label="گام (Hz)", scale=2)
 
 
 
 
469
 
470
- submit_button = gr.Button("🔊 تولید و پخش صدا", variant="primary")
 
 
 
 
 
 
 
471
 
472
- with gr.Column(scale=2):
473
- output_text_status = gr.Textbox(label="📊 وضعیت عملیات", interactive=False, lines=1, placeholder="نتیجه اینجا نمایش داده می‌شود...")
474
- # مهم: type="filepath" و interactive=False برای نمایش فایل در Gradio کافیست.
475
- # autoplay=True برای شروع خودکار پخش پس از بارگذاری فایل.
476
- output_audio = gr.Audio(type="filepath", label="🎧 فایل صوتی خروجی", interactive=False, autoplay=True)
477
 
478
- gr.HTML("<hr style='margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid #dee2e6;'>")
 
 
 
 
 
479
 
480
- gr.Examples(
481
- examples=[
482
- ["سلام بر شما، روز خوبی داشته باشید.", 'فارسی (ایران) - دل‌آرا (زن)', 0, 0, 0],
483
- ["This is a test of the speech synthesis system.", 'انگلیسی - جنی (زن)', +5, 0, 0],
484
- ["تجربه کاربری در این سامانه بسیار روان است.", 'فارسی (ایران) - فرید (مرد)', -5, 0, 0],
485
- ],
486
- # نکته مهم: اگر تابع شما async است، Gradio آن را در یک ترد جداگانه اجرا می‌کند.
487
- # نیازی به wrapper همزمان نیست (حذف text_to_speech_edge_sync_wrapper و جایگزینی با text_to_speech_edge_async)
488
- fn=text_to_speech_edge_async,
489
- inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
490
- outputs=[output_text_status, output_audio],
491
- cache_examples=False, # cache_examples=True ممکن است با این نوع خروجی فایل موقت مشکل ایجاد کند
492
- label="💡 چند نمونه برای شروع"
493
- )
494
 
495
- submit_button.click(
496
- # نکته مهم: اگر تابع شما async است، Gradio آن را در یک ترد جداگانه اجرا می‌کند.
497
- # نیازی به wrapper همزمان نیست (حذف text_to_speech_edge_sync_wrapper و جایگزینی با text_to_speech_edge_async)
498
- fn=text_to_speech_edge_async,
499
- inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
500
- outputs=[output_text_status, output_audio],
501
- )
502
 
503
  if __name__ == "__main__":
504
- demo.launch()
505
 
 
 
1
  import edge_tts
 
2
  import asyncio
3
+ import tempfile
4
  import os
5
+ import sys # برای exit() در صورت خطا
6
 
7
  # --- دیکشنری زبان‌ها و صداها با کلیدهای فارسی (نمونه) ---
8
+ # این دیکشنری چون به منطق اصلی تبدیل متن به گفتار مربوط است، بدون تغییر حفظ می‌شود.
9
  language_dict_persian_keys = {
10
  'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
11
  'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
 
16
  'انگلیسی - میشل (زن)': 'en-US-MichelleNeural',
17
  'انگلیسی - راجر (مرد)': 'en-US-RogerNeural',
18
  'اسپانیایی (مکزیک) - دالیا (زن)': 'es-MX-DaliaNeural',
19
+ 'اسپانیایی (مکزیک) - خورخه (مرد)': 'es-MX-JorgeNeural',
20
  'کره‌ای - سان-هی (زن)': 'ko-KR-SunHiNeural',
21
  'کره‌ای - این‌جون (مرد)': 'ko-KR-InJoonNeural',
22
  'تایلندی - پرموادی (زن)': 'th-TH-PremwadeeNeural',
 
38
  'هلندی - کولت (زن)': 'nl-NL-ColetteNeural',
39
  'هلندی - فنا (زن)': 'nl-NL-FennaNeural',
40
  'هلندی - مارتن (مرد)': 'nl-NL-MaartenNeural',
41
+ 'مالایی - عثمان (مرد)': 'ms-MY-OsmanNeural',
42
  'مالایی - یاسمین (زن)': 'ms-MY-YasminNeural',
43
  'نروژی - پرنیل (زن)': 'nb-NO-PernilleNeural',
44
  'نروژی - فین (مرد)': 'nb-NO-FinnNeural',
45
  'سوئدی - سوفی (زن)': 'sv-SE-SofieNeural',
46
  'سوئدی - ماتیاس (مرد)': 'sv-SE-MattiasNeural',
47
+ 'عربی (عربستان) - حامد (مرد)': 'ar-SA-HamedNeural',
48
  'عربی (عربستان) - زاریا (زن)': 'ar-SA-ZariyahNeural',
49
  'یونانی - آتنا (زن)': 'el-GR-AthinaNeural',
50
  'یونانی - نستوراس (مرد)': 'el-GR-NestorasNeural',
 
52
  'آلمانی - آمالا (زن)': 'de-DE-AmalaNeural',
53
  'آلمانی - کنراد (مرد)': 'de-DE-ConradNeural',
54
  'آلمانی - کیلیان (مرد)': 'de-DE-KillianNeural',
55
+ 'آفریقایی - آدری (زن)': 'af-ZA-AdriNeural',
56
  'آفریقایی - ویلم (مرد)': 'af-ZA-WillemNeural',
57
+ 'اتیوپیایی - آمه‌ها (مرد)': 'am-ET-AmehaNeural',
58
  'اتیوپیایی - مکدس (زن)': 'am-ET-MekdesNeural',
59
  'عربی (امارات) - فاطمه (زن)': 'ar-AE-FatimaNeural',
60
  'عربی (امارات) - حمدان (مرد)': 'ar-AE-HamdanNeural',
 
64
  'عربی (مصر) - سلما (زن)': 'ar-EG-SalmaNeural',
65
  'عربی (مصر) - شاکر (مرد)': 'ar-EG-ShakirNeural',
66
  'عربی (عراق) - باسل (مرد)': 'ar-IQ-BasselNeural',
67
+ 'عربی (عراق) - رعنا (زن)': 'ar-IQ-RanaNeural',
68
  'عربی (اردن) - سانا (زن)': 'ar-JO-SanaNeural',
69
+ 'عربی (اردن) - تایم (مرد)': 'ar-JO-TaimNeural',
70
  'عربی (کویت) - فهد (مرد)': 'ar-KW-FahedNeural',
71
  'عربی (کویت) - نورا (زن)': 'ar-KW-NouraNeural',
72
  'عربی (لبنان) - لیلا (زن)': 'ar-LB-LaylaNeural',
 
77
  'عربی (مراکش) - مونا (زن)': 'ar-MA-MounaNeural',
78
  'عربی (عمان) - عبدالله (مرد)': 'ar-OM-AbdullahNeural',
79
  'عربی (عمان) - عایشه (زن)': 'ar-OM-AyshaNeural',
80
+ 'عربی (قطر) - امل (زن)': 'ar-QA-AmalNeural',
81
  'عربی (قطر) - معاذ (مرد)': 'ar-QA-MoazNeural',
82
  'عربی (سوریه) - امانی (زن)': 'ar-SY-AmanyNeural',
83
  'عربی (سوریه) - لیث (مرد)': 'ar-SY-LaithNeural',
84
+ 'عربی (تونس) - هادی (مرد)': 'ar-TN-HediNeural',
85
  'عربی (تونس) - ریم (زن)': 'ar-TN-ReemNeural',
86
  'عربی (یمن) - مریم (زن)': 'ar-YE-MaryamNeural',
87
  'عربی (یمن) - صالح (مرد)': 'ar-YE-SalehNeural',
 
92
  'بنگالی (بنگلادش) - نابانیتا (زن)': 'bn-BD-NabanitaNeural',
93
  'بنگالی (بنگلادش) - پرادیپ (مرد)': 'bn-BD-PradeepNeural',
94
  'بنگالی (هند) - باشکار (مرد)': 'bn-IN-BashkarNeural',
95
+ 'بنگالی (هند) - تانیشا (زن)': 'bn-IN-TanishaaNeural',
96
+ 'بوسنیایی - گوران (مرد)': 'bs-BA-GoranNeural',
97
  'بوسنیایی - وسنا (زن)': 'bs-BA-VesnaNeural',
98
+ 'کاتالان (اسپانیا) - جوآنا (زن)': 'ca-ES-JoanaNeural',
99
  'کاتالان (اسپانیا) - انریک (مرد)': 'ca-ES-EnricNeural',
100
+ 'چکی - آنتونین (مرد)': 'cs-CZ-AntoninNeural',
101
  'چکی - ولاستا (زن)': 'cs-CZ-VlastaNeural',
102
+ 'ولزی (بریتانیا) - آلد (مرد)': 'cy-GB-AledNeural',
103
  'ولزی (بریتانیا) - نیا (زن)': 'cy-GB-NiaNeural',
104
  'دانمارکی - کریستل (زن)': 'da-DK-ChristelNeural',
105
  'دانمارکی - یپه (مرد)': 'da-DK-JeppeNeural',
 
170
  'اسپانیایی (پاراگوئه) - تانیا (زن)': 'es-PY-TaniaNeural',
171
  'اسپانیایی (السالوادور) - لورنا (زن)': 'es-SV-LorenaNeural',
172
  'اسپانیایی (السالوادور) - رودریگو (مرد)': 'es-SV-RodrigoNeural',
173
+ 'اسپانیایی (آمریکا) - آلونسو (مرد)': 'es-US-AlonsoNeural',
174
  'اسپانیایی (آمریکا) - پالوما (زن)': 'es-US-PalomaNeural',
175
  'اسپانیایی (اروگوئه) - ماتئو (مرد)': 'es-UY-MateoNeural',
176
  'اسپانیایی (اروگوئه) - والنتینا (زن)': 'es-UY-ValentinaNeural',
177
  'اسپانیایی (ونزوئلا) - پائولا (زن)': 'es-VE-PaolaNeural',
178
  'اسپانیایی (ونزوئلا) - سباستین (مرد)': 'es-VE-SebastianNeural',
179
+ 'استونیایی - آنو (زن)': 'et-EE-AnuNeural',
180
  'استونیایی - کرت (مرد)': 'et-EE-KertNeural',
181
  'فارسی (ایران) - دل‌آرا (زن)': 'fa-IR-DilaraNeural',
182
  'فارسی (ایران) - فرید (مرد)': 'fa-IR-FaridNeural',
 
191
  'فرانسوی (سوئیس) - فابریس (مرد)': 'fr-CH-FabriceNeural',
192
  'ایرلندی - کلم (مرد)': 'ga-IE-ColmNeural',
193
  'ایرلندی - اورلا (زن)': 'ga-IE-OrlaNeural',
194
+ 'گالیسی (اسپانیا) - روی (مرد)': 'gl-ES-RoiNeural',
195
  'گالیسی (اسپانیا) - سابلا (زن)': 'gl-ES-SabelaNeural',
196
  'گجراتی (هند) - دوانی (زن)': 'gu-IN-DhwaniNeural',
197
  'گجراتی (هند) - نیرانجان (مرد)': 'gu-IN-NiranjanNeural',
 
199
  'عبری (اسرائیل) - هیلا (زن)': 'he-IL-HilaNeural',
200
  'هندی (هند) - مادور (مرد)': 'hi-IN-MadhurNeural',
201
  'هندی (هند) - سوارا (زن)': 'hi-IN-SwaraNeural',
202
+ 'کروات - گابریلا (زن)': 'hr-HR-GabrijelaNeural',
203
  'کروات - سرچکو (مرد)': 'hr-HR-SreckoNeural',
204
  'مجاری - نوئمی (زن)': 'hu-HU-NoemiNeural',
205
  'مجاری - تاماش (مرد)': 'hu-HU-TamasNeural',
 
207
  'ارمنی - هایک (مرد)': 'hy-AM-HaykNeural',
208
  'ایسلندی - گودرون (زن)': 'is-IS-GudrunNeural',
209
  'ایسلندی - گونار (مرد)': 'is-IS-GunnarNeural',
210
+ 'جاوه‌ای (اندونزی) - دیماس (مرد)': 'jv-ID-DimasNeural',
211
  'جاوه‌ای (اندونزی) - سیتی (زن)': 'jv-ID-SitiNeural',
212
  'گرجی - اکا (زن)': 'ka-GE-EkaNeural',
213
  'گرجی - گیورگی (مرد)': 'ka-GE-GiorgiNeural',
214
  'قزاقی - آیگول (زن)': 'kk-KZ-AigulNeural',
215
  'قزاقی - دولت (مرد)': 'kk-KZ-DauletNeural',
216
+ 'خمر (کامبوج) - پیست (مرد)': 'km-KH-PisethNeural',
217
  'خمر (کامبوج) - سری‌مم (زن)': 'km-KH-SreymomNeural',
218
+ 'کانادایی (هند) - گاگان (مرد)': 'kn-IN-GaganNeural',
219
  'کانادایی (هند) - ساپنا (زن)': 'kn-IN-SapnaNeural',
220
  'لائوسی - چانتاونگ (مرد)': 'lo-LA-ChanthavongNeural',
221
  'لائوسی - کئومانی (زن)': 'lo-LA-KeomanyNeural',
 
231
  'مغولی - یسوی (زن)': 'mn-MN-YesuiNeural',
232
  'مراتی (هند) - آروهی (زن)': 'mr-IN-AarohiNeural',
233
  'مراتی (هند) - مانوهار (مرد)': 'mr-IN-ManoharNeural',
234
+ 'مالتی (مالت) - گریس (زن)': 'mt-MT-GraceNeural',
235
  'مالتی (مالت) - جوزف (مرد)': 'mt-MT-JosephNeural',
236
  'برمه‌ای (میانمار) - نیلار (زن)': 'my-MM-NilarNeural',
237
  'برمه‌ای (میانمار) - تیها (مرد)': 'my-MM-ThihaNeural',
 
249
  'رومانیایی - امیل (مرد)': 'ro-RO-EmilNeural',
250
  'روسی - دیمیتری (مرد)': 'ru-RU-DmitryNeural',
251
  'روسی - سوتلانا (زن)': 'ru-RU-SvetlanaNeural',
252
+ 'سینهالی (سریلانکا) - دینوکا (مرد)': 'si-LK-DinukaNeural',
253
  'سینهالی (سریلانکا) - تیلینی (زن)': 'si-LK-ThiliniNeural',
254
  'اسلواک - لوکاش (مرد)': 'sk-SK-LukasNeural',
255
  'اسلواک - ویکتوریا (زن)': 'sk-SK-ViktoriaNeural',
 
261
  'آلبانیایی - ایلیر (مرد)': 'sq-AL-IlirNeural',
262
  'صربی - نیکولا (مرد)': 'sr-RS-NikolaNeural',
263
  'صربی - سوفی (زن)': 'sr-RS-SophieNeural',
264
+ 'سوندانی (اندونزی) - جاجانگ (مرد)': 'su-ID-JajangNeural',
265
  'سوندانی (اندونزی) - توتی (زن)': 'su-ID-TutiNeural',
266
  'سواحیلی (کنیا) - رفیقی (مرد)': 'sw-KE-RafikiNeural',
267
  'سواحیلی (کنیا) - زوری (زن)': 'sw-KE-ZuriNeural',
 
298
  }
299
 
300
 
301
+ async def text_to_speech_edge_async(text, language_code_persian, output_filename="output.mp3", rate=0, volume=0, pitch=0):
 
302
  """
303
  تابع ناهمزمان برای تبدیل متن به گفتار با استفاده از Edge TTS.
304
+ متن را به گفتار تبدیل می‌کند و در یک فایل MP3 ذخیره می‌نماید.
305
+
306
+ Args:
307
+ text (str): متنی که قرار است به گفتار تبدیل شود.
308
+ language_code_persian (str): نام کلید فارسی گوینده از `language_dict_persian_keys`.
309
+ output_filename (str): نام فایل خروجی صوتی (پیش‌فرض: "output.mp3").
310
+ rate (int): تغییر سرعت به درصد (مثبت برای تندتر، منفی برای کندتر).
311
+ volume (int): تغییر حجم به درصد (مثبت برای بلندتر، منفی برای آرام‌تر).
312
+ pitch (int): تغییر گام به هرتز (مثبت برای گام بالاتر، منفی برای گام پایین‌تر).
313
+
314
+ Returns:
315
+ tuple: (status_message, output_file_path or None)
316
  """
 
317
  try:
318
  if not text:
319
  return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
320
 
321
  voice_id = language_dict_persian_keys.get(language_code_persian)
322
  if voice_id is None:
323
+ return f"خطا: مدل صدای انتخاب شده ('{language_code_persian}') یافت نشد. لطفاً از لیست موجود در دیکشنری انتخاب کنید.", None
324
 
325
+ # تبدیل پارامترهای rate, volume, pitch به فرمت مورد نیاز edge_tts
326
+ # برای Pitch، بر خلاف Rate و Volume که درصد هستند، باید به صورت Hz مشخص شود.
327
+ rate_str = f"{int(rate):+g}%"
328
+ volume_str = f"{int(volume):+g}%"
329
+ pitch_str = f"{int(pitch):+g}Hz"
 
 
 
 
 
330
 
331
+ print(f"در حال پردازش متن: '{text[:50]}...' با صدا: {voice_id}, سرعت: {rate_str}, حجم: {volume_str}, گام: {pitch_str}")
 
 
332
 
333
+ communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
334
+
335
+ await communicate.save(output_filename)
336
+ return f"تبدیل با موفقیت انجام شد. فایل صوتی در '{output_filename}' ذخیره شد.", output_filename
337
 
338
  except edge_tts.exceptions.NoAudioReceived:
339
+ return f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id}). ممکن است سرور Edge TTS در دسترس نباشد یا مشکلی در ورودی وجود داشته باشد.", None
 
 
 
340
  except ValueError as ve:
341
+ return f"خطا در پارامترهای ورودی: {ve}", None
 
 
 
342
  except Exception as e:
343
+ import traceback
344
+ traceback.print_exc() # برای مشاهده traceback کامل خطا در کنسول
345
+ return f"خطای غیرمنتظره: {type(e).__name__} - {e}", None
 
 
346
 
347
+ def display_available_voices():
348
+ """نمایش لیست صدای‌های موجود به صورت مرتب شده و فارسی"""
349
+ print("\n--- لیست صداهای موجود ---")
350
+ sorted_voices = sorted(language_dict_persian_keys.keys())
351
+ for i, voice_name in enumerate(sorted_voices):
352
+ print(f"{i+1}. {voice_name}")
353
+ print("-------------------------\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
 
 
 
 
355
 
356
+ def main():
357
+ """
358
+ تابع اصلی برای اجرای برنامه تبدیل متن به گفتار در خط فرمان.
359
+ """
360
+ print("به مبدل هوشمند متن به گفتار خوش آمدید!")
361
+ print("---------------")
362
 
363
+ while True:
364
+ text_input = input("لطفاً متن خود را وارد کنید (برای خروج 'exit' را بنویسید):\n")
365
+ if text_input.lower() == 'exit':
366
+ print("خداحافظ!")
367
+ sys.exit(0)
368
+ if not text_input.strip():
369
+ print("متن نمی‌تواند خالی باشد. لطفاً دوباره تلاش کنید.")
370
+ continue
371
 
372
+ display_available_voices()
373
+
374
+ # انتخاب صدا به صورت عددی یا تایپ نام کامل
375
+ voice_choice = input("لطفاً شماره یا نام کامل گوینده مورد نظر را وارد کنید (مثال: 15 یا 'فارسی (ایران) - فرید (م��د)'):\n")
376
+
377
+ selected_voice_key = None
378
+ try:
379
+ # تلاش برای تبدیل ورودی به عدد (اگر کاربر شماره وارد کرده باشد)
380
+ idx = int(voice_choice) - 1
381
+ sorted_voices = sorted(language_dict_persian_keys.keys())
382
+ if 0 <= idx < len(sorted_voices):
383
+ selected_voice_key = sorted_voices[idx]
384
+ else:
385
+ print("انتخاب نامعتبر. لطفاً شماره‌ای معتبر از لیست وارد کنید.")
386
+ continue
387
+ except ValueError:
388
+ # اگر عدد نبود، پس کاربر نام کامل را وارد کرده است
389
+ if voice_choice in language_dict_persian_keys:
390
+ selected_voice_key = voice_choice
391
+ else:
392
+ print("نام گوینده یافت نشد. لطفاً از لیست بالا یک نام معتبر یا شماره ورودی کنید.")
393
+ continue
394
 
395
+ # تنظیمات اختیاری
396
+ try:
397
+ rate = int(input("سرعت صدا را وارد کنید (مثال: 0 برای عادی, +20 برای تندتر, -10 برای کندتر): [پیش‌فرض: 0]\n") or "0")
398
+ volume = int(input("حجم صدا را وارد کنید (مثال: 0 برای عادی, +50 برای بلندتر, -30 برای آرام‌تر): [پیش‌فرض: 0]\n") or "0")
399
+ pitch = int(input("گام صدا را وارد کنید (مثال: 0 برای عادی, +5 برای بالاتر, -5 برای پایین‌تر): [پیش‌فرض: 0]\n") or "0")
400
+ except ValueError:
401
+ print("ورودی سرعت، حجم یا گام نامعتبر است. از مقادیر پیش‌فرض استفاده می‌شود.")
402
+ rate, volume, pitch = 0, 0, 0
403
 
404
+ output_file = input("نام فایل خروجی (مثال: my_audio.mp3) [پیش‌فرض: output.mp3]:\n")
405
+ if not output_file.strip():
406
+ output_file = "output.mp3"
407
+ elif not output_file.lower().endswith((".mp3", ".wav")): # اطمینان از پسوند صحیح
408
+ output_file += ".mp3"
409
 
410
+ print(f"\nدر حال تولید صدا...")
411
+
412
+ # اجرای تابع ناهمزمان به صورت همزمان
413
+ status_message, audio_file_path = asyncio.run(
414
+ text_to_speech_edge_async(text_input, selected_voice_key, output_file, rate, volume, pitch)
415
+ )
416
 
417
+ print("\n" + status_message)
418
+ if audio_file_path:
419
+ print(f"فایل صوتی در مسیر: {os.path.abspath(audio_file_path)}")
420
+ # optionally: play the audio after creation
421
+ # For playing audio, you would need another library like 'playsound' or 'simpleaudio'
422
+ # Be aware that playing audio directly from a script can sometimes have platform-specific issues
423
+ # print("اگر می‌خواهید بلافاصله فایل را پخش کنید، 'playsound' را نصب کنید و دستور پخش را اینجا اضافه کنید.")
424
+
425
+ print("\n---------------")
 
 
 
 
 
426
 
 
 
 
 
 
 
 
427
 
428
  if __name__ == "__main__":
429
+ main()
430