Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -1,12 +1,11 @@
|
|
1 |
import gradio as gr
|
2 |
import edge_tts
|
3 |
import tempfile
|
4 |
-
import asyncio
|
|
|
5 |
|
6 |
-
# --- language_dict
|
7 |
-
#
|
8 |
-
# مطمئن شوید مقادیر (value) با لیست صداهای معتبر edge-tts مطابقت دارند.
|
9 |
-
# می توانید با `edge-tts --list-voices` در ترمینال خود لیست کامل را ببینید.
|
10 |
language_dict = {
|
11 |
'English-Jenny (Female)': 'en-US-JennyNeural',
|
12 |
'English-Guy (Male)': 'en-US-GuyNeural',
|
@@ -44,7 +43,7 @@ language_dict = {
|
|
44 |
'Norwegian-Pernille- (Female)': 'nb-NO-PernilleNeural',
|
45 |
'Norwegian-Finn- (Male)': 'nb-NO-FinnNeural',
|
46 |
'Swedish-Sofie- (Female)': 'sv-SE-SofieNeural',
|
47 |
-
'Swedish-Mattias- (Male)': 'sv-SE-MattiasNeural',
|
48 |
'Arabic-Hamed- (Male)': 'ar-SA-HamedNeural',
|
49 |
'Arabic-Zariyah- (Female)': 'ar-SA-ZariyahNeural',
|
50 |
'Greek-Athina- (Female)': 'el-GR-AthinaNeural',
|
@@ -57,7 +56,7 @@ language_dict = {
|
|
57 |
'Afrikaans-Willem- (Male)': 'af-ZA-WillemNeural',
|
58 |
'Ethiopian-Ameha- (Male)': 'am-ET-AmehaNeural',
|
59 |
'Ethiopian-Mekdes- (Female)': 'am-ET-MekdesNeural',
|
60 |
-
'Arabic (UAE)-Fatima- (Female)': 'ar-AE-FatimaNeural',
|
61 |
'Arabic (UAE)-Hamdan- (Male)': 'ar-AE-HamdanNeural',
|
62 |
'Arabic (Bahrain)-Ali- (Male)': 'ar-BH-AliNeural',
|
63 |
'Arabic (Bahrain)-Laila- (Female)': 'ar-BH-LailaNeural',
|
@@ -84,8 +83,8 @@ language_dict = {
|
|
84 |
'Arabic (Syrian Arab Republic)-Laith- (Male)': 'ar-SY-LaithNeural',
|
85 |
'Arabic (Tunisia)-Hedi- (Male)': 'ar-TN-HediNeural',
|
86 |
'Arabic (Tunisia)-Reem- (Female)': 'ar-TN-ReemNeural',
|
87 |
-
'Arabic (Yemen)-Maryam- (Female)': 'ar-YE-MaryamNeural',
|
88 |
-
'Arabic (Yemen)-Saleh- (Male)': 'ar-YE-SalehNeural',
|
89 |
'Azerbaijani-Babek- (Male)': 'az-AZ-BabekNeural',
|
90 |
'Azerbaijani-Banu- (Female)': 'az-AZ-BanuNeural',
|
91 |
'Bulgarian-Borislav- (Male)': 'bg-BG-BorislavNeural',
|
@@ -94,7 +93,7 @@ language_dict = {
|
|
94 |
'Bengali (Bangladesh)-Pradeep- (Male)': 'bn-BD-PradeepNeural',
|
95 |
'Bengali (India)-Bashkar- (Male)': 'bn-IN-BashkarNeural',
|
96 |
'Bengali (India)-Tanishaa- (Female)': 'bn-IN-TanishaaNeural',
|
97 |
-
'Bosnian (Bosnia and Herzegovina)-Goran- (Male)': 'bs-BA-GoranNeural',
|
98 |
'Bosnian (Bosnia and Herzegovina)-Vesna- (Female)': 'bs-BA-VesnaNeural',
|
99 |
'Catalan (Spain)-Joana- (Female)': 'ca-ES-JoanaNeural',
|
100 |
'Catalan (Spain)-Enric- (Male)': 'ca-ES-EnricNeural',
|
@@ -128,7 +127,7 @@ language_dict = {
|
|
128 |
'English (Nigeria)-Abeo- (Male)': 'en-NG-AbeoNeural',
|
129 |
'English (Nigeria)-Ezinne- (Female)': 'en-NG-EzinneNeural',
|
130 |
'English (New Zealand)-Mitchell- (Male)': 'en-NZ-MitchellNeural',
|
131 |
-
'English (New Zealand)-Hazel- (Female)': 'en-NZ-HazelNeural',
|
132 |
'English (Philippines)-James- (Male)': 'en-PH-JamesNeural',
|
133 |
'English (Philippines)-Rosa- (Female)': 'en-PH-RosaNeural',
|
134 |
'English (Singapore)-Luna- (Female)': 'en-SG-LunaNeural',
|
@@ -146,7 +145,7 @@ language_dict = {
|
|
146 |
'Spanish (Costa Rica)-Juan- (Male)': 'es-CR-JuanNeural',
|
147 |
'Spanish (Costa Rica)-Maria- (Female)': 'es-CR-MariaNeural',
|
148 |
'Spanish (Cuba)-Belkys- (Female)': 'es-CU-BelkysNeural',
|
149 |
-
'Spanish (Cuba)-Manuel- (Male)': 'es-CU-ManuelNeural',
|
150 |
'Spanish (Dominican Republic)-Emilio- (Male)': 'es-DO-EmilioNeural',
|
151 |
'Spanish (Dominican Republic)-Ramona- (Female)': 'es-DO-RamonaNeural',
|
152 |
'Spanish (Ecuador)-Andrea- (Female)': 'es-EC-AndreaNeural',
|
@@ -154,7 +153,7 @@ language_dict = {
|
|
154 |
'Spanish (Spain)-Alvaro- (Male)': 'es-ES-AlvaroNeural',
|
155 |
'Spanish (Spain)-Elvira- (Female)': 'es-ES-ElviraNeural',
|
156 |
'Spanish (Equatorial Guinea)-Teresa- (Female)': 'es-GQ-TeresaNeural',
|
157 |
-
'Spanish (Equatorial Guinea)-Emilio- (Male)': 'es-GQ-EmilioNeural',
|
158 |
'Spanish (Guatemala)-Andres- (Male)': 'es-GT-AndresNeural',
|
159 |
'Spanish (Guatemala)-Marta- (Female)': 'es-GT-MartaNeural',
|
160 |
'Spanish (Honduras)-Carlos- (Male)': 'es-HN-CarlosNeural',
|
@@ -196,16 +195,16 @@ language_dict = {
|
|
196 |
'Galician (Spain)-Sabela- (Female)': 'gl-ES-SabelaNeural',
|
197 |
'Gujarati (India)-Dhwani- (Female)': 'gu-IN-DhwaniNeural',
|
198 |
'Gujarati (India)-Niranjan- (Male)': 'gu-IN-NiranjanNeural',
|
199 |
-
'Hebrew (Israel)-Avri- (Male)': 'he-IL-AvriNeural',
|
200 |
-
'Hebrew (Israel)-Hila- (Female)': 'he-IL-HilaNeural',
|
201 |
'Hindi (India)-Madhur- (Male)': 'hi-IN-MadhurNeural',
|
202 |
'Hindi (India)-Swara- (Female)': 'hi-IN-SwaraNeural',
|
203 |
'Croatian (Croatia)-Gabrijela- (Female)': 'hr-HR-GabrijelaNeural',
|
204 |
'Croatian (Croatia)-Srecko- (Male)': 'hr-HR-SreckoNeural',
|
205 |
'Hungarian (Hungary)-Noemi- (Female)': 'hu-HU-NoemiNeural',
|
206 |
'Hungarian (Hungary)-Tamas- (Male)': 'hu-HU-TamasNeural',
|
207 |
-
'Armenian (Armenia)-Anahit- (Female)': 'hy-AM-AnahitNeural',
|
208 |
-
'Armenian (Armenia)-Hayk- (Male)': 'hy-AM-HaykNeural',
|
209 |
'Icelandic (Iceland)-Gudrun- (Female)': 'is-IS-GudrunNeural',
|
210 |
'Icelandic (Iceland)-Gunnar- (Male)': 'is-IS-GunnarNeural',
|
211 |
'Javanese (Indonesia)-Dimas- (Male)': 'jv-ID-DimasNeural',
|
@@ -242,134 +241,171 @@ language_dict = {
|
|
242 |
'Dutch (Belgium)-Dena- (Female)': 'nl-BE-DenaNeural',
|
243 |
'Polish (Poland)-Marek- (Male)': 'pl-PL-MarekNeural',
|
244 |
'Polish (Poland)-Zofia- (Female)': 'pl-PL-ZofiaNeural',
|
245 |
-
'Pashto (Afghanistan)-Gul Nawaz- (Male)': 'ps-AF-GulNawazNeural',
|
246 |
-
'Pashto (Afghanistan)-Latifa- (Female)': 'ps-AF-LatifaNeural',
|
247 |
-
'Portuguese (Portugal)-Duarte- (Male)': 'pt-PT-DuarteNeural',
|
248 |
-
'Portuguese (Portugal)-Fernanda- (Female)': 'pt-PT-FernandaNeural',
|
249 |
-
'Romanian (Romania)-Alina- (Female)': 'ro-RO-AlinaNeural',
|
250 |
-
'Romanian (Romania)-Emil- (Male)': 'ro-RO-EmilNeural',
|
251 |
-
'Russian (Russia)-Dmitry- (Male)': 'ru-RU-DmitryNeural',
|
252 |
-
'Russian (Russia)-Svetlana- (Female)': 'ru-RU-SvetlanaNeural',
|
253 |
-
'Sinhala (Sri Lanka)-Dinuka- (Male)': 'si-LK-DinukaNeural',
|
254 |
-
'Sinhala (Sri Lanka)-Thilini- (Female)': 'si-LK-ThiliniNeural',
|
255 |
-
'Slovak (Slovakia)-Lukas- (Male)': 'sk-SK-LukasNeural',
|
256 |
-
'Slovak (Slovakia)-Viktoria- (Female)': 'sk-SK-ViktoriaNeural',
|
257 |
-
'Slovenian (Slovenia)-Petra- (Female)': 'sl-SI-PetraNeural',
|
258 |
-
'Slovenian (Slovenia)-Rok- (Male)': 'sl-SI-RokNeural',
|
259 |
-
'Somali (Somalia)-Muuse- (Male)': 'so-SO-MuuseNeural',
|
260 |
-
'Somali (Somalia)-Ubax- (Female)': 'so-SO-UbaxNeural',
|
261 |
-
'Albanian (Albania)-Anila- (Female)': 'sq-AL-AnilaNeural',
|
262 |
-
'Albanian (Albania)-Ilir- (Male)': 'sq-AL-IlirNeural',
|
263 |
-
'Serbian (Serbia)-Nikola- (Male)': 'sr-RS-NikolaNeural',
|
264 |
-
'Serbian (Serbia)-Sophie- (Female)': 'sr-RS-SophieNeural',
|
265 |
-
'Sundanese (Indonesia)-Jajang- (Male)': 'su-ID-JajangNeural',
|
266 |
-
'Sundanese (Indonesia)-Tuti- (Female)': 'su-ID-TutiNeural',
|
267 |
-
'Swahili (Kenya)-Rafiki- (Male)': 'sw-KE-RafikiNeural',
|
268 |
-
'Swahili (Kenya)-Zuri- (Female)': 'sw-KE-ZuriNeural',
|
269 |
-
'Swahili (Tanzania)-Daudi- (Male)': 'sw-TZ-DaudiNeural',
|
270 |
-
'Swahili (Tanzania)-Rehema- (Female)': 'sw-TZ-RehemaNeural',
|
271 |
-
'Tamil (India)-Pallavi- (Female)': 'ta-IN-PallaviNeural',
|
272 |
-
'Tamil (India)-Valluvar- (Male)': 'ta-IN-ValluvarNeural',
|
273 |
-
'Tamil (Malaysia)-Kani- (Female)': 'ta-MY-KaniNeural',
|
274 |
-
'Tamil (Malaysia)-Surya- (Male)': 'ta-MY-SuryaNeural',
|
275 |
-
'Tamil (Singapore)-Anbu- (Male)': 'ta-SG-AnbuNeural',
|
276 |
-
'Tamil (Singapore)-Venba- (Female)': 'ta-SG-VenbaNeural',
|
277 |
-
'Tamil (Sri Lanka)-Kumar- (Male)': 'ta-LK-KumarNeural',
|
278 |
-
'Tamil (Sri Lanka)-Saranya- (Female)': 'ta-LK-SaranyaNeural',
|
279 |
-
'Telugu (India)-Mohan- (Male)': 'te-IN-MohanNeural',
|
280 |
-
'Telugu (India)-Shruti- (Female)': 'te-IN-ShrutiNeural',
|
281 |
-
'Turkish (Turkey)-Ahmet- (Male)': 'tr-TR-AhmetNeural',
|
282 |
-
'Turkish (Turkey)-Emel- (Female)': 'tr-TR-EmelNeural',
|
283 |
-
'Ukrainian (Ukraine)-Ostap- (Male)': 'uk-UA-OstapNeural',
|
284 |
-
'Ukrainian (Ukraine)-Polina- (Female)': 'uk-UA-PolinaNeural',
|
285 |
-
'Urdu (India)-Gul- (Female)': 'ur-IN-GulNeural',
|
286 |
-
'Urdu (India)-Salman- (Male)': 'ur-IN-SalmanNeural',
|
287 |
-
'Urdu (Pakistan)-Asad- (Male)': 'ur-PK-AsadNeural',
|
288 |
-
'Urdu (Pakistan)-Uzma- (Female)': 'ur-PK-UzmaNeural',
|
289 |
-
'Uzbek (Uzbekistan)-Madina- (Female)': 'uz-UZ-MadinaNeural',
|
290 |
-
'Uzbek (Uzbekistan)-Sardor- (Male)': 'uz-UZ-SardorNeural',
|
291 |
-
'Chinese (Mandarin, Simplified)-Xiaoxiao- (Female)': 'zh-CN-XiaoxiaoNeural',
|
292 |
-
'Chinese (Mandarin, Simplified)-Yunyang- (Male)': 'zh-CN-YunyangNeural',
|
293 |
-
'Chinese (Cantonese, Traditional)-HiuGaai- (Female)': 'zh-HK-HiuGaaiNeural',
|
294 |
-
'Chinese (Cantonese, Traditional)-WanLung- (Male)': 'zh-HK-WanLungNeural',
|
295 |
-
'Chinese (Taiwanese Mandarin)-HsiaoChen- (Female)': 'zh-TW-HsiaoChenNeural',
|
296 |
-
'Chinese (Taiwanese Mandarin)-YunJhe- (Male)': 'zh-TW-YunJheNeural',
|
297 |
-
'Zulu (South Africa)-Thando- (Female)': 'zu-ZA-ThandoNeural',
|
298 |
-
'Zulu (South Africa)-Themba- (Male)': 'zu-ZA-ThembaNeural',
|
299 |
}
|
|
|
300 |
# --- تابع اصلی با مدیریت خطای بهتر و اجرای async ---
|
301 |
async def text_to_speech_edge_async(text, language_code, rate, volume, pitch):
|
302 |
try:
|
303 |
if not text:
|
304 |
return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
|
305 |
|
306 |
-
|
307 |
-
if
|
308 |
return f"خطا: مدل صدای انتخاب شده ('{language_code}') در دیکشنری یافت نشد یا نامعتبر است.", None
|
309 |
|
310 |
-
#
|
311 |
-
# اگر مقدار 0 باشد،
|
312 |
-
|
313 |
-
|
314 |
-
|
|
|
|
|
|
|
|
|
315 |
|
316 |
communicate = edge_tts.Communicate(
|
317 |
-
text,
|
318 |
)
|
319 |
|
320 |
-
# استفاده از یک فایل موقت که پس از استفاده پاک شود
|
321 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
|
322 |
tmp_path = tmp_file.name
|
323 |
|
324 |
await communicate.save(tmp_path)
|
|
|
325 |
return "تبدیل متن به گفتار با موفقیت انجام شد.", tmp_path
|
326 |
|
327 |
except edge_tts.exceptions.NoAudioReceived:
|
328 |
-
error_msg = f"خطا: هیچ صدایی از سرویس برای متن و صدای انتخاب شده دریافت نشد.
|
329 |
-
print(error_msg)
|
|
|
|
|
|
|
|
|
|
|
330 |
return error_msg, None
|
331 |
except Exception as e:
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
traceback.print_exc() # چاپ کامل traceback در لاگ ها
|
336 |
-
# نمایش یک پیام خطای عمومی تر به کاربر
|
337 |
-
user_error_msg = f"یک خطای غیرمنتظره در سرور رخ داد. لطفاً دوباره امتحان کنید یا با توسعه دهنده تماس بگیرید. جزئیات: {type(e).__name__}"
|
338 |
return user_error_msg, None
|
339 |
|
340 |
-
# --- Wrapper برای اجرای تابع async در محیط Gradio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
341 |
def text_to_speech_edge_sync_wrapper(text, language_code, rate, volume, pitch):
|
342 |
-
#
|
343 |
-
# بنابراین برای فراخوانی تابع async باید یک event loop جدید ایجاد کنیم.
|
344 |
try:
|
345 |
-
loop
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
result = loop.run_until_complete(
|
|
|
|
|
351 |
except RuntimeError as e:
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
else:
|
358 |
-
|
|
|
|
|
|
|
|
|
359 |
return result
|
360 |
|
361 |
|
362 |
# --- تعریف رابط کاربری Gradio ---
|
363 |
-
input_text = gr.Textbox(lines=5, label="متن ورودی (Input Text)")
|
364 |
-
# انتخاب یک صدای پیش فرض که مطمئن هستیم در language_dict وجود دارد
|
365 |
default_voice_key = 'Persian (Iran)-Farid- (Male)'
|
366 |
if default_voice_key not in language_dict:
|
367 |
-
# اگر صدای پیش فرض ما در لیست نبود، اولین مورد لیست را انتخاب کن
|
368 |
default_voice_key = list(language_dict.keys())[0] if language_dict else None
|
369 |
|
370 |
language = gr.Dropdown(
|
371 |
choices=list(language_dict.keys()),
|
372 |
-
value=default_voice_key,
|
373 |
label="انتخاب مدل صدا (Choose the Voice Model)"
|
374 |
)
|
375 |
rate = gr.Slider(
|
@@ -379,7 +415,7 @@ volume = gr.Slider(
|
|
379 |
minimum=-100, maximum=100, step=1, value=0, label="حجم صدا (Volume)"
|
380 |
)
|
381 |
pitch = gr.Slider(
|
382 |
-
minimum=-50, maximum=50, step=1, value=0, label="زیر و بمی (Pitch)", info="مقدار بر حسب هرتز (Hz)
|
383 |
)
|
384 |
|
385 |
output_text_status = gr.Textbox(label="وضعیت (Status)")
|
@@ -387,17 +423,24 @@ output_audio = gr.Audio(type="filepath", label="فایل صوتی خروجی (Ex
|
|
387 |
|
388 |
|
389 |
iface = gr.Interface(
|
390 |
-
fn=text_to_speech_edge_sync_wrapper,
|
391 |
inputs=[input_text, language, rate, volume, pitch],
|
392 |
outputs=[output_text_status, output_audio],
|
393 |
-
title="Edge-TTS فارسی",
|
394 |
-
description="تبدیل متن به گفتار با استفاده از سرویس Microsoft Edge
|
395 |
-
allow_flagging="never",
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
)
|
401 |
|
402 |
-
if __name__ == "__main__":
|
403 |
-
iface.launch(share=True) # share=True
|
|
|
|
1 |
import gradio as gr
|
2 |
import edge_tts
|
3 |
import tempfile
|
4 |
+
import asyncio
|
5 |
+
import traceback # برای چاپ کامل traceback
|
6 |
|
7 |
+
# --- language_dict همانند قبل باقی می ماند ---
|
8 |
+
# (مطمئن شوید لیست صداهای شما معتبر است)
|
|
|
|
|
9 |
language_dict = {
|
10 |
'English-Jenny (Female)': 'en-US-JennyNeural',
|
11 |
'English-Guy (Male)': 'en-US-GuyNeural',
|
|
|
43 |
'Norwegian-Pernille- (Female)': 'nb-NO-PernilleNeural',
|
44 |
'Norwegian-Finn- (Male)': 'nb-NO-FinnNeural',
|
45 |
'Swedish-Sofie- (Female)': 'sv-SE-SofieNeural',
|
46 |
+
'Swedish-Mattias- (Male)': 'sv-SE-MattiasNeural',
|
47 |
'Arabic-Hamed- (Male)': 'ar-SA-HamedNeural',
|
48 |
'Arabic-Zariyah- (Female)': 'ar-SA-ZariyahNeural',
|
49 |
'Greek-Athina- (Female)': 'el-GR-AthinaNeural',
|
|
|
56 |
'Afrikaans-Willem- (Male)': 'af-ZA-WillemNeural',
|
57 |
'Ethiopian-Ameha- (Male)': 'am-ET-AmehaNeural',
|
58 |
'Ethiopian-Mekdes- (Female)': 'am-ET-MekdesNeural',
|
59 |
+
'Arabic (UAE)-Fatima- (Female)': 'ar-AE-FatimaNeural',
|
60 |
'Arabic (UAE)-Hamdan- (Male)': 'ar-AE-HamdanNeural',
|
61 |
'Arabic (Bahrain)-Ali- (Male)': 'ar-BH-AliNeural',
|
62 |
'Arabic (Bahrain)-Laila- (Female)': 'ar-BH-LailaNeural',
|
|
|
83 |
'Arabic (Syrian Arab Republic)-Laith- (Male)': 'ar-SY-LaithNeural',
|
84 |
'Arabic (Tunisia)-Hedi- (Male)': 'ar-TN-HediNeural',
|
85 |
'Arabic (Tunisia)-Reem- (Female)': 'ar-TN-ReemNeural',
|
86 |
+
'Arabic (Yemen)-Maryam- (Female)': 'ar-YE-MaryamNeural',
|
87 |
+
'Arabic (Yemen)-Saleh- (Male)': 'ar-YE-SalehNeural',
|
88 |
'Azerbaijani-Babek- (Male)': 'az-AZ-BabekNeural',
|
89 |
'Azerbaijani-Banu- (Female)': 'az-AZ-BanuNeural',
|
90 |
'Bulgarian-Borislav- (Male)': 'bg-BG-BorislavNeural',
|
|
|
93 |
'Bengali (Bangladesh)-Pradeep- (Male)': 'bn-BD-PradeepNeural',
|
94 |
'Bengali (India)-Bashkar- (Male)': 'bn-IN-BashkarNeural',
|
95 |
'Bengali (India)-Tanishaa- (Female)': 'bn-IN-TanishaaNeural',
|
96 |
+
'Bosnian (Bosnia and Herzegovina)-Goran- (Male)': 'bs-BA-GoranNeural',
|
97 |
'Bosnian (Bosnia and Herzegovina)-Vesna- (Female)': 'bs-BA-VesnaNeural',
|
98 |
'Catalan (Spain)-Joana- (Female)': 'ca-ES-JoanaNeural',
|
99 |
'Catalan (Spain)-Enric- (Male)': 'ca-ES-EnricNeural',
|
|
|
127 |
'English (Nigeria)-Abeo- (Male)': 'en-NG-AbeoNeural',
|
128 |
'English (Nigeria)-Ezinne- (Female)': 'en-NG-EzinneNeural',
|
129 |
'English (New Zealand)-Mitchell- (Male)': 'en-NZ-MitchellNeural',
|
130 |
+
'English (New Zealand)-Hazel- (Female)': 'en-NZ-HazelNeural',
|
131 |
'English (Philippines)-James- (Male)': 'en-PH-JamesNeural',
|
132 |
'English (Philippines)-Rosa- (Female)': 'en-PH-RosaNeural',
|
133 |
'English (Singapore)-Luna- (Female)': 'en-SG-LunaNeural',
|
|
|
145 |
'Spanish (Costa Rica)-Juan- (Male)': 'es-CR-JuanNeural',
|
146 |
'Spanish (Costa Rica)-Maria- (Female)': 'es-CR-MariaNeural',
|
147 |
'Spanish (Cuba)-Belkys- (Female)': 'es-CU-BelkysNeural',
|
148 |
+
'Spanish (Cuba)-Manuel- (Male)': 'es-CU-ManuelNeural',
|
149 |
'Spanish (Dominican Republic)-Emilio- (Male)': 'es-DO-EmilioNeural',
|
150 |
'Spanish (Dominican Republic)-Ramona- (Female)': 'es-DO-RamonaNeural',
|
151 |
'Spanish (Ecuador)-Andrea- (Female)': 'es-EC-AndreaNeural',
|
|
|
153 |
'Spanish (Spain)-Alvaro- (Male)': 'es-ES-AlvaroNeural',
|
154 |
'Spanish (Spain)-Elvira- (Female)': 'es-ES-ElviraNeural',
|
155 |
'Spanish (Equatorial Guinea)-Teresa- (Female)': 'es-GQ-TeresaNeural',
|
156 |
+
'Spanish (Equatorial Guinea)-Emilio- (Male)': 'es-GQ-EmilioNeural',
|
157 |
'Spanish (Guatemala)-Andres- (Male)': 'es-GT-AndresNeural',
|
158 |
'Spanish (Guatemala)-Marta- (Female)': 'es-GT-MartaNeural',
|
159 |
'Spanish (Honduras)-Carlos- (Male)': 'es-HN-CarlosNeural',
|
|
|
195 |
'Galician (Spain)-Sabela- (Female)': 'gl-ES-SabelaNeural',
|
196 |
'Gujarati (India)-Dhwani- (Female)': 'gu-IN-DhwaniNeural',
|
197 |
'Gujarati (India)-Niranjan- (Male)': 'gu-IN-NiranjanNeural',
|
198 |
+
'Hebrew (Israel)-Avri- (Male)': 'he-IL-AvriNeural',
|
199 |
+
'Hebrew (Israel)-Hila- (Female)': 'he-IL-HilaNeural',
|
200 |
'Hindi (India)-Madhur- (Male)': 'hi-IN-MadhurNeural',
|
201 |
'Hindi (India)-Swara- (Female)': 'hi-IN-SwaraNeural',
|
202 |
'Croatian (Croatia)-Gabrijela- (Female)': 'hr-HR-GabrijelaNeural',
|
203 |
'Croatian (Croatia)-Srecko- (Male)': 'hr-HR-SreckoNeural',
|
204 |
'Hungarian (Hungary)-Noemi- (Female)': 'hu-HU-NoemiNeural',
|
205 |
'Hungarian (Hungary)-Tamas- (Male)': 'hu-HU-TamasNeural',
|
206 |
+
'Armenian (Armenia)-Anahit- (Female)': 'hy-AM-AnahitNeural',
|
207 |
+
'Armenian (Armenia)-Hayk- (Male)': 'hy-AM-HaykNeural',
|
208 |
'Icelandic (Iceland)-Gudrun- (Female)': 'is-IS-GudrunNeural',
|
209 |
'Icelandic (Iceland)-Gunnar- (Male)': 'is-IS-GunnarNeural',
|
210 |
'Javanese (Indonesia)-Dimas- (Male)': 'jv-ID-DimasNeural',
|
|
|
241 |
'Dutch (Belgium)-Dena- (Female)': 'nl-BE-DenaNeural',
|
242 |
'Polish (Poland)-Marek- (Male)': 'pl-PL-MarekNeural',
|
243 |
'Polish (Poland)-Zofia- (Female)': 'pl-PL-ZofiaNeural',
|
244 |
+
'Pashto (Afghanistan)-Gul Nawaz- (Male)': 'ps-AF-GulNawazNeural',
|
245 |
+
'Pashto (Afghanistan)-Latifa- (Female)': 'ps-AF-LatifaNeural',
|
246 |
+
'Portuguese (Portugal)-Duarte- (Male)': 'pt-PT-DuarteNeural',
|
247 |
+
'Portuguese (Portugal)-Fernanda- (Female)': 'pt-PT-FernandaNeural',
|
248 |
+
'Romanian (Romania)-Alina- (Female)': 'ro-RO-AlinaNeural',
|
249 |
+
'Romanian (Romania)-Emil- (Male)': 'ro-RO-EmilNeural',
|
250 |
+
'Russian (Russia)-Dmitry- (Male)': 'ru-RU-DmitryNeural',
|
251 |
+
'Russian (Russia)-Svetlana- (Female)': 'ru-RU-SvetlanaNeural',
|
252 |
+
'Sinhala (Sri Lanka)-Dinuka- (Male)': 'si-LK-DinukaNeural',
|
253 |
+
'Sinhala (Sri Lanka)-Thilini- (Female)': 'si-LK-ThiliniNeural',
|
254 |
+
'Slovak (Slovakia)-Lukas- (Male)': 'sk-SK-LukasNeural',
|
255 |
+
'Slovak (Slovakia)-Viktoria- (Female)': 'sk-SK-ViktoriaNeural',
|
256 |
+
'Slovenian (Slovenia)-Petra- (Female)': 'sl-SI-PetraNeural',
|
257 |
+
'Slovenian (Slovenia)-Rok- (Male)': 'sl-SI-RokNeural',
|
258 |
+
'Somali (Somalia)-Muuse- (Male)': 'so-SO-MuuseNeural',
|
259 |
+
'Somali (Somalia)-Ubax- (Female)': 'so-SO-UbaxNeural',
|
260 |
+
'Albanian (Albania)-Anila- (Female)': 'sq-AL-AnilaNeural',
|
261 |
+
'Albanian (Albania)-Ilir- (Male)': 'sq-AL-IlirNeural',
|
262 |
+
'Serbian (Serbia)-Nikola- (Male)': 'sr-RS-NikolaNeural',
|
263 |
+
'Serbian (Serbia)-Sophie- (Female)': 'sr-RS-SophieNeural',
|
264 |
+
'Sundanese (Indonesia)-Jajang- (Male)': 'su-ID-JajangNeural',
|
265 |
+
'Sundanese (Indonesia)-Tuti- (Female)': 'su-ID-TutiNeural',
|
266 |
+
'Swahili (Kenya)-Rafiki- (Male)': 'sw-KE-RafikiNeural',
|
267 |
+
'Swahili (Kenya)-Zuri- (Female)': 'sw-KE-ZuriNeural',
|
268 |
+
'Swahili (Tanzania)-Daudi- (Male)': 'sw-TZ-DaudiNeural',
|
269 |
+
'Swahili (Tanzania)-Rehema- (Female)': 'sw-TZ-RehemaNeural',
|
270 |
+
'Tamil (India)-Pallavi- (Female)': 'ta-IN-PallaviNeural',
|
271 |
+
'Tamil (India)-Valluvar- (Male)': 'ta-IN-ValluvarNeural',
|
272 |
+
'Tamil (Malaysia)-Kani- (Female)': 'ta-MY-KaniNeural',
|
273 |
+
'Tamil (Malaysia)-Surya- (Male)': 'ta-MY-SuryaNeural',
|
274 |
+
'Tamil (Singapore)-Anbu- (Male)': 'ta-SG-AnbuNeural',
|
275 |
+
'Tamil (Singapore)-Venba- (Female)': 'ta-SG-VenbaNeural',
|
276 |
+
'Tamil (Sri Lanka)-Kumar- (Male)': 'ta-LK-KumarNeural',
|
277 |
+
'Tamil (Sri Lanka)-Saranya- (Female)': 'ta-LK-SaranyaNeural',
|
278 |
+
'Telugu (India)-Mohan- (Male)': 'te-IN-MohanNeural',
|
279 |
+
'Telugu (India)-Shruti- (Female)': 'te-IN-ShrutiNeural',
|
280 |
+
'Turkish (Turkey)-Ahmet- (Male)': 'tr-TR-AhmetNeural',
|
281 |
+
'Turkish (Turkey)-Emel- (Female)': 'tr-TR-EmelNeural',
|
282 |
+
'Ukrainian (Ukraine)-Ostap- (Male)': 'uk-UA-OstapNeural',
|
283 |
+
'Ukrainian (Ukraine)-Polina- (Female)': 'uk-UA-PolinaNeural',
|
284 |
+
'Urdu (India)-Gul- (Female)': 'ur-IN-GulNeural',
|
285 |
+
'Urdu (India)-Salman- (Male)': 'ur-IN-SalmanNeural',
|
286 |
+
'Urdu (Pakistan)-Asad- (Male)': 'ur-PK-AsadNeural',
|
287 |
+
'Urdu (Pakistan)-Uzma- (Female)': 'ur-PK-UzmaNeural',
|
288 |
+
'Uzbek (Uzbekistan)-Madina- (Female)': 'uz-UZ-MadinaNeural',
|
289 |
+
'Uzbek (Uzbekistan)-Sardor- (Male)': 'uz-UZ-SardorNeural',
|
290 |
+
'Chinese (Mandarin, Simplified)-Xiaoxiao- (Female)': 'zh-CN-XiaoxiaoNeural',
|
291 |
+
'Chinese (Mandarin, Simplified)-Yunyang- (Male)': 'zh-CN-YunyangNeural',
|
292 |
+
'Chinese (Cantonese, Traditional)-HiuGaai- (Female)': 'zh-HK-HiuGaaiNeural',
|
293 |
+
'Chinese (Cantonese, Traditional)-WanLung- (Male)': 'zh-HK-WanLungNeural',
|
294 |
+
'Chinese (Taiwanese Mandarin)-HsiaoChen- (Female)': 'zh-TW-HsiaoChenNeural',
|
295 |
+
'Chinese (Taiwanese Mandarin)-YunJhe- (Male)': 'zh-TW-YunJheNeural',
|
296 |
+
'Zulu (South Africa)-Thando- (Female)': 'zu-ZA-ThandoNeural',
|
297 |
+
'Zulu (South Africa)-Themba- (Male)': 'zu-ZA-ThembaNeural',
|
298 |
}
|
299 |
+
|
300 |
# --- تابع اصلی با مدیریت خطای بهتر و اجرای async ---
|
301 |
async def text_to_speech_edge_async(text, language_code, rate, volume, pitch):
|
302 |
try:
|
303 |
if not text:
|
304 |
return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
|
305 |
|
306 |
+
voice_id = language_dict.get(language_code)
|
307 |
+
if voice_id is None:
|
308 |
return f"خطا: مدل صدای انتخاب شده ('{language_code}') در دیکشنری یافت نشد یا نامعتبر است.", None
|
309 |
|
310 |
+
# اصلاح فرمت rate, volume, pitch برای edge-tts
|
311 |
+
# اگر مقدار 0 باشد، باید از فرمت "+0%" یا "-0%" استفاده شود.
|
312 |
+
# یا اینکه اگر کتابخانه اجازه می دهد، None یا رشته خالی ارسال کنیم.
|
313 |
+
# با توجه به مستندات edge-tts، باید همیشه با % یا Hz باشند.
|
314 |
+
rate_str = f"{int(rate):+g}%" # :+g علامت را برای صفر هم می گذارد
|
315 |
+
volume_str = f"{int(volume):+g}%"
|
316 |
+
pitch_str = f"{int(pitch):+g}Hz"
|
317 |
+
|
318 |
+
print(f"Requesting TTS with: Text='{text[:30]}...', Voice='{voice_id}', Rate='{rate_str}', Volume='{volume_str}', Pitch='{pitch_str}'")
|
319 |
|
320 |
communicate = edge_tts.Communicate(
|
321 |
+
text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str
|
322 |
)
|
323 |
|
|
|
324 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
|
325 |
tmp_path = tmp_file.name
|
326 |
|
327 |
await communicate.save(tmp_path)
|
328 |
+
print(f"TTS successful. Audio saved to: {tmp_path}")
|
329 |
return "تبدیل متن به گفتار با موفقیت انجام شد.", tmp_path
|
330 |
|
331 |
except edge_tts.exceptions.NoAudioReceived:
|
332 |
+
error_msg = f"خطا: هیچ صدایی از سرویس برای متن و صدای انتخاب شده دریافت نشد. (صدا: {voice_id})"
|
333 |
+
print(f"NoAudioReceived: {error_msg}")
|
334 |
+
return error_msg, None
|
335 |
+
except ValueError as ve: # برای خطاهای مربوط به پارامترهای نامعتبر
|
336 |
+
error_msg = f"خطا در پارامترهای ورودی برای edge-tts: {ve}. (صدا: {voice_id}, نرخ: {rate_str}, حجم: {volume_str}, زیروبمی: {pitch_str})"
|
337 |
+
print(f"ValueError in edge-tts: {error_msg}")
|
338 |
+
traceback.print_exc()
|
339 |
return error_msg, None
|
340 |
except Exception as e:
|
341 |
+
print(f"An unexpected error occurred in text_to_speech_edge_async (PID: {os.getpid()}, Thread: {threading.get_ident()}):")
|
342 |
+
traceback.print_exc()
|
343 |
+
user_error_msg = f"یک خطای غیرمنتظره در سرور رخ داد: {type(e).__name__}"
|
|
|
|
|
|
|
344 |
return user_error_msg, None
|
345 |
|
346 |
+
# --- Wrapper برای اجرای تابع async در محیط Gradio ---
|
347 |
+
# این wrapper حالا robust تر است
|
348 |
+
import threading
|
349 |
+
import os
|
350 |
+
|
351 |
+
_event_loops = {} # برای ذخیره event loop ها به ازای هر thread
|
352 |
+
|
353 |
+
def get_or_create_event_loop_for_thread():
|
354 |
+
thread_id = threading.get_ident()
|
355 |
+
if thread_id not in _event_loops or _event_loops[thread_id].is_closed():
|
356 |
+
loop = asyncio.new_event_loop()
|
357 |
+
asyncio.set_event_loop(loop)
|
358 |
+
_event_loops[thread_id] = loop
|
359 |
+
# print(f"Created new event loop for thread {thread_id} (PID: {os.getpid()})")
|
360 |
+
# else:
|
361 |
+
# print(f"Reusing event loop for thread {thread_id} (PID: {os.getpid()})")
|
362 |
+
return _event_loops[thread_id]
|
363 |
+
|
364 |
+
|
365 |
def text_to_speech_edge_sync_wrapper(text, language_code, rate, volume, pitch):
|
366 |
+
# print(f"Sync wrapper called in PID: {os.getpid()}, Thread: {threading.get_ident()}")
|
|
|
367 |
try:
|
368 |
+
# هر thread در Gradio/FastAPI ممکن است event loop خود را نیاز داشته باشد
|
369 |
+
loop = get_or_create_event_loop_for_thread()
|
370 |
+
# اطمینان از اینکه این لوپ برای این thread به عنوان لوپ فعلی تنظیم شده
|
371 |
+
asyncio.set_event_loop(loop)
|
372 |
+
# اجرای کوروتین در لوپ
|
373 |
+
result = loop.run_until_complete(
|
374 |
+
text_to_speech_edge_async(text, language_code, rate, volume, pitch)
|
375 |
+
)
|
376 |
except RuntimeError as e:
|
377 |
+
print(f"RuntimeError in sync_wrapper (PID: {os.getpid()}, Thread: {threading.get_ident()}): {e}")
|
378 |
+
traceback.print_exc()
|
379 |
+
# اگر هنوز با خطای "no current event loop" مواجه شدیم، یکبار دیگر با لوپ جدید تلاش می کنیم
|
380 |
+
# این بخش شاید دیگر لازم نباشد با get_or_create_event_loop_for_thread
|
381 |
+
if "no current event loop" in str(e).lower() or "cannot be called from a running event loop" in str(e).lower():
|
382 |
+
print("Attempting with a completely new loop as fallback...")
|
383 |
+
new_loop = asyncio.new_event_loop()
|
384 |
+
asyncio.set_event_loop(new_loop) # این خط مهم است
|
385 |
+
try:
|
386 |
+
result = new_loop.run_until_complete(
|
387 |
+
text_to_speech_edge_async(text, language_code, rate, volume, pitch)
|
388 |
+
)
|
389 |
+
finally:
|
390 |
+
new_loop.close() # مهم است که لوپ های موقت را ببندیم
|
391 |
else:
|
392 |
+
return f"خطای اجرایی: {e}", None
|
393 |
+
except Exception as e:
|
394 |
+
print(f"Unexpected Exception in sync_wrapper (PID: {os.getpid()}, Thread: {threading.get_ident()}):")
|
395 |
+
traceback.print_exc()
|
396 |
+
return f"خطای غیرمنتظره در wrapper: {type(e).__name__}", None
|
397 |
return result
|
398 |
|
399 |
|
400 |
# --- تعریف رابط کاربری Gradio ---
|
401 |
+
input_text = gr.Textbox(lines=5, label="متن ورودی (Input Text)", value="سلام من یک ربات فارسی زبان هستم.") # یک مقدار پیش فرض برای تست اولیه
|
|
|
402 |
default_voice_key = 'Persian (Iran)-Farid- (Male)'
|
403 |
if default_voice_key not in language_dict:
|
|
|
404 |
default_voice_key = list(language_dict.keys())[0] if language_dict else None
|
405 |
|
406 |
language = gr.Dropdown(
|
407 |
choices=list(language_dict.keys()),
|
408 |
+
value=default_voice_key,
|
409 |
label="انتخاب مدل صدا (Choose the Voice Model)"
|
410 |
)
|
411 |
rate = gr.Slider(
|
|
|
415 |
minimum=-100, maximum=100, step=1, value=0, label="حجم صدا (Volume)"
|
416 |
)
|
417 |
pitch = gr.Slider(
|
418 |
+
minimum=-50, maximum=50, step=1, value=0, label="زیر و بمی (Pitch)", info="مقدار بر حسب هرتز (Hz)"
|
419 |
)
|
420 |
|
421 |
output_text_status = gr.Textbox(label="وضعیت (Status)")
|
|
|
423 |
|
424 |
|
425 |
iface = gr.Interface(
|
426 |
+
fn=text_to_speech_edge_sync_wrapper,
|
427 |
inputs=[input_text, language, rate, volume, pitch],
|
428 |
outputs=[output_text_status, output_audio],
|
429 |
+
title="Edge-TTS فارسی (اصلاح شده)",
|
430 |
+
description="تبدیل متن به گفتار با استفاده از سرویس Microsoft Edge",
|
431 |
+
allow_flagging="never",
|
432 |
+
# مثال ها را فعلا غیرفعال می کنیم تا مشکل caching حل شود، یا مقادیر را تغییر می دهیم
|
433 |
+
# examples=[
|
434 |
+
# ["سلام دنیا، حال شما چطور است؟", 'Persian (Iran)-Dilara- (Female)', 0, 0, 0], # این باعث خطا میشد
|
435 |
+
# ["Hello world, how are you?", 'English-Jenny (Female)', 10, 0, -10], # این هم
|
436 |
+
# ]
|
437 |
+
# مثال های اصلاح شده:
|
438 |
+
examples=[
|
439 |
+
["سلام دنیا، حال شما چطور است؟", 'Persian (Iran)-Dilara- (Female)', 0, 0, 0], # مقادیر صفر هنوز ممکن است مشکل ساز باشند اگر فرمت اشتباه باشد
|
440 |
+
["Hello world, how are you?", 'English-Jenny (Female)', +10, 0, -10], # rate مثبت
|
441 |
+
]
|
442 |
)
|
443 |
|
444 |
+
# if __name__ == "__main__": # در Hugging Face Spaces این بخش لازم نیست و خودش app.py را اجرا می کند
|
445 |
+
# iface.launch(share=True) # share=True در HF Spaces کار نمی کند و خودش لینک عمومی می دهد
|
446 |
+
iface.launch() # در HF Spaces فقط launch() کافی است
|