Hamed744 commited on
Commit
eb51a4d
·
verified ·
1 Parent(s): 8f4a387

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -253
app.py CHANGED
@@ -24,14 +24,20 @@ SPEAKER_VOICES = [
24
  ]
25
  FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
26
 
27
- def _log(message, log_list):
 
 
 
 
 
 
28
  log_list.append(message)
29
 
30
  def save_binary_file(file_name, data, log_list):
31
  try:
32
  with open(file_name, "wb") as f:
33
  f.write(data)
34
- _log(f"✅ فایل در مسیر زیر ذخیره شد: {file_name}", log_list)
35
  return file_name
36
  except Exception as e:
37
  _log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
@@ -61,74 +67,51 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
61
  for param in parts:
62
  param = param.strip()
63
  if param.lower().startswith("rate="):
64
- try:
65
- rate_str = param.split("=", 1)[1]
66
- rate = int(rate_str)
67
- except (ValueError, IndexError): pass
68
  elif param.startswith("audio/L"):
69
- try:
70
- bits_per_sample = int(param.split("L", 1)[1])
71
- except (ValueError, IndexError): pass
72
  return {"bits_per_sample": bits_per_sample, "rate": rate}
73
 
74
  def smart_text_split(text, max_size=3800, log_list=None):
75
- if len(text) <= max_size:
76
- return [text]
77
- chunks = []
78
- current_chunk = ""
79
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
80
  for sentence in sentences:
81
- sentence_with_space = sentence + " "
82
- if len(current_chunk) + len(sentence_with_space) > max_size:
83
- if current_chunk:
84
- chunks.append(current_chunk.strip())
85
  current_chunk = sentence
86
  while len(current_chunk) > max_size:
87
- split_idx = -1
88
- possible_split_chars = ['،', ',', ';', ':', ' ']
89
- for char_idx in range(max_size - 1, max_size // 2, -1):
90
- if current_chunk[char_idx] in possible_split_chars:
91
- split_idx = char_idx + 1
92
- break
93
- if split_idx != -1:
94
- chunks.append(current_chunk[:split_idx].strip())
95
- current_chunk = current_chunk[split_idx:].strip()
96
- else:
97
- chunks.append(current_chunk[:max_size].strip())
98
- current_chunk = current_chunk[max_size:].strip()
99
  else:
100
  current_chunk += (" " if current_chunk else "") + sentence
101
- if current_chunk:
102
- chunks.append(current_chunk.strip())
103
-
104
  final_chunks = [c for c in chunks if c]
105
- if log_list:
106
- _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
107
  return final_chunks
108
 
109
  def merge_audio_files_func(file_paths, output_path, log_list):
110
  if not PYDUB_AVAILABLE:
111
- _log("❌ pydub در دسترس نیست. نمی‌توان فایل‌ها را ادغام کرد.", log_list)
112
  return False
113
  try:
114
- _log(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...", log_list)
115
  combined = AudioSegment.empty()
116
  for i, file_path in enumerate(file_paths):
117
  if os.path.exists(file_path):
118
- audio = AudioSegment.from_file(file_path)
119
- combined += audio
120
- if i < len(file_paths) - 1:
121
- combined += AudioSegment.silent(duration=200)
122
- else:
123
- _log(f"⚠️ فایل پیدا نشد: {file_path}", log_list)
124
  combined.export(output_path, format="wav")
125
- _log(f"✅ فایل ادغام شده ذخیره شد: {output_path}", log_list)
126
  return True
127
  except Exception as e:
128
- _log(f"❌ خطا در ادغام فایل‌ها: {e}", log_list)
129
  return False
130
 
131
- def create_zip_file(file_paths, zip_name, log_list):
132
  try:
133
  with zipfile.ZipFile(zip_name, 'w') as zipf:
134
  for file_path in file_paths:
@@ -137,245 +120,212 @@ def create_zip_file(file_paths, zip_name, log_list):
137
  _log(f"📦 فایل ZIP ایجاد شد: {zip_name}", log_list)
138
  return True
139
  except Exception as e:
140
- _log(f"❌ خطا در ایجاد فایل ZIP: {e}", log_list)
141
  return False
142
 
143
  def core_generate_audio(
144
  text_input, prompt_input, selected_voice, output_base_name,
145
- temperature_val, max_chunk, sleep_time,
146
- log_list
147
  ):
148
- _log("🚀 شروع فرآیند تبدیل متن به گفتار با هوش مصنوعی آلفا...", log_list)
 
 
 
149
  api_key = os.environ.get("GEMINI_API_KEY")
150
  if not api_key:
151
- _log("❌ خطا: کلید API (GEMINI_API_KEY) تنظیم نشده است.", log_list)
152
- return None, None, "خطا: کلید API برای سرویس هوش مصنوعی تنظیم نشده است. لطفاً با مدیر تماس بگیرید یا راهنما را مطالعه کنید."
 
 
153
 
154
  try:
155
- _log("🛠️ در حال ایجاد کلاینت هوش مصنوعی آلفا...", log_list)
156
  client = genai.Client(api_key=api_key)
157
- _log("✅ کلاینت با موفقیت ایجاد شد.", log_list)
158
  except Exception as e:
159
- _log(f"❌ خطا در ایجاد کلاینت: {e}", log_list)
160
- return None, None, "خطا در اتصال به سرویس هوش مصنوعی. لطفاً بعداً تلاش کنید."
161
 
162
- if not text_input or text_input.strip() == "":
163
- _log("❌ خطا: متن ورودی خالی است.", log_list)
164
- return None, None, "خطا: لطفاً متنی را برای تبدیل وارد کنید."
165
 
166
  text_chunks = smart_text_split(text_input, max_chunk, log_list)
167
  if not text_chunks:
168
- _log("❌ خطا: هیچ قطعه متنی برای پردازش وجود ندارد.", log_list)
169
- return None, None, "خطا: مشکلی در پردازش متن ورودی پیش آمد."
170
 
171
  generated_files = []
172
  model_to_use = FIXED_MODEL_NAME
173
 
174
  for i, chunk in enumerate(text_chunks):
175
- _log(f"\n🔊 تولید صدا برای قطعه {i+1}/{len(text_chunks)}...", log_list)
176
  final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
177
  contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
178
  generate_content_config = types.GenerateContentConfig(
179
- temperature=temperature_val,
180
- response_modalities=["audio"],
181
- speech_config=types.SpeechConfig(
182
- voice_config=types.VoiceConfig(
183
- prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)
184
- )
185
- ),
186
  )
187
  current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
188
  try:
189
- response = client.models.generate_content(
190
- model=model_to_use, contents=contents, config=generate_content_config,
191
- )
192
  if (response.candidates and response.candidates[0].content and
193
- response.candidates[0].content.parts and
194
- response.candidates[0].content.parts[0].inline_data):
195
  inline_data = response.candidates[0].content.parts[0].inline_data
196
  data_buffer = inline_data.data
197
- file_extension = mimetypes.guess_extension(inline_data.mime_type)
198
- if file_extension is None or "binary" in inline_data.mime_type or file_extension == ".bin":
199
- file_extension = ".wav"
200
- if "audio/L" in inline_data.mime_type:
201
- data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
202
- elif inline_data.mime_type == "audio/mpeg": file_extension = ".mp3"
203
- elif inline_data.mime_type == "audio/wav": file_extension = ".wav"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_list)
205
- if generated_file_path:
206
- generated_files.append(generated_file_path)
207
- _log(f"✅ قطعه {i+1} تولید شد.", log_list)
208
- elif response.text:
209
- _log(f"ℹ️ پیام API برای قطعه {i+1}: {response.text}", log_list)
210
- if "rate limit" in response.text.lower() or "quota" in response.text.lower():
211
- _log(f"⏳ محدودیت درخواست API.", log_list)
212
- else:
213
- _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی/متنی. باز��ورد: {response.prompt_feedback if response else 'No response'}", log_list)
214
- except types.generation_types.BlockedPromptException as bpe:
215
- _log(f"❌ محتوای قطعه {i+1} مسدود شد: {bpe}", log_list)
216
- return None, None, "خطا: محتوای ورودی شما توسط سیستم ایمنی مسدود شد. لطفاً متن را تغییر دهید."
217
  except Exception as e:
