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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +247 -274
app.py CHANGED
@@ -6,7 +6,7 @@ import re
6
  import struct
7
  import time
8
  import zipfile
9
- from google import genai
10
  from google.genai import types
11
 
12
  # تلاش برای ایمپورت pydub و تنظیم فلگ در دسترس بودن
@@ -15,6 +15,8 @@ try:
15
  PYDUB_AVAILABLE = True
16
  except ImportError:
17
  PYDUB_AVAILABLE = False
 
 
18
 
19
  # --- ثابت‌ها ---
20
  SPEAKER_VOICES = [
@@ -24,18 +26,26 @@ SPEAKER_VOICES = [
24
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
25
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
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:
33
  with open(file_name, "wb") as f:
34
  f.write(data)
35
- log_messages_list.append(f"✅ فایل در مسیر زیر ذخیره شد: {file_name}")
36
  return file_name
37
  except Exception as e:
38
- log_messages_list.append(f"❌ خطا در ذخیره فایل {file_name}: {e}")
39
  return None
40
 
41
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
@@ -48,7 +58,6 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
48
  block_align = num_channels * bytes_per_sample
49
  byte_rate = sample_rate * block_align
50
  chunk_size = 36 + data_size
51
-
52
  header = struct.pack(
53
  "<4sI4s4sIHHIIHH4sI",
54
  b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
@@ -58,7 +67,7 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
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()
@@ -66,137 +75,124 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
66
  try:
67
  rate_str = param.split("=", 1)[1]
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):
75
- pass
76
  return {"bits_per_sample": bits_per_sample, "rate": rate}
77
 
78
- def smart_text_split(text, max_size=3800):
79
  if len(text) <= max_size:
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):
120
  if not PYDUB_AVAILABLE:
121
- log_messages_list.append("❌ pydub در دسترس نیست. نمی‌توان فایل‌ها را ادغام کرد.")
122
  return False
123
  try:
124
- log_messages_list.append(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...")
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")
136
- log_messages_list.append(f"✅ فایل ادغام شده ذخیره شد: {output_path}")
137
  return True
138
  except Exception as e:
139
- log_messages_list.append(f"❌ خطا در ادغام فایل‌ها: {e}")
140
  return False
141
 
142
- def create_zip_file(file_paths, zip_name, log_messages_list):
143
  try:
144
  with zipfile.ZipFile(zip_name, 'w') as zipf:
145
  for file_path in file_paths:
146
  if os.path.exists(file_path):
147
  zipf.write(file_path, os.path.basename(file_path))
148
- log_messages_list.append(f"📦 فایل ZIP ایجاد شد: {zip_name}")
149
  return True
150
  except Exception as e:
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,
158
- max_chunk, sleep_time, merge_files, delete_partials,
159
- log_messages_list # Pass the list to append logs
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)}...")
200
  final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
201
 
202
  contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
@@ -213,8 +209,8 @@ def core_generate_audio(
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,
220
  )
