Spaces:
Runtime error
Runtime error
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() | |