Update app.py
Browse files
app.py
CHANGED
@@ -9,13 +9,14 @@ import zipfile
|
|
9 |
from google import genai
|
10 |
from google.genai import types
|
11 |
|
|
|
12 |
try:
|
13 |
from pydub import AudioSegment
|
14 |
PYDUB_AVAILABLE = True
|
15 |
except ImportError:
|
16 |
PYDUB_AVAILABLE = False
|
17 |
|
18 |
-
# ---
|
19 |
SPEAKER_VOICES = [
|
20 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
21 |
"Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
|
@@ -25,7 +26,7 @@ SPEAKER_VOICES = [
|
|
25 |
]
|
26 |
MODEL_NAMES = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
|
27 |
|
28 |
-
# ---
|
29 |
|
30 |
def save_binary_file(file_name, data, log_messages_list):
|
31 |
try:
|
@@ -57,7 +58,7 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
|
57 |
|
58 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
59 |
bits_per_sample = 16
|
60 |
-
rate = 24000
|
61 |
parts = mime_type.split(";")
|
62 |
for param in parts:
|
63 |
param = param.strip()
|
@@ -67,7 +68,7 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
|
67 |
rate = int(rate_str)
|
68 |
except (ValueError, IndexError):
|
69 |
pass
|
70 |
-
elif param.startswith("audio/L"):
|
71 |
try:
|
72 |
bits_per_sample = int(param.split("L", 1)[1])
|
73 |
except (ValueError, IndexError):
|
@@ -79,31 +80,40 @@ def smart_text_split(text, max_size=3800):
|
|
79 |
return [text]
|
80 |
chunks = []
|
81 |
current_chunk = ""
|
82 |
-
sentences
|
|
|
|
|
83 |
for sentence in sentences:
|
84 |
-
|
85 |
-
|
|
|
86 |
chunks.append(current_chunk.strip())
|
|
|
|
|
87 |
current_chunk = sentence
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
|
|
|
|
103 |
else:
|
104 |
current_chunk += (" " if current_chunk else "") + sentence
|
105 |
-
|
|
|
106 |
chunks.append(current_chunk.strip())
|
|
|
107 |
return [c for c in chunks if c] # Ensure no empty chunks
|
108 |
|
109 |
def merge_audio_files_func(file_paths, output_path, log_messages_list):
|
@@ -115,11 +125,11 @@ def merge_audio_files_func(file_paths, output_path, log_messages_list):
|
|
115 |
combined = AudioSegment.empty()
|
116 |
for i, file_path in enumerate(file_paths):
|
117 |
if os.path.exists(file_path):
|
118 |
-
log_messages_list.append(f"📎 اضافه کردن فایل {i+1}: {file_path}")
|
119 |
-
audio = AudioSegment.from_file(file_path)
|
120 |
combined += audio
|
121 |
if i < len(file_paths) - 1: # Add short silence between segments
|
122 |
-
combined += AudioSegment.silent(duration=
|
123 |
else:
|
124 |
log_messages_list.append(f"⚠️ فایل پیدا نشد: {file_path}")
|
125 |
combined.export(output_path, format="wav")
|
@@ -141,7 +151,7 @@ def create_zip_file(file_paths, zip_name, log_messages_list):
|
|
141 |
log_messages_list.append(f"❌ خطا در ایجاد فایل ZIP: {e}")
|
142 |
return False
|
143 |
|
144 |
-
# ---
|
145 |
def core_generate_audio(
|
146 |
text_input, prompt_input, selected_voice, output_base_name,
|
147 |
model, temperature_val,
|
@@ -150,46 +160,40 @@ def core_generate_audio(
|
|
150 |
):
|
151 |
log_messages_list.append("🚀 شروع فرآیند تبدیل متن به گفتار...")
|
152 |
|
153 |
-
#
|
154 |
api_key = os.environ.get("GEMINI_API_KEY")
|
155 |
if not api_key:
|
156 |
log_messages_list.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) در Secrets این Space تنظیم نشده است.")
|
157 |
log_messages_list.append("لطفاً به تنظیمات Space رفته و یک Secret با نام GEMINI_API_KEY و مقدار کلید خود ایجاد کنید.")
|
158 |
return None, None # No audio path, no download path
|
159 |
|
160 |
-
#
|
161 |
try:
|
162 |
log_messages_list.append("🛠️ در حال ایجاد کلاینت جمینای...")
|
163 |
-
|
164 |
-
client = genai.Client(api_key=api_key) # Pass api_key directly
|
165 |
log_messages_list.append("✅ کلاینت جمینای با موفقیت ایجاد شد.")
|
166 |
except Exception as e:
|
167 |
log_messages_list.append(f"❌ خطا در ایجاد کلاینت جمینای: {e}")
|
168 |
log_messages_list.append("لطفاً از صحت کلید API خود اطمینان حاصل کنید.")
|
169 |
return None, None
|
170 |
|
171 |
-
# Validate Text Input (already done in wrapper, but good to double check)
|
172 |
if not text_input or text_input.strip() == "":
|
173 |
log_messages_list.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است.")
|
174 |
return None, None
|
175 |
|
176 |
-
# Split text into chunks
|
177 |
text_chunks = smart_text_split(text_input, max_chunk)
|
178 |
log_messages_list.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
|
179 |
for i, chunk in enumerate(text_chunks):
|
180 |
log_messages_list.append(f"📝 قطعه {i+1}: {len(chunk)} کاراکتر")
|
181 |
-
|
182 |
-
log_messages_list.append(f"⚠️ هشدار: قطعه {i+1} خالی است و نادیده گرفته میشود.")
|
183 |
-
text_chunks = [c for c in text_chunks if c] # Filter out empty chunks again
|
184 |
|
185 |
if not text_chunks:
|
186 |
log_messages_list.append("❌ خطا: پس از تقسیمبندی، هیچ قطعه متنی برای پردازش وجود ندارد.")
|
187 |
return None, None
|
188 |
|
189 |
generated_files = []
|
190 |
-
#
|
191 |
-
#
|
192 |
-
# os.makedirs(output_dir, exist_ok=True)
|
193 |
|
194 |
for i, chunk in enumerate(text_chunks):
|
195 |
log_messages_list.append(f"\n🔊 تولید صدا برای قطعه {i+1}/{len(text_chunks)}...")
|
@@ -207,12 +211,9 @@ def core_generate_audio(
|
|
207 |
)
|
208 |
|
209 |
current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
|
210 |
-
# current_chunk_filename_base = os.path.join(output_dir, f"{output_base_name}_part{i+1:03d}")
|
211 |
-
|
212 |
|
213 |
try:
|
214 |
-
|
215 |
-
response = client.models.generate_content(
|
216 |
model=model,
|
217 |
contents=contents,
|
218 |
config=generate_content_config,
|
@@ -224,50 +225,51 @@ def core_generate_audio(
|
|
224 |
|
225 |
inline_data = response.candidates[0].content.parts[0].inline_data
|
226 |
data_buffer = inline_data.data
|
|
|
227 |
file_extension = mimetypes.guess_extension(inline_data.mime_type)
|
228 |
|
229 |
-
|
|
|
|
|
230 |
file_extension = ".wav"
|
231 |
-
|
232 |
-
# but if it's audio/L16; rate=24000, convert_to_wav is needed
|
233 |
-
if "audio/L" in inline_data.mime_type: # Needs WAV header
|
234 |
data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
|
240 |
generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_messages_list)
|
241 |
if generated_file_path:
|
242 |
generated_files.append(generated_file_path)
|
243 |
-
log_messages_list.append(f"✅ قطعه {i+1} تولید شد: {generated_file_path}")
|
244 |
|
245 |
-
elif response.text:
|
246 |
log_messages_list.append(f"ℹ️ پیام متنی از API برای قطعه {i+1}: {response.text}")
|
247 |
-
if "rate limit" in response.text.lower():
|
248 |
-
log_messages_list.append(f"⏳ به نظر میرسد به محدودیت تعداد درخواست API رسیدهاید. لطفاً چند دقیقه صبر کنید و دوباره امتحان کنید، یا فاصله زمانی بین درخواستها را افزایش دهید.")
|
249 |
|
250 |
-
else:
|
251 |
-
log_messages_list.append(f"⚠️ پاسخ API برای قطعه {i+1} حاوی داده صوتی یا پیام متنی نبود.")
|
252 |
|
253 |
|
254 |
except types.generation_types.BlockedPromptException as bpe:
|
255 |
log_messages_list.append(f"❌ محتوای پرامپت برای قطعه {i+1} مسدود شد: {bpe}")
|
|
|
256 |
log_messages_list.append("لطفاً متن ورودی یا پرامپت سبک گفتار را بررسی و اصلاح کنید.")
|
257 |
-
continue
|
258 |
except types.generation_types.StopCandidateException as sce:
|
259 |
log_messages_list.append(f"❌ تولید محتوا برای قطعه {i+1} به دلیل نامشخصی متوقف شد: {sce}")
|
260 |
continue
|
261 |
except Exception as e:
|
262 |
log_messages_list.append(f"❌ خطا در تولید قطعه {i+1}: {e}")
|
263 |
-
# Specific check for common API errors
|
264 |
if "API key not valid" in str(e):
|
265 |
log_messages_list.append("خطای کلید API. لطفاً از معتبر بودن کلید و تنظیم صحیح آن در Secrets مطمئن شوید.")
|
266 |
elif "resource has been exhausted" in str(e).lower() or "quota" in str(e).lower():
|
267 |
log_messages_list.append("به نظر میرسد محدودیت استفاده از API (Quota) شما تمام شده است.")
|
268 |
-
continue
|
269 |
|
270 |
-
if i < len(text_chunks) - 1 and len(text_chunks) > 1 :
|
271 |
log_messages_list.append(f"⏱️ انتظار {sleep_time} ثانیه...")
|
272 |
time.sleep(sleep_time)
|
273 |
|
@@ -282,79 +284,74 @@ def core_generate_audio(
|
|
282 |
|
283 |
if merge_files and len(generated_files) > 1:
|
284 |
if not PYDUB_AVAILABLE:
|
285 |
-
log_messages_list.append("⚠️ pydub برای ادغام در دسترس نیست. فایلها به صورت جداگانه ارائه میشوند.")
|
286 |
-
# Offer zip of parts if pydub not available for merging
|
287 |
zip_filename = f"{output_base_name}_all_parts.zip"
|
288 |
if create_zip_file(generated_files, zip_filename, log_messages_list):
|
289 |
download_file = zip_filename
|
290 |
-
playback_file = generated_files[0]
|
291 |
else:
|
292 |
merged_filename = f"{output_base_name}_merged.wav"
|
293 |
-
# merged_filename = os.path.join(output_dir, f"{output_base_name}_merged.wav")
|
294 |
if merge_audio_files_func(generated_files, merged_filename, log_messages_list):
|
295 |
playback_file = merged_filename
|
296 |
download_file = merged_filename
|
297 |
-
log_messages_list.append(f"🎵 فایل نهایی ادغام شده: {merged_filename}")
|
298 |
|
299 |
if delete_partials:
|
300 |
for file_path in generated_files:
|
301 |
try:
|
302 |
-
if file_path != merged_filename:
|
303 |
os.remove(file_path)
|
304 |
-
log_messages_list.append(f"🗑️ فایل جزئی حذف شد: {file_path}")
|
305 |
except Exception as e:
|
306 |
-
log_messages_list.append(f"⚠️ خطا در حذف فایل جزئی {file_path}: {e}")
|
307 |
else:
|
308 |
-
log_messages_list.append("⚠️ ادغام ممکن نبود.
|
309 |
-
# Fallback to zip if merging failed
|
310 |
zip_filename = f"{output_base_name}_all_parts.zip"
|
311 |
-
# zip_filename = os.path.join(output_dir, f"{output_base_name}_all_parts.zip")
|
312 |
if create_zip_file(generated_files, zip_filename, log_messages_list):
|
313 |
download_file = zip_filename
|
314 |
-
playback_file = generated_files[0]
|
315 |
|
316 |
elif len(generated_files) == 1:
|
317 |
playback_file = generated_files[0]
|
318 |
download_file = generated_files[0]
|
319 |
|
320 |
-
else: # Multiple files, no merge requested
|
321 |
zip_filename = f"{output_base_name}_all_parts.zip"
|
322 |
-
# zip_filename = os.path.join(output_dir, f"{output_base_name}_all_parts.zip")
|
323 |
if create_zip_file(generated_files, zip_filename, log_messages_list):
|
324 |
download_file = zip_filename
|
325 |
-
playback_file = generated_files[0]
|
326 |
|
327 |
if playback_file and not os.path.exists(playback_file):
|
328 |
-
log_messages_list.append(f"⚠️ فایل پخش {playback_file} وجود ندارد!")
|
329 |
playback_file = None
|
330 |
if download_file and not os.path.exists(download_file):
|
331 |
-
log_messages_list.append(f"⚠️ فایل دانلود {download_file} وجود ندارد!")
|
332 |
download_file = None
|
333 |
|
334 |
return playback_file, download_file
|
335 |
|
336 |
-
# ---
|
337 |
def gradio_tts_interface(
|
338 |
use_file_input, uploaded_file, text_to_speak,
|
339 |
-
speech_prompt, speaker_voice,
|
340 |
model_name, temperature,
|
341 |
max_chunk_size, sleep_between_requests,
|
342 |
-
|
343 |
-
progress=gr.Progress(track_tqdm=True)
|
344 |
):
|
345 |
-
log_messages = []
|
346 |
|
347 |
-
# Determine actual text input
|
348 |
actual_text_input = ""
|
349 |
if use_file_input:
|
350 |
if uploaded_file is not None:
|
351 |
try:
|
|
|
352 |
with open(uploaded_file.name, 'r', encoding='utf-8') as f:
|
353 |
actual_text_input = f.read().strip()
|
354 |
log_messages.append(f"✅ متن از فایل '{os.path.basename(uploaded_file.name)}' بارگذاری شد: {len(actual_text_input)} کاراکتر.")
|
355 |
log_messages.append(f"📝 نمونه متن فایل: '{actual_text_input[:100]}{'...' if len(actual_text_input) > 100 else ''}'")
|
356 |
if not actual_text_input:
|
357 |
-
log_messages.append("❌ خطا: فایل آپلود شده خالی
|
358 |
return None, None, "\n".join(log_messages)
|
359 |
except Exception as e:
|
360 |
log_messages.append(f"❌ خطا در خواندن فایل آپلود شده: {e}")
|
@@ -370,130 +367,148 @@ def gradio_tts_interface(
|
|
370 |
log_messages.append(f"📖 متن ورودی دستی: {len(actual_text_input)} کاراکتر")
|
371 |
log_messages.append(f"📝 نمونه متن ورودی: '{actual_text_input[:100]}{'...' if len(actual_text_input) > 100 else ''}'")
|
372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
|
374 |
if not PYDUB_AVAILABLE:
|
375 |
log_messages.append("⚠️ کتابخانه pydub در دسترس نیست. امکان ادغام فایلهای صوتی وجود نخواهد داشت و فایلهای صوتی به صورت جداگانه (در صورت وجود چند بخش) در یک فایل ZIP ارائه میشوند.")
|
376 |
-
|
|
|
|
|
377 |
|
378 |
|
379 |
-
# Call the core generation logic
|
380 |
playback_path, download_path = core_generate_audio(
|
381 |
-
actual_text_input,
|
382 |
-
|
383 |
-
|
384 |
-
output_filename_base if output_filename_base else "gemini_tts_output",
|
385 |
-
model_name,
|
386 |
-
temperature,
|
387 |
-
max_chunk_size,
|
388 |
-
sleep_between_requests,
|
389 |
-
merge_audio_files,
|
390 |
-
delete_partial_files,
|
391 |
-
log_messages # Pass the list
|
392 |
)
|
393 |
|
394 |
-
|
395 |
|
396 |
-
# Ensure paths are valid before returning
|
397 |
valid_playback_path = playback_path if playback_path and os.path.exists(playback_path) else None
|
398 |
valid_download_path = download_path if download_path and os.path.exists(download_path) else None
|
399 |
|
400 |
-
if not valid_playback_path and not valid_download_path and not actual_text_input:
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
|
|
|
406 |
|
407 |
-
|
408 |
-
|
409 |
-
# --- Gradio UI Definition ---
|
410 |
css = """
|
411 |
-
body { font-family: 'Arial', sans-serif; }
|
412 |
-
.gradio-container { max-width:
|
|
|
413 |
footer { display: none !important; }
|
414 |
-
.gr-button { background-color: #
|
415 |
-
.gr-button:hover { background-color: #
|
416 |
-
|
417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
418 |
"""
|
419 |
|
420 |
-
with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
|
421 |
-
gr.Markdown("## 🔊 تبدیل متن به گفتار با Gemini API")
|
422 |
-
gr.Markdown("
|
423 |
-
gr.
|
424 |
-
gr.
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
|
|
|
|
|
|
434 |
|
435 |
with gr.Row():
|
436 |
-
with gr.Column(scale=
|
437 |
-
gr.Markdown("###
|
438 |
-
use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی ورودی", value=False)
|
439 |
|
440 |
-
#
|
441 |
-
|
442 |
-
|
|
|
|
|
|
|
443 |
text_to_speak_tb = gr.Textbox(
|
444 |
-
label="
|
445 |
placeholder="متن مورد نظر برای تبدیل به گفتار را اینجا وارد کنید...",
|
446 |
-
lines=
|
447 |
-
value="سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با استفاده از مدل جمینای است."
|
|
|
|
|
448 |
)
|
449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
450 |
speech_prompt_tb = gr.Textbox(
|
451 |
-
label="🗣️ پرامپت برای تنظیم سبک گفتار",
|
452 |
-
placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای",
|
453 |
-
value="به زبان یک گوینده
|
|
|
454 |
)
|
455 |
|
456 |
-
with gr.Column(scale=
|
457 |
-
gr.Markdown("### تنظیمات مدل و خروجی")
|
458 |
model_name_dd = gr.Dropdown(
|
459 |
-
MODEL_NAMES, label="🤖 انتخاب مدل", value="gemini-2.5-flash-preview-tts"
|
460 |
)
|
461 |
speaker_voice_dd = gr.Dropdown(
|
462 |
SPEAKER_VOICES, label="🎤 انتخاب گوینده", value="Charon"
|
463 |
)
|
464 |
temperature_slider = gr.Slider(
|
465 |
-
minimum=0, maximum=2, step=0.05, value=
|
466 |
-
)
|
467 |
output_filename_base_tb = gr.Textbox(
|
468 |
-
label="📛 نام پایه فایل خروجی (بدون پسوند)", value="
|
469 |
)
|
470 |
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
merge_audio_files_cb = gr.Checkbox(label="🔗 ادغام فایلهای صوتی در یک فایل", value=True)
|
482 |
-
delete_partial_files_cb = gr.Checkbox(label="🗑️ حذف فایلهای جزئی پس از ادغام (اگر ادغام فعال باشد)", value=False)
|
483 |
|
484 |
-
gr.
|
485 |
-
|
486 |
-
gr.Markdown("---")
|
487 |
|
488 |
gr.Markdown("### 🎧 خروجی صوتی و دانلود 📥")
|
489 |
with gr.Row():
|
490 |
-
|
491 |
-
|
|
|
|
|
492 |
|
493 |
-
gr.Markdown("### 📜 لاگها و
|
494 |
-
logs_output_tb = gr.Textbox(label=" ", lines=10, interactive=False, autoscroll=True)
|
495 |
|
496 |
-
# Connect button to function
|
497 |
generate_button.click(
|
498 |
fn=gradio_tts_interface,
|
499 |
inputs=[
|
@@ -506,11 +521,11 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
|
|
506 |
outputs=[output_audio, download_file_output, logs_output_tb]
|
507 |
)
|
508 |
|
509 |
-
# Example texts
|
510 |
gr.Examples(
|
511 |
examples=[
|
512 |
-
[False, None, "سلام، این یک تست کوتاه است.", "یک صدای دوستانه و واضح.", "Charon", "
|
513 |
-
[False, None, "به دنیای هوش مصنوعی خوش آمدید. امیدوارم از این ابزار لذت
|
|
|
514 |
],
|
515 |
inputs=[
|
516 |
use_file_input_cb, uploaded_file_input, text_to_speak_tb,
|
@@ -519,18 +534,21 @@ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
|
|
519 |
max_chunk_size_slider, sleep_between_requests_slider,
|
520 |
merge_audio_files_cb, delete_partial_files_cb
|
521 |
],
|
522 |
-
outputs=[output_audio, download_file_output, logs_output_tb],
|
523 |
-
fn=gradio_tts_interface,
|
524 |
-
cache_examples=False # Set to True if
|
525 |
)
|
526 |
|
527 |
gr.Markdown(
|
528 |
-
"<div style='text-align: center; margin-top:
|
529 |
-
"این ابزار از
|
530 |
-
"لطفاً به محدودیتهای استفاده و شرایط خدمات Gemini API توجه
|
|
|
531 |
"</div>"
|
532 |
)
|
533 |
|
534 |
-
|
535 |
if __name__ == "__main__":
|
536 |
-
|
|
|
|
|
|
|
|
9 |
from google import genai
|
10 |
from google.genai import types
|
11 |
|
12 |
+
# تلاش برای ایمپورت pydub و تنظیم فلگ در دسترس بودن
|
13 |
try:
|
14 |
from pydub import AudioSegment
|
15 |
PYDUB_AVAILABLE = True
|
16 |
except ImportError:
|
17 |
PYDUB_AVAILABLE = False
|
18 |
|
19 |
+
# --- ثابتها ---
|
20 |
SPEAKER_VOICES = [
|
21 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
22 |
"Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
|
|
|
26 |
]
|
27 |
MODEL_NAMES = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
|
28 |
|
29 |
+
# --- توابع کمکی (سازگار شده برای لاگنویسی در Gradio) ---
|
30 |
|
31 |
def save_binary_file(file_name, data, log_messages_list):
|
32 |
try:
|
|
|
58 |
|
59 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
60 |
bits_per_sample = 16
|
61 |
+
rate = 24000 # Default rate for Gemini TTS
|
62 |
parts = mime_type.split(";")
|
63 |
for param in parts:
|
64 |
param = param.strip()
|
|
|
68 |
rate = int(rate_str)
|
69 |
except (ValueError, IndexError):
|
70 |
pass
|
71 |
+
elif param.startswith("audio/L"): # e.g., audio/L16
|
72 |
try:
|
73 |
bits_per_sample = int(param.split("L", 1)[1])
|
74 |
except (ValueError, IndexError):
|
|
|
80 |
return [text]
|
81 |
chunks = []
|
82 |
current_chunk = ""
|
83 |
+
# Split by sentences, keeping delimiters. Prioritize common Persian sentence enders.
|
84 |
+
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
85 |
+
|
86 |
for sentence in sentences:
|
87 |
+
sentence_with_space = sentence + " " # Add potential space for length calculation
|
88 |
+
if len(current_chunk) + len(sentence_with_space) > max_size:
|
89 |
+
if current_chunk: # Add the current chunk if it's not empty
|
90 |
chunks.append(current_chunk.strip())
|
91 |
+
# Now, current_chunk becomes the new sentence.
|
92 |
+
# If this new sentence itself is too long, it needs to be split further.
|
93 |
current_chunk = sentence
|
94 |
+
while len(current_chunk) > max_size:
|
95 |
+
# Find a good split point (e.g., comma, space) near max_size
|
96 |
+
# Fallback to hard split if no good point found
|
97 |
+
split_idx = -1
|
98 |
+
# Try splitting at Persian/English punctuation within the oversized chunk
|
99 |
+
possible_split_chars = ['،', ',', ';', ':', ' ']
|
100 |
+
for char_idx in range(max_size - 1, max_size // 2, -1): # Search backwards from max_size
|
101 |
+
if current_chunk[char_idx] in possible_split_chars:
|
102 |
+
split_idx = char_idx + 1
|
103 |
+
break
|
104 |
+
|
105 |
+
if split_idx != -1:
|
106 |
+
chunks.append(current_chunk[:split_idx].strip())
|
107 |
+
current_chunk = current_chunk[split_idx:].strip()
|
108 |
+
else: # Hard split
|
109 |
+
chunks.append(current_chunk[:max_size].strip())
|
110 |
+
current_chunk = current_chunk[max_size:].strip()
|
111 |
else:
|
112 |
current_chunk += (" " if current_chunk else "") + sentence
|
113 |
+
|
114 |
+
if current_chunk: # Add any remaining part
|
115 |
chunks.append(current_chunk.strip())
|
116 |
+
|
117 |
return [c for c in chunks if c] # Ensure no empty chunks
|
118 |
|
119 |
def merge_audio_files_func(file_paths, output_path, log_messages_list):
|
|
|
125 |
combined = AudioSegment.empty()
|
126 |
for i, file_path in enumerate(file_paths):
|
127 |
if os.path.exists(file_path):
|
128 |
+
log_messages_list.append(f"📎 اضافه کردن فایل {i+1}: {os.path.basename(file_path)}")
|
129 |
+
audio = AudioSegment.from_file(file_path) # pydub usually infers format
|
130 |
combined += audio
|
131 |
if i < len(file_paths) - 1: # Add short silence between segments
|
132 |
+
combined += AudioSegment.silent(duration=200) # 200ms silence
|
133 |
else:
|
134 |
log_messages_list.append(f"⚠️ فایل پیدا نشد: {file_path}")
|
135 |
combined.export(output_path, format="wav")
|
|
|
151 |
log_messages_list.append(f"❌ خطا در ایجاد فایل ZIP: {e}")
|
152 |
return False
|
153 |
|
154 |
+
# --- تابع اصلی تولید (سازگار شده برای Gradio) ---
|
155 |
def core_generate_audio(
|
156 |
text_input, prompt_input, selected_voice, output_base_name,
|
157 |
model, temperature_val,
|
|
|
160 |
):
|
161 |
log_messages_list.append("🚀 شروع فرآیند تبدیل متن به گفتار...")
|
162 |
|
163 |
+
# دریافت کلید API
|
164 |
api_key = os.environ.get("GEMINI_API_KEY")
|
165 |
if not api_key:
|
166 |
log_messages_list.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) در Secrets این Space تنظیم نشده است.")
|
167 |
log_messages_list.append("لطفاً به تنظیمات Space رفته و یک Secret با نام GEMINI_API_KEY و مقدار کلید خود ایجاد کنید.")
|
168 |
return None, None # No audio path, no download path
|
169 |
|
170 |
+
# مقداردهی اولیه کلاینت GenAI
|
171 |
try:
|
172 |
log_messages_list.append("🛠️ در حال ایجاد کلاینت جمینای...")
|
173 |
+
client = genai.Client(api_key=api_key)
|
|
|
174 |
log_messages_list.append("✅ کلاینت جمینای با موفقیت ایجاد شد.")
|
175 |
except Exception as e:
|
176 |
log_messages_list.append(f"❌ خطا در ایجاد کلاینت جمینای: {e}")
|
177 |
log_messages_list.append("لطفاً از صحت کلید API خود اطمینان حاصل کنید.")
|
178 |
return None, None
|
179 |
|
|
|
180 |
if not text_input or text_input.strip() == "":
|
181 |
log_messages_list.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است.")
|
182 |
return None, None
|
183 |
|
|
|
184 |
text_chunks = smart_text_split(text_input, max_chunk)
|
185 |
log_messages_list.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
|
186 |
for i, chunk in enumerate(text_chunks):
|
187 |
log_messages_list.append(f"📝 قطعه {i+1}: {len(chunk)} کاراکتر")
|
188 |
+
text_chunks = [c for c in text_chunks if c] # فیلتر کردن قطعات خالی احتمالی
|
|
|
|
|
189 |
|
190 |
if not text_chunks:
|
191 |
log_messages_list.append("❌ خطا: پس از تقسیمبندی، هیچ قطعه متنی برای پردازش وجود ندارد.")
|
192 |
return None, None
|
193 |
|
194 |
generated_files = []
|
195 |
+
# نامگذاری فایلها بدون مسیر اضافی برای سادگی در محیط Space
|
196 |
+
# فایلها در ریشه فضای کاری Space ذخیره میشوند
|
|
|
197 |
|
198 |
for i, chunk in enumerate(text_chunks):
|
199 |
log_messages_list.append(f"\n🔊 تولید صدا برای قطعه {i+1}/{len(text_chunks)}...")
|
|
|
211 |
)
|
212 |
|
213 |
current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
|
|
|
|
|
214 |
|
215 |
try:
|
216 |
+
response = client.models.generate_content( # استفاده از generate_content برای سادگی
|
|
|
217 |
model=model,
|
218 |
contents=contents,
|
219 |
config=generate_content_config,
|
|
|
225 |
|
226 |
inline_data = response.candidates[0].content.parts[0].inline_data
|
227 |
data_buffer = inline_data.data
|
228 |
+
# حدس پسوند فایل بر اساس MIME type
|
229 |
file_extension = mimetypes.guess_extension(inline_data.mime_type)
|
230 |
|
231 |
+
# اگر پسوند قابل تشخیص نبود یا باینری عمومی بود، WAV را در نظر میگیریم
|
232 |
+
# و در صورت نیاز (مثلاً برای audio/L16) هدر WAV اضافه میکنیم
|
233 |
+
if file_extension is None or "binary" in inline_data.mime_type or file_extension == ".bin":
|
234 |
file_extension = ".wav"
|
235 |
+
if "audio/L" in inline_data.mime_type: # نیاز به هدر WAV
|
|
|
|
|
236 |
data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
|
237 |
+
elif inline_data.mime_type == "audio/mpeg":
|
238 |
+
file_extension = ".mp3" # اگر API مستقیما MP3 داد
|
239 |
+
elif inline_data.mime_type == "audio/wav":
|
240 |
+
file_extension = ".wav" # اگر API مستقیما WAV داد
|
241 |
|
242 |
generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_messages_list)
|
243 |
if generated_file_path:
|
244 |
generated_files.append(generated_file_path)
|
245 |
+
log_messages_list.append(f"✅ قطعه {i+1} تولید شد: {os.path.basename(generated_file_path)}")
|
246 |
|
247 |
+
elif response.text:
|
248 |
log_messages_list.append(f"ℹ️ پیام متنی از API برای قطعه {i+1}: {response.text}")
|
249 |
+
if "rate limit" in response.text.lower() or "quota" in response.text.lower():
|
250 |
+
log_messages_list.append(f"⏳ به نظر میرسد به محدودیت تعداد درخواست API (Quota) رسیدهاید. لطفاً چند دقیقه صبر کنید و دوباره امتحان کنید، یا فاصله زمانی بین درخواستها را افزایش دهید.")
|
251 |
|
252 |
+
else:
|
253 |
+
log_messages_list.append(f"⚠️ پاسخ API برای قطعه {i+1} حاوی داده صوتی یا پیام متنی نبود. جزئیات پاسخ: {response.prompt_feedback if response else 'No response'}")
|
254 |
|
255 |
|
256 |
except types.generation_types.BlockedPromptException as bpe:
|
257 |
log_messages_list.append(f"❌ محتوای پرامپت برای قطعه {i+1} مسدود شد: {bpe}")
|
258 |
+
log_messages_list.append(f"علت مسدود شدن: {bpe.response.prompt_feedback if bpe.response else 'نامشخص'}")
|
259 |
log_messages_list.append("لطفاً متن ورودی یا پرامپت سبک گفتار را بررسی و اصلاح کنید.")
|
260 |
+
continue
|
261 |
except types.generation_types.StopCandidateException as sce:
|
262 |
log_messages_list.append(f"❌ تولید محتوا برای قطعه {i+1} به دلیل نامشخصی متوقف شد: {sce}")
|
263 |
continue
|
264 |
except Exception as e:
|
265 |
log_messages_list.append(f"❌ خطا در تولید قطعه {i+1}: {e}")
|
|
|
266 |
if "API key not valid" in str(e):
|
267 |
log_messages_list.append("خطای کلید API. لطفاً از معتبر بودن کلید و تنظیم صحیح آن در Secrets مطمئن شوید.")
|
268 |
elif "resource has been exhausted" in str(e).lower() or "quota" in str(e).lower():
|
269 |
log_messages_list.append("به نظر میرسد محدودیت استفاده از API (Quota) شما تمام شده است.")
|
270 |
+
continue
|
271 |
|
272 |
+
if i < len(text_chunks) - 1 and len(text_chunks) > 1 :
|
273 |
log_messages_list.append(f"⏱️ انتظار {sleep_time} ثانیه...")
|
274 |
time.sleep(sleep_time)
|
275 |
|
|
|
284 |
|
285 |
if merge_files and len(generated_files) > 1:
|
286 |
if not PYDUB_AVAILABLE:
|
287 |
+
log_messages_list.append("⚠️ pydub برای ادغام در دسترس نیست. فایلها به صورت جداگانه در یک فایل ZIP ارائه میشوند.")
|
|
|
288 |
zip_filename = f"{output_base_name}_all_parts.zip"
|
289 |
if create_zip_file(generated_files, zip_filename, log_messages_list):
|
290 |
download_file = zip_filename
|
291 |
+
if generated_files: playback_file = generated_files[0]
|
292 |
else:
|
293 |
merged_filename = f"{output_base_name}_merged.wav"
|
|
|
294 |
if merge_audio_files_func(generated_files, merged_filename, log_messages_list):
|
295 |
playback_file = merged_filename
|
296 |
download_file = merged_filename
|
297 |
+
log_messages_list.append(f"🎵 فایل نهایی ادغام شده: {os.path.basename(merged_filename)}")
|
298 |
|
299 |
if delete_partials:
|
300 |
for file_path in generated_files:
|
301 |
try:
|
302 |
+
if os.path.abspath(file_path) != os.path.abspath(merged_filename):
|
303 |
os.remove(file_path)
|
304 |
+
log_messages_list.append(f"🗑️ فایل جزئی حذف شد: {os.path.basename(file_path)}")
|
305 |
except Exception as e:
|
306 |
+
log_messages_list.append(f"⚠️ خطا در حذف فایل جزئی {os.path.basename(file_path)}: {e}")
|
307 |
else:
|
308 |
+
log_messages_list.append("⚠️ ادغام ممکن نبود. فایلها به صورت جداگانه در یک فایل ZIP ارائه میشوند.")
|
|
|
309 |
zip_filename = f"{output_base_name}_all_parts.zip"
|
|
|
310 |
if create_zip_file(generated_files, zip_filename, log_messages_list):
|
311 |
download_file = zip_filename
|
312 |
+
if generated_files: playback_file = generated_files[0]
|
313 |
|
314 |
elif len(generated_files) == 1:
|
315 |
playback_file = generated_files[0]
|
316 |
download_file = generated_files[0]
|
317 |
|
318 |
+
else: # Multiple files, no merge requested (or PYDUB_AVAILABLE is False and merge_files was True)
|
319 |
zip_filename = f"{output_base_name}_all_parts.zip"
|
|
|
320 |
if create_zip_file(generated_files, zip_filename, log_messages_list):
|
321 |
download_file = zip_filename
|
322 |
+
if generated_files: playback_file = generated_files[0]
|
323 |
|
324 |
if playback_file and not os.path.exists(playback_file):
|
325 |
+
log_messages_list.append(f"⚠️ فایل پخش {os.path.basename(playback_file)} وجود ندارد!")
|
326 |
playback_file = None
|
327 |
if download_file and not os.path.exists(download_file):
|
328 |
+
log_messages_list.append(f"⚠️ فایل دانلود {os.path.basename(download_file)} وجود ندارد!")
|
329 |
download_file = None
|
330 |
|
331 |
return playback_file, download_file
|
332 |
|
333 |
+
# --- تابع رابط کاربری Gradio ---
|
334 |
def gradio_tts_interface(
|
335 |
use_file_input, uploaded_file, text_to_speak,
|
336 |
+
speech_prompt, speaker_voice, output_filename_base_in,
|
337 |
model_name, temperature,
|
338 |
max_chunk_size, sleep_between_requests,
|
339 |
+
merge_audio_files_flag, delete_partial_files_flag,
|
340 |
+
progress=gr.Progress(track_tqdm=True) # track_tqdm for visual progress if using loops with tqdm
|
341 |
):
|
342 |
+
log_messages = []
|
343 |
|
|
|
344 |
actual_text_input = ""
|
345 |
if use_file_input:
|
346 |
if uploaded_file is not None:
|
347 |
try:
|
348 |
+
# Gradio file objects have a .name attribute which is the temp path
|
349 |
with open(uploaded_file.name, 'r', encoding='utf-8') as f:
|
350 |
actual_text_input = f.read().strip()
|
351 |
log_messages.append(f"✅ متن از فایل '{os.path.basename(uploaded_file.name)}' بارگذاری شد: {len(actual_text_input)} کاراکتر.")
|
352 |
log_messages.append(f"📝 نمونه متن فایل: '{actual_text_input[:100]}{'...' if len(actual_text_input) > 100 else ''}'")
|
353 |
if not actual_text_input:
|
354 |
+
log_messages.append("❌ خطا: فایل آپلود شده خالی است یا قابل خواندن نیست.")
|
355 |
return None, None, "\n".join(log_messages)
|
356 |
except Exception as e:
|
357 |
log_messages.append(f"❌ خطا در خواندن فایل آپلود شده: {e}")
|
|
|
367 |
log_messages.append(f"📖 متن ورودی دستی: {len(actual_text_input)} کاراکتر")
|
368 |
log_messages.append(f"📝 نمونه متن ورودی: '{actual_text_input[:100]}{'...' if len(actual_text_input) > 100 else ''}'")
|
369 |
|
370 |
+
# Sanitize output_filename_base to prevent path traversal or invalid characters
|
371 |
+
output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "gemini_tts_output")
|
372 |
+
if not output_filename_base: # If sanitization results in empty string
|
373 |
+
output_filename_base = "gemini_tts_output"
|
374 |
+
log_messages.append(f"🏷️ نام پایه فایل خروجی: {output_filename_base}")
|
375 |
+
|
376 |
|
377 |
if not PYDUB_AVAILABLE:
|
378 |
log_messages.append("⚠️ کتابخانه pydub در دسترس نیست. امکان ادغام فایلهای صوتی وجود نخواهد داشت و فایلهای صوتی به صورت جداگانه (در صورت وجود چند بخش) در یک فایل ZIP ارائه میشوند.")
|
379 |
+
current_merge_audio_files = False # Force disable merge if pydub is not available
|
380 |
+
else:
|
381 |
+
current_merge_audio_files = merge_audio_files_flag
|
382 |
|
383 |
|
|
|
384 |
playback_path, download_path = core_generate_audio(
|
385 |
+
actual_text_input, speech_prompt, speaker_voice, output_filename_base,
|
386 |
+
model_name, temperature, max_chunk_size, sleep_between_requests,
|
387 |
+
current_merge_audio_files, delete_partial_files_flag, log_messages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
)
|
389 |
|
390 |
+
log_output_str = "\n".join(log_messages)
|
391 |
|
|
|
392 |
valid_playback_path = playback_path if playback_path and os.path.exists(playback_path) else None
|
393 |
valid_download_path = download_path if download_path and os.path.exists(download_path) else None
|
394 |
|
395 |
+
if not valid_playback_path and not valid_download_path and not actual_text_input.strip():
|
396 |
+
pass # Avoid error message if it was just an empty input from the start
|
397 |
+
elif not valid_playback_path and not valid_download_path and actual_text_input.strip():
|
398 |
+
# Add this only if there was text input but no output files
|
399 |
+
log_output_str += "\n🛑 هیچ فایل صوتی برای پخش یا دانلود در دسترس نیست."
|
400 |
|
401 |
+
return valid_playback_path, valid_download_path, log_output_str
|
402 |
|
403 |
+
# --- تعریف رابط کاربری Gradio ---
|
|
|
|
|
404 |
css = """
|
405 |
+
body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; }
|
406 |
+
.gradio-container { max-width: 95% !important; margin: auto !important; padding: 10px !important; }
|
407 |
+
@media (min-width: 768px) { .gradio-container { max-width: 800px !important; } }
|
408 |
footer { display: none !important; }
|
409 |
+
.gr-button { background-color: #1d67a3 !important; color: white !important; border-radius: 8px !important; }
|
410 |
+
.gr-button:hover { background-color: #164f7e !important; }
|
411 |
+
.gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius: 6px !important; }
|
412 |
+
.gr-panel { padding: 15px !important; border-radius: 8px !important; box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; }
|
413 |
+
h2, h3 { color: #1d67a3; text-align: center; }
|
414 |
+
label { font-weight: bold; color: #333; }
|
415 |
+
#output_audio .gallery, #download_file_output .gallery { display: none !important; }
|
416 |
+
/* Ensure text inputs and textareas are also LTR for code/API keys if needed, but general UI is RTL */
|
417 |
+
textarea, input[type="text"] { direction: rtl; text-align: right; }
|
418 |
+
/* Override for specific LTR elements if any, e.g. API key input if it were visible */
|
419 |
"""
|
420 |
|
421 |
+
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
|
422 |
+
gr.Markdown("## 🔊 تبدیل متن به گفتار با Gemini API (فارسی)")
|
423 |
+
gr.Markdown("<p style='text-align:center;'>ساخته شده بر اساس کد کولب توسط: aigolden</p>")
|
424 |
+
gr.HTML("<hr>") # Using HTML for a styled horizontal rule
|
425 |
+
with gr.Accordion("⚠️ راهنمای مهم: تنظیم کلید API جمینای", open=False):
|
426 |
+
gr.Markdown(
|
427 |
+
"**برای استفاده از این ابزار، ابتدا باید کلید API جمینای خود را در بخش Secrets این Space در Hugging Face اضافه کنید:**\n"
|
428 |
+
"1. به صفحه اصلی این Space بروید (جایی که این اپلیکیشن را میبینید).\n"
|
429 |
+
"2. در بالای صفحه، روی نام Space خود و سپس 'Settings' (آیکن چرخدنده ⚙️) کلیک کنید.\n"
|
430 |
+
"3. در منوی سمت چپ صفحه تنظیمات، به بخش 'Secrets' بروید.\n"
|
431 |
+
"4. روی دکمه '+ New secret' کلیک کنید.\n"
|
432 |
+
"5. در فیلد 'Name'، دقیقاً عبارت `GEMINI_API_KEY` را وارد کنید (با حروف بزرگ).\n"
|
433 |
+
"6. در فیلد 'Value (secret)'، کلید API جمینای خود را که از Google AI Studio یا Google Cloud Console دریافت کردهاید، وارد کنید.\n"
|
434 |
+
"7. روی 'Save secret' کلیک کنید.\n"
|
435 |
+
"**توجه:** پس از افزودن یا تغییر Secret، ممکن است لازم باشد Space را یکبار Restart کنید. برای این کار، از منوی سهنقطه (⋮) در کنار دکمه 'Settings' در صفحه اصلی Space، گزینه 'Restart this Space' را انتخاب کنید."
|
436 |
+
)
|
437 |
+
gr.HTML("<hr>")
|
438 |
|
439 |
with gr.Row():
|
440 |
+
with gr.Column(scale=3, min_width=300):
|
441 |
+
gr.Markdown("### ���� تنظیمات ورودی و پرامپت")
|
442 |
+
use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی ورودی (.txt)", value=False, elem_id="use_file_cb")
|
443 |
|
444 |
+
# فایل ورودی و متن ورودی با توجه به چکباکس نمایش داده میشوند (منطق در تابع اصلی)
|
445 |
+
uploaded_file_input = gr.File(
|
446 |
+
label="📂 آپلود فایل متنی (فقط شامل متن اصلی، UTF-8)",
|
447 |
+
file_types=['.txt'],
|
448 |
+
visible=False # Initially hidden, controlled by checkbox interaction
|
449 |
+
)
|
450 |
text_to_speak_tb = gr.Textbox(
|
451 |
+
label="⌨️ متن ورودی (اگر گزینه فایل فعال نیست)",
|
452 |
placeholder="متن مورد نظر برای تبدیل به گفتار را اینجا وارد کنید...",
|
453 |
+
lines=8,
|
454 |
+
value="سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با استفاده از مدل جمینای است.",
|
455 |
+
elem_id="text_input_main",
|
456 |
+
visible=True # Initially visible
|
457 |
)
|
458 |
|
459 |
+
# JavaScript to toggle visibility
|
460 |
+
use_file_input_cb.change(
|
461 |
+
fn=lambda x: (gr.update(visible=x), gr.update(visible=not x)),
|
462 |
+
inputs=use_file_input_cb,
|
463 |
+
outputs=[uploaded_file_input, text_to_speak_tb]
|
464 |
+
)
|
465 |
+
|
466 |
speech_prompt_tb = gr.Textbox(
|
467 |
+
label="🗣️ پرامپت برای تنظیم سبک گفتار (اختیاری)",
|
468 |
+
placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای صحبت کن",
|
469 |
+
value="به زبان یک گوینده رادیو با صدای گرم و واضح صحبت کن.",
|
470 |
+
lines=2
|
471 |
)
|
472 |
|
473 |
+
with gr.Column(scale=2, min_width=250):
|
474 |
+
gr.Markdown("### ⚙️ تنظیمات مدل و خروجی")
|
475 |
model_name_dd = gr.Dropdown(
|
476 |
+
MODEL_NAMES, label="🤖 انتخاب مدل Gemini TTS", value="gemini-2.5-flash-preview-tts"
|
477 |
)
|
478 |
speaker_voice_dd = gr.Dropdown(
|
479 |
SPEAKER_VOICES, label="🎤 انتخاب گوینده", value="Charon"
|
480 |
)
|
481 |
temperature_slider = gr.Slider(
|
482 |
+
minimum=0.0, maximum=2.0, step=0.05, value=0.9, label="🌡️ دمای مدل (تنوع خروجی)"
|
483 |
+
) # Adjusted default temp
|
484 |
output_filename_base_tb = gr.Textbox(
|
485 |
+
label="📛 نام پایه فایل خروجی (بدون پسوند)", value="gemini_tts_farsi_output"
|
486 |
)
|
487 |
|
488 |
+
gr.Markdown("#### تنظیمات پیشرفته")
|
489 |
+
max_chunk_size_slider = gr.Slider(
|
490 |
+
minimum=1500, maximum=4000, step=100, value=3800, label="📏 حداکثر کاراکتر در هر قطعه"
|
491 |
+
) # Adjusted min chunk size
|
492 |
+
sleep_between_requests_slider = gr.Slider(
|
493 |
+
minimum=3, maximum=25, step=0.5, value=12, label="⏱️ فاصله بین درخواستها (ثانیه)"
|
494 |
+
) # Adjusted sleep range and default
|
495 |
+
merge_audio_files_cb = gr.Checkbox(label="🔗 ادغام فایلهای صوتی در یک فایل WAV (نیازمند pydub)", value=True)
|
496 |
+
delete_partial_files_cb = gr.Checkbox(label="🗑️ حذف فایلهای جزئی پس از ادغام (اگر ادغام فعال باشد)", value=False)
|
497 |
+
|
|
|
|
|
498 |
|
499 |
+
generate_button = gr.Button("🎙️ تولید صدا", variant="primary", elem_id="generate_button_main")
|
500 |
+
gr.HTML("<hr>")
|
|
|
501 |
|
502 |
gr.Markdown("### 🎧 خروجی صوتی و دانلود 📥")
|
503 |
with gr.Row():
|
504 |
+
with gr.Column(scale=1):
|
505 |
+
output_audio = gr.Audio(label="🔊 فایل صوتی تولید شده (قابل پخش)", type="filepath", elem_id="output_audio_player")
|
506 |
+
with gr.Column(scale=1):
|
507 |
+
download_file_output = gr.File(label="💾 دانلود فایل نهایی (WAV یا ZIP)", elem_id="download_file_link")
|
508 |
|
509 |
+
gr.Markdown("### 📜 لاگها و پیامهای فرآیند")
|
510 |
+
logs_output_tb = gr.Textbox(label=" ", lines=10, interactive=False, autoscroll=True, elem_id="logs_textbox")
|
511 |
|
|
|
512 |
generate_button.click(
|
513 |
fn=gradio_tts_interface,
|
514 |
inputs=[
|
|
|
521 |
outputs=[output_audio, download_file_output, logs_output_tb]
|
522 |
)
|
523 |
|
|
|
524 |
gr.Examples(
|
525 |
examples=[
|
526 |
+
[False, None, "سلام، این یک تست کوتاه است.", "یک صدای دوستانه و واضح.", "Charon", "test_output_1", "gemini-2.5-flash-preview-tts", 0.9, 3800, 12, True, False],
|
527 |
+
[False, None, "به دنیای شگفتانگیز هوش مصنوعی خوش آمدید. امیدوارم از این ابزار لذت ببرید و برایتان مفید باشد.", "با هیجان و انرژی زیاد صحبت کن، انگار که یک خبر فوقالعاده را اعلام میکنی.", "Zephyr", "ai_voice_farsi", "gemini-2.5-flash-preview-tts", 1.1, 3500, 10, True, True],
|
528 |
+
[False, None, "این یک نمونه متن طولانیتر است که برای آزمایش تقسیمبندی هوشمند به کار میرود. باید دید که چگونه به قطعات کوچکتر تقسیم شده و سپس در صورت انتخاب گزینه ادغام، به یک فایل صوتی واحد تبدیل میشود. امیدواریم که همه چیز به خوبی کار کند.", "با لحنی آرام و روایی، مانند یک داستانگو.", "Achird", "long_text_sample", "gemini-2.5-pro-preview-tts", 0.8, 2500, 15, True, True],
|
529 |
],
|
530 |
inputs=[
|
531 |
use_file_input_cb, uploaded_file_input, text_to_speak_tb,
|
|
|
534 |
max_chunk_size_slider, sleep_between_requests_slider,
|
535 |
merge_audio_files_cb, delete_partial_files_cb
|
536 |
],
|
537 |
+
outputs=[output_audio, download_file_output, logs_output_tb],
|
538 |
+
fn=gradio_tts_interface,
|
539 |
+
cache_examples=False # Set to True if inputs/outputs are static and pre-computation is desired
|
540 |
)
|
541 |
|
542 |
gr.Markdown(
|
543 |
+
"<div style='text-align: center; margin-top: 30px; font-size: 0.9em; color: grey;'>"
|
544 |
+
"این ابزار از Google Gemini API برای تبدیل متن به گفتار استفاده میکند. "
|
545 |
+
"لطفاً به محدودیتهای استفاده و شرایط خدمات Gemini API توجه فرمایید.<br>"
|
546 |
+
"برای بهترین نتیجه، از مرورگرهای بهروز استفاده کنید."
|
547 |
"</div>"
|
548 |
)
|
549 |
|
|
|
550 |
if __name__ == "__main__":
|
551 |
+
# برای اجرای محلی با قابلیت hot-reload و debug
|
552 |
+
# demo.launch(debug=True, share=False)
|
553 |
+
# برای اجرای عادی (مثلا در محیط Hugging Face Spaces، این خط معمولا لازم نیست چون Gradio خودش هندل میکنه)
|
554 |
+
demo.launch()
|