@@ -225,330 +221,307 @@ def core_generate_audio(
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
 
276
  if not generated_files:
277
- log_messages_list.append("❌ هیچ فایل صوتی تولید نشد!")
278
- return None, None
279
 
280
- log_messages_list.append(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!")
281
 
282
  playback_file = None
283
  download_file = None
 
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}")
358
- return None, None, "\n".join(log_messages)
359
  else:
360
- log_messages.append("خطا: گزینه 'استفاده از فایل ورودی' انتخاب شده اما هیچ فایلی آپلود نشده است.")
361
- return None, None, "\n".join(log_messages)
362
  else:
363
  actual_text_input = text_to_speak
364
  if not actual_text_input or not actual_text_input.strip():
365
- log_messages.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است. لطفاً متنی را وارد کنید یا گزینه فایل را فعال کنید.")
366
- return None, None, "\n".join(log_messages)
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=[
515
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
516
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
517
- model_name_dd, temperature_slider,
518
- max_chunk_size_slider, sleep_between_requests_slider,
519
- merge_audio_files_cb, delete_partial_files_cb
520
  ],
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,
532
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
533
- model_name_dd, temperature_slider,
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()
 
6
  import struct
7
  import time
8
  import zipfile
9
+ from google import genai # همچنان برای ارتباط با API لازم است
10
  from google.genai import types
11
 
12
  # تلاش برای ایمپورت pydub و تنظیم فلگ در دسترس بودن
 
15
  PYDUB_AVAILABLE = True
16
  except ImportError:
17
  PYDUB_AVAILABLE = False
18
+ # اگر pydub نباشد، ادغام خودکار ممکن نیست و باید به کاربر اطلاع داده شود یا فقط اولین قطعه ارائه شود.
19
+ # برای این نسخه، فرض می‌کنیم اگر pydub نباشد، فقط اولین قطعه برگردانده می‌شود یا ZIP ارائه می‌شود.
20
 
21
  # --- ثابت‌ها ---
22
  SPEAKER_VOICES = [
 
26
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
27
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
28
  ]
29
+ # مدل ثابت است و دیگر نیازی به لیست مدل‌ها نیست
30
+ FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
31
 
32
+ # --- توابع کمکی (بدون تغییر زیاد، فقط لاگ‌ها دیگر به کاربر نمایش داده نمی‌شوند) ---
33
+ # لاگ‌ها همچنان در لیست جمع‌آوری می‌شوند اما در خروجی نهایی Gradio نمایش داده نمی‌شوند.
34
 
35
+ def _log(message, log_list):
36
+ """تابع داخلی برای افزودن پیام به لیست لاگ‌ها."""
37
+ # print(message) # برای دیباگ در کنسول Hugging Face Spaces مفید است
38
+ log_list.append(message)
39
+
40
+
41
+ def save_binary_file(file_name, data, log_list):
42
  try:
43
  with open(file_name, "wb") as f:
44
  f.write(data)
45
+ _log(f"✅ فایل در مسیر زیر ذخیره شد: {file_name}", log_list)
46
  return file_name
47
  except Exception as e:
48
+ _log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
49
  return None
50
 
51
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
 
58
  block_align = num_channels * bytes_per_sample
59
  byte_rate = sample_rate * block_align
60
  chunk_size = 36 + data_size
 
61
  header = struct.pack(
62
  "<4sI4s4sIHHIIHH4sI",
63
  b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
 
67
 
68
  def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
69
  bits_per_sample = 16
70
+ rate = 24000
71
  parts = mime_type.split(";")
72
  for param in parts:
73
  param = param.strip()
 
75
  try:
76
  rate_str = param.split("=", 1)[1]
77
  rate = int(rate_str)
78
+ except (ValueError, IndexError): pass
79
+ elif param.startswith("audio/L"):
 
80
  try:
81
  bits_per_sample = int(param.split("L", 1)[1])
82
+ except (ValueError, IndexError): pass
 
83
  return {"bits_per_sample": bits_per_sample, "rate": rate}
84
 
85
+ def smart_text_split(text, max_size=3800, log_list=None):
86
  if len(text) <= max_size:
87
  return [text]
88
  chunks = []
89
  current_chunk = ""
 
90
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
 
91
  for sentence in sentences:
92
+ sentence_with_space = sentence + " "
93
  if len(current_chunk) + len(sentence_with_space) > max_size:
94
+ if current_chunk:
95
  chunks.append(current_chunk.strip())
 
 
96
  current_chunk = sentence
97
  while len(current_chunk) > max_size:
 
 
98
  split_idx = -1
 
99
  possible_split_chars = ['،', ',', ';', ':', ' ']
100
+ for char_idx in range(max_size - 1, max_size // 2, -1):
101
  if current_chunk[char_idx] in possible_split_chars:
102
  split_idx = char_idx + 1
103
  break
 
104
  if split_idx != -1:
105
  chunks.append(current_chunk[:split_idx].strip())
106
  current_chunk = current_chunk[split_idx:].strip()
107
+ else:
108
  chunks.append(current_chunk[:max_size].strip())
109
  current_chunk = current_chunk[max_size:].strip()
110
  else:
111
  current_chunk += (" " if current_chunk else "") + sentence
112
+ if current_chunk:
 
113
  chunks.append(current_chunk.strip())
114
+
115
+ final_chunks = [c for c in chunks if c]
116
+ if log_list: # Optional logging
117
+ _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
118
+ for i, chunk_text in enumerate(final_chunks):
119
+ _log(f"📝 قطعه {i+1}: {len(chunk_text)} کاراکتر", log_list)
120
+ return final_chunks
121
 
 
122
 
123
+ def merge_audio_files_func(file_paths, output_path, log_list):
124
  if not PYDUB_AVAILABLE:
125
+ _log("❌ pydub در دسترس نیست. نمی‌توان فایل‌ها را ادغام کرد.", log_list)
126
  return False
127
  try:
128
+ _log(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...", log_list)
129
  combined = AudioSegment.empty()
130
  for i, file_path in enumerate(file_paths):
131
  if os.path.exists(file_path):
132
+ _log(f"📎 اضافه کردن فایل {i+1}: {os.path.basename(file_path)}", log_list)
133
+ audio = AudioSegment.from_file(file_path)
134
  combined += audio
135
+ if i < len(file_paths) - 1:
136
+ combined += AudioSegment.silent(duration=200)
137
  else:
138
+ _log(f"⚠️ فایل پیدا نشد: {file_path}", log_list)
139
  combined.export(output_path, format="wav")
140
+ _log(f"✅ فایل ادغام شده ذخیره شد: {output_path}", log_list)
141
  return True
142
  except Exception as e:
143
+ _log(f"❌ خطا در ادغام فایل‌ها: {e}", log_list)
144
  return False
145
 
146
+ def create_zip_file(file_paths, zip_name, log_list):
147
  try:
148
  with zipfile.ZipFile(zip_name, 'w') as zipf:
149
  for file_path in file_paths:
150
  if os.path.exists(file_path):
151
  zipf.write(file_path, os.path.basename(file_path))
152
+ _log(f"📦 فایل ZIP ایجاد شد: {zip_name}", log_list)
153
  return True
154
  except Exception as e:
155
+ _log(f"❌ خطا در ایجاد فایل ZIP: {e}", log_list)
156
  return False
157
 
158
+ # --- تابع اصلی تولید (با تغییرات درخواستی) ---
159
  def core_generate_audio(
160
  text_input, prompt_input, selected_voice, output_base_name,
161
+ temperature_val, max_chunk, sleep_time,
162
+ log_list # Pass the list to append internal logs
 
163
  ):
164
+ _log("🚀 شروع فرآیند تبدیل متن به گفتار با هوش مصنوعی آلفا...", log_list)
165
 
166
+ api_key = os.environ.get("GEMINI_API_KEY") # نام Secret در HF Spaces
 
167
  if not api_key:
168
+ _log("❌ خطا: کلید API برای هوش مصنوعی آلفا (GEMINI_API_KEY) در Secrets این Space تنظیم نشده است.", log_list)
169
+ # چون لاگ نمایش داده نمی‌شود، کاربر فقط خروجی خالی دریافت می‌کند.
170
+ # بهتر است یک پیام خطا به کاربر نمایش داده شود اگر کلید نیست.
171
+ # اما طبق درخواست، لاگ حذف شده، پس فعلا اینطور می‌ماند.
172
+ return None, None, "خطا: کلید API تنظیم نشده است. لطفاً با مدیر تماس بگیرید." # پیام خطا برای کاربر
173
 
 
174
  try:
175
+ _log("🛠️ در حال ایجاد کلاینت هوش مصنوعی آلفا...", log_list)
176
  client = genai.Client(api_key=api_key)
177
+ _log("✅ کلاینت با موفقیت ایجاد شد.", log_list)
178
  except Exception as e:
179
+ _log(f"❌ خطا در ایجاد کلاینت: {e}", log_list)
180
+ return None, None, "خطا در اتصال به سرویس هوش مصنوعی. لطفاً بعداً تلاش کنید."
 
181
 
182
  if not text_input or text_input.strip() == "":
183
+ _log("❌ خطا: متن ورودی خالی است.", log_list)
184
+ return None, None, "خطا: لطفاً متنی را برای تبدیل وارد کنید."
 
 
 
 
 
 
185
 
186
+ text_chunks = smart_text_split(text_input, max_chunk, log_list)
187
  if not text_chunks:
188
+ _log("❌ خطا: هیچ قطعه متنی برای پردازش وجود ندارد.", log_list)
189
+ return None, None, "خطا: مشکلی در پردازش متن ورودی پیش آمد."
190
 
191
  generated_files = []
192
+ model_to_use = FIXED_MODEL_NAME # استفاده از مدل ثابت
 
193
 
194
  for i, chunk in enumerate(text_chunks):
195
+ _log(f"\n🔊 تولید صدا برای قطعه {i+1}/{len(text_chunks)} با مدل {model_to_use}...", log_list)
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)])]
 
