Hamed744 commited on
Commit
e4bf7cb
·
verified ·
1 Parent(s): e4d0150

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +195 -177
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
- # --- Constants ---
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
- # --- Helper functions (Adapted for Gradio logging) ---
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 = re.split(r'(?<=[.!?])\s+', text)
 
 
83
  for sentence in sentences:
84
- if len(current_chunk) + len(sentence) + 1 > max_size:
85
- if current_chunk:
 
86
  chunks.append(current_chunk.strip())
 
 
87
  current_chunk = sentence
88
- # If a single sentence is too long, split it by words/chars
89
- if len(current_chunk) > max_size:
90
- words = current_chunk.split()
91
- temp_word_chunk = ""
92
- for word in words:
93
- if len(temp_word_chunk) + len(word) + 1 > max_size:
94
- if temp_word_chunk: chunks.append(temp_word_chunk.strip())
95
- temp_word_chunk = word
96
- while len(temp_word_chunk) > max_size: # Force split very long words
97
- chunks.append(temp_word_chunk[:max_size])
98
- temp_word_chunk = temp_word_chunk[max_size:]
99
- else:
100
- temp_word_chunk += (" " if temp_word_chunk else "") + word
101
- if temp_word_chunk: chunks.append(temp_word_chunk.strip())
102
- current_chunk = "" # Reset current_chunk as it was processed
 
 
103
  else:
104
  current_chunk += (" " if current_chunk else "") + sentence
105
- if current_chunk:
 
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=100) # 100ms silence
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
- # --- Main generation function (Adapted for Gradio) ---
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
- # API Key Retrieval
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
- # Initialize GenAI Client
161
  try:
162
  log_messages_list.append("🛠️ در حال ایجاد کلاینت جمینای...")
163
- # os.environ["GEMINI_API_KEY"] = api_key # Already set if from secrets
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
- if len(chunk) == 0: # Safety check from smart_text_split
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
- # Ensure output directory exists (optional, can write to current dir)
191
- # output_dir = "outputs"
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
- # Using generate_content, not stream, for simplicity with single audio part expected
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
- if file_extension is None or "binary" in inline_data.mime_type: # Fallback for generic mime types
 
 
230
  file_extension = ".wav"
231
- # Assuming Gemini TTS API now more consistently returns audio/* mimetypes
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
- # If it's already audio/wav or audio/mpeg, it might be fine.
236
- # Forcing .wav for consistency as pydub handles WAV well.
237
- # If Gemini sends actual WAV, convert_to_wav might not be strictly needed
238
- # but better safe than sorry if mime is generic.
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: # If API returns text (e.g. error or info)
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: # No audio, no text
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 # Skip to next chunk
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 # Skip to next chunk
269
 
270
- if i < len(text_chunks) - 1 and len(text_chunks) > 1 : # Only sleep if there are more chunks
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] # Play first part
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: # Don't delete the merged file itself if it was in generated_files
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] # Play first part
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] # Play first part
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
- # --- Gradio Interface Function ---
337
  def gradio_tts_interface(
338
  use_file_input, uploaded_file, text_to_speak,
339
- speech_prompt, speaker_voice, output_filename_base,
340
  model_name, temperature,
341
  max_chunk_size, sleep_between_requests,
342
- merge_audio_files, delete_partial_files,
343
- progress=gr.Progress(track_tqdm=True)
344
  ):
345
- log_messages = [] # Initialize list for logs for this run
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
- merge_audio_files = False # Force disable merge if pydub is not available
 
 
377
 
378
 