218
  _log(f"❌ خطا در تولید قطعه {i+1}: {e}", log_list)
219
- if "API key not valid" in str(e): return None, None, "خطا: کلید API نامعتبر است."
220
- elif "quota" in str(e).lower(): return None, None, "خطا: محدودیت استفاده از سرویس به پایان رسیده است."
221
  continue
222
- if i < len(text_chunks) - 1 and len(text_chunks) > 1 :
223
- _log(f"⏱️ انتظار {sleep_time} ثانیه...", log_list)
224
  time.sleep(sleep_time)
225
 
226
  if not generated_files:
227
- _log("❌ هیچ فایل صوتی تولید نشد!", log_list)
228
- return None, None, "متاسفانه هیچ فایل صوتی تولید نشد. لطفاً ورودی خود را بررسی کرده و مجدداً تلاش کنید."
229
 
230
- _log(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!", log_list)
231
- playback_file = None
232
- download_file = None
233
- user_message = "صدا با موفقیت تولید شد."
234
 
235
  if len(generated_files) > 1:
236
  if PYDUB_AVAILABLE:
237
- merged_filename = f"{output_base_name}_final_audio.wav"
238
  if merge_audio_files_func(generated_files, merged_filename, log_list):
239
- playback_file = merged_filename
240
- download_file = merged_filename
241
- for file_path in generated_files:
242
- try:
243
- if os.path.abspath(file_path) != os.path.abspath(merged_filename):
244
- os.remove(file_path)
245
- except Exception as e:
246
- _log(f"⚠️ خطا در حذف فایل جزئی {os.path.basename(file_path)}: {e}", log_list)
247
- else:
248
- user_message = "ادغام فایل‌ها ممکن نبود. فایل ZIP از قطعات صوتی برای دانلود آماده شد."
249
- zip_filename = f"{output_base_name}_all_parts.zip"
250
- if create_zip_file(generated_files, zip_filename, log_list): download_file = zip_filename
251
- if generated_files: playback_file = generated_files[0]
252
- else:
253
- user_message = "فایل‌های صوتی به صورت جداگانه در یک فایل ZIP آماده شدند (امکان ادغام خودکار فراهم نبود)."
254
- zip_filename = f"{output_base_name}_all_parts.zip"
255
- if create_zip_file(generated_files, zip_filename, log_list): download_file = zip_filename
256
- if generated_files: playback_file = generated_files[0]
257
  elif len(generated_files) == 1:
258
- playback_file = generated_files[0]
259
- download_file = generated_files[0]
 
 
 
 
 
260
 
261
- if playback_file and not os.path.exists(playback_file): playback_file = None
262
- if download_file and not os.path.exists(download_file): download_file = None
263
- if not playback_file and not download_file and generated_files:
264
- user_message = "خطا در آماده‌سازی فایل نهایی. ممکن است قطعات جداگانه تولید شده باشند اما ادغام یا فشرده‌سازی ناموفق بوده."
265
- return playback_file, download_file, user_message
266
 
267
  def gradio_tts_interface(
268
  use_file_input, uploaded_file, text_to_speak,
269
  speech_prompt, speaker_voice, output_filename_base_in,
270
- temperature, max_chunk_size, sleep_between_requests,
271
  progress=gr.Progress(track_tqdm=True)
272
  ):
273
- internal_logs = []
274
  actual_text_input = ""
275
  if use_file_input:
276
  if uploaded_file is not None:
277
  try:
278
  with open(uploaded_file.name, 'r', encoding='utf-8') as f:
279
  actual_text_input = f.read().strip()
280
- _log(f"✅ متن از فایل '{os.path.basename(uploaded_file.name)}' بارگذاری شد.", internal_logs)
281
- if not actual_text_input:
282
- return None, None, "خطا: فایل آپلود شده خالی است."
283
  except Exception as e:
284
- _log(f"❌ خطا در خواندن فایل آپلود شده: {e}", internal_logs)
285
- return None, None, f"خطا در خواندن فایل: {e}"
286
- else:
287
- return None, None, "خطا: گزینه فایل انتخاب شده اما فایلی آپلود نشده."
288
  else:
289
  actual_text_input = text_to_speak
290
- if not actual_text_input or not actual_text_input.strip():
291
- return None, None, "خطا: لطفاً متنی را وارد کنید."
292
 
293
  output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "alpha_tts_output")
