|
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 |
|
|
|
|
|
try: |
|
from pydub import AudioSegment |
|
PYDUB_AVAILABLE = True |
|
except ImportError: |
|
PYDUB_AVAILABLE = False |
|
|
|
|
|
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" |
|
] |
|
MODEL_NAMES = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"] |
|
|
|
|
|
|
|
def save_binary_file(file_name, data, log_messages_list): |
|
try: |
|
with open(file_name, "wb") as f: |
|
f.write(data) |
|
log_messages_list.append(f"✅ فایل در مسیر زیر ذخیره شد: {file_name}") |
|
return file_name |
|
except Exception as e: |
|
log_messages_list.append(f"❌ خطا در ذخیره فایل {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 |
|
parts = mime_type.split(";") |
|
for param in parts: |
|
param = param.strip() |
|
if param.lower().startswith("rate="): |
|
try: |
|
rate_str = param.split("=", 1)[1] |
|
rate = int(rate_str) |
|
except (ValueError, IndexError): |
|
pass |
|
elif param.startswith("audio/L"): |
|
try: |
|
bits_per_sample = int(param.split("L", 1)[1]) |
|
except (ValueError, IndexError): |
|
pass |
|
return {"bits_per_sample": bits_per_sample, "rate": rate} |
|
|
|
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: |
|
sentence_with_space = sentence + " " |
|
if len(current_chunk) + len(sentence_with_space) > max_size: |
|
if current_chunk: |
|
chunks.append(current_chunk.strip()) |
|
|
|
|
|
current_chunk = sentence |
|
while len(current_chunk) > max_size: |
|
|
|
|
|
split_idx = -1 |
|
|
|
possible_split_chars = ['،', ',', ';', ':', ' '] |
|
for char_idx in range(max_size - 1, max_size // 2, -1): |
|
if current_chunk[char_idx] in possible_split_chars: |
|
split_idx = char_idx + 1 |
|
break |
|
|
|
if split_idx != -1: |
|
chunks.append(current_chunk[:split_idx].strip()) |
|
current_chunk = current_chunk[split_idx:].strip() |
|
else: |
|
chunks.append(current_chunk[:max_size].strip()) |
|
current_chunk = current_chunk[max_size:].strip() |
|
else: |
|
current_chunk += (" " if current_chunk else "") + sentence |
|
|
|
if current_chunk: |
|
chunks.append(current_chunk.strip()) |
|
|
|
return [c for c in chunks if c] |
|
|
|
def merge_audio_files_func(file_paths, output_path, log_messages_list): |
|
if not PYDUB_AVAILABLE: |
|
log_messages_list.append("❌ pydub در دسترس نیست. نمیتوان فایلها را ادغام کرد.") |
|
return False |
|
try: |
|
log_messages_list.append(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...") |
|
combined = AudioSegment.empty() |
|
for i, file_path in enumerate(file_paths): |
|
if os.path.exists(file_path): |
|
log_messages_list.append(f"📎 اضافه کردن فایل {i+1}: {os.path.basename(file_path)}") |
|
audio = AudioSegment.from_file(file_path) |
|
combined += audio |
|
if i < len(file_paths) - 1: |
|
combined += AudioSegment.silent(duration=200) |
|
else: |
|
log_messages_list.append(f"⚠️ فایل پیدا نشد: {file_path}") |
|
combined.export(output_path, format="wav") |
|
log_messages_list.append(f"✅ فایل ادغام شده ذخیره شد: {output_path}") |
|
return True |
|
except Exception as e: |
|
log_messages_list.append(f"❌ خطا در ادغام فایلها: {e}") |
|
return False |
|
|
|
def create_zip_file(file_paths, zip_name, log_messages_list): |
|
try: |
|
with zipfile.ZipFile(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)) |
|
log_messages_list.append(f"📦 فایل ZIP ایجاد شد: {zip_name}") |
|
return True |
|
except Exception as e: |
|
log_messages_list.append(f"❌ خطا در ایجاد فایل ZIP: {e}") |
|
return False |
|
|
|
|
|
def core_generate_audio( |
|
text_input, prompt_input, selected_voice, output_base_name, |
|
model, temperature_val, |
|
max_chunk, sleep_time, merge_files, delete_partials, |
|
log_messages_list |
|
): |
|
log_messages_list.append("🚀 شروع فرآیند تبدیل متن به گفتار...") |
|
|
|
|
|
api_key = os.environ.get("GEMINI_API_KEY") |
|
if not api_key: |
|
log_messages_list.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) در Secrets این Space تنظیم نشده است.") |
|
log_messages_list.append("لطفاً به تنظیمات Space رفته و یک Secret با نام GEMINI_API_KEY و مقدار کلید خود ایجاد کنید.") |
|
return None, None |
|
|
|
|
|
try: |
|
log_messages_list.append("🛠️ در حال ایجاد کلاینت جمینای...") |
|
client = genai.Client(api_key=api_key) |
|
log_messages_list.append("✅ کلاینت جمینای با موفقیت ایجاد شد.") |
|
except Exception as e: |
|
log_messages_list.append(f"❌ خطا در ایجاد کلاینت جمینای: {e}") |
|
log_messages_list.append("لطفاً از صحت کلید API خود اطمینان حاصل کنید.") |
|
return None, None |
|
|
|
if not text_input or text_input.strip() == "": |
|
log_messages_list.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است.") |
|
return None, None |
|
|
|
text_chunks = smart_text_split(text_input, max_chunk) |
|
log_messages_list.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.") |
|
for i, chunk in enumerate(text_chunks): |
|
log_messages_list.append(f"📝 قطعه {i+1}: {len(chunk)} کاراکتر") |
|
text_chunks = [c for c in text_chunks if c] |
|
|
|
if not text_chunks: |
|
log_messages_list.append("❌ خطا: پس از تقسیمبندی، هیچ قطعه متنی برای پردازش وجود ندارد.") |
|
return None, None |
|
|
|
generated_files = [] |
|
|
|
|
|
|
|
for i, chunk in enumerate(text_chunks): |
|
log_messages_list.append(f"\n🔊 تولید صدا برای قطعه {i+1}/{len(text_chunks)}...") |
|
final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk |
|
|
|
contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])] |
|
generate_content_config = types.GenerateContentConfig( |
|
temperature=temperature_val, |
|
response_modalities=["audio"], |
|
speech_config=types.SpeechConfig( |
|
voice_config=types.VoiceConfig( |
|
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice) |
|
) |
|
), |
|
) |
|
|
|
current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}" |
|
|
|
try: |
|
response = client.models.generate_content( |
|
model=model, |
|
contents=contents, |
|
config=generate_content_config, |
|
) |
|
|
|
if (response.candidates and response.candidates[0].content and |
|
response.candidates[0].content.parts and |
|
response.candidates[0].content.parts[0].inline_data): |
|
|
|
inline_data = response.candidates[0].content.parts[0].inline_data |
|
data_buffer = inline_data.data |
|
|
|
file_extension = mimetypes.guess_extension(inline_data.mime_type) |
|
|
|
|
|
|
|
if file_extension is None or "binary" in inline_data.mime_type or file_extension == ".bin": |
|
file_extension = ".wav" |
|
if "audio/L" in inline_data.mime_type: |
|
data_buffer = convert_to_wav(data_buffer, inline_data.mime_type) |
|
elif inline_data.mime_type == "audio/mpeg": |
|
file_extension = ".mp3" |
|
elif inline_data.mime_type == "audio/wav": |
|
file_extension = ".wav" |
|
|
|
generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_messages_list) |
|
if generated_file_path: |
|
generated_files.append(generated_file_path) |
|
log_messages_list.append(f"✅ قطعه {i+1} تولید شد: {os.path.basename(generated_file_path)}") |
|
|
|
elif response.text: |
|
log_messages_list.append(f"ℹ️ پیام متنی از API برای قطعه {i+1}: {response.text}") |
|
if "rate limit" in response.text.lower() or "quota" in response.text.lower(): |
|
log_messages_list.append(f"⏳ به نظر میرسد به محدودیت تعداد درخواست API (Quota) رسیدهاید. لطفاً چند دقیقه صبر کنید و دوباره امتحان کنید، یا فاصله زمانی بین درخواستها را افزایش دهید.") |
|
|
|
else: |
|
log_messages_list.append(f"⚠️ پاسخ API برای قطعه {i+1} حاوی داده صوتی یا پیام متنی نبود. جزئیات پاسخ: {response.prompt_feedback if response else 'No response'}") |
|
|
|
|
|
except types.generation_types.BlockedPromptException as bpe: |
|
log_messages_list.append(f"❌ محتوای پرامپت برای قطعه {i+1} مسدود شد: {bpe}") |
|
log_messages_list.append(f"علت مسدود شدن: {bpe.response.prompt_feedback if bpe.response else 'نامشخص'}") |
|
log_messages_list.append("لطفاً متن ورودی یا پرامپت سبک گفتار را بررسی و اصلاح کنید.") |
|
continue |
|
except types.generation_types.StopCandidateException as sce: |
|
log_messages_list.append(f"❌ تولید محتوا برای قطعه {i+1} به دلیل نامشخصی متوقف شد: {sce}") |
|
continue |
|
except Exception as e: |
|
log_messages_list.append(f"❌ خطا در تولید قطعه {i+1}: {e}") |
|
if "API key not valid" in str(e): |
|
log_messages_list.append("خطای کلید API. لطفاً از معتبر بودن کلید و تنظیم صحیح آن در Secrets مطمئن شوید.") |
|
elif "resource has been exhausted" in str(e).lower() or "quota" in str(e).lower(): |
|
log_messages_list.append("به نظر میرسد محدودیت استفاده از API (Quota) شما تمام شده است.") |
|
continue |
|
|
|
if i < len(text_chunks) - 1 and len(text_chunks) > 1 : |
|
log_messages_list.append(f"⏱️ انتظار {sleep_time} ثانیه...") |
|
time.sleep(sleep_time) |
|
|
|
if not generated_files: |
|
log_messages_list.append("❌ هیچ فایل صوتی تولید نشد!") |
|
return None, None |
|
|
|
log_messages_list.append(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!") |
|
|
|
playback_file = None |
|
download_file = None |
|
|
|
if merge_files and len(generated_files) > 1: |
|
if not PYDUB_AVAILABLE: |
|
log_messages_list.append("⚠️ pydub برای ادغام در دسترس نیست. فایلها به صورت جداگانه در یک فایل ZIP ارائه میشوند.") |
|
zip_filename = f"{output_base_name}_all_parts.zip" |
|
if create_zip_file(generated_files, zip_filename, log_messages_list): |
|
download_file = zip_filename |
|
if generated_files: playback_file = generated_files[0] |
|
else: |
|
merged_filename = f"{output_base_name}_merged.wav" |
|
if merge_audio_files_func(generated_files, merged_filename, log_messages_list): |
|
playback_file = merged_filename |
|
download_file = merged_filename |
|
log_messages_list.append(f"🎵 فایل نهایی ادغام شده: {os.path.basename(merged_filename)}") |
|
|
|
if delete_partials: |
|
for file_path in generated_files: |
|
try: |
|
if os.path.abspath(file_path) != os.path.abspath(merged_filename): |
|
os.remove(file_path) |
|
log_messages_list.append(f"🗑️ فایل جزئی حذف شد: {os.path.basename(file_path)}") |
|
except Exception as e: |
|
log_messages_list.append(f"⚠️ خطا در حذف فایل جزئی {os.path.basename(file_path)}: {e}") |
|
else: |
|
log_messages_list.append("⚠️ ادغام ممکن نبود. فایلها به صورت جداگانه در یک فایل ZIP ارائه میشوند.") |
|
zip_filename = f"{output_base_name}_all_parts.zip" |
|
if create_zip_file(generated_files, zip_filename, log_messages_list): |
|
download_file = zip_filename |
|
if generated_files: playback_file = generated_files[0] |
|
|
|
elif len(generated_files) == 1: |
|
playback_file = generated_files[0] |
|
download_file = generated_files[0] |
|
|
|
else: |
|
zip_filename = f"{output_base_name}_all_parts.zip" |
|
if create_zip_file(generated_files, zip_filename, log_messages_list): |
|
download_file = zip_filename |
|
if generated_files: playback_file = generated_files[0] |
|
|
|
if playback_file and not os.path.exists(playback_file): |
|
log_messages_list.append(f"⚠️ فایل پخش {os.path.basename(playback_file)} وجود ندارد!") |
|
playback_file = None |
|
if download_file and not os.path.exists(download_file): |
|
log_messages_list.append(f"⚠️ فایل دانلود {os.path.basename(download_file)} وجود ندارد!") |
|
download_file = None |
|
|
|
return playback_file, download_file |
|
|
|
|
|
def gradio_tts_interface( |
|
use_file_input, uploaded_file, text_to_speak, |
|
speech_prompt, speaker_voice, output_filename_base_in, |
|
model_name, temperature, |
|
max_chunk_size, sleep_between_requests, |
|
merge_audio_files_flag, delete_partial_files_flag, |
|
progress=gr.Progress(track_tqdm=True) |
|
): |
|
log_messages = [] |
|
|
|
actual_text_input = "" |
|
if use_file_input: |
|
if uploaded_file is not None: |
|
try: |
|
|
|
with open(uploaded_file.name, 'r', encoding='utf-8') as f: |
|
actual_text_input = f.read().strip() |
|
log_messages.append(f"✅ متن از فایل '{os.path.basename(uploaded_file.name)}' بارگذاری شد: {len(actual_text_input)} کاراکتر.") |
|
log_messages.append(f"📝 نمونه متن فایل: '{actual_text_input[:100]}{'...' if len(actual_text_input) > 100 else ''}'") |
|
if not actual_text_input: |
|
log_messages.append("❌ خطا: فایل آپلود شده خالی است یا قابل خواندن نیست.") |
|
return None, None, "\n".join(log_messages) |
|
except Exception as e: |
|
log_messages.append(f"❌ خطا در خواندن فایل آپلود شده: {e}") |
|
return None, None, "\n".join(log_messages) |
|
else: |
|
log_messages.append("❌ خطا: گزینه 'استفاده از فایل ورودی' انتخاب شده اما هیچ فایلی آپلود نشده است.") |
|
return None, None, "\n".join(log_messages) |
|
else: |
|
actual_text_input = text_to_speak |
|
if not actual_text_input or not actual_text_input.strip(): |
|
log_messages.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است. لطفاً متنی را وارد کنید یا گزینه فایل را فعال کنید.") |
|
return None, None, "\n".join(log_messages) |
|
log_messages.append(f"📖 متن ورودی دستی: {len(actual_text_input)} کاراکتر") |
|
log_messages.append(f"📝 نمونه متن ورودی: '{actual_text_input[:100]}{'...' if len(actual_text_input) > 100 else ''}'") |
|
|
|
|
|
output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "gemini_tts_output") |
|
if not output_filename_base: |
|
output_filename_base = "gemini_tts_output" |
|
log_messages.append(f"🏷️ نام پایه فایل خروجی: {output_filename_base}") |
|
|
|
|
|
if not PYDUB_AVAILABLE: |
|
log_messages.append("⚠️ کتابخانه pydub در دسترس نیست. امکان ادغام فایلهای صوتی وجود نخواهد داشت و فایلهای صوتی به صورت جداگانه (در صورت وجود چند بخش) در یک فایل ZIP ارائه میشوند.") |
|
current_merge_audio_files = False |
|
else: |
|
current_merge_audio_files = merge_audio_files_flag |
|
|
|
|
|
playback_path, download_path = core_generate_audio( |
|
actual_text_input, speech_prompt, speaker_voice, output_filename_base, |
|
model_name, temperature, max_chunk_size, sleep_between_requests, |
|
current_merge_audio_files, delete_partial_files_flag, log_messages |
|
) |
|
|
|
log_output_str = "\n".join(log_messages) |
|
|
|
valid_playback_path = playback_path if playback_path and os.path.exists(playback_path) else None |
|
valid_download_path = download_path if download_path and os.path.exists(download_path) else None |
|
|
|
if not valid_playback_path and not valid_download_path and not actual_text_input.strip(): |
|
pass |
|
elif not valid_playback_path and not valid_download_path and actual_text_input.strip(): |
|
|
|
log_output_str += "\n🛑 هیچ فایل صوتی برای پخش یا دانلود در دسترس نیست." |
|
|
|
return valid_playback_path, valid_download_path, log_output_str |
|
|
|
|
|
css = """ |
|
body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; } |
|
.gradio-container { max-width: 95% !important; margin: auto !important; padding: 10px !important; } |
|
@media (min-width: 768px) { .gradio-container { max-width: 800px !important; } } |
|
footer { display: none !important; } |
|
.gr-button { background-color: #1d67a3 !important; color: white !important; border-radius: 8px !important; } |
|
.gr-button:hover { background-color: #164f7e !important; } |
|
.gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius: 6px !important; } |
|
.gr-panel { padding: 15px !important; border-radius: 8px !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; } |
|
h2, h3 { color: #1d67a3; text-align: center; } |
|
label { font-weight: bold; color: #333; } |
|
#output_audio .gallery, #download_file_output .gallery { display: none !important; } |
|
/* Ensure text inputs and textareas are also LTR for code/API keys if needed, but general UI is RTL */ |
|
textarea, input[type="text"] { direction: rtl; text-align: right; } |
|
/* Override for specific LTR elements if any, e.g. API key input if it were visible */ |
|
""" |
|
|
|
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo: |
|
gr.Markdown("## 🔊 تبدیل متن به گفتار با Gemini API (فارسی)") |
|
gr.Markdown("<p style='text-align:center;'>ساخته شده بر اساس کد کولب توسط: aigolden</p>") |
|
gr.HTML("<hr>") |
|
with gr.Accordion("⚠️ راهنمای مهم: تنظیم کلید API جمینای", open=False): |
|
gr.Markdown( |
|
"**برای استفاده از این ابزار، ابتدا باید کلید API جمینای خود را در بخش Secrets این Space در Hugging Face اضافه کنید:**\n" |
|
"1. به صفحه اصلی این Space بروید (جایی که این اپلیکیشن را میبینید).\n" |
|
"2. در بالای صفحه، روی نام Space خود و سپس 'Settings' (آیکن چرخدنده ⚙️) کلیک کنید.\n" |
|
"3. در منوی سمت چپ صفحه تنظیمات، به بخش 'Secrets' بروید.\n" |
|
"4. روی دکمه '+ New secret' کلیک کنید.\n" |
|
"5. در فیلد 'Name'، دقیقاً عبارت `GEMINI_API_KEY` را وارد کنید (با حروف بزرگ).\n" |
|
"6. در فیلد 'Value (secret)'، کلید API جمینای خود را که از Google AI Studio یا Google Cloud Console دریافت کردهاید، وارد کنید.\n" |
|
"7. روی 'Save secret' کلیک کنید.\n" |
|
"**توجه:** پس از افزودن یا تغییر Secret، ممکن است لازم باشد Space را یکبار Restart کنید. برای این کار، از منوی سهنقطه (⋮) در کنار دکمه 'Settings' در صفحه اصلی Space، گزینه 'Restart this Space' را انتخاب کنید." |
|
) |
|
gr.HTML("<hr>") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=3, min_width=300): |
|
gr.Markdown("### 📝 تنظیمات ورودی و پرامپت") |
|
use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی ورودی (.txt)", value=False, elem_id="use_file_cb") |
|
|
|
|
|
uploaded_file_input = gr.File( |
|
label="📂 آپلود فایل متنی (فقط شامل متن اصلی، UTF-8)", |
|
file_types=['.txt'], |
|
visible=False |
|
) |
|
text_to_speak_tb = gr.Textbox( |
|
label="⌨️ متن ورودی (اگر گزینه فایل فعال نیست)", |
|
placeholder="متن مورد نظر برای تبدیل به گفتار را اینجا وارد کنید...", |
|
lines=8, |
|
value="سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با استفاده از مدل جمینای است.", |
|
elem_id="text_input_main", |
|
visible=True |
|
) |
|
|
|
|
|
use_file_input_cb.change( |
|
fn=lambda x: (gr.update(visible=x), gr.update(visible=not x)), |
|
inputs=use_file_input_cb, |
|
outputs=[uploaded_file_input, text_to_speak_tb] |
|
) |
|
|
|
speech_prompt_tb = gr.Textbox( |
|
label="🗣️ پرامپت برای تنظیم سبک گفتار (اختیاری)", |
|
placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای صحبت کن", |
|
value="به زبان یک گوینده رادیو با صدای گرم و واضح صحبت کن.", |
|
lines=2 |
|
) |
|
|
|
with gr.Column(scale=2, min_width=250): |
|
gr.Markdown("### ⚙️ تنظیمات مدل و خروجی") |
|
model_name_dd = gr.Dropdown( |
|
MODEL_NAMES, label="🤖 انتخاب مدل Gemini TTS", value="gemini-2.5-flash-preview-tts" |
|
) |
|
speaker_voice_dd = gr.Dropdown( |
|
SPEAKER_VOICES, label="🎤 انتخاب گوینده", value="Charon" |
|
) |
|
temperature_slider = gr.Slider( |
|
minimum=0.0, maximum=2.0, step=0.05, value=0.9, label="🌡️ دمای مدل (تنوع خروجی)" |
|
) |
|
output_filename_base_tb = gr.Textbox( |
|
label="📛 نام پایه فایل خروجی (بدون پسوند)", value="gemini_tts_farsi_output" |
|
) |
|
|
|
gr.Markdown("#### تنظیمات پیشرفته") |
|
max_chunk_size_slider = gr.Slider( |
|
minimum=1500, maximum=4000, step=100, value=3800, label="📏 حداکثر کاراکتر در هر قطعه" |
|
) |
|
sleep_between_requests_slider = gr.Slider( |
|
minimum=3, maximum=25, step=0.5, value=12, label="⏱️ فاصله بین درخواستها (ثانیه)" |
|
) |
|
merge_audio_files_cb = gr.Checkbox(label="🔗 ادغام فایلهای صوتی در یک فایل WAV (نیازمند pydub)", value=True) |
|
delete_partial_files_cb = gr.Checkbox(label="🗑️ حذف فایلهای جزئی پس از ادغام (اگر ادغام فعال باشد)", value=False) |
|
|
|
|
|
generate_button = gr.Button("🎙️ تولید صدا", variant="primary", elem_id="generate_button_main") |
|
gr.HTML("<hr>") |
|
|
|
gr.Markdown("### 🎧 خروجی صوتی و دانلود 📥") |
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
output_audio = gr.Audio(label="🔊 فایل صوتی تولید شده (قابل پخش)", type="filepath", elem_id="output_audio_player") |
|
with gr.Column(scale=1): |
|
download_file_output = gr.File(label="💾 دانلود فایل نهایی (WAV یا ZIP)", elem_id="download_file_link") |
|
|
|
gr.Markdown("### 📜 لاگها و پیامهای فرآیند") |
|
logs_output_tb = gr.Textbox(label=" ", lines=10, interactive=False, autoscroll=True, elem_id="logs_textbox") |
|
|
|
generate_button.click( |
|
fn=gradio_tts_interface, |
|
inputs=[ |
|
use_file_input_cb, uploaded_file_input, text_to_speak_tb, |
|
speech_prompt_tb, speaker_voice_dd, output_filename_base_tb, |
|
model_name_dd, temperature_slider, |
|
max_chunk_size_slider, sleep_between_requests_slider, |
|
merge_audio_files_cb, delete_partial_files_cb |
|
], |
|
outputs=[output_audio, download_file_output, logs_output_tb] |
|
) |
|
|
|
gr.Examples( |
|
examples=[ |
|
[False, None, "سلام، این یک تست کوتاه است.", "یک صدای دوستانه و واضح.", "Charon", "test_output_1", "gemini-2.5-flash-preview-tts", 0.9, 3800, 12, True, False], |
|
[False, None, "به دنیای شگفتانگیز هوش مصنوعی خوش آمدید. امیدوارم از این ابزار لذت ببرید و برایتان مفید باشد.", "با هیجان و انرژی زیاد صحبت کن، انگار که یک خبر فوقالعاده را اعلام میکنی.", "Zephyr", "ai_voice_farsi", "gemini-2.5-flash-preview-tts", 1.1, 3500, 10, True, True], |
|
[False, None, "این یک نمونه متن طولانیتر است که برای آزمایش تقسیمبندی هوشمند به کار میرود. باید دید که چگونه به قطعات کوچکتر تقسیم شده و سپس در صورت انتخاب گزینه ادغام، به یک فایل صوتی واحد تبدیل میشود. امیدواریم که همه چیز به خوبی کار کند.", "با لحنی آرام و روایی، مانند یک داستانگو.", "Achird", "long_text_sample", "gemini-2.5-pro-preview-tts", 0.8, 2500, 15, True, True], |
|
], |
|
inputs=[ |
|
use_file_input_cb, uploaded_file_input, text_to_speak_tb, |
|
speech_prompt_tb, speaker_voice_dd, output_filename_base_tb, |
|
model_name_dd, temperature_slider, |
|
max_chunk_size_slider, sleep_between_requests_slider, |
|
merge_audio_files_cb, delete_partial_files_cb |
|
], |
|
outputs=[output_audio, download_file_output, logs_output_tb], |
|
fn=gradio_tts_interface, |
|
cache_examples=False |
|
) |
|
|
|
gr.Markdown( |
|
"<div style='text-align: center; margin-top: 30px; font-size: 0.9em; color: grey;'>" |
|
"این ابزار از Google Gemini API برای تبدیل متن به گفتار استفاده میکند. " |
|
"لطفاً به محدودیتهای استفاده و شرایط خدمات Gemini API توجه فرمایید.<br>" |
|
"برای بهترین نتیجه، از مرورگرهای بهروز استفاده کنید." |
|
"</div>" |
|
) |
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
|
|
demo.launch() |