Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -3,11 +3,10 @@ import edge_tts
|
|
3 |
import tempfile
|
4 |
import asyncio
|
5 |
import traceback
|
6 |
-
import threading
|
7 |
import os
|
8 |
|
9 |
# --- دیکشنری زبانها و صداها با کلیدهای فارسی (نمونه) ---
|
10 |
-
#
|
11 |
language_dict_persian_keys = {
|
12 |
'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
|
13 |
'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
|
@@ -299,16 +298,19 @@ language_dict_persian_keys = {
|
|
299 |
'زولو (آفریقای جنوبی) - تمبا (مرد)': 'zu-ZA-ThembaNeural',
|
300 |
}
|
301 |
|
302 |
-
|
|
|
303 |
async def text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch):
|
304 |
"""
|
305 |
تابع ناهمزمان برای تبدیل متن به گفتار با استفاده از Edge TTS.
|
|
|
306 |
خروجی: (پیام وضعیت, مسیر فایل صوتی یا None)
|
307 |
"""
|
308 |
-
temp_path = None
|
309 |
try:
|
310 |
if not text:
|
311 |
return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
|
|
|
312 |
voice_id = language_dict_persian_keys.get(language_code_persian)
|
313 |
if voice_id is None:
|
314 |
return f"خطا: مدل صدای انتخاب شده ('{language_code_persian}') یافت نشد.", None
|
@@ -316,19 +318,24 @@ async def text_to_speech_edge_async(text, language_code_persian, rate, volume, p
|
|
316 |
rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
|
317 |
communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
|
318 |
|
319 |
-
#
|
320 |
-
#
|
321 |
-
|
|
|
322 |
temp_path = tmp_file.name
|
323 |
|
324 |
await communicate.save(temp_path)
|
|
|
|
|
|
|
|
|
|
|
325 |
return "تبدیل با موفقیت انجام شد.", temp_path
|
326 |
|
327 |
except edge_tts.exceptions.NoAudioReceived:
|
328 |
error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
|
329 |
-
# اگر خطا وجود داشت و فایل موقت ایجاد شده بود، آن را حذف میکنیم
|
330 |
if temp_path and os.path.exists(temp_path):
|
331 |
-
os.remove(temp_path)
|
332 |
return error_msg, None
|
333 |
except ValueError as ve:
|
334 |
error_msg = f"خطا در پارامترهای ورودی: {ve}"
|
@@ -337,41 +344,12 @@ async def text_to_speech_edge_async(text, language_code_persian, rate, volume, p
|
|
337 |
return error_msg, None
|
338 |
except Exception as e:
|
339 |
error_msg = f"خطای غیرمنتظره در سرور: {type(e).__name__} - {e}"
|
340 |
-
traceback.print_exc() # برای مشاهده traceback در کنسول
|
341 |
if temp_path and os.path.exists(temp_path):
|
342 |
os.remove(temp_path)
|
343 |
return error_msg, None
|
344 |
|
345 |
-
|
346 |
-
|
347 |
-
def _get_or_create_event_loop():
|
348 |
-
"""
|
349 |
-
برای مدیریت حلقه رویداد asyncio در تردهای مختلف.
|
350 |
-
هر ترد یک حلقه رویداد مخصوص به خود را دارد.
|
351 |
-
"""
|
352 |
-
thread_id = threading.get_ident()
|
353 |
-
loop = _event_loops_by_thread.get(thread_id)
|
354 |
-
if loop is None or loop.is_closed():
|
355 |
-
loop = asyncio.new_event_loop()
|
356 |
-
_event_loops_by_thread[thread_id] = loop
|
357 |
-
return loop
|
358 |
-
|
359 |
-
def text_to_speech_edge_sync_wrapper(text, language_code_persian, rate, volume, pitch):
|
360 |
-
"""
|
361 |
-
یک wrapper همزمان برای تابع ناهمزمان text_to_speech_edge_async.
|
362 |
-
این تابع توسط Gradio فراخوانی میشود.
|
363 |
-
"""
|
364 |
-
try:
|
365 |
-
loop = _get_or_create_event_loop()
|
366 |
-
# اجرای تابع ناهمزمان در حلقه رویداد
|
367 |
-
# run_until_complete مسدودکننده است تا عملیات ناهمزمان کامل شود
|
368 |
-
result = loop.run_until_complete(text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch))
|
369 |
-
return result
|
370 |
-
except Exception as e:
|
371 |
-
# در صورت بروز هرگونه خطا در زمان اجرا، آن را گزارش میکنیم
|
372 |
-
return f"خطای حین اجرای عملیات تبدیل: {type(e).__name__} - {e}", None
|
373 |
-
|
374 |
-
# --- تعریف تم و CSS ---
|
375 |
app_theme = gr.themes.Soft(
|
376 |
primary_hue=gr.themes.colors.blue,
|
377 |
secondary_hue=gr.themes.colors.sky,
|
@@ -459,8 +437,7 @@ default_voice_key_persian = 'فارسی (ایران) - فرید (مرد)'
|
|
459 |
if default_voice_key_persian not in language_dict_persian_keys:
|
460 |
default_voice_key_persian = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
|
461 |
|
462 |
-
|
463 |
-
LOGO_URL = "https://www.gstatic.com/lamda/images/gemini/google_bard_logo_150_v2_dark_color_1x.png"
|
464 |
|
465 |
with gr.Blocks(theme=app_theme, css=custom_css) as demo:
|
466 |
with gr.Row():
|
@@ -493,9 +470,10 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
|
|
493 |
submit_button = gr.Button("🔊 تولید و پخش صدا", variant="primary")
|
494 |
|
495 |
with gr.Column(scale=2):
|
496 |
-
# اطمینان از اینکه خروجیها به درستی به کامپوننتهای هدف متصل شوند
|
497 |
output_text_status = gr.Textbox(label="📊 وضعیت عملیات", interactive=False, lines=1, placeholder="نتیجه اینجا نمایش داده میشود...")
|
498 |
-
|
|
|
|
|
499 |
|
500 |
gr.HTML("<hr style='margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid #dee2e6;'>")
|
501 |
|
@@ -505,21 +483,23 @@ with gr.Blocks(theme=app_theme, css=custom_css) as demo:
|
|
505 |
["This is a test of the speech synthesis system.", 'انگلیسی - جنی (زن)', +5, 0, 0],
|
506 |
["تجربه کاربری در این سامانه بسیار روان است.", 'فارسی (ایران) - فرید (مرد)', -5, 0, 0],
|
507 |
],
|
|
|
|
|
|
|
508 |
inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
|
509 |
-
outputs=[output_text_status, output_audio],
|
510 |
-
|
511 |
-
cache_examples=False,
|
512 |
label="💡 چند نمونه برای شروع"
|
513 |
)
|
514 |
|
515 |
-
# اتصال دکمه submit به تابع تبدیل
|
516 |
submit_button.click(
|
517 |
-
|
|
|
|
|
518 |
inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
|
519 |
-
outputs=[output_text_status, output_audio],
|
520 |
)
|
521 |
|
522 |
-
# برای اجرای برنامه Gradio
|
523 |
if __name__ == "__main__":
|
524 |
demo.launch()
|
525 |
|
|
|
3 |
import tempfile
|
4 |
import asyncio
|
5 |
import traceback
|
|
|
6 |
import os
|
7 |
|
8 |
# --- دیکشنری زبانها و صداها با کلیدهای فارسی (نمونه) ---
|
9 |
+
# ... (بدون تغییر) ...
|
10 |
language_dict_persian_keys = {
|
11 |
'انگلیسی - جنی (زن)': 'en-US-JennyNeural',
|
12 |
'انگلیسی - گای (مرد)': 'en-US-GuyNeural',
|
|
|
298 |
'زولو (آفریقای جنوبی) - تمبا (مرد)': 'zu-ZA-ThembaNeural',
|
299 |
}
|
300 |
|
301 |
+
|
302 |
+
# --- توابع تبدیل متن به گفتار (نسخه Async که مستقیماً در Gradio استفاده میشود) ---
|
303 |
async def text_to_speech_edge_async(text, language_code_persian, rate, volume, pitch):
|
304 |
"""
|
305 |
تابع ناهمزمان برای تبدیل متن به گفتار با استفاده از Edge TTS.
|
306 |
+
این تابع مستقیماً توسط Gradio فراخوانی میشود، نیازی به wrapper همزمان نیست.
|
307 |
خروجی: (پیام وضعیت, مسیر فایل صوتی یا None)
|
308 |
"""
|
309 |
+
temp_path = None
|
310 |
try:
|
311 |
if not text:
|
312 |
return "خطا: لطفاً متنی را برای تبدیل وارد کنید.", None
|
313 |
+
|
314 |
voice_id = language_dict_persian_keys.get(language_code_persian)
|
315 |
if voice_id is None:
|
316 |
return f"خطا: مدل صدای انتخاب شده ('{language_code_persian}') یافت نشد.", None
|
|
|
318 |
rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
|
319 |
communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
|
320 |
|
321 |
+
# Gradio خودش یک پوشه 'temp' محلی برای فایلهای موقت دارد.
|
322 |
+
# استفاده از tempfile.gettempdir() برای اطمینان از ایجاد فایل در مکانی قابل دسترسی توسط سیستم.
|
323 |
+
# Gradio فایلها را از این مکان به درستی مدیریت میکند.
|
324 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav", dir=tempfile.gettempdir()) as tmp_file:
|
325 |
temp_path = tmp_file.name
|
326 |
|
327 |
await communicate.save(temp_path)
|
328 |
+
|
329 |
+
# مهم: Gradio پس از استفاده از فایل (که به output_audio منتقل میشود)، آن را خودش پاک میکند.
|
330 |
+
# نیازی به os.remove در اینجا نیست مگر اینکه بخواهید فایل را بلافاصله پس از اولین استفاده (و قبل از اینکه Gradio آن را به کلاینت بفرستد) پاک کنید.
|
331 |
+
# اما برای نمایش در Gradio، باید فایل تا زمانی که Gradio آن را Serving کند وجود داشته باشد.
|
332 |
+
|
333 |
return "تبدیل با موفقیت انجام شد.", temp_path
|
334 |
|
335 |
except edge_tts.exceptions.NoAudioReceived:
|
336 |
error_msg = f"خطا: صدایی برای متن و صدای انتخاب شده دریافت نشد (صدا: {voice_id})."
|
|
|
337 |
if temp_path and os.path.exists(temp_path):
|
338 |
+
os.remove(temp_path) # در صورت خطا، فایل موقت را پاک میکنیم
|
339 |
return error_msg, None
|
340 |
except ValueError as ve:
|
341 |
error_msg = f"خطا در پارامترهای ورودی: {ve}"
|
|
|
344 |
return error_msg, None
|
345 |
except Exception as e:
|
346 |
error_msg = f"خطای غیرمنتظره در سرور: {type(e).__name__} - {e}"
|
347 |
+
traceback.print_exc() # برای مشاهده traceback در کنسول (در محیط Hugging Face در logs دیده میشود)
|
348 |
if temp_path and os.path.exists(temp_path):
|
349 |
os.remove(temp_path)
|
350 |
return error_msg, None
|
351 |
|
352 |
+
# --- تعریف تم و CSS (بدون تغییر) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
353 |
app_theme = gr.themes.Soft(
|
354 |
primary_hue=gr.themes.colors.blue,
|
355 |
secondary_hue=gr.themes.colors.sky,
|
|
|
437 |
if default_voice_key_persian not in language_dict_persian_keys:
|
438 |
default_voice_key_persian = list(language_dict_persian_keys.keys())[0] if language_dict_persian_keys else None
|
439 |
|
440 |
+
LOGO_URL = "https://www.gstatic.com/lamda/images/gemini/google_bard_logo_150_v2_dark_color_1x.png" # بدون تغییر
|
|
|
441 |
|
442 |
with gr.Blocks(theme=app_theme, css=custom_css) as demo:
|
443 |
with gr.Row():
|
|
|
470 |
submit_button = gr.Button("🔊 تولید و پخش صدا", variant="primary")
|
471 |
|
472 |
with gr.Column(scale=2):
|
|
|
473 |
output_text_status = gr.Textbox(label="📊 وضعیت عملیات", interactive=False, lines=1, placeholder="نتیجه اینجا نمایش داده میشود...")
|
474 |
+
# مهم: type="filepath" و interactive=False برای نمایش فایل در Gradio کافیست.
|
475 |
+
# autoplay=True برای شروع خودکار پخش پس از بارگذاری فایل.
|
476 |
+
output_audio = gr.Audio(type="filepath", label="🎧 فایل صوتی خروجی", interactive=False, autoplay=True)
|
477 |
|
478 |
gr.HTML("<hr style='margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid #dee2e6;'>")
|
479 |
|
|
|
483 |
["This is a test of the speech synthesis system.", 'انگلیسی - جنی (زن)', +5, 0, 0],
|
484 |
["تجربه کاربری در این سامانه بسیار روان است.", 'فارسی (ایران) - فرید (مرد)', -5, 0, 0],
|
485 |
],
|
486 |
+
# نکته مهم: اگر تابع شما async است، Gradio آن را در یک ترد جداگانه اجرا میکند.
|
487 |
+
# نیازی به wrapper همزمان نیست (حذف text_to_speech_edge_sync_wrapper و جایگزینی با text_to_speech_edge_async)
|
488 |
+
fn=text_to_speech_edge_async,
|
489 |
inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
|
490 |
+
outputs=[output_text_status, output_audio],
|
491 |
+
cache_examples=False, # cache_examples=True ممکن است با این نوع خروجی فایل موقت مشکل ایجاد کند
|
|
|
492 |
label="💡 چند نمونه برای شروع"
|
493 |
)
|
494 |
|
|
|
495 |
submit_button.click(
|
496 |
+
# نکته مهم: اگر تابع شما async است، Gradio آن را در یک ترد جداگانه اجرا میکند.
|
497 |
+
# نیازی به wrapper همزمان نیست (حذف text_to_speech_edge_sync_wrapper و جایگزینی با text_to_speech_edge_async)
|
498 |
+
fn=text_to_speech_edge_async,
|
499 |
inputs=[input_text, language_dropdown, rate_slider, volume_slider, pitch_slider],
|
500 |
+
outputs=[output_text_status, output_audio],
|
501 |
)
|
502 |
|
|
|
503 |
if __name__ == "__main__":
|
504 |
demo.launch()
|
505 |
|