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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +430 -345
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import base64
2
  import mimetypes
3
  import os
@@ -5,28 +6,36 @@ import re
5
  import struct
6
  import time
7
  import zipfile
8
- import shutil
9
-
10
  from google import genai
11
  from google.genai import types
 
12
  try:
13
  from pydub import AudioSegment
14
  PYDUB_AVAILABLE = True
15
  except ImportError:
16
  PYDUB_AVAILABLE = False
17
- print("⚠️ pydub در دسترس نیست. فایل‌های صوتی به صورت جداگانه ذخیره می‌شوند.")
18
 
19
- import gradio as gr
 
 
 
 
 
 
 
 
 
 
20
 
21
- # --- Helper functions ---
22
- def save_binary_file(file_name, data):
23
- output_dir = "outputs"
24
- os.makedirs(output_dir, exist_ok=True)
25
- full_file_path = os.path.join(output_dir, file_name)
26
- with open(full_file_path, "wb") as f:
27
- f.write(data)
28
- print(f" فایل در مسیر زیر ذخیره شد: {full_file_path}")
29
- return full_file_path
30
 
31
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
32
  parameters = parse_audio_mime_type(mime_type)
@@ -38,6 +47,7 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
38
  block_align = num_channels * bytes_per_sample
39
  byte_rate = sample_rate * block_align
40
  chunk_size = 36 + data_size
 