294
  if not output_filename_base: output_filename_base = "alpha_tts_output"
295
 
296
- playback_path, download_path, user_message_from_core = core_generate_audio(
 
297
  actual_text_input, speech_prompt, speaker_voice, output_filename_base,
298
- temperature, max_chunk_size, sleep_between_requests, internal_logs
299
  )
300
- # for log_entry in internal_logs: # For debugging in HF Spaces console
301
- # print(log_entry)
302
- return playback_path, download_path, user_message_from_core
303
-
304
- def format_user_message(message_text):
305
- if not message_text:
306
- return "<div class='user_message_output'></div>"
307
 
308
- # از gr.utils.escape_html یا معادل آن در نسخه‌های جدیدتر استفاده کنید
309
- # در Gradio 3.x به بالا، escape کردن خودکار است اگر از f-string در gr.HTML استفاده نشود.
310
- # برای اطمینان، می‌توانیم خودمان escape کنیم اگر از f-string استفاده می‌کنیم.
311
- # اما اینجا ما رشته HTML را کامل می‌سازیم، پس نیازی به gr.zwoel نیست.
312
- escaped_message = gr.utils.escape_html(message_text)
313
-
314
- if "خطا:" in message_text or "متاسفانه" in message_text or "مسدود شد" in message_text or "نامعتبر" in message_text:
315
- return f"<div class='user_message_output error'>{escaped_message}</div>"
316
- elif "موفقیت" in message_text or "آماده شد" in message_text:
317
- return f"<div class='user_message_output success'>{escaped_message}</div>"
318
- else:
319
- return f"<div class='user_message_output info'>{escaped_message}</div>"
320
 
321
  css = """
322
- body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; background-color: #f0f2f5; }
323
- .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); }
324
- @media (min-width: 768px) { .gradio-container { max-width: 700px !important; } }
325
  footer { display: none !important; }
326
- .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; }
327
- .gr-button:hover { background-color: #0056b3 !important; }
328
- .gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius: 6px !important; border: 1px solid #ced4da; }
329
- .gr-panel { padding: 15px !important; border-radius: 8px !important; background-color: #f8f9fa; border: 1px solid #e9ecef; margin-bottom:15px; }
330
- h1, h2, h3 { color: #343a40; text-align: center; }
331
- h1 { font-size: 1.8em; margin-bottom: 5px;}
332
- h2 { font-size: 1.2em; margin-bottom: 15px; color: #495057;}
333
- label { font-weight: 500; color: #495057; margin-bottom: 5px; display: block; }
334
- #output_audio .gallery, #download_file_output .gallery { display: none !important; }
335
- textarea, input[type="text"] { direction: rtl; text-align: right; padding: 10px; font-size: 1em; }
336
- .gr-form { gap: 20px !important; }
337
- .user_message_output { padding: 12px 15px; margin-top: 15px; border-radius: 6px; text-align: center; font-weight: 500; border: 1px solid transparent; }
338
- .user_message_output.success { background-color: #d1e7dd; color: #0f5132; border-color: #badbcc; }
339
- .user_message_output.error { background-color: #f8d7da; color: #842029; border-color: #f5c2c7; }
340
- .user_message_output.info { background-color: #cff4fc; color: #055160; border-color: #b6effb; }
341
- #api_key_accordion details { border: 1px solid #ddd; border-radius: 6px; margin-bottom: 15px; }
342
- #api_key_accordion summary { font-weight: bold; padding: 10px; cursor: pointer; background-color: #f7f7f7; border-radius: 6px 6px 0 0;}
343
- #api_key_accordion div[class^="prose"] { padding: 10px; border-top: 1px solid #ddd;}
344
  """
345
 
346
- alpha_intro = """
347
- <div style='text-align:center; padding:10px;'>
348
- <img src='https://img.icons8.com/fluency/96/artificial-intelligence.png' alt='AI Icon' style='width:60px; height:60px; margin-bottom:5px;'/>
349
  <h1>تبدیل متن به صدا با هوش مصنوعی آلفا</h1>
350
- <p style='font-size:1.1em; color:#555;'>به سادگی متن خود را وارد کنید و صدای طبیعی و رسا تحویل بگیرید!</p>
351
  </div>
352
  """