379
- # Call the core generation logic
380
  playback_path, download_path = core_generate_audio(
381
- actual_text_input,
382
- speech_prompt,
383
- speaker_voice,
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
- log_output = "\n".join(log_messages)
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
- # Avoid error message if it was just an empty input from the start
402
- pass
403
- elif not valid_playback_path and not valid_download_path :
404
- log_output += "\n🛑 هیچ فایل صوتی برای پخش یا دانلود در دسترس نیست."
405
 
 
406
 
407
- return valid_playback_path, valid_download_path, log_output
408
-
409
- # --- Gradio UI Definition ---
410
  css = """
411
- body { font-family: 'Arial', sans-serif; }
412
- .gradio-container { max-width: 800px !important; margin: auto !important; }
 
413
  footer { display: none !important; }
414
- .gr-button { background-color: #007bff !important; color: white !important; }
415
- .gr-button:hover { background-color: #0056b3 !important; }
416
- #output_audio .gallery { display: none !important; } /* Hide gallery view for audio if it appears */
417
- #download_file_output .gallery { display: none !important; } /* Hide gallery view for file if it appears */
 
 
 
 
 
 
418
  """
419
 
420
- with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
421
- gr.Markdown("## 🔊 تبدیل متن به گفتار با Gemini API")
422
- gr.Markdown("ساخته شده بر اساس کد کولب توسط: [aigolden](https://github.com/aigolden)") # Assuming aigolden is a GitHub user or similar
423
- gr.Markdown("---")
424
- gr.Markdown(
425
- "**راهنما:** برای استفاده از این ابزار، ابتدا باید کلید API جمینای خود را در بخش **Secrets** این Space در Hugging Face اضافه کنید.\n"
426
- "1. به صفحه Space خود بروید.\n"
427
- "2. روی 'Settings' کلیک کنید.\n"
428
- "3. در بخش 'Repository secrets'، روی 'New secret' کلیک کنید.\n"
429
- "4. در فیلد 'Name'، عبارت `GEMINI_API_KEY` را وارد کنید.\n"
430
- "5. در فیلد 'Value'، کلید API جمینای خود را وارد کنید و 'Save secret' را بزنید.\n"
431
- "پس از تنظیم Secret، می‌توانید از این ابزار استفاده کنید."
432
- )
433
- gr.Markdown("---")
 
 
 
434
 
435
  with gr.Row():
436
- with gr.Column(scale=2):
437
- gr.Markdown("### تنظیمات ورودی و پرامپت")
438
- use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی ورودی", value=False)
439
 
440
- # Conditional visibility for text_to_speak_tb vs uploaded_file_input
441
- # Gradio handles this by just having both and user interaction defines which is used via the wrapper
442
- uploaded_file_input = gr.File(label="📂 آپلود فایل متنی (فقط شامل متن اصلی)", file_types=['.txt'])
 
 
 
443
  text_to_speak_tb = gr.Textbox(
444
- label="📝 متن ورودی (اگر گزینه فایل فعال نیست)",
445
  placeholder="متن مورد نظر برای تبدیل به گفتار را اینجا وارد کنید...",
446
- lines=7,
447
- value="سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با استفاده از مدل جمینای است."
 
 
448
  )
449
 
 
 
 
 
 
 
 
450
  speech_prompt_tb = gr.Textbox(
451
- label="🗣️ پرامپت برای تنظیم سبک گفتار",
452
- placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای",
453
- value="به زبان یک گوینده حرفه‌ای و آرام صحبت کن."
 
454
  )
455
 
456
- with gr.Column(scale=1):
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=1.0, label="🌡️ دمای مدل (تنوع خروجی)"
466
- )
467
  output_filename_base_tb = gr.Textbox(
468
- label="📛 نام پایه فایل خروجی (بدون پسوند)", value="gemini_tts_output"
469
  )
470
 
471
- gr.Markdown("---")
472
- gr.Markdown("### تنظیمات پیشرفته")
473
- with gr.Row():
474
- max_chunk_size_slider = gr.Slider(
475
- minimum=2000, maximum=4000, step=100, value=3800, label="📏 حداکثر کاراکتر در هر قطعه"
476
- )
477
- sleep_between_requests_slider = gr.Slider(
478
- minimum=5, maximum=20, step=0.5, value=14, label="⏱️ فاصله زمانی بین درخواست‌ها (ثانیه)"
479
- ) # Increased min sleep a bit
480
- with gr.Row():
481
- merge_audio_files_cb = gr.Checkbox(label="🔗 ادغام فایل‌های صوتی در یک فایل", value=True)
482
- delete_partial_files_cb = gr.Checkbox(label="🗑️ حذف فایل‌های جزئی پس از ادغام (اگر ادغام فعال باشد)", value=False)
483
 
484
- gr.Markdown("---")
485
- generate_button = gr.Button("🎙️ تولید صدا", variant="primary")
486
- gr.Markdown("---")
487
 
488
  gr.Markdown("### 🎧 خروجی صوتی و دانلود 📥")
489
  with gr.Row():
490
- output_audio = gr.Audio(label="🔊 فایل صوتی تولید شده", elem_id="output_audio")
491
- download_file_output = gr.File(label="💾 دانلود فایل نهایی (WAV یا ZIP)", elem_id="download_file_output")
 
 
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", "gemini_tts_output", "gemini-2.5-flash-preview-tts", 0.9, 3800, 12, True, False],
513
- [False, None, "به دنیای هوش مصنوعی خوش آمدید. امیدوارم از این ابزار لذت ببرید.", "با هیجان و انرژی صحبت کن.", "Zephyr", "ai_voice_test", "gemini-2.5-flash-preview-tts", 1.1, 3000, 10, True, True],
 
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], # outputs are optional for examples
523
- fn=gradio_tts_interface, # function to call for examples
524
- cache_examples=False # Set to True if you want to precompute and cache example outputs
525
  )
526
 
527
  gr.Markdown(
528
- "<div style='text-align: center; margin-top: 20px; font-size: 0.9em; color: grey;'>"
529
- "این ابزار از API شرکت Google Gemini برای تبدیل متن ب�� گفتار استفاده می‌کند. "
530
- "لطفاً به محدودیت‌های استفاده و شرایط خدمات Gemini API توجه فرمایید."
 
531
  "</div>"
532
  )
533
 
534
-
535
  if __name__ == "__main__":
536
- demo.launch(debug=True) # debug=True for local testing
 
 
 
 
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()