41
  header = struct.pack(
42
  "<4sI4s4sIHHIIHH4sI",
43
  b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
@@ -47,7 +57,7 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
47
 
48
  def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
49
  bits_per_sample = 16
50
- rate = 24000
51
  parts = mime_type.split(";")
52
  for param in parts:
53
  param = param.strip()
@@ -55,397 +65,472 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
55
  try:
56
  rate_str = param.split("=", 1)[1]
57
  rate = int(rate_str)
58
- except (ValueError, IndexError): pass
59
- elif param.lower().startswith("audio/l"):
 
60
  try:
61
- bits_str = param.split("L", 1)[1]
62
- bits_per_sample = int(re.match(r'\d+', bits_str).group(0))
63
- except (ValueError, IndexError, AttributeError):
64
  pass
65
  return {"bits_per_sample": bits_per_sample, "rate": rate}
66
 
67
-
68
- def load_text_from_gradio_file(file_obj):
69
- if file_obj is None: return ""
70
- try:
71
- with open(file_obj.name, 'r', encoding='utf-8') as f:
72
- content = f.read().strip()
73
- print(f"📖 متن بارگذاری شده: {len(content)} کاراکتر")
74
- return content
75
- except Exception as e:
76
- print(f"❌ خطا در خواندن فایل: {e}")
77
- return ""
78
-
79
  def smart_text_split(text, max_size=3800):
80
- if len(text) <= max_size: return [text]
 
81
  chunks = []
82
  current_chunk = ""
83
- processed_text = re.sub(r'\s+', ' ', text).strip()
84
- sentences = re.split(r'(?<=[.!?؟])\s+', processed_text)
85
-
86
  for sentence in sentences:
87
- if not sentence.strip(): continue
88
-
89
- if len(current_chunk) + (1 if current_chunk else 0) + len(sentence) > max_size:
90
  if current_chunk:
91
  chunks.append(current_chunk.strip())
92
-
93
- current_chunk = ""
94
- while len(sentence) > max_size:
95
- split_at = sentence[:max_size].rfind(' ')
96
- if split_at <= 0 or split_at < max_size // 3:
97
- split_at = max_size
98
- chunks.append(sentence[:split_at].strip())
99
- sentence = sentence[split_at:].strip()
100
- current_chunk = sentence
 
 
 
 
 
 
 
101
  else:
102
- if current_chunk:
103
- current_chunk += " " + sentence
104
- else:
105
- current_chunk = sentence
106
-
107
- if current_chunk.strip():
108
  chunks.append(current_chunk.strip())
109
-
110
- return [c for c in chunks if c]
111
 
112
- def merge_audio_files_func(file_paths, output_path):
113
  if not PYDUB_AVAILABLE:
114
- return None, "خطا: کتابخانه pydub برای ادغام فایل‌ها در دسترس نیست."
115
- if not file_paths: return None, "خطا: هیچ فایلی برای ادغام وجود ندارد."
116
- output_dir = os.path.dirname(output_path)
117
- if output_dir: os.makedirs(output_dir, exist_ok=True)
118
  try:
119
- log_messages = [f"🔗 در حال ادغام {len(file_paths)} فایل صوتی..."]
120
  combined = AudioSegment.empty()
121
- valid_files_merged = 0
122
  for i, file_path in enumerate(file_paths):
123
- if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
124
- try:
125
- log_messages.append(f"📎 اضافه کردن فایل {i+1}: {os.path.basename(file_path)}")
126
- audio = AudioSegment.from_file(file_path)
127
- combined += audio
128
- if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
129
- valid_files_merged += 1
130
- except Exception as e:
131
- log_messages.append(f"⚠️ خطا در خواندن یا اضافه کردن فایل {os.path.basename(file_path)}: {e}")
132
  else:
133
- log_messages.append(f"⚠️ فایل پیدا نشد یا خالی است: {os.path.basename(file_path)}")
134
- if valid_files_merged == 0:
135
- return None, "\n".join(log_messages) + "\n❌ هیچ فایل معتبری برای ادغام پیدا نشد."
136
- combined.export(output_path, format="wav")
137
- log_messages.append(f"✅ فایل ادغام شده ذخیره شد: {os.path.basename(output_path)}")
138
- return output_path, "\n".join(log_messages)
139
  except Exception as e:
140
- return None, f"❌ خطا در ادغام فایل‌ها: {e}"
 
141
 
142
- def create_zip_file(file_paths, zip_name_base):
143
- output_dir = "outputs"
144
- os.makedirs(output_dir, exist_ok=True)
145
- zip_path = os.path.join(output_dir, f"{zip_name_base}.zip")
146
  try:
147
- with zipfile.ZipFile(zip_path, 'w') as zipf:
148
  for file_path in file_paths:
149
  if os.path.exists(file_path):
150
  zipf.write(file_path, os.path.basename(file_path))
151
- return zip_path, f"📦 فایل ZIP ایجاد شد: {os.path.basename(zip_path)}"
 
152
  except Exception as e:
153
- return None, f"❌ خطا در ایجاد فایل ZIP: {e}"
154
-
155
- def cleanup_temp_files(files_to_delete):
156
- if not files_to_delete: return
157
- for f_path in files_to_delete:
158
- if f_path and os.path.exists(f_path):
159
- try:
160
- os.remove(f_path)
161
- except Exception as e:
162
- print(f"⚠️ خطا در حذف فایل {f_path}: {e}")
163
-
164
- # --- Main generation function for Gradio ---
165
- def generate_audio_gradio(
166
- use_file_input, text_file_obj, text_to_speak, speech_prompt,
167
- selected_voice, output_filename_base, model_name, temperature,
168
- max_chunk_size, sleep_between_requests, merge_audio_files, delete_partial_files,
169
- progress=gr.Progress(track_tqdm=True)
170
  ):
171
- log_messages = ["🚀 شروع فرآیند تبدیل متن به گفتار...\n"]
172
- output_main_dir = "outputs"
173
- os.makedirs(output_main_dir, exist_ok=True)
174
-
175
- if os.path.exists(output_main_dir) and output_filename_base:
176
- for item in os.listdir(output_main_dir):
177
- if item.startswith(output_filename_base):
178
- try:
179
- item_path = os.path.join(output_main_dir, item)
180
- if os.path.isfile(item_path) or os.path.islink(item_path): os.unlink(item_path)
181
- elif os.path.isdir(item_path): shutil.rmtree(item_path)
182
- log_messages.append(f"🗑️ فایل قدیمی '{item}' حذف شد.")
183
- except Exception as e:
184
- log_messages.append(f"⚠️ خطا در حذف فایل قدیمی '{item}': {e}")
185
-
186
- text_input = load_text_from_gradio_file(text_file_obj) if use_file_input else text_to_speak
187
- if use_file_input:
188
- log_messages.append(f"📁 حالت فایل فعال است.")
189
- if text_file_obj is None:
190
- log_messages.append("❌ خطا: هیچ فایلی آپلود نشده است.")
191
- return gr.update(visible=False), gr.update(visible=False), "\n".join(log_messages)
192
- if not text_input:
193
- log_messages.append("❌ خطا: متن استخراج شده از فایل خالی است.")
194
- return gr.update(visible=False), gr.update(visible=False), "\n".join(log_messages)
195
- log_messages.append(f"✅ متن از '{os.path.basename(text_file_obj.name)}' با موفقیت بارگذاری شد.")
196
- else:
197
- log_messages.append("⌨️ حالت ورودی دستی فعال است.")
198
-
199
- if not text_input or not text_input.strip():
200
- log_messages.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است.")
201
- return gr.update(visible=False), gr.update(visible=False), "\n".join(log_messages)
202
-
203
- api_key_env = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
204
- if not api_key_env:
205
- log_messages.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY یا GOOGLE_API_KEY) در Secrets پیدا نشد.")
206
- log_messages.append("لطفاً کلید API خود را در بخش Settings -> Secrets این Space تنظیم کنید.")
207
- return gr.update(visible=False), gr.update(visible=False), "\n".join(log_messages)
208
-
209
- log_messages.append("🔑 کلید API از Hugging Face Secrets بارگذاری شد (کتابخانه باید آن را به طور خودکار بخواند).")
210
-
211
- text_chunks = smart_text_split(text_input, int(max_chunk_size))
212
- if not text_chunks:
213
- log_messages.append("❌ خطا: پس از تقسیم‌بندی، هیچ قطعه متنی برای پردازش وجود ندارد.")
214
- return gr.update(visible=False), gr.update(visible=False), "\n".join(log_messages)
215
 