353
 
354
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
355
- gr.HTML(alpha_intro)
356
-
357
- with gr.Accordion("⚠️ راهنمای مهم: تنظیم کلید API", open=False, elem_id="api_key_accordion"):
358
- gr.Markdown(
359
- "**برای استفاده از این ابزار، نیاز به تنظیم یک کلید API در تنظیمات این Space دارید:**\n"
360
- "1. به صفحه اصلی این Space بروید.\n"
361
- "2. روی نام Space و سپس 'Settings' (⚙️) کلیک کنید.\n"
362
- "3. در منوی سمت چپ، به 'Secrets' بروید.\n"
363
- "4. روی '+ New secret' کلیک کنید.\n"
364
- "5. نام Secret را `GEMINI_API_KEY` (با حروف بزرگ) وارد کنید.\n"
365
- "6. کلید API خود را در فیلد 'Value' وارد کنید.\n"
366
- "7. 'Save secret' را بزنید و در صورت نیاز Space را Restart کنید."
367
- )
368
 
369
  with gr.Row(elem_classes="gr-form"):
370
- with gr.Column(scale=3, min_width=300):
371
- gr.Markdown("### ۱. متن و سبک گفتار خود را وارد کنید")
372
- use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی ورودی (.txt)", value=False)
373
  uploaded_file_input = gr.File(label="📂 آپلود فایل متنی (UTF-8)", file_types=['.txt'], visible=False)
374
  text_to_speak_tb = gr.Textbox(
375
- label="⌨️ متن برای تبدیل به گفتار:",
376
  placeholder="اینجا بنویسید...",
377
- lines=8,
378
- value="سلام! من هوش مصنوعی آلفا هستم و می‌توانم متن شما را به صدا تبدیل کنم.",
379
  visible=True
380
  )
381
  use_file_input_cb.change(
@@ -389,84 +339,56 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue,
389
  value="با لحنی دوستانه و واضح صحبت کن.",
390
  lines=2
391
  )
392
-
393
- with gr.Column(scale=2, min_width=250):
394
- gr.Markdown("### ۲. تنظیمات صدا")
395
  speaker_voice_dd = gr.Dropdown(
396
- SPEAKER_VOICES, label="🎤 انتخاب گوینده:", value="Charon"
397
  )
398
  temperature_slider = gr.Slider(
399
- minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="🌡️ خلاقیت صدا (دما):"
400
  )
 
 
 
401
  output_filename_base_tb = gr.Textbox(
402
- label="📛 نام پایه فایل خروجی (اختیاری):", value="alpha_audio_output"
403
- )
404
-
405
- gr.Markdown("#### تنظیمات فنی (پیشرفته)")
406
- max_chunk_size_slider = gr.Slider(
407
- minimum=1500, maximum=4000, step=100, value=3800, label="📏 حداکثر کاراکتر هر بخش:"
408
- )
409
- sleep_between_requests_slider = gr.Slider(
410
- minimum=3, maximum=20, step=0.5, value=10, label="⏱️ تاخیر بین بخش‌ها (ثانیه):"
411
  )
412
 
413
- generate_button = gr.Button("🎧 تولید صدا با آلفا", variant="primary", elem_id="generate_button_main")
414
 
415
- # کامپوننت مخفی برای نگهداری پیام متنی خام
416
- raw_user_message_holder = gr.Textbox(visible=False)
417
 
418
- user_message_display = gr.HTML(value="<div class='user_message_output'>پیام وضعیت اینجا نمایش داده می‌شود...</div>")
419
-
420
- gr.HTML("<hr style='margin: 20px 0;'>")
421
- gr.Markdown("<h3 style='text-align:center; margin-bottom:10px;'>📢 نتیجه و دانلود 📢</h3>")
422
- with gr.Row():
423
- with gr.Column(scale=1):
424
- output_audio = gr.Audio(label="🔊 فایل صوتی تولید شده:", type="filepath", elem_id="output_audio_player")
425
- with gr.Column(scale=1):
426
- download_file_output = gr.File(label="💾 دانلود فایل نهایی (WAV یا ZIP):", elem_id="download_file_link")
427
-
428
- # رویداد کلیک دکمه
429
  generate_button.click(
430
  fn=gradio_tts_interface,
431
  inputs=[
432
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
433
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
434
- temperature_slider, max_chunk_size_slider, sleep_between_requests_slider
435
  ],
436
- outputs=[output_audio, download_file_output, raw_user_message_holder] # پیام خام به کامپوننت مخفی می‌رود
437
- ).then( # سپس، کامپوننت مخفی به عنوان ورودی به تابع قالب‌بندی داده می‌شود
438
- fn=format_user_message,
439
- inputs=[raw_user_message_holder], # ورودی از کامپوننت مخفی
440
- outputs=user_message_display # خروجی به کامپوننت HTML
441
  )
442
 