209
  current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
210
 
211
  try:
212
+ response = client.models.generate_content(
213
+ model=model_to_use,
214
  contents=contents,
215
  config=generate_content_config,
216
  )
 
221
 
222
  inline_data = response.candidates[0].content.parts[0].inline_data
223
  data_buffer = inline_data.data
 
224
  file_extension = mimetypes.guess_extension(inline_data.mime_type)
225
 
 
 
226
  if file_extension is None or "binary" in inline_data.mime_type or file_extension == ".bin":
227
  file_extension = ".wav"
228
+ if "audio/L" in inline_data.mime_type:
229
  data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
230
+ elif inline_data.mime_type == "audio/mpeg": file_extension = ".mp3"
231
+ elif inline_data.mime_type == "audio/wav": file_extension = ".wav"
 
 
232
 
233
+ generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_list)
234
  if generated_file_path:
235
  generated_files.append(generated_file_path)
236
+ _log(f"✅ قطعه {i+1} تولید شد: {os.path.basename(generated_file_path)}", log_list)
237
 
238
  elif response.text:
239
+ _log(f"ℹ️ پیام متنی از API برای قطعه {i+1}: {response.text}", log_list)
240
  if "rate limit" in response.text.lower() or "quota" in response.text.lower():
241
+ _log(f"⏳ محدودیت تعداد درخواست API. افزایش فاصله زمانی.", log_list)
242
+ # می‌توان sleep_time را به صورت پویا افزایش داد
243
 