216
- log_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد:")
217
- for i_chunk, chunk_content in enumerate(text_chunks): log_messages.append(f" 📝 قطعه {i_chunk+1}: {len(chunk_content)} کاراکتر - '{chunk_content[:50].replace(chr(10), ' ')}...'")
 
 
 
 
218
 
219
- generated_files = []
220
- total_chunks = len(text_chunks)
221
-
222
  try:
223
- log_messages.append(f"🛠️ در حال آماده‌سازی مدل جمینای ({model_name})...")
224
- generative_model_client = genai.GenerativeModel(model_name=model_name)
225
- log_messages.append(f"✅ مدل جمینای ({model_name}) با موفقیت آماده شد.")
 
226
  except Exception as e:
227
- log_messages.append(f"❌ خطا در آماده‌سازی مدل جمینای ({model_name}): {type(e).__name__} - {e}")
228
- if "API_KEY_INVALID" in str(e).upper() or "API key" in str(e).lower() or "credential" in str(e).lower() or "permission_denied" in str(e).lower():
229
- log_messages.append(" 🔑 مشکل احتمالی با کلید API یا دسترسی‌ها. مطمئن شوید GEMINI_API_KEY (یا GOOGLE_API_KEY) در Secrets صحیح و فعال است و به مدل دسترسی دارید.")
230
- import traceback
231
- log_messages.append(f" Traceback: {traceback.format_exc()}")
232
- return gr.update(visible=False), gr.update(visible=False), "\n".join(log_messages)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
- for i, chunk_text in enumerate(progress.tqdm(text_chunks, desc="تولید قطعات صوتی")):
236
- current_log = [f"\n🔊 تولید صدا برای قطعه {i+1}/{total_chunks}..."]
237
- final_text_for_api = f"{speech_prompt}\n{chunk_text}" if speech_prompt and speech_prompt.strip() else chunk_text
238
- current_log.append(f" متن ارسالی به API (اول 50 کاراکتر): '{final_text_for_api[:50].replace(chr(10), ' ')}...'")
239
 
240
  try:
241
- contents_for_api = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
242
- gen_config = types.GenerateContentConfig(
243
- temperature=float(temperature),
244
- response_modalities=["audio"],
245
- speech_config=types.SpeechConfig(
246
- voice_config=types.VoiceConfig(
247
- prebuilt_voice_config=types.PrebuiltVoiceConfig(
248
- voice_name=selected_voice
249
- )
250
- )
251
- )
252
- )
253
-
254
- response = generative_model_client.generate_content(
255
- contents=contents_for_api,
256
- generation_config=gen_config
257
  )
258
 