443
  gr.Examples(
444
- label="✨ نمونه‌های آماده برای امتحان کردن ✨",
445
  examples=[
446
- [False, None, "سلام به همه دوستان! امروز می‌خواهیم درباره آخرین دستاوردهای هوش مصنوعی صحبت کنیم.", "با لحنی پر انرژی و هیجان‌زده، مانند یک مجری برنامه علمی.", "Zephyr", "alpha_demo_1", 0.95, 3800, 8],
447
- [False, None, "داستان از آنجا شروع شد که در یک شب تاریک و طوفانی، قهرمان ما به کلبه‌ای مرموز رسید.", "با صدایی آرام و داستانی، مناسب برای قصه‌گویی شبانه.", "Achird", "alpha_story_1", 0.8, 3500, 12],
448
- [False, None, "آخرین اخبار ورزشی: تیم ملی فوتبال کشورمان با یک بازی درخشان به پیروزی رسید!", "مانند یک گزارشگر ورزشی هیجان‌زده و سریع.", "Orus", "alpha_news_1", 1.0, 3000, 7],
449
- [False, None, "آموزش پخت کیک شکلاتی: ابتدا فر را با دمای ۱۸۰ درجه سانتی‌گراد گرم کنید. سپس آرد، شکر و کاکائو را با هم مخلوط نمایید.", "با صدایی واضح، آموزشی و کمی آهسته‌تر از حد معمول.", "Vindemiatrix", "alpha_recipe_1", 0.75, 3800, 10],
450
- [False, None, "به پادکست هفتگی ما خوش آمدید. این هفته به بررسی عمیق تاثیرات فناوری بر زندگی روزمره خواهیم پرداخت.", "مانند یک میزبان پادکست، صمیمی و متفکر.", "Laomedeia", "alpha_podcast_1", 0.85, 3600, 11],
451
  ],
452
- inputs=[
453
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
454
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
455
- temperature_slider, max_chunk_size_slider, sleep_between_requests_slider
456
  ],
457
- # خروجی Examples باید با خروجی تابع اصلی مطابقت داشته باشد
458
- outputs=[output_audio, download_file_output, raw_user_message_holder],
459
  fn=gradio_tts_interface,
460
- # برای Examples، نمی‌توانیم به سادگی .then را زنجیر کنیم تا user_message_display آپدیت شود.
461
- # پیام وضعیت برای Examples نمایش داده نخواهد شد مگر اینکه یک wrapper پیچیده‌تر بنویسیم.
462
- # فعلا برای سادگی، پیام وضعیت برای Examples آپدیت نمی‌شود.
463
  cache_examples=False
464
  )
465
 
466
  gr.Markdown(
467
- "<div style='text-align: center; margin-top: 30px; padding-top:15px; border-top: 1px solid #eee; font-size: 0.9em; color: #6c757d;'>"
468
  "قدرت گرفته از فناوری پیشرفته هوش مصنوعی آلفا.<br>"
469
- "لطفاً از این ابزار به صورت مسئولانه استفاده کنید."
470
  "</div>"
471
  )
472
 
 
24
  ]
25
  FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
26
 
27
+ # مقادیر پیش‌فرض برای تنظیمات فنی که از UI حذف می‌شوند
28
+ DEFAULT_MAX_CHUNK_SIZE = 3800
29
+ DEFAULT_SLEEP_BETWEEN_REQUESTS = 8 # کمی کاهش داده شد چون دیگر قابل تنظیم نیست
30
+
31
+
32
+ def _log(message, log_list): # برای دیباگ داخلی
33
+ # print(message) # برای نمایش در کنسول Hugging Face Spaces
34
  log_list.append(message)
35
 
36
  def save_binary_file(file_name, data, log_list):
37
  try:
38
  with open(file_name, "wb") as f:
39
  f.write(data)
40
+ _log(f"✅ فایل ذخیره شد: {file_name}", log_list)
41
  return file_name
42
  except Exception as e:
43
  _log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
 
67
  for param in parts:
68
  param = param.strip()
69
  if param.lower().startswith("rate="):
70
+ try: rate = int(param.split("=", 1)[1])
71
+ except: pass
 
 
72
  elif param.startswith("audio/L"):
73
+ try: bits_per_sample = int(param.split("L", 1)[1])
74
+ except: pass
 
75
  return {"bits_per_sample": bits_per_sample, "rate": rate}
76
 
77
  def smart_text_split(text, max_size=3800, log_list=None):
78
+ if len(text) <= max_size: return [text]
79
+ chunks, current_chunk = [], ""
 
 
80
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
81
  for sentence in sentences:
82
+ if len(current_chunk) + len(sentence) + 1 > max_size:
83
+ if current_chunk: chunks.append(current_chunk.strip())
 
 
84
  current_chunk = sentence
85
  while len(current_chunk) > max_size:
86
+ split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
87
+ part, current_chunk = (current_chunk[:split_idx+1], current_chunk[split_idx+1:]) if split_idx != -1 else (current_chunk[:max_size], current_chunk[max_size:])
88
+ chunks.append(part.strip())
 
 
 
 
 
 
 
 
 
89
  else:
90
  current_chunk += (" " if current_chunk else "") + sentence
91
+ if current_chunk: chunks.append(current_chunk.strip())
 
 
92
  final_chunks = [c for c in chunks if c]
93
+ if log_list: _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
 
94
  return final_chunks
95
 
96
  def merge_audio_files_func(file_paths, output_path, log_list):
97
  if not PYDUB_AVAILABLE:
98
+ _log("❌ pydub در دسترس نیست.", log_list)
99
  return False
100
  try:
101
+ _log(f"🔗 ادغام {len(file_paths)} فایل صوتی...", log_list)
102
  combined = AudioSegment.empty()
103
  for i, file_path in enumerate(file_paths):
104
  if os.path.exists(file_path):
105
+ combined += AudioSegment.from_file(file_path) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
106
+ else: _log(f"⚠️ فا��ل پیدا نشد: {file_path}", log_list)
 
 
 
 
107
  combined.export(output_path, format="wav")