244
  else:
245
+ _log(f"⚠️ پاسخ API برای قطعه {i+1} حاوی داده صوتی یا پیام متنی نبود. جزئیات: {response.prompt_feedback if response else 'No response'}", log_list)
 
246
 
247
  except types.generation_types.BlockedPromptException as bpe:
248
+ _log(f"❌ محتوای پرامپت برای قطعه {i+1} مسدود شد: {bpe}", log_list)
249
+ return None, None, "خطا: محتوای ورودی شما توسط سیستم ایمنی مسدود شد. لطفاً متن را تغییر دهید."
 
 
 
 
 
250
  except Exception as e:
251
+ _log(f"❌ خطا در تولید قطعه {i+1}: {e}", log_list)
252
+ if "API key not valid" in str(e): return None, None, "خطا: کلید API نامعتبر است."
253
+ elif "quota" in str(e).lower(): return None, None, "خطا: محدودیت استفاده از سرویس به پایان رسیده است."
254
+ # برای سایر خطاها، یک پیام عمومی
255
+ # return None, None, "خطا در تولید صدا. لطفاً دوباره ��لاش کنید." # این باعث توقف کل فرآیند می‌شود.
256
+ # بهتر است ادامه دهد و اگر هیچ فایلی تولید نشد، آنگاه پیام خطا بدهد.
257
+ continue # ادامه به قطعه بعدی
258
 
259
  if i < len(text_chunks) - 1 and len(text_chunks) > 1 :
260
+ _log(f"⏱️ انتظار {sleep_time} ثانیه...", log_list)
261
  time.sleep(sleep_time)
262
 
263
  if not generated_files:
264
+ _log("❌ هیچ فایل صوتی تولید نشد!", log_list)
265
+ return None, None, "متاسفانه هیچ فایل صوتی تولید نشد. لطفاً ورودی خود را بررسی کرده و مجدداً تلاش کنید."
266
 