259
- audio_data_buffer = b""
260
- final_mime_type = "audio/wav"
261
- audio_part_found = False
262
-
263
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
264
- for part in response.candidates[0].content.parts:
265
- if part.inline_data and part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
266
- audio_data_buffer = part.inline_data.data
267
- final_mime_type = part.inline_data.mime_type
268
- audio_part_found = True
269
- current_log.append(f" دریافت داده صوتی با MIME-type: {final_mime_type}")
270
- break
271
-
272
- if not audio_part_found:
273
- current_log.append(f" ⚠️ داده صوتی برای قطعه {i+1} در پاسخ API یافت نشد.")
274
- if hasattr(response, 'text') and response.text:
275
- current_log.append(f" متن پاسخ API: {response.text}")
276
- if hasattr(response, 'prompt_feedback') and response.prompt_feedback and response.prompt_feedback.block_reason:
277
- current_log.append(f" مسدودسازی پرامپت: {response.prompt_feedback.block_reason.name} - {response.prompt_feedback.block_reason_message}")
278
- current_log.append(f" پاسخ کامل (اول 200 کاراکتر): {str(response)[:200]}")
279
-
280
-
281
- if audio_data_buffer:
282
- file_extension = mimetypes.guess_extension(final_mime_type)
283
- if not file_extension or file_extension.lower() == ".bin":
284
- file_extension = ".wav"
285
 
286
- processed_data_buffer = audio_data_buffer
 
 
287
 
288
- chunk_filename_base = f"{output_filename_base}_part_{i+1:03d}"
289
- generated_file_path = save_binary_file(f"{chunk_filename_base}{file_extension}", processed_data_buffer)
290
- generated_files.append(generated_file_path)
291
- current_log.append(f"✅ قطعه {i+1} تولید و ذخیره شد: {os.path.basename(generated_file_path)}")
 
 
 
 
 
 
 
 
 
 
 
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  except Exception as e:
294
- current_log.append(f"❌ خطا در تولید قطعه {i+1}: {type(e).__name__} - {e}")
295
- if "API key" in str(e).lower() or "permission_denied" in str(e).lower() or "quota" in str(e).lower():
296
- current_log.append(" 🔑 مشکل احتمالی با کلید API، دسترسی به مدل، یا محدودیت‌های استفاده. لطفاً تنظیمات Secrets و مدل انتخابی را بررسی کنید و از محدودیت‌های حساب خود مطلع باشید.")
297
- import traceback
298
- current_log.append(f" Traceback: {traceback.format_exc()}")
299
-
300
- log_messages.extend(current_log)
301
- if i < total_chunks - 1 and float(sleep_between_requests) > 0:
302
- log_messages.append(f"⏱️ انتظار {float(sleep_between_requests)} ثانیه...")
303
- time.sleep(float(sleep_between_requests))
304
-
 
305
  if not generated_files:
306
- log_messages.append("\n❌ هیچ فایل صوتی تولید نشد!")
307
- return gr.update(value=None, visible=False), gr.update(value=None, visible=False), "\n".join(log_messages)
308
-
309
- log_messages.append(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!")
310
- final_audio_output_path = None
311
- zip_file_output_path = None
312
- audio_output_visible = False
313
- file_output_visible = False
314
-
315
- if merge_audio_files and len(generated_files) > 1:
316
- merged_filename_path = os.path.join(output_main_dir, f"{output_filename_base}_merged.wav")
317
- merged_path, merge_log = merge_audio_files_func(generated_files, merged_filename_path)
318
- log_messages.append(merge_log or "")
319
- if merged_path:
320
- final_audio_output_path = merged_path
321
- audio_output_visible = True
322
- file_output_visible = True
323
- if delete_partial_files:
324
- cleanup_temp_files([f for f in generated_files if f != final_audio_output_path])
325
- else:
326
- zip_file_output_path, zip_log = create_zip_file(generated_files, f"{output_filename_base}_all_parts")
327
- log_messages.append(zip_log or "")
328
- if zip_file_output_path: file_output_visible = True
329
- if generated_files: final_audio_output_path = generated_files[0]; audio_output_visible = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  elif len(generated_files) == 1:
331
- final_audio_output_path = generated_files[0]
332
- audio_output_visible = True
333
- file_output_visible = True
334
- else:
335
- if not merge_audio_files and len(generated_files) > 1:
336
- zip_file_output_path, zip_log = create_zip_file(generated_files, f"{output_filename_base}_all_parts")
337
- log_messages.append(zip_log or "")
338
- if zip_file_output_path: file_output_visible = True
339
- if generated_files: final_audio_output_path = generated_files[0]; audio_output_visible = True
340
- elif generated_files:
341
- final_audio_output_path = generated_files[0]; audio_output_visible = True; file_output_visible = True
342
-
343
-
344
- primary_audio_to_play = final_audio_output_path
345
- downloadable_file = zip_file_output_path if zip_file_output_path else final_audio_output_path
346
-
347
- return (
348
- gr.update(value=primary_audio_to_play, visible=audio_output_visible),
349
- gr.update(value=downloadable_file, visible=file_output_visible),
350
- "\n".join(log_messages)
351
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- # --- Gradio Interface ---
354
- css = """
355
- body { font-family: 'Tahoma', 'Arial', sans-serif; }
356
- .gradio-container { max-width: 850px !important; margin: auto !important; padding-top: 1.5rem; }
357
- footer { display: none !important; } .gr-button { min-width: 180px; }
358
- h1 { text-align: center; color: #2E7D32; margin-bottom: 0.5rem;}
359
- .gr-input, .gr-output {border-radius: 8px !important;}
360
- .small-info {font-size: 0.85em; color: #555; margin-top: -5px; margin-bottom: 10px;}
361
- .output-header { margin-top: 20px; }
362
- """
363
- speaker_choices = ["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"]
364
 
365
- # Using model names EXACTLY from the user's FIRST Colab code block:
366
- # #@param ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
367
- model_choices = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"] # <<<< THIS IS NOW CORRECTED TO "2.5"
368
- selected_model_default = model_choices[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
 
 
 
 
 
370
 
371
- aigolden_logo_encoded = "Q3JlYXRlIGJ5IDogYWlnb2xkZW4="
372
- try: aigolden_logo_decoded = base64.b64decode(aigolden_logo_encoded.encode()).decode()
373
- except: aigolden_logo_decoded = "Created by: aigolden"
 
 
374
 
375
- with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="green", secondary_hue="lime")) as demo:
376
- gr.Markdown(f"<h1>تبدیل متن به گفتار با Gemini گوگل</h1>")
377
- gr.Markdown(f"<p style='text-align: center; color: gray; margin-bottom: 20px;'>{aigolden_logo_decoded}</p>")
378
-
379
- with gr.Accordion("⚠️ راهنما و نکات مهم", open=False):
380
- gr.Markdown("- **کلید API:** کلید API خود از Google AI Studio را در بخش `Settings` -> `Secrets` این Space با نام `GEMINI_API_KEY` (یا `GOOGLE_API_KEY`) ذخیره کنید.")
381
- gr.Markdown("- **مدل‌ها:** مطمئن شوید مدل‌های انتخابی (`gemini-X.X-...-tts`) برای حساب شما فعال و قابل دسترس هستند.")
382
- gr.Markdown("- **محدودیت‌ها:** API گوگل ممکن است محدودیت درخواست (quota) داشته باشد. در صورت خطا، صبر کرده و دوباره تلاش کنید، یا فاصله زمانی بین درخواست‌ها را افزایش دهید.")
383
- gr.Markdown("- **فایل‌های حجیم:** پردازش متن‌های بسیار طولانی ممکن است زمان‌بر باشد و به محدودیت‌های منابع در Spaces برخورد کند.")
384
- gr.Markdown("- **پاکسازی:** فایل‌های قدیمی با نام مشابه قبل از هر اجرای جدید پاک می‌شوند.")
385
 
386
- with gr.Row():
387
- with gr.Column(scale=3):
388
- gr.Markdown("### ⚙️ تنظیمات اصلی")
389
- use_file_input_cb = gr.Checkbox(label="استفاده از فایل متنی ورودی (.txt)", value=False)
390
- gr.Markdown("اگر فعال شود، متن از فایل آپلود شده خوانده می‌شود.", elem_classes="small-info")
391
-
392
- text_file_upload = gr.File(label="آپلود فایل متنی (.txt)", file_types=[".txt"], visible=False)
393
- text_to_speak_input = gr.Textbox(lines=7, label="متن برای تبدیل به گفتار", placeholder="متن خود را اینجا وارد کنید...", visible=True)
394
 
395
- def toggle_input_method(use_file):
396
- return {text_file_upload: gr.update(visible=use_file), text_to_speak_input: gr.update(visible=not use_file)}
397
- use_file_input_cb.change(toggle_input_method, inputs=use_file_input_cb, outputs=[text_file_upload, text_to_speak_input])
 
 
 
 
 
 
 
398
 
399
- speech_prompt_input = gr.Textbox(label="پرامپت راهنمای سبک گفتار (اختیاری)", placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای")
400
- gr.Markdown("این پرامپت به تنظیم سبک و لحن گفتار کمک می‌کند.", elem_classes="small-info")
401
-
402
- output_filename_base_input = gr.Textbox(label="نام پایه فایل خروجی (بدون پسوند)", value="gemini_tts_output")
403
- gr.Markdown("برای نامگذاری فایل‌های صوتی تولید شده استفاده می‌شود.", elem_classes="small-info")
404
-
405
- with gr.Column(scale=2):
406
- gr.Markdown("### 🗣️ تنظیمات مدل و گوینده")
407
- model_name_dd = gr.Dropdown(model_choices, label="انتخاب مدل Gemini TTS", value=selected_model_default)
408
- gr.Markdown("مطمئن شوید این مدل‌ها برای TTS بهینه شده و برای شما فعال هستند.", elem_classes="small-info")
409
-
410
- speaker_voice_dd = gr.Dropdown(speaker_choices, label="انتخاب گوینده", value="Charon")
411
- gr.Markdown("گوینده مورد نظر را برای تولید صدا انتخاب کنید.", elem_classes="small-info")
412
-
413
- temperature_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.7, label="دمای مدل (Temperature)")
414
- gr.Markdown("مقادیر بالاتر = خلاقیت بیشتر، مقادیر پایین‌تر = قابل پیش‌بینی‌تر.", elem_classes="small-info")
415
 
416
- gr.Markdown("### ✂️ تقسیم‌بندی و خروجی")
417
- max_chunk_size_slider = gr.Slider(minimum=1000, maximum=4000, step=100, value=3800, label="حداکثر کاراکتر در هر قطعه")
418
- gr.Markdown("متن‌های طولانی به قطعات کوچکتر تقسیم می‌شوند.", elem_classes="small-info")
 
419
 
420
- sleep_between_requests_slider = gr.Slider(minimum=1, maximum=30, step=0.5, value=10, label="فاصله زمانی بین درخواست‌ها (ثانیه)")
421
- gr.Markdown("برای جلوگیری از خطاهای محدودیت API (quota errors).", elem_classes="small-info")
 
 
 
 
 
 
 
422
 
423
- merge_audio_files_cb = gr.Checkbox(label="ادغام فایل‌های صوتی جزئی", value=True)
424
- gr.Markdown("ادغام فایل‌های صوتی بخش‌های مختلف متن طولانی در یک فایل WAV.", elem_classes="small-info")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- delete_partial_files_cb = gr.Checkbox(label="حذف فایل‌های جزئی پس از ادغام", value=True)
427
- gr.Markdown("فقط در صورتی اعمال می‌شود که ادغام فایل‌ها فعال و موفق باشد.", elem_classes="small-info")
 
 
 
 
 
 
 
 
 
 
428
 
429
- submit_button = gr.Button("🎧 تبدیل متن به گفتار 🎧", variant="primary", scale=1)
430
-
431
- gr.Markdown("### 🔊 خروجی و گزارش", elem_classes="output-header")
 
 
 
 
 
432
 
433
- with gr.Row():
434
- audio_output = gr.Audio(label="فایل صوتی نهایی", type="filepath", visible=False)
435
- file_download_output = gr.File(label="دانلود فایل (ادغام شده یا ZIP)", type="filepath", visible=False)
436
 
437
- status_output = gr.Textbox(label="وضعیت و گزارش‌ها", lines=10, interactive=False, show_copy_button=True)
 
 
 
 
 
 
 
 
 
 
 
438
 
439
- submit_button.click(
440
- generate_audio_gradio,
 
 
 
 
441
  inputs=[
442
- use_file_input_cb, text_file_upload, text_to_speak_input, speech_prompt_input,
443
- speaker_voice_dd, output_filename_base_input, model_name_dd, temperature_slider,
444
- max_chunk_size_slider, sleep_between_requests_slider, merge_audio_files_cb, delete_partial_files_cb
 
 
445
  ],
446
- outputs=[audio_output, file_download_output, status_output]
 
 
447
  )
 
 
 
 
 
 
 
 
448
 
449
  if __name__ == "__main__":
450
- if not os.path.exists("outputs"): os.makedirs("outputs")
451
- demo.launch(debug=True)
 
1
+ import gradio as gr
2
  import base64
3
  import mimetypes
4
  import os
 
6
  import struct
7
  import time
8
  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",
22
+ "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib",
23
+ "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
24
+ "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
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:
32
+ with open(file_name, "wb") as f:
33
+ f.write(data)
34
+ log_messages_list.append(f"✅ فایل در مسیر زیر ذخیره شد: {file_name}")
35
+ return file_name
36
+ except Exception as e:
37
+ log_messages_list.append(f" خطا در ذخیره فایل {file_name}: {e}")
38
+ return None
39
 
40
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
41
  parameters = parse_audio_mime_type(mime_type)
 
47
  block_align = num_channels * bytes_per_sample
48
  byte_rate = sample_rate * block_align
49
  chunk_size = 36 + data_size
50
+
51
  header = struct.pack(
52
  "<4sI4s4sIHHIIHH4sI",
53
  b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
 
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()
 
65
  try:
66
  rate_str = param.split("=", 1)[1]
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):
 
74
  pass
75
  return {"bits_per_sample": bits_per_sample, "rate": rate}
76
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def smart_text_split(text, max_size=3800):
78
+ if len(text) <= max_size:
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):
110
  if not PYDUB_AVAILABLE:
111
+ log_messages_list.append("❌ pydub در دسترس نیست. نمی‌توان فایل‌ها را ادغام کرد.")
112
+ return False
 
 
113
  try:
114
+ log_messages_list.append(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...")
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")
126
+ log_messages_list.append(f" فایل ادغام شده ذخیره شد: {output_path}")
127
+ return True
 
 
128
  except Exception as e:
129
+ log_messages_list.append(f"❌ خطا در ادغام فایل‌ها: {e}")
130
+ return False
131
 
132
+ def create_zip_file(file_paths, zip_name, log_messages_list):
 
 
 
133
  try:
134
+ with zipfile.ZipFile(zip_name, 'w') as zipf:
135
  for file_path in file_paths:
136
  if os.path.exists(file_path):
137
  zipf.write(file_path, os.path.basename(file_path))
138
+ log_messages_list.append(f"📦 فایل ZIP ایجاد شد: {zip_name}")
139
+ return True
140
  except Exception as e:
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,
148
+ max_chunk, sleep_time, merge_files, delete_partials,
149
+ log_messages_list # Pass the list to append logs
 
 
 
 
 
 
 
 
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)}...")
196
+ final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
197
+
198
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
199
+ generate_content_config = types.GenerateContentConfig(
200
+ temperature=temperature_val,
201
+ response_modalities=["audio"],
202
+ speech_config=types.SpeechConfig(
203
+ voice_config=types.VoiceConfig(
204
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)
205
+ )
206
+ ),
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,
 
 
 
 
 
 
 
 
 
 
 
