Ttspro / app.py
Hamed744's picture
Update app.py
1cae8c0 verified
raw
history blame
29.3 kB
import gradio as gr
import base64
import mimetypes
import os
import re
import struct
import time
import zipfile
from google import genai
from google.genai import types
import traceback # برای نمایش خطاهای دقیق‌تر
# خواندن کلید API از Hugging Face Secrets
HF_GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
try:
from pydub import AudioSegment
PYDUB_AVAILABLE = True
except ImportError:
PYDUB_AVAILABLE = False
print("⚠️ کتابخانه pydub در دسترس نیست. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
# --- ثابت‌ها ---
SPEAKER_VOICES = [
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat",
"Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima",
"Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalthgeti",
"Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus",
"Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
]
MODELS = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
MODEL_NAMES_FARSI = {
"gemini-2.5-flash-preview-tts": "جمینای ۲.۵ فلش (سریع‌تر، اقتصادی‌تر)",
"gemini-2.5-pro-preview-tts": "جمینای ۲.۵ پرو (کیفیت بالاتر)"
}
SPEAKER_VOICES_FARSI_SAMPLE = { # می‌توانید برای همه گوینده‌ها نام فارسی تعریف کنید
"Charon": "شارون (پیش‌فرض)",
"Achernar": "آخرالنهر",
"Vindemiatrix": "vindemiatrix (ستاره‌شناس)",
# ... بقیه گوینده‌ها
}
# --- توابع کمکی (بدون تغییر زیاد در منطق، فقط پیام‌ها فارسی می‌شوند) ---
def save_binary_file(file_name, data):
abs_file_name = os.path.abspath(file_name)
try:
with open(abs_file_name, "wb") as f:
f.write(data)
print(f"✅ فایل در مسیر ذخیره شد: {abs_file_name}")
return abs_file_name
except Exception as e:
print(f"❌ خطا در ذخیره فایل {abs_file_name}: {e}")
return None
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
parameters = parse_audio_mime_type(mime_type)
bits_per_sample = parameters["bits_per_sample"]
sample_rate = parameters["rate"]
num_channels = 1
data_size = len(audio_data)
bytes_per_sample = bits_per_sample // 8
block_align = num_channels * bytes_per_sample
byte_rate = sample_rate * block_align
chunk_size = 36 + data_size
header = struct.pack(
"<4sI4s4sIHHIIHH4sI",
b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
sample_rate, byte_rate, block_align, bits_per_sample, b"data", data_size
)
return header + audio_data
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
bits_per_sample = 16
rate = 24000
if mime_type:
mime_type_lower = mime_type.lower()
parts = mime_type_lower.split(";")
for param in parts:
param = param.strip()
if param.startswith("rate="):
try: rate = int(param.split("=", 1)[1])
except: pass
elif param.startswith("audio/l"):
try:
potential_bits = param.split("l", 1)[1]
if potential_bits.isdigit(): bits_per_sample = int(potential_bits)
except: pass
return {"bits_per_sample": bits_per_sample, "rate": rate}
def load_text_from_gr_file(file_obj):
if file_obj is None:
return "", "فایلی برای ورودی متن ارائه نشده است."
try:
with open(file_obj.name, 'r', encoding='utf-8') as f:
content = f.read().strip()
if not content:
return "", "فایل متنی خالی است."
return content, f"متن با موفقیت از فایل '{os.path.basename(file_obj.name)}' ({len(content)} کاراکتر) بارگذاری شد."
except Exception as e:
return "", f"خطا در خواندن فایل متنی: {e}"
def smart_text_split(text, max_size=3800):
if len(text) <= max_size: return [text]
chunks, current_chunk = [], ""
sentences = re.split(r'(?<=[.!?؟])\s+', text)
for sentence in sentences:
if not sentence: continue
if len(current_chunk) + len(sentence) + 1 > max_size:
if current_chunk: chunks.append(current_chunk.strip())
if len(sentence) > max_size:
words, temp_part = sentence.split(' '), ""
for word in words:
if len(temp_part) + len(word) + 1 > max_size:
if temp_part: chunks.append(temp_part.strip())
if len(word) > max_size:
for i in range(0, len(word), max_size): chunks.append(word[i:i+max_size])
temp_part = ""
else: temp_part = word
else: temp_part += (" " if temp_part else "") + word
if temp_part: chunks.append(temp_part.strip())
current_chunk = ""
else: current_chunk = sentence
else: current_chunk += (" " if current_chunk else "") + sentence
if current_chunk: chunks.append(current_chunk.strip())
return chunks
def merge_audio_files_func(file_paths, output_path):
if not PYDUB_AVAILABLE: return False, "pydub در دسترس نیست. امکان ادغام فایل‌ها وجود ندارد.", None
if not file_paths: return False, "هیچ فایل صوتی برای ادغام وجود ندارد.", None
try:
combined = AudioSegment.empty()
for i, file_path in enumerate(file_paths):
if os.path.exists(file_path):
try:
audio = AudioSegment.from_file(file_path, format="wav") # فرض می‌کنیم ورودی‌ها WAV هستند
combined += audio
if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
except Exception as e_load:
msg = f"خطا در بارگذاری فایل صوتی '{os.path.basename(file_path)}' با pydub: {e_load}"
print(f"⚠️ {msg}")
return False, msg, None
else:
msg = f"فایل برای ادغام یافت نشد: {os.path.basename(file_path)}"
print(f"⚠️ {msg}")
return False, msg, None
abs_output_path = os.path.abspath(output_path)
combined.export(abs_output_path, format="wav")
return True, f"فایل ادغام شده با موفقیت در '{os.path.basename(abs_output_path)}' ذخیره شد.", abs_output_path
except Exception as e:
msg = f"خطا در ادغام فایل‌ها: {e}"
print(f"❌ {msg}")
return False, msg, None
def create_zip_file(file_paths, zip_name):
abs_zip_name = os.path.abspath(zip_name)
try:
with zipfile.ZipFile(abs_zip_name, 'w') as zipf:
for file_path in file_paths:
if os.path.exists(file_path):
zipf.write(file_path, os.path.basename(file_path))
return True, f"فایل ZIP با نام '{os.path.basename(abs_zip_name)}' ایجاد شد.", abs_zip_name
except Exception as e:
return False, f"خطا در ایجاد فایل ZIP: {e}", None
# --- تابع اصلی تولید صدا ---
def generate_audio_for_gradio(
use_file_input_checkbox, text_file_obj,
speech_prompt_input, text_to_speak_input,
max_chunk_slider, sleep_slider, temperature_slider,
model_dropdown_key, # کلید مدل (انگلیسی)
speaker_dropdown, output_filename_base_input,
merge_checkbox, delete_partials_checkbox,
progress=gr.Progress(track_tqdm=True)
):
status_messages = []
status_messages.append("🚀 فرآیند تبدیل متن به گفتار آغاز شد...")
progress(0, desc="در حال آماده‌سازی...")
api_key_to_use = HF_GEMINI_API_KEY
if not api_key_to_use:
status_messages.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) در تنظیمات Secret این Space یافت نشد.")
status_messages.append("⬅️ لطفاً آن را در بخش Settings > Secrets مربوط به این Space تنظیم کنید.")
return None, None, "\n".join(status_messages)
os.environ["GEMINI_API_KEY"] = api_key_to_use
status_messages.append("🔑 کلید API با موفقیت از Secrets بارگذاری شد.")
actual_text_input = ""
if use_file_input_checkbox:
if text_file_obj is None:
status_messages.append("❌ خطا: گزینه 'استفاده از فایل متنی' انتخاب شده، اما هیچ فایلی آپلود نشده است.")
return None, None, "\n".join(status_messages)
actual_text_input, msg = load_text_from_gr_file(text_file_obj)
status_messages.append(msg)
if not actual_text_input: return None, None, "\n".join(status_messages)
else:
actual_text_input = text_to_speak_input
status_messages.append("⌨️ از متن وارد شده به صورت دستی استفاده می‌شود.")
if not actual_text_input or actual_text_input.strip() == "":
status_messages.append("❌ خطا: متن ورودی خالی است.")
return None, None, "\n".join(status_messages)
try:
status_messages.append("🛠️ در حال مقداردهی اولیه کلاینت جمینای...")
progress(0.1, desc="اتصال به جمینای...")
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
status_messages.append("✅ کلاینت جمینای با موفقیت ایجاد شد.")
except Exception as e:
status_messages.append(f"❌ خطا در ایجاد کلاینت جمینای: {e}")
return None, None, "\n".join(status_messages)
text_chunks = smart_text_split(actual_text_input, int(max_chunk_slider))
status_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
for i, chunk_text_content in enumerate(text_chunks):
status_messages.append(f" 📝 قطعه {i+1}: {len(chunk_text_content)} کاراکتر")
generated_audio_files = []
run_id = base64.urlsafe_b64encode(os.urandom(6)).decode()
temp_output_dir = f"temp_audio_{run_id}"
os.makedirs(temp_output_dir, exist_ok=True)
output_base_name_safe = re.sub(r'[\s\\\/\:\*\?\"\<\>\|\%]+', '_', output_filename_base_input)
total_chunks = len(text_chunks)
for i, chunk_text_content in enumerate(text_chunks):
progress_val = 0.1 + (0.7 * (i / total_chunks))
progress(progress_val, desc=f"در حال تولید قطعه {i+1} از {total_chunks}...")
status_messages.append(f"\n🔊 در حال تولید صدا برای قطعه {i+1}/{total_chunks}...")
final_text_for_api = f'"{speech_prompt_input}"\n{chunk_text_content}' if speech_prompt_input.strip() else chunk_text_content
contents_for_api = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
generate_content_config = types.GenerateContentConfig(
temperature=float(temperature_slider),
response_modalities=["audio"],
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=speaker_dropdown) # گوینده از ورودی
)
)
)
try:
chunk_filename_base = f"{output_base_name_safe}_part_{i+1:03d}"
chunk_filepath_prefix = os.path.join(temp_output_dir, chunk_filename_base)
audio_data_received = False
for stream_response_chunk in client.models.generate_content_stream(
model=model_dropdown_key, contents=contents_for_api, config=generate_content_config,
):
if (stream_response_chunk.candidates and stream_response_chunk.candidates[0].content and
stream_response_chunk.candidates[0].content.parts and
stream_response_chunk.candidates[0].content.parts[0].inline_data):
inline_data = stream_response_chunk.candidates[0].content.parts[0].inline_data
data_buffer = inline_data.data
api_mime_type = inline_data.mime_type
audio_data_received = True
status_messages.append(f"ℹ️ MIME Type دریافتی از API: {api_mime_type}")
file_extension = ".wav"
if api_mime_type and ("mp3" in api_mime_type.lower() or "mpeg" in api_mime_type.lower()):
file_extension = ".mp3"
status_messages.append(f"ℹ️ ذخیره با فرمت MP3 بر اساس MIME Type: {api_mime_type}")
elif api_mime_type and "wav" in api_mime_type.lower() and \
not ("audio/l16" in api_mime_type.lower() or "audio/l24" in api_mime_type.lower()):
file_extension = ".wav"
status_messages.append(f"ℹ️ ذخیره با فرمت WAV بر اساس MIME Type: {api_mime_type}")
else:
file_extension = ".wav"
status_messages.append(f"ℹ️ تبدیل به فرمت WAV برای MIME Type: {api_mime_type or 'نامشخص'}")
data_buffer = convert_to_wav(data_buffer, api_mime_type)
status_messages.append(f"ℹ️ پسوند فایل نهایی: {file_extension}")
generated_file_path = save_binary_file(f"{chunk_filepath_prefix}{file_extension}", data_buffer)
if generated_file_path:
generated_audio_files.append(generated_file_path)
status_messages.append(f"✅ قطعه {i+1} ذخیره شد: {os.path.basename(generated_file_path)}")
else:
status_messages.append(f"❌ عدم موفقیت در ذخیره قطعه {i+1}.")
break
elif stream_response_chunk.text:
status_messages.append(f"ℹ️ پیام متنی از API (حین استریم): {stream_response_chunk.text}")
if not audio_data_received:
status_messages.append(f"❌ هیچ داده صوتی برای قطعه {i+1} دریافت نشد.")
if stream_response_chunk and stream_response_chunk.prompt_feedback and stream_response_chunk.prompt_feedback.block_reason:
status_messages.append(f"🛑 دلیل مسدود شدن توسط API: {stream_response_chunk.prompt_feedback.block_reason_message or stream_response_chunk.prompt_feedback.block_reason}")
except types.BlockedPromptException as bpe:
status_messages.append(f"❌ محتوای قطعه {i+1} توسط API مسدود شد: {bpe}")
status_messages.append(f" بازخورد API: {bpe.response.prompt_feedback}")
except types.StopCandidateException as sce:
status_messages.append(f"❌ تولید صدا برای قطعه {i+1} متوقف شد: {sce}")
status_messages.append(f" بازخورد API: {sce.response.prompt_feedback}")
except Exception as e:
status_messages.append(f"❌ خطا در تولید/پردازش قطعه {i+1}: {type(e).__name__} - {e}")
status_messages.append(traceback.format_exc())
continue
if i < total_chunks - 1 and float(sleep_slider) > 0 :
status_messages.append(f"⏱️ انتظار به مدت {sleep_slider} ثانیه...")
time.sleep(float(sleep_slider))
progress(0.85, desc="پردازش فایل‌های نهایی...")
if not generated_audio_files:
status_messages.append("❌ هیچ فایل صوتی با موفقیت تولید یا ذخیره نشد!")
final_status = "\n".join(status_messages)
print(final_status)
progress(1, desc="پایان با خطا.")
return None, None, final_status
status_messages.append(f"\n🎉 {len(generated_audio_files)} فایل(های) صوتی تولید شد!")
output_audio_path_for_player = None
output_path_for_download = None
if merge_checkbox and len(generated_audio_files) > 1 and PYDUB_AVAILABLE:
status_messages.append(f"🔗 در حال ادغام {len(generated_audio_files)} فایل صوتی...")
merged_filename_path = os.path.join(temp_output_dir, f"{output_base_name_safe}_merged.wav")
success_merge, msg_merge, merged_p = merge_audio_files_func(generated_audio_files, merged_filename_path)
status_messages.append(msg_merge)
if success_merge:
output_audio_path_for_player = merged_p
output_path_for_download = merged_p
if delete_partials_checkbox:
status_messages.append("🗑️ در حال حذف فایل‌های جزئی...")
for file_p in generated_audio_files:
try: os.remove(file_p); status_messages.append(f" 🗑️ حذف شد: {os.path.basename(file_p)}")
except Exception as e_del: status_messages.append(f" ⚠️ عدم موفقیت در حذف {os.path.basename(file_p)}: {e_del}")
else:
status_messages.append("⚠️ ادغام ناموفق بود. فایل ZIP از قطعات ارائه می‌شود.")
success_zip, msg_zip, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
status_messages.append(msg_zip)
if success_zip: output_path_for_download = zip_p
elif len(generated_audio_files) == 1:
single_file_path = generated_audio_files[0]
output_audio_path_for_player = single_file_path
output_path_for_download = single_file_path
status_messages.append(f"🎵 فایل صوتی تکی: {os.path.basename(single_file_path)}")
elif len(generated_audio_files) > 1: # No merge or pydub not available
if not PYDUB_AVAILABLE and merge_checkbox:
status_messages.append("⚠️ pydub در دسترس نیست، امکان ادغام وجود ندارد. فایل ZIP ارائه می‌شود.")
status_messages.append("📦 چندین قطعه تولید شد. در حال ایجاد فایل ZIP...")
success_zip, msg_zip, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
status_messages.append(msg_zip)
if success_zip: output_path_for_download = zip_p
final_status = "\n".join(status_messages)
print(final_status)
print(f"DEBUG مسیر فایل برای پخش کننده: {output_audio_path_for_player}")
print(f"DEBUG مسیر فایل برای دانلود: {output_path_for_download}")
progress(1, desc="انجام شد!")
return output_audio_path_for_player, output_path_for_download, final_status
# --- تعریف رابط کاربری Gradio ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky), title="تبدیل متن به گفتار با Gemini") as demo:
gr.Markdown("# 🎵 تبدیل متن به گفتار با Gemini API 🗣️", elem_id="app-title")
if not HF_GEMINI_API_KEY:
gr.Warning(
"کلید API جمینای (GEMINI_API_KEY) در Hugging Face Secrets یافت نشد. "
"لطفاً آن را در بخش 'Settings' > 'Secrets' این Space با نام `GEMINI_API_KEY` اضافه کنید تا برنامه کار کند."
)
else:
gr.Info("کلید API جمینای با موفقیت از Secrets بارگذاری شد. آماده تولید صدا!")
gr.Markdown(
"این ابزار متن شما را با استفاده از API قدرتمند Gemini گوگل به گفتار تبدیل می‌کند. "
"برای استفاده، باید کلید API جمینای خود را در بخش Secrets این Space تنظیم کرده باشید."
"\n\nمی‌توانید کلید API خود را از [استودیوی هوش مصنوعی گوگل (Google AI Studio)](https://aistudio.google.com/app/apikey) دریافت کنید."
)
with gr.Row():
with gr.Column(scale=3): # ستون ورودی متن
gr.Markdown("### ۱. متن ورودی")
use_file = gr.Checkbox(label="📁 استفاده از فایل متنی (.txt) به جای ورود دستی", value=False)
text_file = gr.File(
label="آپلود فایل متنی",
file_types=['.txt'],
visible=False
)
text_to_speak = gr.Textbox(
label="📝 متنی که می‌خواهید به گفتار تبدیل شود:",
lines=10,
placeholder="متن خود را در اینجا وارد کنید یا فایل متنی را در بالا آپلود نمایید...",
visible=True,
text_align="right" # برای متون فارسی
)
use_file.change(
lambda x: (gr.update(visible=x), gr.update(visible=not x)),
[use_file],
[text_file, text_to_speak]
)
speech_prompt = gr.Textbox(
label="🗣️ فرمان سبک گفتار (اختیاری)",
placeholder="مثال: «با لحنی دوستانه و پرانرژی، مانند یک مجری پادکست صحبت کن»",
info="این فرمان به تنظیم سبک، احساسات و ویژگی‌های صدای خروجی کمک می‌کند.",
text_align="right"
)
with gr.Column(scale=2): # ستون تنظیمات
gr.Markdown("### ۲. تنظیمات تولید صدا")
# تبدیل دیکشنری نام‌های فارسی به لیست تاپل‌ها برای Dropdown
model_choices_farsi = [(MODEL_NAMES_FARSI[key], key) for key in MODELS]
model_name_dropdown = gr.Dropdown(
choices=model_choices_farsi, # نمایش نام فارسی، ارسال کلید انگلیسی
label="🤖 انتخاب مدل Gemini",
value=MODELS[0] # مقدار پیش‌فرض کلید انگلیسی
)
speaker_choices_farsi = [(SPEAKER_VOICES_FARSI_SAMPLE.get(v, v), v) for v in SPEAKER_VOICES]
speaker_voice_dropdown = gr.Dropdown(
choices=speaker_choices_farsi,
label="🎤 انتخاب گوینده",
value="Charon"
)
temperature_slider = gr.Slider(
minimum=0.0, maximum=1.0, step=0.05, value=0.7,
label="🌡️ دمای مدل (Temperature)",
info="میزان خلاقیت و تنوع در خروجی (0.0 تا 1.0). مقادیر بالاتر = تنوع بیشتر."
)
max_chunk_size_slider = gr.Slider(
minimum=1000, maximum=4000, step=100, value=3800,
label="🧩 حداکثر کاراکتر در هر قطعه",
info="متن برای ارسال به API به قطعات کوچکتر تقسیم می‌شود."
)
sleep_between_requests_slider = gr.Slider(
minimum=0, maximum=15, step=0.5, value=1, # کاهش مقدار پیش‌فرض برای سرعت بیشتر
label="⏱️ تاخیر بین درخواست‌ها (ثانیه)",
info="برای مدیریت محدودیت‌های API (مثلاً Gemini Flash دارای محدودیت ۶۰ درخواست در دقیقه است)."
)
output_filename_base_input = gr.Textbox(
label="💾 نام پایه فایل خروجی", value="gemini_tts_farsi"
)
with gr.Group(elem_id="merge-options"):
gr.Markdown("تنظیمات ادغام (در صورت تولید بیش از یک قطعه):")
merge_audio_checkbox = gr.Checkbox(label="🔗 ادغام قطعات صوتی", value=True, visible=PYDUB_AVAILABLE)
delete_partials_checkbox = gr.Checkbox(label="🗑️ حذف قطعات پس از ادغام", value=True, visible=PYDUB_AVAILABLE and True) # نمایش اگر ادغام فعال و pydub موجود باشد
if PYDUB_AVAILABLE:
merge_audio_checkbox.change(lambda x: gr.update(visible=x), [merge_audio_checkbox], [delete_partials_checkbox])
else:
gr.Markdown("<p style='color:orange; font-size:small;'>⚠️ قابلیت ادغام فایل‌ها به دلیل عدم دسترسی به کتابخانه `pydub` غیرفعال است.</p>")
submit_button = gr.Button("✨ تولید فایل صوتی ✨", variant="primary", elem_id="submit-button-main")
gr.Markdown("### ۳. خروجی")
with gr.Row():
with gr.Column(scale=1):
output_audio_player_component = gr.Audio(label="🎧 فایل صوتی تولید شده (قابل پخش)", type="filepath")
with gr.Column(scale=1):
output_file_download_component = gr.File(label="📥 دانلود فایل خروجی (صوتی یا ZIP)", type="filepath")
status_textbox_component = gr.Textbox(label="📊 گزارش وضعیت و پیام‌ها", lines=10, interactive=False, max_lines=20, text_align="right")
submit_button.click(
fn=generate_audio_for_gradio,
inputs=[
use_file, text_file, speech_prompt, text_to_speak,
max_chunk_size_slider, sleep_between_requests_slider, temperature_slider,
model_name_dropdown, speaker_voice_dropdown, output_filename_base_input,
merge_audio_checkbox, delete_partials_checkbox
],
outputs=[output_audio_player_component, output_file_download_component, status_textbox_component]
)
gr.Markdown("---")
# اطلاعات سازنده
encoded_text_creator = "Q3JlYXRlZCBieSA6IEhhbWVkNzQ0IChBSUdPTERFTikgZm9yIEh1Z2dpbmcgRmFjZSBTcGFjZXMu"
# "Created by : Hamed744 (AIGOLDEN) for Hugging Face Spaces."
try:
decoded_text_creator = base64.b64decode(encoded_text_creator.encode('utf-8')).decode('utf-8')
gr.Markdown(f"<p style='text-align:center; font-size:small; color:grey;'><em>{decoded_text_creator}</em></p>")
except Exception: pass
gr.Examples(
label="چند مثال برای شروع:",
examples=[
[False, None, "یک راوی مهربان و آموزنده.", "سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با جمینای در گریدیا است. امیدوارم خوب کار کند!", 3800, 1, 0.7, MODELS[0], "Charon", "hello_farsi", True, True],
[False, None, "یک گزارشگر خبری هیجان‌زده.", "خبر فوری! هوش مصنوعی اکنون می‌تواند گفتاری شبیه به انسان با وضوح باورنکردنی تولید کند. این فناوری به سرعت در حال پیشرفت است!", 3000, 1, 0.8, MODELS[1], "Achernar", "news_farsi", True, True],
],
fn=generate_audio_for_gradio,
inputs=[
use_file, text_file, speech_prompt, text_to_speak,
max_chunk_size_slider, sleep_between_requests_slider, temperature_slider,
model_name_dropdown, speaker_voice_dropdown, output_filename_base_input,
merge_audio_checkbox, delete_partials_checkbox
],
outputs=[output_audio_player_component, output_file_download_component, status_textbox_component],
cache_examples=False # چون با API کار می‌کند و ورودی‌ها داینامیک هستند
)
# اجرای برنامه در صورت اجرای مستقیم فایل (برای تست محلی)
if __name__ == "__main__":
if not PYDUB_AVAILABLE:
print("هشدار: کتابخانه pydub نصب نشده یا کار نمی‌کند. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
if not HF_GEMINI_API_KEY:
print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است. اگر برنامه برای کلید API به آن متکی باشد، ممکن است در حالت محلی کار نکند.")
demo.launch(debug=True, share=False) # share=False برای اجرای محلی، هاگینگ فیس لینک عمومی را مدیریت می‌کند