267
+ _log(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!", log_list)
268
 
269
  playback_file = None
270
  download_file = None
271
+ user_message = "صدا با موفقیت تولید شد." # پیام پیش‌فرض
272
 
273
+ # ادغام خودکار و حذف فایل‌های جزئی
274
+ if len(generated_files) > 1:
275
+ if PYDUB_AVAILABLE:
276
+ merged_filename = f"{output_base_name}_final_audio.wav"
277
+ if merge_audio_files_func(generated_files, merged_filename, log_list):
 
 
 
 
 
278
  playback_file = merged_filename
279
  download_file = merged_filename
280
+ _log(f"🎵 فایل نهایی ادغام شده: {os.path.basename(merged_filename)}", log_list)
281
+ # حذف خودکار فایل‌های جزئی
282
+ for file_path in generated_files:
283
+ try:
284
+ if os.path.abspath(file_path) != os.path.abspath(merged_filename):
285
+ os.remove(file_path)
286
+ _log(f"🗑️ فایل جزئی حذف شد: {os.path.basename(file_path)}", log_list)
287
+ except Exception as e:
288
+ _log(f"⚠️ خطا در حذف فایل جزئی {os.path.basename(file_path)}: {e}", log_list)
 
289
  else:
290
+ _log("⚠️ ادغام ممکن نبود. فایل ZIP از قطعات ارائه می‌شود.", log_list)
291
+ user_message = "ادغام فایل‌ها ممکن نبود. فایل ZIP از قطعات صوتی برای دانلود آماده شد."
292
  zip_filename = f"{output_base_name}_all_parts.zip"
293
+ if create_zip_file(generated_files, zip_filename, log_list):
294
  download_file = zip_filename
295
+ if generated_files: playback_file = generated_files[0] # پخش اولین قطعه
296
+ else: # pydub در دسترس نیست
297
+ _log("⚠️ pydub برای ادغام در دسترس نیست. فایل ZIP از قطعات ارائه می‌شود.", log_list)
298
+ user_message = "فایل‌های صوتی به صورت جداگانه در یک فایل ZIP آماده شدند (امکان ادغام وجود نداشت)."
299
+ zip_filename = f"{output_base_name}_all_parts.zip"
300
+ if create_zip_file(generated_files, zip_filename, log_list):
301
+ download_file = zip_filename
302
+ if generated_files: playback_file = generated_files[0]
303
 
304
  elif len(generated_files) == 1:
305
  playback_file = generated_files[0]
306
  download_file = generated_files[0]
 
 
 
 
 
 
307
 
308
  if playback_file and not os.path.exists(playback_file):
309
+ _log(f"⚠️ فایل پخش {os.path.basename(playback_file)} وجود ندارد!", log_list)
310
  playback_file = None
311
  if download_file and not os.path.exists(download_file):
312
+ _log(f"⚠️ فایل دانلود {os.path.basename(download_file)} وجود ندارد!", log_list)
313
  download_file = None
314
+
315
+ if not playback_file and not download_file:
316
+ user_message = "خطا در تولید یا آماده‌سازی فایل نهایی."
317
 
318
+ return playback_file, download_file, user_message
319
 
320
+ # --- تابع رابط کاربری Gradio (با تغییرات درخواستی) ---
321
  def gradio_tts_interface(
322
  use_file_input, uploaded_file, text_to_speak,
323
  speech_prompt, speaker_voice, output_filename_base_in,
324
+ temperature, max_chunk_size, sleep_between_requests,
325
+ progress=gr.Progress(track_tqdm=True)
 
 
326
  ):
327
+ internal_logs = [] # لیست برای لاگ‌های داخلی، به کاربر نمایش داده نمی‌شود
328
 
329
  actual_text_input = ""
330
  if use_file_input:
331
  if uploaded_file is not None:
332
  try:
 
333
  with open(uploaded_file.name, 'r', encoding='utf-8') as f:
334
  actual_text_input = f.read().strip()
335
+ _log(f"✅ متن از فایل '{os.path.basename(uploaded_file.name)}' بارگذاری شد.", internal_logs)
 
336
  if not actual_text_input:
337
+ return None, None, "خطا: فایل آپلود شده خالی است."
 
338
  except Exception as e:
339
+ _log(f"❌ خطا در خواندن فایل آپلود شده: {e}", internal_logs)
340
+ return None, None, f"خطا در خواندن فایل: {e}"
341
  else:
342
+ return None, None, "خطا: گزینه فایل انتخاب شده اما فایلی آپلود نشده."
 
343
  else:
344
  actual_text_input = text_to_speak
345
  if not actual_text_input or not actual_text_input.strip():
346
+ return None, None, "خطا: لطفاً متنی را وارد کنید."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
+ output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "alpha_tts_output")
349
+ if not output_filename_base: output_filename_base = "alpha_tts_output"
350
 
