|
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 |
|
|
|
|
|
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") |
|
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: |
|
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 |
|
|
|
|
|
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("### ۲. تنظیمات تولید صدا") |
|
|
|
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) |
|
|
|
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" |
|
|
|
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 |
|
) |
|
|
|
|
|
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) |