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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +465 -127
app.py CHANGED
@@ -1,11 +1,16 @@
 
 
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,7 +21,7 @@ language_dict_persian_keys = {
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',
@@ -29,7 +34,7 @@ language_dict_persian_keys = {
29
  'فرانسوی - الوئیز (زن)': 'fr-FR-EloiseNeural',
30
  'فرانسوی - هانری (مرد)': 'fr-FR-HenriNeural',
31
  'پرتغالی (برزیل) - فرانسیسکا (زن)': 'pt-BR-FranciscaNeural',
32
- 'پرتغالی (برزیل) - آنتونیو (مرد)': 'pt-BR-AntonioNeural',
33
  'اندونزیایی - آردی (مرد)': 'id-ID-ArdiNeural',
34
  'اندونزیایی - گادیس (زن)': 'id-ID-GadisNeural',
35
  'ایتالیایی - ایزابلا (زن)': 'it-IT-IsabellaNeural',
@@ -38,13 +43,13 @@ language_dict_persian_keys = {
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,9 +57,9 @@ language_dict_persian_keys = {
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,9 +69,9 @@ language_dict_persian_keys = {
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,11 +82,11 @@ language_dict_persian_keys = {
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,14 +97,14 @@ language_dict_persian_keys = {
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,13 +175,13 @@ language_dict_persian_keys = {
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,7 +196,7 @@ language_dict_persian_keys = {
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,23 +204,23 @@ language_dict_persian_keys = {
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',
206
  'ارمنی - آناهیت (زن)': 'hy-AM-AnahitNeural',
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,7 +236,7 @@ language_dict_persian_keys = {
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,7 +254,7 @@ language_dict_persian_keys = {
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,7 +266,7 @@ language_dict_persian_keys = {
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,133 +303,466 @@ language_dict_persian_keys = {
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
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import ttk, messagebox, filedialog
3
  import edge_tts
4
  import asyncio
5
  import tempfile
6
  import os
7
+ import threading
8
+ import sys # برای بررسی اینکه آیا در محیط PyInstaller اجرا می شود
9
+ import pygame # برای پخش صدا - Pygame نیاز به نصب دارد اما سبک است.
10
 
11
  # --- دیکشنری زبان‌ها و صداها با کلیدهای فارسی (نمونه) ---
12
+ # ... (همان دیکشنری قبلی باید اینجا کپی شود) ...
13
+ # اگر دیکشنری خیلی بزرگ است، می‌توانید آن را از یک فایل جداگانه import کنید.
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',
 
34
  'فرانسوی - الوئیز (زن)': 'fr-FR-EloiseNeural',
35
  'فرانسوی - هانری (مرد)': 'fr-FR-HenriNeural',
36
  'پرتغالی (برزیل) - فرانسیسکا (زن)': 'pt-BR-FranciscaNeural',
37
+ 'پرتغالی (برزیل) - آنتونیو (مord)': 'pt-BR-AntonioNeural',
38
  'اندونزیایی - آردی (مرد)': 'id-ID-ArdiNeural',
39
  'اندونزیایی - گادیس (زن)': 'id-ID-GadisNeural',
40
  'ایتالیایی - ایزابلا (زن)': 'it-IT-IsabellaNeural',
 
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',
211
  'ارمنی - آناهیت (زن)': 'hy-AM-AnahitNeural',
212
+ 'ارمنی - هایک (hy-AM-HaykNeural)': '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',
 
303
  }
304
 
305
 
306
+ # --- تابع کمکی برای پیدا کردن مسیر صحیح در زمان اجرای PyInstaller ---
307
+ def resource_path(relative_path):
308
+ """ Get absolute path to resource, works for dev and for PyInstaller """
309
+ try:
310
+ # PyInstaller creates a temp folder and stores path in _MEIPASS
311
+ base_path = sys._MEIPASS
312
+ except Exception:
313
+ base_path = os.path.abspath(".")
314
+ return os.path.join(base_path, relative_path)
315
+
316
+ # --- تابع ناهمزمان برای تبدیل متن به گفتار ---
317
+ async def generate_speech(text, voice_key, rate, volume, pitch, output_file_path=None):
318
  """
319
  تابع ناهمزمان برای تبدیل متن به گفتار با استفاده از Edge TTS.
320
+ output_file_path: اگر None باشد، یک فایل موقت ایجاد می‌کند و مسیر آن را برمی‌گرداند.
321
+ در غیر این صورت، در مسیر مشخص شده ذخیره می‌کند.
 
 
 
 
 
 
 
 
 
 
322
  """
323
+ temp_path = None # مسیر فایل موقت اگر output_file_path None باشد.
324
  try:
325
  if not text:
326
  return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
327
 
328
+ voice_id = language_dict_persian_keys.get(voice_key)
329
  if voice_id is None:
330
+ return f"خطا: مدل صدای انتخاب شده ('{voice_key}') یافت نشد.", None
331
 
332
+ # تبدیل مقادیر اسلایدر به فرمت مورد نیاز edge_tts
333
+ rate_str = f"{int(rate):+g}%" # +g برای نمایش A+0 به عنوان A+0
 
334
  volume_str = f"{int(volume):+g}%"
335
+ pitch_str = f"{int(pitch):+g}Hz"
 
 
336
 
337
  communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
338
 
339
+ if output_file_path:
340
+ file_to_save_to = output_file_path
341
+ else:
342
+ # ایجاد فایل موقت فقط برای پخش
343
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
344
+ temp_path = tmp_file.name
345
+ file_to_save_to = temp_path
346
+
347
+ await communicate.save(file_to_save_to)
348
+
349
+ return "تبدیل با موفقیت انجام شد.", file_to_save_to
350
 
351
  except edge_tts.exceptions.NoAudioReceived:
352
+ error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
353
+ if temp_path and os.path.exists(temp_path):
354
+ os.remove(temp_path)
355
+ return error_msg, None
356
  except ValueError as ve:
357
+ error_msg = f"خطا در پارامترهای ورودی: {ve}"
358
+ if temp_path and os.path.exists(temp_path):
359
+ os.remove(temp_path)
360
+ return error_msg, None
361
  except Exception as e:
362
+ error_msg = f"خطای غیرمنتظره: {type(e).__name__} - {e}"
363
+ if temp_path and os.path.exists(temp_path):
364
+ os.remove(temp_path)
365
+ return error_msg, None
366
 
367
+ # --- کلاس اصلی برنامه Tkinter ---
368
+ class TTSApp:
369
+ def __init__(self, master):
370
+ self.master = master
371
+ master.title("مبدل هوشمند متن به گفتار")
372
+ master.geometry("800x650")
373
+ master.resizable(False, False)
374
+ master.option_add('*Font', 'Vazirmatn 10') # تنظیم فونت پیش‌فرض (نیاز به نصب Vazirmatn در سیستم)
375
 
376
+ # تلاش برای بارگذاری فونت‌های RTL
377
+ try:
378
+ # Tkinter در ویندوز و مک از نام فونت استفاده می‌کند.
379
+ # برای لینوکس ممکن است نیاز به نصب farsi-fonts یا مشابه آن باشد.
380
+ master.tk.call('font', 'create', 'Vazirmatn', '-family', 'Vazirmatn', '-size', '10')
381
+ master.tk.call('font', 'configure', 'TkDefaultFont', '-family', 'Vazirmatn')
382
+ master.tk.call('font', 'configure', 'TkTextFont', '-family', 'Vazirmatn')
383
+ master.tk.call('font', 'configure', 'TkFixedFont', '-family', 'Vazirmatn')
384
+ master.tk.call('font', 'configure', 'heading', '-family', 'Vazirmatn', '-size', '12', '-weight', 'bold')
385
+ # Fallback for systems without Vazirmatn
386
+ master.option_add('*Font', 'Vazirmatn 10', 'Arial 10', 'Helvetica 10')
387
+ master.option_add('*Text.font', 'Vazirmatn 10', 'Arial 10', 'Helvetica 10')
388
+ master.option_add('*Entry.font', 'Vazirmatn 10', 'Arial 10', 'Helvetica 10')
389
+ except tk.TclError:
390
+ print("Vazirmatn font not found, falling back to default.")
391
+ master.option_add('*Font', 'Arial 10', 'Helvetica 10')
392
 
393
+
394
+ self.current_audio_file = None # برای نگهداری مسیر فایل صوتی فعلی برای پخش یا حذف
395
+
396
+ # Styles (Styling Tkinter widgets)
397
+ style = ttk.Style()
398
+ style.theme_use('clam') # 'default', 'alt', 'clam', 'classic'
399
+ style.configure('TFrame', background='#f4f7f6')
400
+ style.configure('TLabel', background='#f4f7f6', foreground='#333', font=('Vazirmatn', 10))
401
+ style.configure('TButton', background='#3498db', foreground='white', font=('Vazirmatn', 10, 'bold'), borderwidth=1, focusthickness=3, focuscolor='none')
402
+ style.map('TButton', background=[('active', '#2980b9')])
403
+ style.configure('TEntry', fieldbackground='white', foreground='#333', borderwidth=1, relief='solid')
404
+ style.configure('TCombobox', fieldbackground='white', foreground='#333', selectbackground='#3498db', selectforeground='white')
405
+ style.configure('Horizontal.TScale', background='#f4f7f6', troughcolor='#ddd', sliderrelief='flat', sliderthickness=15, borderwidth=0)
406
+ style.configure('TText', background='white', foreground='#333', borderwidth=1, relief='solid')
407
+
408
+
409
+ # Main Frame with padding
410
+ main_frame = ttk.Frame(master, padding="20 20 20 20")
411
+ main_frame.pack(fill=tk.BOTH, expand=True)
412
+
413
+ # Header (similar to Gradio's header)
414
+ header_frame = ttk.Frame(main_frame, style='TFrame', relief="raised", padding="15", borderwidth=0)
415
+ # style.configure('Header.TFrame', background='#34495e', borderwidth=0)
416
+ header_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 20))
417
+ header_frame.grid_columnconfigure(0, weight=1) # Center the content
418
+
419
+ header_label = ttk.Label(header_frame, text="مبدل هوشمند متن به گفتار", font=('Vazirmatn', 16, 'bold'), foreground='white', background='#34495e')
420
+ header_label.pack(pady=5)
421
+ sub_header_label = ttk.Label(header_frame, text="با کیفیت صدای طبیعی و روان، متن خود را زنده کنید", font=('Vazirmatn', 10), foreground='#bdc3c7', background='#34495e')
422
+ sub_header_label.pack(pady=5)
423
+ # Apply header background explicitly
424
+ header_frame.config(background='#34495e')
425
+
426
+
427
+ # Input Text
428
+ ttk.Label(main_frame, text="📝 متن خود را برای تبدیل وارد نمایید").grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 5))
429
+ self.text_input = tk.Text(main_frame, height=8, width=70, font=('Vazirmatn', 10), wrap=tk.WORD, bd=1, relief="solid")
430
+ self.text_input.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(0, 10))
431
+ self.text_input.insert(tk.END, "سلام بر شما، روز خوبی داشته باشید. این یک تست اولیه برای تبدیل متن به گفتار است.")
432
+
433
+ # Language Dropdown
434
+ ttk.Label(main_frame, text="🗣️ زبان و گوینده را انتخاب کنید").grid(row=3, column=0, columnspan=2, sticky="w", pady=(0, 5))
435
+ voices = list(language_dict_persian_keys.keys())
436
+ default_voice_key_persian = 'فارسی (ایران) - فرید (مرد)'
437
+ if default_voice_key_persian not in voices:
438
+ default_voice_key_persian = voices[0] if voices else ''
439
+
440
+ self.language_var = tk.StringVar(master)
441
+ self.language_var.set(default_voice_key_persian)
442
+ self.language_dropdown = ttk.Combobox(main_frame, textvariable=self.language_var, values=voices, state="readonly", width=50) # width adjusted for better appearance
443
+ self.language_dropdown.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 15))
444
+
445
+
446
+ # Advanced Settings (Accordion-like structure)
447
+ self.advanced_frame = ttk.Frame(main_frame, style='TFrame', relief="groove", borderwidth=1, padding="10")
448
+ self.advanced_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 15))
449
+ self.advanced_frame.grid_columnconfigure(0, weight=1)
450
+ self.advanced_frame.grid_columnconfigure(1, weight=1)
451
+
452
+ # Header for accordion (clickable)
453
+ self.advanced_settings_open = False
454
+ self.accordion_header = ttk.Label(self.advanced_frame, text="⚙️ تنظیمات پیشرفته صدا (اختیاری)", font=('Vazirmatn', 10, 'bold'), foreground='#444', background='#f0f0f0', cursor="hand2")
455
+ self.accordion_header.grid(row=0, column=0, columnspan=2, sticky="ew", ipadx=5, ipady=5)
456
+ self.accordion_header.bind("<Button-1>", self.toggle_advanced_settings) # Bind click event
457
+ self.advanced_content_frame = ttk.Frame(self.advanced_frame, style='TFrame') # Frame for sliders
458
+
459
+ # Sliders
460
+ ttk.Label(self.advanced_content_frame, text="سرعت (%)").grid(row=0, column=0, sticky="w", padx=5, pady=2)
461
+ self.rate_slider = ttk.Scale(self.advanced_content_frame, from_=-100, to=100, orient="horizontal", style='Horizontal.TScale')
462
+ self.rate_slider.set(0)
463
+ self.rate_slider.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
464
+ self.rate_value_label = ttk.Label(self.advanced_content_frame, text=f"0%", font=('Vazirmatn', 9))
465
+ self.rate_value_label.grid(row=1, column=1, sticky="w", padx=(0,5), pady=5)
466
+ self.rate_slider.bind("<Motion>", lambda e: self.rate_value_label.config(text=f"{int(self.rate_slider.get()):+g}%"))
467
+
468
+
469
+ ttk.Label(self.advanced_content_frame, text="حجم (%)").grid(row=2, column=0, sticky="w", padx=5, pady=2)
470
+ self.volume_slider = ttk.Scale(self.advanced_content_frame, from_=-100, to=100, orient="horizontal", style='Horizontal.TScale')
471
+ self.volume_slider.set(0)
472
+ self.volume_slider.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
473
+ self.volume_value_label = ttk.Label(self.advanced_content_frame, text=f"0%", font=('Vazirmatn', 9))
474
+ self.volume_value_label.grid(row=3, column=1, sticky="w", padx=(0,5), pady=5)
475
+ self.volume_slider.bind("<Motion>", lambda e: self.volume_value_label.config(text=f"{int(self.volume_slider.get()):+g}%"))
476
+
477
+
478
+ ttk.Label(self.advanced_content_frame, text="گام (Hz)").grid(row=4, column=0, sticky="w", padx=5, pady=2)
479
+ self.pitch_slider = ttk.Scale(self.advanced_content_frame, from_=-50, to=50, orient="horizontal", style='Horizontal.TScale')
480
+ self.pitch_slider.set(0)
481
+ self.pitch_slider.grid(row=5, column=0, sticky="ew", padx=5, pady=5)
482
+ self.pitch_value_label = ttk.Label(self.advanced_content_frame, text=f"0Hz", font=('Vazirmatn', 9))
483
+ self.pitch_value_label.grid(row=5, column=1, sticky="w", padx=(0,5), pady=5)
484
+ self.pitch_slider.bind("<Motion>", lambda e: self.pitch_value_label.config(text=f"{int(self.pitch_slider.get()):+g}Hz"))
485
+
486
+ self.advanced_content_frame.grid_columnconfigure(0, weight=1)
487
+ # Initially hide advanced settings
488
+ self.toggle_advanced_settings(None) # Call once to set initial state
489
+
490
+ # Buttons
491
+ button_frame = ttk.Frame(main_frame, style='TFrame')
492
+ button_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
493
+ button_frame.grid_columnconfigure(0, weight=1)
494
+ button_frame.grid_columnconfigure(1, weight=1)
495
+
496
+ self.play_button = ttk.Button(button_frame, text="🔊 تولید و پخش صدا", command=self.on_generate_and_play)
497
+ self.play_button.grid(row=0, column=0, sticky="ew", padx=(0, 5))
498
+
499
+ self.save_button = ttk.Button(button_frame, text="💾 ذخیره فایل صوتی", command=self.on_save_audio)
500
+ self.save_button.grid(row=0, column=1, sticky="ew", padx=(5, 0))
501
+
502
+
503
+ # Status and Output Audio Player
504
+ ttk.Label(main_frame, text="📊 وضعیت عملیات").grid(row=7, column=0, columnspan=2, sticky="w", pady=(10, 5))
505
+ self.status_label = ttk.Label(main_frame, text="آماده کار", relief="sunken", anchor="w", padding=5, background="#e0f2f7", foreground="#00796b")
506
+ self.status_label.grid(row=8, column=0, columnspan=2, sticky="ew", pady=(0, 10))
507
+
508
+ ttk.Label(main_frame, text="🎧 پخش صدا").grid(row=9, column=0, columnspan=2, sticky="w", pady=(0, 5))
509
+ # Player controls (Placeholder for now)
510
+ player_frame = ttk.Frame(main_frame, style='TFrame', relief="solid", borderwidth=1, padding="5")
511
+ player_frame.grid(row=10, column=0, columnspan=2, sticky="ew")
512
+ player_frame.grid_columnconfigure(0, weight=1)
513
+ player_frame.grid_columnconfigure(1, weight=1)
514
+ player_frame.grid_columnconfigure(2, weight=1)
515
+
516
+ self.player_status_label = ttk.Label(player_frame, text="فایلی برای پخش موجود نیست.", foreground='#555')
517
+ self.player_status_label.grid(row=0, column=0, columnspan=3, sticky="ew")
518
+
519
+ self.play_current_button = ttk.Button(player_frame, text="▶️ پخش", command=self.play_generated_audio, state=tk.DISABLED)
520
+ self.play_current_button.grid(row=1, column=0, sticky="ew", padx=2, pady=5)
521
 
522
+ self.stop_button = ttk.Button(player_frame, text="⏹️ توقف", command=self.stop_audio, state=tk.DISABLED, style='TButton', background='#e74c3c')
523
+ self.stop_button.grid(row=1, column=1, sticky="ew", padx=2, pady=5)
524
+ style.map('TButton', background=[('active', '#c0392b')], style='TButton')
525
+
526
+ self.clear_audio_button = ttk.Button(player_frame, text="🗑️ پاک کردن", command=self.clear_audio, state=tk.DISABLED, style='TButton', background='#95a5a6')
527
+ self.clear_audio_button.grid(row=1, column=2, sticky="ew", padx=2, pady=5)
528
+ style.map('TButton', background=[('active', '#7f8c8d')], style='TButton')
529
+
530
+
531
+ # Configure grid weights for resizing
532
+ main_frame.grid_columnconfigure(0, weight=1)
533
+ main_frame.grid_columnconfigure(1, weight=1) # The second column is hidden but needs to exist
534
 
535
+ # Initialize pygame mixer for audio playback
536
  try:
537
+ pygame.mixer.init()
538
+ # Set a lower buffer size for less latency but might need more CPU on some systems
539
+ # pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
540
+ except Exception as e:
541
+ messagebox.showerror("خطا در Pygame", f"مشکلی در راه‌اندازی Pygame Mixer وجود دارد. پخش صدا ممکن است کار نکند.\n{e}")
542
+ self.play_current_button.config(state=tk.DISABLED)
543
+ self.stop_button.config(state=tk.DISABLED)
544
+ self.clear_audio_button.config(state=tk.DISABLED)
545
+ self.player_status_label.config(text="پخش صدا غیرفعال است (مشکل در Pygame).", foreground='red')
546
+
547
+
548
+ def toggle_advanced_settings(self, event):
549
+ """Toggle the visibility of advanced settings sliders."""
550
+ if self.advanced_settings_open:
551
+ self.advanced_content_frame.grid_forget()
552
+ self.accordion_header.config(text="⚙️ تنظیمات پیشرفته صدا (اختیاری)")
553
+ else:
554
+ self.advanced_content_frame.grid(row=1, column=0, columnspan=2, sticky="ew") # Place content frame
555
+ self.accordion_header.config(text="���️ تنظیمات پیشرفته صدا (اختیاری) - باز")
556
+ self.advanced_settings_open = not self.advanced_settings_open
557
+ self.master.update_idletasks() # Force update layout
558
+
559
+
560
+ def update_status(self, message, is_error=False):
561
+ """Updates the status label in the UI."""
562
+ if is_error:
563
+ self.status_label.config(text=message, background="#ffebee", foreground="#c62828") # Light red
564
+ else:
565
+ self.status_label.config(text=message, background="#e0f2f7", foreground="#00796b") # Light blue-green
566
+ self.master.update_idletasks() # Ensure UI updates immediately
567
+
568
+ def on_generate_and_play(self):
569
+ """Handles the 'Generate and Play' button click."""
570
+ text = self.text_input.get("1.0", tk.END).strip()
571
+ voice_key = self.language_var.get()
572
+ rate = self.rate_slider.get()
573
+ volume = self.volume_slider.get()
574
+ pitch = self.pitch_slider.get()
575
+
576
+ self.update_status("در حال تولید صدا، لطفاً صبر کنید...", is_error=False)
577
+ self.play_button.config(state=tk.DISABLED)
578
+ self.save_button.config(state=tk.DISABLED)
579
+ self.play_current_button.config(state=tk.DISABLED)
580
+ self.stop_button.config(state=tk.DISABLED)
581
+ self.clear_audio_button.config(state=tk.DISABLED)
582
+ self.player_status_label.config(text="در حال تولید...", foreground='blue')
583
+
584
+ # Run the async TTS operation in a separate thread to not block the UI
585
+ threading.Thread(target=self._run_tts_async, args=(text, voice_key, rate, volume, pitch)).start()
586
+
587
+ def _run_tts_async(self, text, voice_key, rate, volume, pitch):
588
+ """Helper to run the async generate_speech function."""
589
+ loop = asyncio.new_event_loop()
590
+ asyncio.set_event_loop(loop)
591
+
592
+ status_msg, audio_path = loop.run_until_complete(generate_speech(text, voice_key, rate, volume, pitch))
593
 
594
+ # Schedule the UI update back on the main Tkinter thread
595
+ self.master.after(0, self._process_tts_result, status_msg, audio_path)
596
+
597
+ def _process_tts_result(self, status_msg, audio_path):
598
+ """Processes the result of the TTS operation and updates UI."""
599
+ self.play_button.config(state=tk.NORMAL)
600
+ self.save_button.config(state=tk.NORMAL)
601
+
602
+ if audio_path:
603
+ self.update_status(status_msg, is_error=False)
604
+ self.clear_audio() # Clear any previous played audio
605
+ self.current_audio_file = audio_path
606
+ self.play_generated_audio() # Automatically play the audio
607
+ self.play_current_button.config(state=tk.NORMAL)
608
+ self.player_status_label.config(text=f"فایل صوتی آماده: {os.path.basename(audio_path)}", foreground='green')
609
+ self.stop_button.config(state=tk.NORMAL) # Enable stop button after initiating play
610
+ self.clear_audio_button.config(state=tk.NORMAL)
611
+ else:
612
+ self.update_status(status_msg, is_error=True)
613
+ self.player_status_label.config(text="خطا در تولید یا پخش.", foreground='red')
614
+ self.clear_audio_button.config(state=tk.DISABLED)
615
+ self.stop_button.config(state=tk.DISABLED)
616
+
617
+
618
+ def play_generated_audio(self):
619
+ """Plays the audio file if one has been generated."""
620
+ if self.current_audio_file and os.path.exists(self.current_audio_file):
621
+ try:
622
+ # Stop any currently playing audio before loading new one
623
+ if pygame.mixer.music.get_busy():
624
+ pygame.mixer.music.stop()
625
+
626
+ pygame.mixer.music.load(self.current_audio_file)
627
+ pygame.mixer.music.play()
628
+ self.player_status_label.config(text="در حال پخش...", foreground='blue')
629
+ self.play_current_button.config(state=tk.DISABLED) # Disable play button while playing
630
+ self.stop_button.config(state=tk.NORMAL) # Enable stop button
631
+ self.clear_audio_button.config(state=tk.NORMAL)
632
+
633
+ # Check if playback has finished in a separate thread
634
+ threading.Thread(target=self._monitor_playback).start()
635
+
636
+ except pygame.error as e:
637
+ messagebox.showerror("خطای پخش صدا", f"مشکلی در پخش فایل صوتی وجود دارد: {e}")
638
+ self.player_status_label.config(text="خطا در پخش.", foreground='red')
639
+ self.play_current_button.config(state=tk.NORMAL) # Re-enable if error
640
+ self.stop_button.config(state=tk.DISABLED) # Disable if error
641
+ else:
642
+ self.player_status_label.config(text="فایلی برای پخش موجود نیست.", foreground='#555')
643
+ self.play_current_button.config(state=tk.DISABLED)
644
+ self.stop_button.config(state=tk.DISABLED)
645
+ self.clear_audio_button.config(state=tk.DISABLED)
646
+
647
+ def _monitor_playback(self):
648
+ """Monitors Pygame music playback to update UI when finished."""
649
+ while pygame.mixer.music.get_busy():
650
+ pygame.time.Clock().tick(10) # Check every 100ms
651
+
652
+ # Schedule UI update on main Tkinter thread
653
+ self.master.after(0, self._on_playback_finished)
654
+
655
+ def _on_playback_finished(self):
656
+ """Updates UI after audio playback finishes."""
657
+ if not pygame.mixer.music.get_busy():
658
+ self.player_status_label.config(text="پخش به پایان رسید.", foreground='green')
659
+ self.play_current_button.config(state=tk.NORMAL) # Re-enable play button
660
+ self.stop_button.config(state=tk.DISABLED)
661
+
662
+ def stop_audio(self):
663
+ """Stops current audio playback."""
664
+ if pygame.mixer.music.get_busy():
665
+ pygame.mixer.music.stop()
666
+ self.player_status_label.config(text="پخش متوقف شد.")
667
+ self.play_current_button.config(state=tk.NORMAL)
668
+ self.stop_button.config(state=tk.DISABLED)
669
+
670
+ def clear_audio(self):
671
+ """Clears the current audio file and updates UI."""
672
+ self.stop_audio() # Ensure playback is stopped
673
+ if self.current_audio_file and os.path.exists(self.current_audio_file):
674
+ try:
675
+ os.remove(self.current_audio_file)
676
+ self.current_audio_file = None
677
+ self.player_status_label.config(text="فایل صوتی پاک شد.", foreground='#555')
678
+ except OSError as e:
679
+ messagebox.showerror("خطا در حذف فایل", f"فایل صوتی قابل حذف نیست: {e}")
680
+
681
+ self.play_current_button.config(state=tk.DISABLED)
682
+ self.stop_button.config(state=tk.DISABLED)
683
+ self.clear_audio_button.config(state=tk.DISABLED)
684
+
685
+
686
+ def on_save_audio(self):
687
+ """Handles the 'Save Audio' button click."""
688
+ text = self.text_input.get("1.0", tk.END).strip()
689
+ if not text:
690
+ messagebox.showwarning("ورودی خالی", "لطفاً متنی برای ذخیره وارد کنید.")
691
+ return
692
+
693
+ voice_key = self.language_var.get()
694
+ rate = self.rate_slider.get()
695
+ volume = self.volume_slider.get()
696
+ pitch = self.pitch_slider.get()
697
+
698
+ file_path = filedialog.asksaveasfilename(
699
+ defaultextension=".wav",
700
+ filetypes=[("WAV files", "*.wav")],
701
+ title="ذخیره فایل صوتی به عنوان..."
702
  )
703
+ if not file_path:
704
+ return # User cancelled save dialog
705
+
706
+ self.update_status("در حال تولید و ذخیره صدا، لطفاً صبر کنید...", is_error=False)
707
+ self.play_button.config(state=tk.DISABLED)
708
+ self.save_button.config(state=tk.DISABLED)
709
+
710
+ # Run the async TTS operation for saving in a separate thread
711
+ threading.Thread(target=self._run_tts_async_for_save, args=(text, voice_key, rate, volume, pitch, file_path)).start()
712
 
713
+ def _run_tts_async_for_save(self, text, voice_key, rate, volume, pitch, file_path):
714
+ """Helper to run the async generate_speech for saving."""
715
+ loop = asyncio.new_event_loop()
716
+ asyncio.set_event_loop(loop)
717
+
718
+ status_msg, _ = loop.run_until_complete(generate_speech(text, voice_key, rate, volume, pitch, file_path))
 
719
 
720
+ # Schedule the UI update back on the main Tkinter thread
721
+ self.master.after(0, self._process_save_result, status_msg, file_path)
722
+
723
+ def _process_save_result(self, status_msg, file_path):
724
+ """Processes the result of the save operation and updates UI."""
725
+ self.play_button.config(state=tk.NORMAL)
726
+ self.save_button.config(state=tk.NORMAL)
727
 
728
+ if "موفقیت" in status_msg:
729
+ self.update_status(f"فایل با موفقیت ذخیره شد: {os.path.basename(file_path)}", is_error=False)
730
+ messagebox.showinfo("ذخیره موفق", f"فایل صوتی با موفقیت در \n{file_path}\nذخیره شد.")
731
+ else:
732
+ self.update_status(status_msg, is_error=True)
733
+ messagebox.showerror("خطا در ذخیره", status_msg)
734
 
735
+ def on_closing(self):
736
+ """Handle window closing event."""
737
+ if messagebox.askokcancel("خروج", "آیا می‌خواهید از برنامه خارج شوید؟"):
738
+ self.clear_audio() # Clean up temporary audio file if exists
739
+ try:
740
+ if pygame.mixer.get_init():
741
+ pygame.mixer.quit()
742
+ except Exception:
743
+ pass # Ignore if mixer was not initialized
744
+ self.master.destroy()
745
+
746
+ # --- نقطه شروع برنامه ---
747
  if __name__ == "__main__":
748
+ # بررسی و نصب پیش‌نیازها
749
+ try:
750
+ import edge_tts
751
+ except ImportError:
752
+ messagebox.showerror("پیش‌نیاز از دست رفته", "کتابخانه 'edge-tts' نصب نیست. لطفاً آن را با دستور 'pip install edge-tts' نصب کنید.")
753
+ sys.exit(1)
754
+
755
+ try:
756
+ import pygame
757
+ pygame.init() # Initialize Pygame components, especially for fonts (if trying to load images later)
758
+ except ImportError:
759
+ messagebox.showerror("پیش‌نیاز از دست رفته", "کتابخانه 'pygame' نصب نیست. لطفاً آن را با دستور 'pip install pygame' نصب کنید. این کتابخانه برای پخش صدا ضروری است.")
760
+ sys.exit(1)
761
+ except Exception as e:
762
+ messagebox.showwarning("هشدار Pygame", f"هنگام راه‌اندازی Pygame مشکلی پیش آمد: {e}\nپخش صدا ممکن است دچار اختلال شود.")
763
+
764
 
765
+ root = tk.Tk()
766
+ app = TTSApp(root)
767
+ root.protocol("WM_DELETE_WINDOW", app.on_closing) # Handle close button
768
+ root.mainloop()