351
+ playback_path, download_path, user_message_from_core = core_generate_audio(
352
  actual_text_input, speech_prompt, speaker_voice, output_filename_base,
353
+ temperature, max_chunk_size, sleep_between_requests, internal_logs
 
354
  )
 
 
355
 
356
+ # پیام نهایی برای کاربر
357
+ final_user_message = user_message_from_core
358
+ if not PYDUB_AVAILABLE and len(smart_text_split(actual_text_input, max_chunk_size)) > 1 and download_path and download_path.endswith(".zip"):
359
+ final_user_message = "صدا با موفقیت تولید شد. چون قطعات متعدد بودند و امکان ادغام خودکار فراهم نبود، فایل ZIP برای دانلود آماده شد."
 
 
 
 
360
 
361
+ return playback_path, download_path, final_user_message
362
 
363
  # --- تعریف رابط کاربری Gradio ---
364
  css = """
365
+ body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; background-color: #f0f2f5; }
366
+ .gradio-container { max-width: 95% !important; margin: 20px auto !important; padding: 15px !important; background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
367
+ @media (min-width: 768px) { .gradio-container { max-width: 700px !important; } }
368
  footer { display: none !important; }
369
+ .gr-button { font-weight: bold; background-color: #007bff !important; color: white !important; border-radius: 8px !important; padding: 10px 20px !important; transition: background-color 0.3s ease; }
370
+ .gr-button:hover { background-color: #0056b3 !important; }
371
+ .gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius: 6px !important; border: 1px solid #ced4da; }
372
+ .gr-panel { padding: 15px !important; border-radius: 8px !important; background-color: #f8f9fa; border: 1px solid #e9ecef; margin-bottom:15px; }
373
+ h1, h2, h3 { color: #343a40; text-align: center; }
374
+ h1 { font-size: 1.8em; margin-bottom: 5px;}
375
+ h2 { font-size: 1.2em; margin-bottom: 15px; color: #495057;}
376
+ label { font-weight: 500; color: #495057; margin-bottom: 5px; display: block; }
377
  #output_audio .gallery, #download_file_output .gallery { display: none !important; }
378
+ textarea, input[type="text"] { direction: rtl; text-align: right; padding: 10px; font-size: 1em; }
379
+ .gr-form { gap: 20px !important; }
380
+ .user_message_output { padding: 10px; margin-top: 15px; border-radius: 6px; text-align: center; font-weight: 500; }
381
+ .user_message_output.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
382
+ .user_message_output.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
383
+ """
384
+
385
+ alpha_intro = """
386
+ <div style='text-align:center; padding:10px;'>
387
+ <img src='https://img.icons8.com/fluency/96/artificial-intelligence.png' alt='AI Icon' style='width:60px; height:60px; margin-bottom:5px;'/>
388
+ <h1>تبدیل متن به صدا با هوش مصنوعی آلفا</h1>
389
+ <p style='font-size:1.1em; color:#555;'>به سادگی متن خود را وارد کنید و صدای طبیعی و رسا تحویل بگیرید!</p>
390
+ </div>
391
  """
392
 
393
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
394
+ gr.HTML(alpha_intro)
395
+
396
+ with gr.Accordion("⚠️ راهنمای مهم: تنظیم کلید API", open=False, elem_id="api_key_accordion"):
 
397
  gr.Markdown(
398
+ "**برای استفاده از این ابزار، نیاز به تنظیم یک کلید API در تنظیمات این Space دارید:**\n"
399
+ "1. به صفحه اصلی این Space بروید.\n"
400
+ "2. روی نام Space و سپس 'Settings' (⚙️) کلیک کنید.\n"
401
+ "3. در منوی سمت چپ، به 'Secrets' بروید.\n"
402
+ "4. روی '+ New secret' کلیک کنید.\n"
403
+ "5. نام Secret را `GEMINI_API_KEY` (با حروف بزرگ) وارد کنید.\n"
404
+ "6. کلید API خود را در فیلد 'Value' وارد کنید.\n"
405
+ "7. 'Save secret' را بزنید و در صورت نیاز Space را Restart کنید."
 
406
  )
407
+
408
+ with gr.Row(elem_classes="gr-form"):
 