219
  )
220
 
221
+ if (response.candidates and response.candidates[0].content and
222
+ response.candidates[0].content.parts and
223
+ response.candidates[0].content.parts[0].inline_data):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
274
  if not generated_files:
275
+ log_messages_list.append("❌ هیچ فایل صوتی تولید نشد!")
276
+ return None, None
277
+
278
+ log_messages_list.append(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!")
279
+
280
+ playback_file = None
281
+ download_file = None
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}")
361
+ return None, None, "\n".join(log_messages)
362
+ else:
363
+ log_messages.append("❌ خطا: گزینه 'استفاده از فایل ورودی' انتخاب شده اما هیچ فایلی آپلود نشده است.")
364
+ return None, None, "\n".join(log_messages)
365
+ else:
366
+ actual_text_input = text_to_speak
367
+ if not actual_text_input or not actual_text_input.strip():
368
+ log_messages.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است. لطفاً متنی را وارد کنید یا گزینه فایل را فعال کنید.")
369
+ return None, None, "\n".join(log_messages)
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=[
500
+ use_file_input_cb, uploaded_file_input, text_to_speak_tb,
501
+ speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
502
+ model_name_dd, temperature_slider,
503
+ max_chunk_size_slider, sleep_between_requests_slider,
504
+ merge_audio_files_cb, delete_partial_files_cb
505
+ ],
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,
517
+ speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
518
+ model_name_dd, temperature_slider,
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