108
+ _log(f"✅ فایل ادغام شده: {output_path}", log_list)
109
  return True
110
  except Exception as e:
111
+ _log(f"❌ خطا در ادغام: {e}", log_list)
112
  return False
113
 
114
+ def create_zip_file(file_paths, zip_name, log_list): # این تابع دیگر استفاده نمی‌شود چون بخش دانلود مجزا حذف شده
115
  try:
116
  with zipfile.ZipFile(zip_name, 'w') as zipf:
117
  for file_path in file_paths:
 
120
  _log(f"📦 فایل ZIP ایجاد شد: {zip_name}", log_list)
121
  return True
122
  except Exception as e:
123
+ _log(f"❌ خطا در ایجاد ZIP: {e}", log_list)
124
  return False
125
 
126
  def core_generate_audio(
127
  text_input, prompt_input, selected_voice, output_base_name,
128
+ temperature_val,
129
+ log_list # فقط برای لاگ‌های داخلی
130
  ):
131
+ max_chunk = DEFAULT_MAX_CHUNK_SIZE
132
+ sleep_time = DEFAULT_SLEEP_BETWEEN_REQUESTS
133
+
134
+ _log("🚀 شروع فرآیند...", log_list)
135
  api_key = os.environ.get("GEMINI_API_KEY")
136
  if not api_key:
137
+ _log("❌ کلید API تنظیم نشده.", log_list)
138
+ # چون پیام وضعیت حذف شده، کاربر فقط خروجی خالی دریافت می‌کند.
139
+ # بهتر است در README.md تاکید زیادی روی تنظیم کلید شود.
140
+ return None # فقط فایل صوتی برگردانده می‌شود
141
 
142
  try:
 
143
  client = genai.Client(api_key=api_key)
 
144
  except Exception as e:
145
+ _log(f"❌ خطا در کلاینت: {e}", log_list)
146
+ return None
147
 
148
+ if not text_input or not text_input.strip():
149
+ _log("❌ متن ورودی خالی.", log_list)
150
+ return None
151
 
152
  text_chunks = smart_text_split(text_input, max_chunk, log_list)
153
  if not text_chunks:
154
+ _log("❌ متن قابل پردازش نیست.", log_list)
155
+ return None
156
 
157
  generated_files = []
158
  model_to_use = FIXED_MODEL_NAME
159
 
160
  for i, chunk in enumerate(text_chunks):
161
+ _log(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...", log_list)
162
  final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
163
  contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
164
  generate_content_config = types.GenerateContentConfig(
165
+ temperature=temperature_val, response_modalities=["audio"],
166
+ speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
167
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
 
 
 
 
168
  )
169
  current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
170
  try:
171
+ response = client.models.generate_content(model=model_to_use, contents=contents, config=generate_content_config)
 
 
172
  if (response.candidates and response.candidates[0].content and
173
+ response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data):
 
174
  inline_data = response.candidates[0].content.parts[0].inline_data
175
  data_buffer = inline_data.data
176
+ file_extension = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
177
+ if "audio/L" in inline_data.mime_type and file_extension == ".wav":
178
+ data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
179
+
180
+ # اطمینان از پسوند مناسب برای pydub
181
+ if not file_extension.startswith("."): file_extension = "." + file_extension
182
+ if file_extension not in [".wav", ".mp3", ".ogg", ".flac"]: # اگر فرمت ناشناخته بود، به wav تبدیل می‌کنیم اگر ممکن باشد
183
+ if PYDUB_AVAILABLE and file_extension != ".wav": # سعی در تبدیل به wav
184
+ try:
185
+ temp_path = f"{current_chunk_filename_base}{file_extension}"
186
+ save_binary_file(temp_path, data_buffer, log_list)
187
+ audio_seg = AudioSegment.from_file(temp_path)
188
+ # پاک کردن فایل موقت با پسوند اصلی
189
+ if os.path.exists(temp_path): os.remove(temp_path)
190
+
191
+ file_extension = ".wav" # تغییر پسوند به wav
192
+ # فایل را با پسوند wav ذخیره می‌کنیم
193
+ # data_buffer حالا باید بایت‌های wav باشد
194
+ # این بخش نیاز به بازبینی دارد که چگونه بایت‌های wav را از audio_seg بگیریم یا مستقیما ذخیره کنیم
195
+ # برای سادگی، اگر فرمت اولیه توسط pydub خوانا باشد، همان را ذخیره می‌کنیم
196
+ # و اگر قرار است ادغام شود، pydub خودش هندل می‌کند.
197
+ # اگر فرمت اولیه mp3 و ... باشد، ذخیره و بعدا توسط pydub خوانده میشود.
198
+ # فعلا فرض میکنیم فرمت دریافتی از API توسط pydub قابل خواندن است.
199
+ pass # ادامه با file_extension اصلی
200
+ except Exception as e_conv:
201
+ _log(f"⚠️ خطا در تبدیل فرمت {file_extension} به wav برای قطعه {i+1}: {e_conv}", log_list)
202
+ # اگر تبدیل ناموفق بود، با همان فرمت اولیه ادامه می‌دهیم و امیدواریم pydub آن را بخواند
203
+ else: # اگر pydub نباشد و فرمت هم wav نباشد، ممکن است در ادغام مشکل پیش بیاید
204
+ _log(f"⚠️ فرمت ناشناخته {file_extension} برای قطعه {i+1} و pydub در دسترس نیست یا فرمت wav نیست.", log_list)
205
+ # اگر فرمت شناخته شده‌ای برای pydub نباشد و pydub هم نباشد، فقط wav ذخیره می‌کنیم
206
+ if file_extension not in [".wav",".mp3"]: file_extension = ".wav"
207
+
208
+
209
  generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_list)