409
  with gr.Column(scale=3, min_width=300):
410
+ gr.Markdown("### ۱. متن و سبک گفتار خود را وارد کنید")
411
+ use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی ورودی (.txt)", value=False)
412
+ uploaded_file_input = gr.File(label="📂 آپلود فایل متنی (UTF-8)", file_types=['.txt'], visible=False)
 
 
 
 
 
 
413
  text_to_speak_tb = gr.Textbox(
414
+ label="⌨️ متن برای تبدیل به گفتار:",
415
+ placeholder="اینجا بنویسید...",
416
  lines=8,
417
+ value="سلام! من هوش مصنوعی آلفا هستم و می‌توانم متن شما را به صدا تبدیل کنم.",
418
+ visible=True
 
419
  )
 
 
420
  use_file_input_cb.change(
421
  fn=lambda x: (gr.update(visible=x), gr.update(visible=not x)),
422
  inputs=use_file_input_cb,
423
  outputs=[uploaded_file_input, text_to_speak_tb]
424
  )
 
425
  speech_prompt_tb = gr.Textbox(
426
+ label="🗣️ سبک گفتار (اختیاری):",
427
+ placeholder="مثال: یک گوینده خبر حرفه‌ای، یک دوست صمیمی، پرانرژی و شاد",
428
+ value="با لحنی دوستانه و واضح صحبت کن.",
429
  lines=2
430
  )
431
 
432
  with gr.Column(scale=2, min_width=250):
433
+ gr.Markdown("### ۲. تنظیمات صدا")
 
 
 
434
  speaker_voice_dd = gr.Dropdown(
435
+ SPEAKER_VOICES, label="🎤 انتخاب گوینده:", value="Charon"
436
  )
437
  temperature_slider = gr.Slider(
438
+ minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="🌡️ خلاقیت صدا (دما):"
439
+ )
440
  output_filename_base_tb = gr.Textbox(
441
+ label="📛 نام پایه فایل خروجی (اختیاری):", value="alpha_audio_output"
442
  )
443
 
444
+ gr.Markdown("#### تنظیمات فنی (پیشرفته)")
445
  max_chunk_size_slider = gr.Slider(
446
+ minimum=1500, maximum=4000, step=100, value=3800, label="📏 حداکثر کاراکتر هر بخش:"
447
+ )
448
  sleep_between_requests_slider = gr.Slider(
449
+ minimum=3, maximum=20, step=0.5, value=10, label="⏱️ تاخیر بین بخش‌ها (ثانیه):"
450
+ )
 
 
 
 
 
 
451
 
452
+ generate_button = gr.Button("🎧 تولید صدا با آلفا", variant="primary", elem_id="generate_button_main")
453
+
454
+ user_message_display = gr.Textbox(
455
+ label=" ",
456
+ interactive=False,
457
+ elem_classes="user_message_output",
458
+ placeholder="پیام وضعیت اینجا نمایش داده می‌شود..."
459
+ )
460
+
461
+ gr.HTML("<hr style='margin: 20px 0;'>")
462
+ gr.Markdown("<h3 style='text-align:center; margin-bottom:10px;'>📢 نتیجه و دانلود 📢</h3>")
463
  with gr.Row():
464
  with gr.Column(scale=1):
465
+ output_audio = gr.Audio(label="🔊 فایل صوتی تولید شده:", type="filepath", elem_id="output_audio_player")
466
  with gr.Column(scale=1):
467
+ download_file_output = gr.File(label="💾 دانلود فایل نهایی (WAV یا ZIP):", elem_id="download_file_link")
 
 
 
468
 
469
  generate_button.click(
470
  fn=gradio_tts_interface,
471
  inputs=[
472
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
473
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
474
+ temperature_slider, max_chunk_size_slider, sleep_between_requests_slider
 
 
475
  ],
476
+ outputs=[output_audio, download_file_output, user_message_display]
477
+ ).then(
478
+ # JavaScript to update user_message_display class based on content
479
+ None, None, None,
480
+ _js="""
481
+ (audio, file, msg_text) => {
482
+ const msg_box = document.querySelector('.user_message_output textarea'); // Gradio Textbox is a textarea
483
+ if (msg_box) {
484
+ msg_box.classList.remove('success', 'error'); // Clear previous classes
485
+ if (msg_text.toLowerCase().includes('خطا') || msg_text.toLowerCase().includes('متاسفانه')) {
486
+ msg_box.classList.add('error');
487
+ } else if (msg_text) {
488
+ msg_box.classList.add('success');
489
+ }
490
+ }
491
+ return [audio, file, msg_text]; // Must return all outputs
492
+ }
493
+ """
494
  )
