Persian-tts-fa / app.py
suprimedev's picture
Update app.py
a7ac83a verified
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import edge_tts
import asyncio
import tempfile
import os
import threading
import sys # برای بررسی اینکه آیا در محیط PyInstaller اجرا می شود
import pygame # برای پخش صدا - Pygame نیاز به نصب دارد اما سبک است.
# --- دیکشنری زبان‌ها و صداها با کلیدهای فارسی (نمونه) ---
# ... (همان دیکشنری قبلی باید اینجا کپی شود) ...
# اگر دیکشنری خیلی بزرگ است، می‌توانید آن را از یک فایل جداگانه import کنید.
language_dict_persian_keys = {
'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
'انگلیسی - آنا (زن)': 'en-US-AnaNeural',
'انگلیسی - آریا (زن)': 'en-US-AriaNeural',
'انگلیسی - کریستوفر (مرد)': 'en-US-ChristopherNeural',
'انگلیسی - اریک (مرد)': 'en-US-EricNeural',
'انگلیسی - میشل (زن)': 'en-US-MichelleNeural',
'انگلیسی - راجر (مرد)': 'en-US-RogerNeural',
'اسپانیایی (مکزیک) - دالیا (زن)': 'es-MX-DaliaNeural',
'اسپانیایی (مکزیک) - خورخه (مرد)': 'es-MX-JorgeNeural', # نام خورخه ممکن است دقیق نباشد
'کره‌ای - سان-هی (زن)': 'ko-KR-SunHiNeural',
'کره‌ای - این‌جون (مرد)': 'ko-KR-InJoonNeural',
'تایلندی - پرموادی (زن)': 'th-TH-PremwadeeNeural',
'تایلندی - نیوات (مرد)': 'th-TH-NiwatNeural',
'ویتنامی - هوای‌می (زن)': 'vi-VN-HoaiMyNeural',
'ویتنامی - نام‌مین (مرد)': 'vi-VN-NamMinhNeural',
'ژاپنی - نانامی (زن)': 'ja-JP-NanamiNeural',
'ژاپنی - کیتا (مرد)': 'ja-JP-KeitaNeural',
'فرانسوی - دنیز (زن)': 'fr-FR-DeniseNeural',
'فرانسوی - الوئیز (زن)': 'fr-FR-EloiseNeural',
'فرانسوی - هانری (مرد)': 'fr-FR-HenriNeural',
'پرتغالی (برزیل) - فرانسیسکا (زن)': 'pt-BR-FranciscaNeural',
'پرتغالی (برزیل) - آنتونیو (مord)': 'pt-BR-AntonioNeural',
'اندونزیایی - آردی (مرد)': 'id-ID-ArdiNeural',
'اندونزیایی - گادیس (زن)': 'id-ID-GadisNeural',
'ایتالیایی - ایزابلا (زن)': 'it-IT-IsabellaNeural',
'ایتالیایی - دیگو (مرد)': 'it-IT-DiegoNeural',
'ایتالیایی - السا (زن)': 'it-IT-ElsaNeural',
'هلندی - کولت (زن)': 'nl-NL-ColetteNeural',
'هلندی - فنا (زن)': 'nl-NL-FennaNeural',
'هلندی - مارتن (مرد)': 'nl-NL-MaartenNeural',
'مالایی - عثمان (مرد)': 'ms-MY-OsmanNeural', # "Malese" به "مالایی"
'مالایی - یاسمین (زن)': 'ms-MY-YasminNeural',
'نروژی - پرنیل (زن)': 'nb-NO-PernilleNeural',
'نروژی - فین (مرد)': 'nb-NO-FinnNeural',
'سوئدی - سوفی (زن)': 'sv-SE-SofieNeural',
'سوئدی - ماتیاس (مرد)': 'sv-SE-MattiasNeural',
'عربی (عربستان) - حامد (مرد)': 'ar-SA-HamedNeural', # "عربی" به "عربی (عربستان)"
'عربی (عربستان) - زاریا (زن)': 'ar-SA-ZariyahNeural',
'یونانی - آتنا (زن)': 'el-GR-AthinaNeural',
'یونانی - نستوراس (مرد)': 'el-GR-NestorasNeural',
'آلمانی - کاتیا (زن)': 'de-DE-KatjaNeural',
'آلمانی - آمالا (زن)': 'de-DE-AmalaNeural',
'آلمانی - کنراد (مرد)': 'de-DE-ConradNeural',
'آلمانی - کیلیان (مرد)': 'de-DE-KillianNeural',
'آفریقایی - آدری (زن)': 'af-ZA-AdriNeural', # "Afrikaans" به "آفریقایی"
'آفریقایی - ویلم (مرد)': 'af-ZA-WillemNeural',
'اتیوپیایی - آمه‌ها (مرد)': 'am-ET-AmehaNeural', # "Ethiopian" به "اتیوپیایی"
'اتیوپیایی - مکدس (زن)': 'am-ET-MekdesNeural',
'عربی (امارات) - فاطمه (زن)': 'ar-AE-FatimaNeural',
'عربی (امارات) - حمدان (مرد)': 'ar-AE-HamdanNeural',
'عربی (بحرین) - علی (مرد)': 'ar-BH-AliNeural',
'عربی (بحرین) - لیلا (زن)': 'ar-BH-LailaNeural',
'عربی (الجزایر) - اسماعیل (مرد)': 'ar-DZ-IsmaelNeural',
'عربی (مصر) - سلما (زن)': 'ar-EG-SalmaNeural',
'عربی (مصر) - شاکر (مرد)': 'ar-EG-ShakirNeural',
'عربی (عراق) - باسل (مرد)': 'ar-IQ-BasselNeural',
'عربی (عراق) - رعنا (زن)': 'ar-IQ-RanaNeural', # "Rana" به "رعنا"
'عربی (اردن) - سانا (زن)': 'ar-JO-SanaNeural',
'عربی (اردن) - تایم (مرد)': 'ar-JO-TaimNeural', # "Taim"
'عربی (کویت) - فهد (مرد)': 'ar-KW-FahedNeural',
'عربی (کویت) - نورا (زن)': 'ar-KW-NouraNeural',
'عربی (لبنان) - لیلا (زن)': 'ar-LB-LaylaNeural',
'عربی (لبنان) - رامی (مرد)': 'ar-LB-RamiNeural',
'عربی (لیبی) - ایمان (زن)': 'ar-LY-ImanNeural',
'عربی (لیبی) - عمر (مرد)': 'ar-LY-OmarNeural',
'عربی (مراکش) - جمال (مرد)': 'ar-MA-JamalNeural',
'عربی (مراکش) - مونا (زن)': 'ar-MA-MounaNeural',
'عربی (عمان) - عبدالله (مرد)': 'ar-OM-AbdullahNeural',
'عربی (عمان) - عایشه (زن)': 'ar-OM-AyshaNeural',
'عربی (قطر) - امل (زن)': 'ar-QA-AmalNeural', # "Amal"
'عربی (قطر) - معاذ (مرد)': 'ar-QA-MoazNeural',
'عربی (سوریه) - امانی (زن)': 'ar-SY-AmanyNeural',
'عربی (سوریه) - لیث (مرد)': 'ar-SY-LaithNeural',
'عربی (تونس) - هادی (مرد)': 'ar-TN-HediNeural', # "Hedi"
'عربی (تونس) - ریم (زن)': 'ar-TN-ReemNeural',
'عربی (یمن) - مریم (زن)': 'ar-YE-MaryamNeural',
'عربی (یمن) - صالح (مرد)': 'ar-YE-SalehNeural',
'آذربایجانی - بابک (مرد)': 'az-AZ-BabekNeural',
'آذربایجانی - بانو (زن)': 'az-AZ-BanuNeural',
'بلغاری - بوریسلاو (مرد)': 'bg-BG-BorislavNeural',
'بلغاری - کالینا (زن)': 'bg-BG-KalinaNeural',
'بنگالی (بنگلادش) - نابانیتا (زن)': 'bn-BD-NabanitaNeural',
'بنگالی (بنگلادش) - پرادیپ (مرد)': 'bn-BD-PradeepNeural',
'بنگالی (هند) - باشکار (مرد)': 'bn-IN-BashkarNeural',
'بنگالی (هند) - تانیشا (زن)': 'bn-IN-TanishaaNeural', # "Tanishaa"
'بوسنیایی - گوران (مرد)': 'bs-BA-GoranNeural', # "Bosnian" به "بوسنیایی"
'بوسنیایی - وسنا (زن)': 'bs-BA-VesnaNeural',
'کاتالان (اسپانیا) - جوآنا (زن)': 'ca-ES-JoanaNeural', # "Catalan"
'کاتالان (اسپانیا) - انریک (مرد)': 'ca-ES-EnricNeural',
'چکی - آنتونین (مرد)': 'cs-CZ-AntoninNeural', # "Czech" به "چکی"
'چکی - ولاستا (زن)': 'cs-CZ-VlastaNeural',
'ولزی (بریتانیا) - آلد (مرد)': 'cy-GB-AledNeural', # "Welsh"
'ولزی (بریتانیا) - نیا (زن)': 'cy-GB-NiaNeural',
'دانمارکی - کریستل (زن)': 'da-DK-ChristelNeural',
'دانمارکی - یپه (مرد)': 'da-DK-JeppeNeural',
'آلمانی (اتریش) - اینگرید (زن)': 'de-AT-IngridNeural',
'آلمانی (اتریش) - یوناس (مرد)': 'de-AT-JonasNeural',
'آلمانی (سوئیس) - یان (مرد)': 'de-CH-JanNeural',
'آلمانی (سوئیس) - لنی (زن)': 'de-CH-LeniNeural',
'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural',
'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural',
'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural',
'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural',
'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural',
'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural',
'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural',
'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural',
'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
'انگلیسی (کنیا) - آسیلیا (زن)': 'en-KE-AsiliaNeural',
'انگلیسی (کنیا) - چیلمبا (مرد)': 'en-KE-ChilembaNeural',
'انگلیسی (نیجریه) - آبئو (مرد)': 'en-NG-AbeoNeural',
'انگلیسی (نیجریه) - ازینه (زن)': 'en-NG-EzinneNeural',
'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
'انگلیسی (نیوزیلند) - هیزل (زن)': 'en-NZ-HazelNeural',
'انگلیسی (فیلیپین) - جیمز (مرد)': 'en-PH-JamesNeural',
'انگلیسی (فیلیپین) - روزا (زن)': 'en-PH-RosaNeural',
'انگلیسی (سنگاپور) - لونا (زن)': 'en-SG-LunaNeural',
'انگلیسی (سنگاپور) - وین (مرد)': 'en-SG-WayneNeural',
'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural',
'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural',
'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
'اسپانیایی (آرژانتین) - النا (زن)': 'es-AR-ElenaNeural',
'اسپانیایی (آرژانتین) - توماس (مرد)': 'es-AR-TomasNeural',
'اسپانیایی (بولیوی) - مارسلو (مرد)': 'es-BO-MarceloNeural',
'اسپانیایی (بولیوی) - سوفیا (زن)': 'es-BO-SofiaNeural',
'اسپانیایی (کلمبیا) - گونزالو (مرد)': 'es-CO-GonzaloNeural',
'اسپانیایی (کلمبیا) - سالومه (زن)': 'es-CO-SalomeNeural',
'اسپانیایی (کاستاریکا) - خوان (مرد)': 'es-CR-JuanNeural',
'اسپانیایی (کاستاریکا) - ماریا (زن)': 'es-CR-MariaNeural',
'اسپانیایی (کوبا) - بلکیس (زن)': 'es-CU-BelkysNeural',
'اسپانیایی (کوبا) - مانوئل (مرد)': 'es-CU-ManuelNeural',
'اسپانیایی (جمهوری دومینیکن) - امیلیو (مرد)': 'es-DO-EmilioNeural',
'اسپانیایی (جمهوری دومینیکن) - رامونا (زن)': 'es-DO-RamonaNeural',
'اسپانیایی (اکوادور) - آندریا (زن)': 'es-EC-AndreaNeural',
'اسپانیایی (اکوادور) - لوئیس (مرد)': 'es-EC-LuisNeural',
'اسپانیایی (اسپانیا) - آلوارو (مرد)': 'es-ES-AlvaroNeural',
'اسپانیایی (اسپانیا) - الویرا (زن)': 'es-ES-ElviraNeural',
'اسپانیایی (گینه استوایی) - ترزا (زن)': 'es-GQ-TeresaNeural',
'اسپانیایی (گینه استوایی) - امیلیو (مرد)': 'es-GQ-EmilioNeural',
'اسپانیایی (گواتمالا) - آندرس (مرد)': 'es-GT-AndresNeural',
'اسپانیایی (گواتمالا) - مارتا (زن)': 'es-GT-MartaNeural',
'اسپانیایی (هندوراس) - کارلوس (مرد)': 'es-HN-CarlosNeural',
'اسپانیایی (هندوراس) - کارلا (زن)': 'es-HN-KarlaNeural',
'اسپانیایی (نیکاراگوئه) - فدریکو (مرد)': 'es-NI-FedericoNeural',
'اسپانیایی (نیکاراگوئه) - یولاندا (زن)': 'es-NI-YolandaNeural',
'اسپانیایی (پاناما) - مارگاریتا (زن)': 'es-PA-MargaritaNeural',
'اسپانیایی (پاناما) - روبرتو (مرد)': 'es-PA-RobertoNeural',
'اسپانیایی (پرو) - الکس (مرد)': 'es-PE-AlexNeural',
'اسپانیایی (پرو) - کامیلا (زن)': 'es-PE-CamilaNeural',
'اسپانیایی (پورتوریکو) - کارینا (زن)': 'es-PR-KarinaNeural',
'اسپانیایی (پورتوریکو) - ویکتور (مرد)': 'es-PR-VictorNeural',
'اسپانیایی (پاراگوئه) - ماریو (مرد)': 'es-PY-MarioNeural',
'اسپانیایی (پاراگوئه) - تانیا (زن)': 'es-PY-TaniaNeural',
'اسپانیایی (السالوادور) - لورنا (زن)': 'es-SV-LorenaNeural',
'اسپانیایی (السالوادور) - رودریگو (مرد)': 'es-SV-RodrigoNeural',
'اسپانیایی (آمریکا) - آلونسو (مرد)': 'es-US-AlonsoNeural', # "United States" به "آمریکا"
'اسپانیایی (آمریکا) - پالوما (زن)': 'es-US-PalomaNeural',
'اسپانیایی (اروگوئه) - ماتئو (مرد)': 'es-UY-MateoNeural',
'اسپانیایی (اروگوئه) - والنتینا (زن)': 'es-UY-ValentinaNeural',
'اسپانیایی (ونزوئلا) - پائولا (زن)': 'es-VE-PaolaNeural',
'اسپانیایی (ونزوئلا) - سباستین (مرد)': 'es-VE-SebastianNeural',
'استونیایی - آنو (زن)': 'et-EE-AnuNeural', # "Estonian"
'استونیایی - کرت (مرد)': 'et-EE-KertNeural',
'فارسی (ایران) - دل‌آرا (زن)': 'fa-IR-DilaraNeural',
'فارسی (ایران) - فرید (مرد)': 'fa-IR-FaridNeural',
'فنلاندی - هاری (مرد)': 'fi-FI-HarriNeural',
'فنلاندی - نوورا (زن)': 'fi-FI-NooraNeural',
'فرانسوی (بلژیک) - شارلین (زن)': 'fr-BE-CharlineNeural',
'فرانسوی (بلژیک) - جرارد (مرد)': 'fr-BE-GerardNeural',
'فرانسوی (کانادا) - سیلوی (زن)': 'fr-CA-SylvieNeural',
'فرانسوی (کانادا) - آنتوان (مرد)': 'fr-CA-AntoineNeural',
'فرانسوی (کانادا) - ژان (مرد)': 'fr-CA-JeanNeural',
'فرانسوی (سوئیس) - آریان (زن)': 'fr-CH-ArianeNeural',
'فرانسوی (سوئیس) - فابریس (مرد)': 'fr-CH-FabriceNeural',
'ایرلندی - کلم (مرد)': 'ga-IE-ColmNeural',
'ایرلندی - اورلا (زن)': 'ga-IE-OrlaNeural',
'گالیسی (اسپانیا) - روی (مرد)': 'gl-ES-RoiNeural', # "Galician"
'گالیسی (اسپانیا) - سابلا (زن)': 'gl-ES-SabelaNeural',
'گجراتی (هند) - دوانی (زن)': 'gu-IN-DhwaniNeural',
'گجراتی (هند) - نیرانجان (مرد)': 'gu-IN-NiranjanNeural',
'عبری (اسرائیل) - آوری (مرد)': 'he-IL-AvriNeural',
'عبری (اسرائیل) - هیلا (زن)': 'he-IL-HilaNeural',
'هندی (هند) - مادور (مرد)': 'hi-IN-MadhurNeural',
'هندی (هند) - سوارا (زن)': 'hi-IN-SwaraNeural',
'کروات - گابریلا (زن)': 'hr-HR-GabrijelaNeural', # "Croatian"
'کروات - سرچکو (مرد)': 'hr-HR-SreckoNeural',
'مجاری - نوئمی (زن)': 'hu-HU-NoemiNeural',
'مجاری - تاماش (مرد)': 'hu-HU-TamasNeural',
'ارمنی - آناهیت (زن)': 'hy-AM-AnahitNeural',
'ارمنی - هایک (hy-AM-HaykNeural)': 'hy-AM-HaykNeural',
'ایسلندی - گودرون (زن)': 'is-IS-GudrunNeural',
'ایسلندی - گونار (مرد)': 'is-IS-GunnarNeural',
'جاوه‌ای (اندونزی) - دیماس (مرد)': 'jv-ID-DimasNeural', # "Javanese"
'جاوه‌ای (اندونزی) - سیتی (زن)': 'jv-ID-SitiNeural',
'گرجی - اکا (زن)': 'ka-GE-EkaNeural',
'گرجی - گیورگی (مرد)': 'ka-GE-GiorgiNeural',
'قزاقی - آیگول (زن)': 'kk-KZ-AigulNeural',
'قزاقی - دولت (مرد)': 'kk-KZ-DauletNeural',
'خمر (کامبوج) - پیست (مرد)': 'km-KH-PisethNeural', # "Khmer"
'خمر (کامبوج) - سری‌مم (زن)': 'km-KH-SreymomNeural',
'کانادایی (هند) - گاگان (مرد)': 'kn-IN-GaganNeural', # "Kannada"
'کانادایی (هند) - ساپنا (زن)': 'kn-IN-SapnaNeural',
'لائوسی - چانتاونگ (مرد)': 'lo-LA-ChanthavongNeural',
'لائوسی - کئومانی (زن)': 'lo-LA-KeomanyNeural',
'لیتوانیایی - لئوناس (مرد)': 'lt-LT-LeonasNeural',
'لیتوانیایی - اونا (زن)': 'lt-LT-OnaNeural',
'لتونیایی - اوریتا (زن)': 'lv-LV-EveritaNeural',
'لتونیایی - نیلس (مرد)': 'lv-LV-NilsNeural',
'مقدونیه‌ای - الکساندر (مرد)': 'mk-MK-AleksandarNeural',
'مقدونیه‌ای - ماریا (زن)': 'mk-MK-MarijaNeural',
'مالایالام (هند) - میدون (مرد)': 'ml-IN-MidhunNeural',
'مالایالام (هند) - سوبهانا (زن)': 'ml-IN-SobhanaNeural',
'مغولی - باتا (مرد)': 'mn-MN-BataaNeural',
'مغولی - یسوی (زن)': 'mn-MN-YesuiNeural',
'مراتی (هند) - آروهی (زن)': 'mr-IN-AarohiNeural',
'مراتی (هند) - مانوهار (مرد)': 'mr-IN-ManoharNeural',
'مالتی (مالت) - گریس (زن)': 'mt-MT-GraceNeural', # "Maltese"
'مالتی (مالت) - جوزف (مرد)': 'mt-MT-JosephNeural',
'برمه‌ای (میانمار) - نیلار (زن)': 'my-MM-NilarNeural',
'برمه‌ای (میانمار) - تیها (مرد)': 'my-MM-ThihaNeural',
'نپالی - همکالا (زن)': 'ne-NP-HemkalaNeural',
'نپالی - ساگار (مرد)': 'ne-NP-SagarNeural',
'هلندی (بلژیک) - آرنو (مرد)': 'nl-BE-ArnaudNeural',
'هلندی (بلژیک) - دنا (زن)': 'nl-BE-DenaNeural',
'لهستانی - مارک (مرد)': 'pl-PL-MarekNeural',
'لهستانی - زوفیا (زن)': 'pl-PL-ZofiaNeural',
'پشتو (افغانستان) - گل‌نواز (مرد)': 'ps-AF-GulNawazNeural',
'پشتو (افغانستان) - لطیفه (زن)': 'ps-AF-LatifaNeural',
'پرتغالی (پرتغال) - دوآرته (مرد)': 'pt-PT-DuarteNeural',
'پرتغالی (پرتغال) - فرناندا (زن)': 'pt-PT-FernandaNeural',
'رومانیایی - آلینا (زن)': 'ro-RO-AlinaNeural',
'رومانیایی - امیل (مرد)': 'ro-RO-EmilNeural',
'روسی - دیمیتری (مرد)': 'ru-RU-DmitryNeural',
'روسی - سوتلانا (زن)': 'ru-RU-SvetlanaNeural',
'سینهالی (سریلانکا) - دینوکا (مرد)': 'si-LK-DinukaNeural', # "Sinhala"
'سینهالی (سریلانکا) - تیلینی (زن)': 'si-LK-ThiliniNeural',
'اسلواک - لوکاش (مرد)': 'sk-SK-LukasNeural',
'اسلواک - ویکتوریا (زن)': 'sk-SK-ViktoriaNeural',
'اسلوونیایی - پترا (زن)': 'sl-SI-PetraNeural',
'اسلوونیایی - روک (مرد)': 'sl-SI-RokNeural',
'سومالیایی - موسه (مرد)': 'so-SO-MuuseNeural',
'سومالیایی - اوباکس (زن)': 'so-SO-UbaxNeural',
'آلبانیایی - آنیلا (زن)': 'sq-AL-AnilaNeural',
'آلبانیایی - ایلیر (مرد)': 'sq-AL-IlirNeural',
'صربی - نیکولا (مرد)': 'sr-RS-NikolaNeural',
'صربی - سوفی (زن)': 'sr-RS-SophieNeural',
'سوندانی (اندونزی) - جاجانگ (مرد)': 'su-ID-JajangNeural', # "Sundanese"
'سوندانی (اندونزی) - توتی (زن)': 'su-ID-TutiNeural',
'سواحیلی (کنیا) - رفیقی (مرد)': 'sw-KE-RafikiNeural',
'سواحیلی (کنیا) - زوری (زن)': 'sw-KE-ZuriNeural',
'سواحیلی (تانزانیا) - داوودی (مرد)': 'sw-TZ-DaudiNeural',
'سواحیلی (تانزانیا) - رهمه (زن)': 'sw-TZ-RehemaNeural',
'تامیلی (هند) - پالاوی (زن)': 'ta-IN-PallaviNeural',
'تامیلی (هند) - والووار (مرد)': 'ta-IN-ValluvarNeural',
'تامیلی (مالزی) - کانی (زن)': 'ta-MY-KaniNeural',
'تامیلی (مالزی) - سوریا (مرد)': 'ta-MY-SuryaNeural',
'تامیلی (سنگاپور) - آنبو (مرد)': 'ta-SG-AnbuNeural',
'تامیلی (سنگاپور) - ونبا (زن)': 'ta-SG-VenbaNeural',
'تامیلی (سریلانکا) - کومار (مرد)': 'ta-LK-KumarNeural',
'تامیلی (سریلانکا) - سارانیا (زن)': 'ta-LK-SaranyaNeural',
'تلوگو (هند) - موهان (مرد)': 'te-IN-MohanNeural',
'تلوگو (هند) - شروتی (زن)': 'te-IN-ShrutiNeural',
'ترکی - احمد (مرد)': 'tr-TR-AhmetNeural',
'ترکی - امل (زن)': 'tr-TR-EmelNeural',
'اوکراینی - اوستاپ (مرد)': 'uk-UA-OstapNeural',
'اوکراینی - پولینا (زن)': 'uk-UA-PolinaNeural',
'اردو (هند) - گل (زن)': 'ur-IN-GulNeural',
'اردو (هند) - سلمان (مرد)': 'ur-IN-SalmanNeural',
'اردو (پاکستان) - اسد (مرد)': 'ur-PK-AsadNeural',
'اردو (پاکستان) - عظما (زن)': 'ur-PK-UzmaNeural',
'ازبکی - مدینه (زن)': 'uz-UZ-MadinaNeural',
'ازبکی - سردار (مرد)': 'uz-UZ-SardorNeural',
'چینی (ماندارین ساده) - شیائوشیاو (زن)': 'zh-CN-XiaoxiaoNeural',
'چینی (ماندارین ساده) - یون‌یانگ (مرد)': 'zh-CN-YunyangNeural',
'چینی (کانتونی سنتی) - هیوگای (زن)': 'zh-HK-HiuGaaiNeural',
'چینی (کانتونی سنتی) - وان‌لونگ (مرد)': 'zh-HK-WanLungNeural',
'چینی (ماندارین تایوانی) - شیائوچن (زن)': 'zh-TW-HsiaoChenNeural',
'چینی (ماندارین تایوانی) - یون‌جه (مرد)': 'zh-TW-YunJheNeural',
'زولو (آفریقای جنوبی) - تاندو (زن)': 'zu-ZA-ThandoNeural',
'زولو (آفریقای جنوبی) - تمبا (مرد)': 'zu-ZA-ThembaNeural',
}
# --- تابع کمکی برای پیدا کردن مسیر صحیح در زمان اجرای PyInstaller ---
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# --- تابع ناهمزمان برای تبدیل متن به گفتار ---
async def generate_speech(text, voice_key, rate, volume, pitch, output_file_path=None):
"""
تابع ناهمزمان برای تبدیل متن به گفتار با استفاده از Edge TTS.
output_file_path: اگر None باشد، یک فایل موقت ایجاد می‌کند و مسیر آن را برمی‌گرداند.
در غیر این صورت، در مسیر مشخص شده ذخیره می‌کند.
"""
temp_path = None # مسیر فایل موقت اگر output_file_path None باشد.
try:
if not text:
return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
voice_id = language_dict_persian_keys.get(voice_key)
if voice_id is None:
return f"خطا: مدل صدای انتخاب شده ('{voice_key}') یافت نشد.", None
# تبدیل مقادیر اسلایدر به فرمت مورد نیاز edge_tts
rate_str = f"{int(rate):+g}%" # +g برای نمایش A+0 به عنوان A+0
volume_str = f"{int(volume):+g}%"
pitch_str = f"{int(pitch):+g}Hz"
communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
if output_file_path:
file_to_save_to = output_file_path
else:
# ایجاد فایل موقت فقط برای پخش
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
temp_path = tmp_file.name
file_to_save_to = temp_path
await communicate.save(file_to_save_to)
return "تبدیل با موفقیت انجام شد.", file_to_save_to
except edge_tts.exceptions.NoAudioReceived:
error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
return error_msg, None
except ValueError as ve:
error_msg = f"خطا در پارامترهای ورودی: {ve}"
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
return error_msg, None
except Exception as e:
error_msg = f"خطای غیرمنتظره: {type(e).__name__} - {e}"
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
return error_msg, None
# --- کلاس اصلی برنامه Tkinter ---
class TTSApp:
def __init__(self, master):
self.master = master
master.title("مبدل هوشمند متن به گفتار")
master.geometry("800x650")
master.resizable(False, False)
master.option_add('*Font', 'Vazirmatn 10') # تنظیم فونت پیش‌فرض (نیاز به نصب Vazirmatn در سیستم)
# تلاش برای بارگذاری فونت‌های RTL
try:
# Tkinter در ویندوز و مک از نام فونت استفاده می‌کند.
# برای لینوکس ممکن است نیاز به نصب farsi-fonts یا مشابه آن باشد.
master.tk.call('font', 'create', 'Vazirmatn', '-family', 'Vazirmatn', '-size', '10')
master.tk.call('font', 'configure', 'TkDefaultFont', '-family', 'Vazirmatn')
master.tk.call('font', 'configure', 'TkTextFont', '-family', 'Vazirmatn')
master.tk.call('font', 'configure', 'TkFixedFont', '-family', 'Vazirmatn')
master.tk.call('font', 'configure', 'heading', '-family', 'Vazirmatn', '-size', '12', '-weight', 'bold')
# Fallback for systems without Vazirmatn
master.option_add('*Font', 'Vazirmatn 10', 'Arial 10', 'Helvetica 10')
master.option_add('*Text.font', 'Vazirmatn 10', 'Arial 10', 'Helvetica 10')
master.option_add('*Entry.font', 'Vazirmatn 10', 'Arial 10', 'Helvetica 10')
except tk.TclError:
print("Vazirmatn font not found, falling back to default.")
master.option_add('*Font', 'Arial 10', 'Helvetica 10')
self.current_audio_file = None # برای نگهداری مسیر فایل صوتی فعلی برای پخش یا حذف
# Styles (Styling Tkinter widgets)
style = ttk.Style()
style.theme_use('clam') # 'default', 'alt', 'clam', 'classic'
style.configure('TFrame', background='#f4f7f6')
style.configure('TLabel', background='#f4f7f6', foreground='#333', font=('Vazirmatn', 10))
style.configure('TButton', background='#3498db', foreground='white', font=('Vazirmatn', 10, 'bold'), borderwidth=1, focusthickness=3, focuscolor='none')
style.map('TButton', background=[('active', '#2980b9')])
style.configure('TEntry', fieldbackground='white', foreground='#333', borderwidth=1, relief='solid')
style.configure('TCombobox', fieldbackground='white', foreground='#333', selectbackground='#3498db', selectforeground='white')
style.configure('Horizontal.TScale', background='#f4f7f6', troughcolor='#ddd', sliderrelief='flat', sliderthickness=15, borderwidth=0)
style.configure('TText', background='white', foreground='#333', borderwidth=1, relief='solid')
# Main Frame with padding
main_frame = ttk.Frame(master, padding="20 20 20 20")
main_frame.pack(fill=tk.BOTH, expand=True)
# Header (similar to Gradio's header)
header_frame = ttk.Frame(main_frame, style='TFrame', relief="raised", padding="15", borderwidth=0)
# style.configure('Header.TFrame', background='#34495e', borderwidth=0)
header_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 20))
header_frame.grid_columnconfigure(0, weight=1) # Center the content
header_label = ttk.Label(header_frame, text="مبدل هوشمند متن به گفتار", font=('Vazirmatn', 16, 'bold'), foreground='white', background='#34495e')
header_label.pack(pady=5)
sub_header_label = ttk.Label(header_frame, text="با کیفیت صدای طبیعی و روان، متن خود را زنده کنید", font=('Vazirmatn', 10), foreground='#bdc3c7', background='#34495e')
sub_header_label.pack(pady=5)
# Apply header background explicitly
header_frame.config(background='#34495e')
# Input Text
ttk.Label(main_frame, text="📝 متن خود را برای تبدیل وارد نمایید").grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 5))
self.text_input = tk.Text(main_frame, height=8, width=70, font=('Vazirmatn', 10), wrap=tk.WORD, bd=1, relief="solid")
self.text_input.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(0, 10))
self.text_input.insert(tk.END, "سلام بر شما، روز خوبی داشته باشید. این یک تست اولیه برای تبدیل متن به گفتار است.")
# Language Dropdown
ttk.Label(main_frame, text="🗣️ زبان و گوینده را انتخاب کنید").grid(row=3, column=0, columnspan=2, sticky="w", pady=(0, 5))
voices = list(language_dict_persian_keys.keys())
default_voice_key_persian = 'فارسی (ایران) - فرید (مرد)'
if default_voice_key_persian not in voices:
default_voice_key_persian = voices[0] if voices else ''
self.language_var = tk.StringVar(master)
self.language_var.set(default_voice_key_persian)
self.language_dropdown = ttk.Combobox(main_frame, textvariable=self.language_var, values=voices, state="readonly", width=50) # width adjusted for better appearance
self.language_dropdown.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(0, 15))
# Advanced Settings (Accordion-like structure)
self.advanced_frame = ttk.Frame(main_frame, style='TFrame', relief="groove", borderwidth=1, padding="10")
self.advanced_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(0, 15))
self.advanced_frame.grid_columnconfigure(0, weight=1)
self.advanced_frame.grid_columnconfigure(1, weight=1)
# Header for accordion (clickable)
self.advanced_settings_open = False
self.accordion_header = ttk.Label(self.advanced_frame, text="⚙️ تنظیمات پیشرفته صدا (اختیاری)", font=('Vazirmatn', 10, 'bold'), foreground='#444', background='#f0f0f0', cursor="hand2")
self.accordion_header.grid(row=0, column=0, columnspan=2, sticky="ew", ipadx=5, ipady=5)
self.accordion_header.bind("<Button-1>", self.toggle_advanced_settings) # Bind click event
self.advanced_content_frame = ttk.Frame(self.advanced_frame, style='TFrame') # Frame for sliders
# Sliders
ttk.Label(self.advanced_content_frame, text="سرعت (%)").grid(row=0, column=0, sticky="w", padx=5, pady=2)
self.rate_slider = ttk.Scale(self.advanced_content_frame, from_=-100, to=100, orient="horizontal", style='Horizontal.TScale')
self.rate_slider.set(0)
self.rate_slider.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
self.rate_value_label = ttk.Label(self.advanced_content_frame, text=f"0%", font=('Vazirmatn', 9))
self.rate_value_label.grid(row=1, column=1, sticky="w", padx=(0,5), pady=5)
self.rate_slider.bind("<Motion>", lambda e: self.rate_value_label.config(text=f"{int(self.rate_slider.get()):+g}%"))
ttk.Label(self.advanced_content_frame, text="حجم (%)").grid(row=2, column=0, sticky="w", padx=5, pady=2)
self.volume_slider = ttk.Scale(self.advanced_content_frame, from_=-100, to=100, orient="horizontal", style='Horizontal.TScale')
self.volume_slider.set(0)
self.volume_slider.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
self.volume_value_label = ttk.Label(self.advanced_content_frame, text=f"0%", font=('Vazirmatn', 9))
self.volume_value_label.grid(row=3, column=1, sticky="w", padx=(0,5), pady=5)
self.volume_slider.bind("<Motion>", lambda e: self.volume_value_label.config(text=f"{int(self.volume_slider.get()):+g}%"))
ttk.Label(self.advanced_content_frame, text="گام (Hz)").grid(row=4, column=0, sticky="w", padx=5, pady=2)
self.pitch_slider = ttk.Scale(self.advanced_content_frame, from_=-50, to=50, orient="horizontal", style='Horizontal.TScale')
self.pitch_slider.set(0)
self.pitch_slider.grid(row=5, column=0, sticky="ew", padx=5, pady=5)
self.pitch_value_label = ttk.Label(self.advanced_content_frame, text=f"0Hz", font=('Vazirmatn', 9))
self.pitch_value_label.grid(row=5, column=1, sticky="w", padx=(0,5), pady=5)
self.pitch_slider.bind("<Motion>", lambda e: self.pitch_value_label.config(text=f"{int(self.pitch_slider.get()):+g}Hz"))
self.advanced_content_frame.grid_columnconfigure(0, weight=1)
# Initially hide advanced settings
self.toggle_advanced_settings(None) # Call once to set initial state
# Buttons
button_frame = ttk.Frame(main_frame, style='TFrame')
button_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
button_frame.grid_columnconfigure(0, weight=1)
button_frame.grid_columnconfigure(1, weight=1)
self.play_button = ttk.Button(button_frame, text="🔊 تولید و پخش صدا", command=self.on_generate_and_play)
self.play_button.grid(row=0, column=0, sticky="ew", padx=(0, 5))
self.save_button = ttk.Button(button_frame, text="💾 ذخیره فایل صوتی", command=self.on_save_audio)
self.save_button.grid(row=0, column=1, sticky="ew", padx=(5, 0))
# Status and Output Audio Player
ttk.Label(main_frame, text="📊 وضعیت عملیات").grid(row=7, column=0, columnspan=2, sticky="w", pady=(10, 5))
self.status_label = ttk.Label(main_frame, text="آماده کار", relief="sunken", anchor="w", padding=5, background="#e0f2f7", foreground="#00796b")
self.status_label.grid(row=8, column=0, columnspan=2, sticky="ew", pady=(0, 10))
ttk.Label(main_frame, text="🎧 پخش صدا").grid(row=9, column=0, columnspan=2, sticky="w", pady=(0, 5))
# Player controls (Placeholder for now)
player_frame = ttk.Frame(main_frame, style='TFrame', relief="solid", borderwidth=1, padding="5")
player_frame.grid(row=10, column=0, columnspan=2, sticky="ew")
player_frame.grid_columnconfigure(0, weight=1)
player_frame.grid_columnconfigure(1, weight=1)
player_frame.grid_columnconfigure(2, weight=1)
self.player_status_label = ttk.Label(player_frame, text="فایلی برای پخش موجود نیست.", foreground='#555')
self.player_status_label.grid(row=0, column=0, columnspan=3, sticky="ew")
self.play_current_button = ttk.Button(player_frame, text="▶️ پخش", command=self.play_generated_audio, state=tk.DISABLED)
self.play_current_button.grid(row=1, column=0, sticky="ew", padx=2, pady=5)
self.stop_button = ttk.Button(player_frame, text="⏹️ توقف", command=self.stop_audio, state=tk.DISABLED, style='TButton', background='#e74c3c')
self.stop_button.grid(row=1, column=1, sticky="ew", padx=2, pady=5)
style.map('TButton', background=[('active', '#c0392b')], style='TButton')
self.clear_audio_button = ttk.Button(player_frame, text="🗑️ پاک کردن", command=self.clear_audio, state=tk.DISABLED, style='TButton', background='#95a5a6')
self.clear_audio_button.grid(row=1, column=2, sticky="ew", padx=2, pady=5)
style.map('TButton', background=[('active', '#7f8c8d')], style='TButton')
# Configure grid weights for resizing
main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_columnconfigure(1, weight=1) # The second column is hidden but needs to exist
# Initialize pygame mixer for audio playback
try:
pygame.mixer.init()
# Set a lower buffer size for less latency but might need more CPU on some systems
# pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
except Exception as e:
messagebox.showerror("خطا در Pygame", f"مشکلی در راه‌اندازی Pygame Mixer وجود دارد. پخش صدا ممکن است کار نکند.\n{e}")
self.play_current_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
self.clear_audio_button.config(state=tk.DISABLED)
self.player_status_label.config(text="پخش صدا غیرفعال است (مشکل در Pygame).", foreground='red')
def toggle_advanced_settings(self, event):
"""Toggle the visibility of advanced settings sliders."""
if self.advanced_settings_open:
self.advanced_content_frame.grid_forget()
self.accordion_header.config(text="⚙️ تنظیمات پیشرفته صدا (اختیاری)")
else:
self.advanced_content_frame.grid(row=1, column=0, columnspan=2, sticky="ew") # Place content frame
self.accordion_header.config(text="⚙️ تنظیمات پیشرفته صدا (اختیاری) - باز")
self.advanced_settings_open = not self.advanced_settings_open
self.master.update_idletasks() # Force update layout
def update_status(self, message, is_error=False):
"""Updates the status label in the UI."""
if is_error:
self.status_label.config(text=message, background="#ffebee", foreground="#c62828") # Light red
else:
self.status_label.config(text=message, background="#e0f2f7", foreground="#00796b") # Light blue-green
self.master.update_idletasks() # Ensure UI updates immediately
def on_generate_and_play(self):
"""Handles the 'Generate and Play' button click."""
text = self.text_input.get("1.0", tk.END).strip()
voice_key = self.language_var.get()
rate = self.rate_slider.get()
volume = self.volume_slider.get()
pitch = self.pitch_slider.get()
self.update_status("در حال تولید صدا، لطفاً صبر کنید...", is_error=False)
self.play_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
self.play_current_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
self.clear_audio_button.config(state=tk.DISABLED)
self.player_status_label.config(text="در حال تولید...", foreground='blue')
# Run the async TTS operation in a separate thread to not block the UI
threading.Thread(target=self._run_tts_async, args=(text, voice_key, rate, volume, pitch)).start()
def _run_tts_async(self, text, voice_key, rate, volume, pitch):
"""Helper to run the async generate_speech function."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
status_msg, audio_path = loop.run_until_complete(generate_speech(text, voice_key, rate, volume, pitch))
# Schedule the UI update back on the main Tkinter thread
self.master.after(0, self._process_tts_result, status_msg, audio_path)
def _process_tts_result(self, status_msg, audio_path):
"""Processes the result of the TTS operation and updates UI."""
self.play_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
if audio_path:
self.update_status(status_msg, is_error=False)
self.clear_audio() # Clear any previous played audio
self.current_audio_file = audio_path
self.play_generated_audio() # Automatically play the audio
self.play_current_button.config(state=tk.NORMAL)
self.player_status_label.config(text=f"فایل صوتی آماده: {os.path.basename(audio_path)}", foreground='green')
self.stop_button.config(state=tk.NORMAL) # Enable stop button after initiating play
self.clear_audio_button.config(state=tk.NORMAL)
else:
self.update_status(status_msg, is_error=True)
self.player_status_label.config(text="خطا در تولید یا پخش.", foreground='red')
self.clear_audio_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
def play_generated_audio(self):
"""Plays the audio file if one has been generated."""
if self.current_audio_file and os.path.exists(self.current_audio_file):
try:
# Stop any currently playing audio before loading new one
if pygame.mixer.music.get_busy():
pygame.mixer.music.stop()
pygame.mixer.music.load(self.current_audio_file)
pygame.mixer.music.play()
self.player_status_label.config(text="در حال پخش...", foreground='blue')
self.play_current_button.config(state=tk.DISABLED) # Disable play button while playing
self.stop_button.config(state=tk.NORMAL) # Enable stop button
self.clear_audio_button.config(state=tk.NORMAL)
# Check if playback has finished in a separate thread
threading.Thread(target=self._monitor_playback).start()
except pygame.error as e:
messagebox.showerror("خطای پخش صدا", f"مشکلی در پخش فایل صوتی وجود دارد: {e}")
self.player_status_label.config(text="خطا در پخش.", foreground='red')
self.play_current_button.config(state=tk.NORMAL) # Re-enable if error
self.stop_button.config(state=tk.DISABLED) # Disable if error
else:
self.player_status_label.config(text="فایلی برای پخش موجود نیست.", foreground='#555')
self.play_current_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
self.clear_audio_button.config(state=tk.DISABLED)
def _monitor_playback(self):
"""Monitors Pygame music playback to update UI when finished."""
while pygame.mixer.music.get_busy():
pygame.time.Clock().tick(10) # Check every 100ms
# Schedule UI update on main Tkinter thread
self.master.after(0, self._on_playback_finished)
def _on_playback_finished(self):
"""Updates UI after audio playback finishes."""
if not pygame.mixer.music.get_busy():
self.player_status_label.config(text="پخش به پایان رسید.", foreground='green')
self.play_current_button.config(state=tk.NORMAL) # Re-enable play button
self.stop_button.config(state=tk.DISABLED)
def stop_audio(self):
"""Stops current audio playback."""
if pygame.mixer.music.get_busy():
pygame.mixer.music.stop()
self.player_status_label.config(text="پخش متوقف شد.")
self.play_current_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
def clear_audio(self):
"""Clears the current audio file and updates UI."""
self.stop_audio() # Ensure playback is stopped
if self.current_audio_file and os.path.exists(self.current_audio_file):
try:
os.remove(self.current_audio_file)
self.current_audio_file = None
self.player_status_label.config(text="فایل صوتی پاک شد.", foreground='#555')
except OSError as e:
messagebox.showerror("خطا در حذف فایل", f"فایل صوتی قابل حذف نیست: {e}")
self.play_current_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
self.clear_audio_button.config(state=tk.DISABLED)
def on_save_audio(self):
"""Handles the 'Save Audio' button click."""
text = self.text_input.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("ورودی خالی", "لطفاً متنی برای ذخیره وارد کنید.")
return
voice_key = self.language_var.get()
rate = self.rate_slider.get()
volume = self.volume_slider.get()
pitch = self.pitch_slider.get()
file_path = filedialog.asksaveasfilename(
defaultextension=".wav",
filetypes=[("WAV files", "*.wav")],
title="ذخیره فایل صوتی به عنوان..."
)
if not file_path:
return # User cancelled save dialog
self.update_status("در حال تولید و ذخیره صدا، لطفاً صبر کنید...", is_error=False)
self.play_button.config(state=tk.DISABLED)
self.save_button.config(state=tk.DISABLED)
# Run the async TTS operation for saving in a separate thread
threading.Thread(target=self._run_tts_async_for_save, args=(text, voice_key, rate, volume, pitch, file_path)).start()
def _run_tts_async_for_save(self, text, voice_key, rate, volume, pitch, file_path):
"""Helper to run the async generate_speech for saving."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
status_msg, _ = loop.run_until_complete(generate_speech(text, voice_key, rate, volume, pitch, file_path))
# Schedule the UI update back on the main Tkinter thread
self.master.after(0, self._process_save_result, status_msg, file_path)
def _process_save_result(self, status_msg, file_path):
"""Processes the result of the save operation and updates UI."""
self.play_button.config(state=tk.NORMAL)
self.save_button.config(state=tk.NORMAL)
if "موفقیت" in status_msg:
self.update_status(f"فایل با موفقیت ذخیره شد: {os.path.basename(file_path)}", is_error=False)
messagebox.showinfo("ذخیره موفق", f"فایل صوتی با موفقیت در \n{file_path}\nذخیره شد.")
else:
self.update_status(status_msg, is_error=True)
messagebox.showerror("خطا در ذخیره", status_msg)
def on_closing(self):
"""Handle window closing event."""
if messagebox.askokcancel("خروج", "آیا می‌خواهید از برنامه خارج شوید؟"):
self.clear_audio() # Clean up temporary audio file if exists
try:
if pygame.mixer.get_init():
pygame.mixer.quit()
except Exception:
pass # Ignore if mixer was not initialized
self.master.destroy()
# --- نقطه شروع برنامه ---
if __name__ == "__main__":
# بررسی و نصب پیش‌نیازها
try:
import edge_tts
except ImportError:
messagebox.showerror("پیش‌نیاز از دست رفته", "کتابخانه 'edge-tts' نصب نیست. لطفاً آن را با دستور 'pip install edge-tts' نصب کنید.")
sys.exit(1)
try:
import pygame
pygame.init() # Initialize Pygame components, especially for fonts (if trying to load images later)
except ImportError:
messagebox.showerror("پیش‌نیاز از دست رفته", "کتابخانه 'pygame' نصب نیست. لطفاً آن را با دستور 'pip install pygame' نصب کنید. این کتابخانه برای پخش صدا ضروری است.")
sys.exit(1)
except Exception as e:
messagebox.showwarning("هشدار Pygame", f"هنگام راه‌اندازی Pygame مشکلی پیش آمد: {e}\nپخش صدا ممکن است دچار اختلال شود.")
root = tk.Tk()
app = TTSApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing) # Handle close button
root.mainloop()