210
+ if generated_file_path: generated_files.append(generated_file_path)
211
+ else: _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی.", log_list)
 
 
 
 
 
 
 
 
 
 
212
  except Exception as e:
213
  _log(f"❌ خطا در تولید قطعه {i+1}: {e}", log_list)
214
+ # اگر خطایی در یک قطعه رخ دهد، ادامه می‌دهیم تا بقیه تولید شوند
215
+ # کاربر در نهایت هرچه تولید شده را دریافت می‌کند.
216
  continue
217
+ if i < len(text_chunks) - 1 and len(text_chunks) > 1:
 
218
  time.sleep(sleep_time)
219
 
220
  if not generated_files:
221
+ _log("❌ هیچ فایلی تولید نشد.", log_list)
222
+ return None
223
 
224
+ _log(f"🎉 {len(generated_files)} فایل(های) صوتی تولید شد.", log_list)
225
+ final_audio_file = None
 
 
226
 
227
  if len(generated_files) > 1:
228
  if PYDUB_AVAILABLE:
229
+ merged_filename = f"{output_base_name}_final_audio.wav" # همیشه WAV برای ادغام شده
230
  if merge_audio_files_func(generated_files, merged_filename, log_list):
231
+ final_audio_file = merged_filename
232
+ for file_path in generated_files: # حذف فایل‌های جزئی
233
+ if os.path.abspath(file_path) != os.path.abspath(merged_filename):
234
+ try: os.remove(file_path)
235
+ except: pass
236
+ else: # اگر ادغام ناموفق بود، اولین فایل را برمی‌گردانیم
237
+ final_audio_file = generated_files[0] if generated_files else None
238
+ else: # اگر pydub نباشد، فقط اولین فایل را برمی‌گردانیم
239
+ _log("⚠️ pydub برای ادغام در دسترس نیست. فقط اولین قطعه ارائه می‌شود.", log_list)
240
+ final_audio_file = generated_files[0] if generated_files else None
 
 
 
 
 
 
 
 
241
  elif len(generated_files) == 1:
242
+ final_audio_file = generated_files[0]
243
+
244
+ if final_audio_file and not os.path.exists(final_audio_file):
245
+ _log(f"⚠️ فایل نهایی {final_audio_file} وجود ندارد!", log_list)
246
+ return None
247
+
248
+ return final_audio_file
249
 
 
 
 
 
 
250
 
251
  def gradio_tts_interface(
252
  use_file_input, uploaded_file, text_to_speak,
253
  speech_prompt, speaker_voice, output_filename_base_in,
254
+ temperature,
255
  progress=gr.Progress(track_tqdm=True)
256
  ):
257
+ internal_logs = [] # برای دیباگ داخلی
258
  actual_text_input = ""
259
  if use_file_input:
260
  if uploaded_file is not None:
261
  try:
262
  with open(uploaded_file.name, 'r', encoding='utf-8') as f:
263
  actual_text_input = f.read().strip()
264
+ if not actual_text_input: return None # خطا: فایل خالی
 
 
265
  except Exception as e:
266
+ _log(f"❌ خطا در خواندن فایل: {e}", internal_logs)
267
+ return None # خطا
268
+ else: return None # خطا: فایل انتخاب نشده
 
269
  else:
270
  actual_text_input = text_to_speak
271
+ if not actual_text_input or not actual_text_input.strip(): return None
 
272
 
273
  output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "alpha_tts_output")
274
  if not output_filename_base: output_filename_base = "alpha_tts_output"
275
 
276
+ # تابع core_generate_audio فقط مسیر فایل صوتی نهایی را برمی‌گرداند
277
+ final_audio_path = core_generate_audio(
278
  actual_text_input, speech_prompt, speaker_voice, output_filename_base,
279
+ temperature, internal_logs
280
  )
 
 
 
 
 
 
 
281
 
282
+ # for log_entry in internal_logs: print(log_entry) # برای دیباگ در کنسول HF
283
+
284
+ return final_audio_path
285
+
 
 
 
 
 
 
 
 
286
 
287
  css = """
288
+ body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; background-color: #f4f7f6; color: #333; }
289
+ .gradio-container { max-width: 90% !important; margin: 20px auto !important; padding: 20px !important; background-color: #ffffff; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.08); }
290
+ @media (min-width: 768px) { .gradio-container { max-width: 650px !important; } }
291
  footer { display: none !important; }
292
+ .gr-button { font-weight: bold; background: linear-gradient(135deg, #007bff, #0056b3) !important; color: white !important; border:none !important; border-radius: 8px !important; padding: 12px 25px !important; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.15); }
293
+ .gr-button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); }
294
+ .gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius: 8px !important; border: 1px solid #d1d5db; transition: border-color 0.2s ease, box-shadow 0.2s ease; }
295
+ .gr-input:focus-within, .gr-textbox:focus-within { border-color: #007bff !important; box-shadow: 0 0 0 2px rgba(0,123,255,0.25) !important; }
296
+ h1 { font-size: 1.9em; margin-bottom: 8px; color: #2c3e50; }
297
+ h2 { font-size: 1.1em; margin-bottom: 18px; color: #555;}
298
+ h3 { font-size: 1.3em; color: #0056b3; margin-top: 25px; margin-bottom:15px; border-bottom: 2px solid #007bff30; padding-bottom: 8px;}
299
+ label { font-weight: 600; color: #4a5568; margin-bottom: 6px; display: block; font-size: 0.95em; }
300
+ textarea, input[type="text"] { direction: rtl; text-align: right; padding: 12px; font-size: 1em; }
301
+ .gr-form > div { margin-bottom: 15px !important; } /* فاصله بین ردیف‌های فرم */
302
+ #output_audio_player audio { width: 100%; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
303
+ .temperature_description { font-size: 0.85em; color: #666; margin-top: -8px; margin-bottom: 10px; padding-right: 5px; }
304
+ .main_title_container {text-align:center; padding-bottom:15px; border-bottom: 1px solid #eee; margin-bottom: 20px;}
305
+ .main_title_container img {width:60px; height:60px; margin-bottom:5px;}
 
 
 
 
306
  """