495
 
496
+
497
  gr.Examples(
498
+ label="✨ نمونه‌های آماده برای امتحان کردن ✨",
499
  examples=[
500
+ [False, None, "سلام به همه دوستان! امروز می‌خواهیم درباره آخرین دستاوردهای هوش مصنوعی صحبت کنیم.", "با لحنی پر انرژی و هیجان‌زده، مانند یک مجری برنامه علمی.", "Zephyr", "alpha_demo_1", 0.95, 3800, 8],
501
+ [False, None, "داستان از آنجا شروع شد که در یک شب تاریک و طوفانی، قهرمان ما به کلبه‌ای مرموز رسید.", "با صدایی آرام و داستانی، مناسب برای قصه‌گویی شبانه.", "Achird", "alpha_story_1", 0.8, 3500, 12],
502
+ [False, None, "آخرین اخبار ورزشی: تیم ملی فوتبال کشورمان با یک بازی درخشان به پیروزی رسید!", "مانند یک گزارشگر ورزشی هیجان‌زده و سریع.", "Orus", "alpha_news_1", 1.0, 3000, 7],
503
+ [False, None, "آموزش پخت کیک شکلاتی: ابتدا فر را با دمای ۱۸۰ درجه سانتی‌گراد گرم کنید. سپس آرد، شکر و کاکائو را با هم مخلوط نمایید.", "با صدایی واضح، آموزشی و کمی آهسته‌تر از حد معمول.", "Vindemiatrix", "alpha_recipe_1", 0.75, 3800, 10],
504
+ [False, None, "به پادکست هفتگی ما خوش آمدید. این هفته به بررسی عمیق تاثیرات فناوری بر زندگی روزمره خواهیم پرداخت.", "مانند یک میزبان پادکست، صمیمی و متفکر.", "Laomedeia", "alpha_podcast_1", 0.85, 3600, 11],
505
+ [False, None, "اعلامیه مهم: پرواز شماره ۳۷۲ به مقصد تهران با یک ساعت تاخیر انجام خواهد شد. از صبر و شکیبایی شما سپاسگزاریم.", "با صدایی رسمی و واضح، مانند اعلانات فرودگاه.", "Schedar", "alpha_announce_1", 0.7, 3200, 9],
506
+ [False, None, "یک شعر زیبا از حافظ: «یوسف گمگشته بازآید به کنعان غم مخور / کلبه احزان شود روزی گلستان غم مخور».", "با لحنی ادیبانه و احساسی، مناسب برای دکلمه شعر.", "Alnilam", "alpha_poem_1", 0.9, 3700, 13],
507
+ [False, None, "تماس با پشتیبانی: برای ارتباط با واحد فروش عدد ۱، واحد پشتیبانی فنی عدد ۲ و برای سایر موارد عدد ۳ را شماره‌گیری فرمایید.", "با صدایی ماشینی و راهنما، مانند سیستم پاسخگویی تلفنی.", "Puck", "alpha_ivr_1", 0.6, 3000, 6],
508
  ],
509
  inputs=[
510
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
511
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
512
+ temperature_slider, max_chunk_size_slider, sleep_between_requests_slider
 
 
513
  ],
514
+ outputs=[output_audio, download_file_output, user_message_display],
515
  fn=gradio_tts_interface,
516
+ cache_examples=False
517
  )
518
 
519
  gr.Markdown(
520
+ "<div style='text-align: center; margin-top: 30px; padding-top:15px; border-top: 1px solid #eee; font-size: 0.9em; color: #6c757d;'>"
521
+ "قدرت گرفته از فناوری پیشرفته هوش مصنوعی آلفا.<br>"
522
+ "لطفاً از این ابزار به صورت مسئولانه استفاده کنید."
 
523
  "</div>"
524
  )
525
 
526
  if __name__ == "__main__":
 
 
 
527
  demo.launch()