307
 
308
+ alpha_intro_html = """
309
+ <div class='main_title_container'>
310
+ <img src='https://img.icons8.com/fluency/96/artificial-intelligence.png' alt='AI Icon'/>
311
  <h1>تبدیل متن به صدا با هوش مصنوعی آلفا</h1>
312
+ <p style='font-size:1.1em; color:#555;'>متن خود را به صدای طبیعی و رسا تبدیل کنید!</p>
313
  </div>
314
  """
315
 
316
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
317
+ gr.HTML(alpha_intro_html)
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
  with gr.Row(elem_classes="gr-form"):
320
+ with gr.Column(scale=3): # ستون اصلی برای ورودی‌ها
321
+ gr.Markdown("### ۱. متن و تنظیمات صدا")
322
+ use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False)
323
  uploaded_file_input = gr.File(label="📂 آپلود فایل متنی (UTF-8)", file_types=['.txt'], visible=False)
324
  text_to_speak_tb = gr.Textbox(
325
+ label="⌨️ متنی که می‌خواهید به صدا تبدیل شود:",
326
  placeholder="اینجا بنویسید...",
327
+ lines=7,
328
+ value="سلام! من هوش مصنوعی آلفا هستم.",
329
  visible=True
330
  )
331
  use_file_input_cb.change(
 
339
  value="با لحنی دوستانه و واضح صحبت کن.",
340
  lines=2
341
  )
 
 
 
342
  speaker_voice_dd = gr.Dropdown(
343
+ SPEAKER_VOICES, label="🎤 انتخاب نوع صدا (گوینده):", value="Charon"
344
  )
345
  temperature_slider = gr.Slider(
346
+ minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="🌡️ خلاقیت و تنوع صدا:"
347
  )
348
+ gr.Markdown("<p class='temperature_description'>مقادیر بالاتر صدایی خلاقانه‌تر و متنوع‌تر، و مقادیر پایین‌تر صدایی قابل پیش‌بینی‌تر و یکنواخت‌تر ایجاد می‌کنند.</p>",
349
+ elem_classes="temperature_description_container")
350
+
351
  output_filename_base_tb = gr.Textbox(
352
+ label="📛 نام فایل خروجی (اختیاری، بدون پسوند):", value="alpha_audio"
 
 
 
 
 
 
 
 
353
  )
354
 
355
+ generate_button = gr.Button("🎧 تولید و پخش صدا", variant="primary", elem_id="generate_button_main")
356
 
357
+ gr.Markdown("### 🔊 نتیجه تولید صدا")
358
+ output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player") # لیبل خالی شد
359
 
 
 
 
 
 
 
 
 
 
 
 
360
  generate_button.click(
361
  fn=gradio_tts_interface,
362
  inputs=[
363
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
364
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
365
+ temperature_slider
366
  ],
367
+ outputs=[output_audio]
 
 
 
 
368
  )
369
 
370
  gr.Examples(
371
+ label="✨ چند نمونه برای شروع ✨",
372
  examples=[
373
+ [False, None, "به نام خداوند بخشنده مهربان. سلام بر شما شنوندگان عزیز.", "با لحنی آرام و معنوی.", "Achird", "quran_intro_sample", 0.7],
374
+ [False, None, "خبر فوری! قیمت‌ها در بازار طلا و سکه با نوسانات شدیدی همراه بوده است.", "مانند یک گوینده خبر اقتصادی، سریع و دقیق.", "Orus", "news_flash_sample", 1.0],
375
+ [False, None, "در این ویدیو قصد داریم به شما آموزش دهیم چگونه یک وبسایت ساده با پایتون بسازید.", "آموزشی، واضح و با سرعت متوسط.", "Vindemiatrix", "tutorial_sample", 0.8],
376
+ [False, None, "کتاب صوتی «بوف کور» اثر صادق هدایت. فصل اول.", "روایی، با احساس و کمی غمگین.", "Alnilam", "audiobook_sample", 0.85],
 
377
  ],
378
+ inputs=[ # ورودی‌ها باید با ورودی‌های تابع اصلی مطابقت داشته باشند
379
  use_file_input_cb, uploaded_file_input, text_to_speak_tb,
380
  speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
381
+ temperature_slider
382
  ],
383
+ outputs=[output_audio], # خروجی Examples هم فقط پلیر صوتی است
 
384
  fn=gradio_tts_interface,
 
 
 
385
  cache_examples=False
386
  )
387
 
388
  gr.Markdown(
389
+ "<div style='text-align: center; margin-top: 40px; padding-top:20px; border-top: 1px solid #eee; font-size: 0.9em; color: #6c757d;'>"
390
  "قدرت گرفته از فناوری پیشرفته هوش مصنوعی آلفا.<br>"
391
+ "لطفاً به قوانین و مقررات مربوط به تولید محتوا احترام بگذارید."
392
  "</div>"
393
  )
394