Hamed744 commited on
Commit
b62be89
·
verified ·
1 Parent(s): 2757c04

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +341 -394
app.py CHANGED
@@ -6,11 +6,11 @@ import re
6
  import struct
7
  import time
8
  import zipfile
9
- import google.generativeai as genai
10
- from google.generativeai import types
11
- import traceback
12
 
13
  # خواندن کلید API از Hugging Face Secrets
 
14
  HF_GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
15
 
16
  try:
@@ -19,48 +19,64 @@ try:
19
  except ImportError:
20
  PYDUB_AVAILABLE = False
21
  print("⚠️ کتابخانه pydub در دسترس نیست. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
 
22
 
23
  # --- ثابت‌ها ---
24
- SPEAKER_VOICES = [ # لیست کامل گوینده‌ها از کد اولیه شما
25
- "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat",
26
- "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima",
27
- "Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalthgeti",
28
- "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus",
29
- "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
30
- ]
31
- # **بازگشت به نام مدل‌های اصلی TTS شما**
32
- MODELS = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
33
- MODEL_NAMES_FARSI = {
34
- "gemini-2.5-flash-preview-tts": "جمینای ۲.۵ فلش (اختصاصی TTS، سریع)", # نام اصلی
35
- "gemini-2.5-pro-preview-tts": "جمینای ۲.۵ پرو (اختصاصی TTS، کیفیت بالا)" # نام اصلی
36
  }
37
- SPEAKER_VOICES_FARSI_SAMPLE = { # نمونه نام‌های فارسی برای گوینده‌ها
38
- "Charon": "شارون (مردانه، پیش‌فرض)", "Achernar": "آخرالنهر (مردانه)",
39
- "Vindemiatrix": "ویندمیاتريکس (زنانه)", "Schedar": "صدر (مردانه)",
40
- "Laomedeia": "لائومدیا (زنانه)", "Sulafat": "سولافات (مردانه)"
41
- # ... می‌توانید برای همه گوینده‌ها نام فارسی تعریف کنید
42
  }
 
 
 
43
 
44
  # --- توابع کمکی ---
45
  def save_binary_file(file_name, data):
46
  abs_file_name = os.path.abspath(file_name)
47
  try:
48
- with open(abs_file_name, "wb") as f: f.write(data)
49
- print(f"✅ فایل در مسیر ذخیره شد: {abs_file_name}"); return abs_file_name
50
- except Exception as e: print(f" خطا در ذخیره فایل {abs_file_name}: {e}"); return None
 
 
 
 
51
 
52
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
53
  parameters = parse_audio_mime_type(mime_type)
54
- bits_per_sample, rate, num_channels = parameters["bits_per_sample"], parameters["rate"], 1
55
- data_size = len(audio_data); bytes_per_sample = bits_per_sample // 8
56
- block_align = num_channels * bytes_per_sample; byte_rate = rate * block_align
 
 
 
 
57
  chunk_size = 36 + data_size
58
- return struct.pack("<4sI4s4sIHHIIHH4sI", b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels, rate, byte_rate, block_align, bits_per_sample, b"data", data_size) + audio_data
 
 
 
 
 
59
 
60
  def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
61
- bits_per_sample, rate = 16, 24000
 
62
  if mime_type:
63
- mime_type_lower = mime_type.lower(); parts = mime_type_lower.split(";")
 
64
  for param in parts:
65
  param = param.strip()
66
  if param.startswith("rate="):
@@ -68,455 +84,386 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
68
  except: pass
69
  elif param.startswith("audio/l"):
70
  try:
71
- potential_bits = param.split("l", 1)[1].split(";",1)[0]
72
  if potential_bits.isdigit(): bits_per_sample = int(potential_bits)
73
  except: pass
74
  return {"bits_per_sample": bits_per_sample, "rate": rate}
75
 
76
  def load_text_from_gr_file(file_obj):
77
- if file_obj is None: return "", "فایلی برای ورودی متن ارائه نشده است."
 
78
  try:
79
- with open(file_obj.name, 'r', encoding='utf-8') as f: content = f.read().strip()
80
- if not content: return "", "فایل متنی خالی است."
 
 
81
  return content, f"متن با موفقیت از فایل '{os.path.basename(file_obj.name)}' ({len(content)} کاراکتر) بارگذاری شد."
82
- except Exception as e: return "", f"خطا در خواندن فایل متنی: {e}"
83
-
84
- # ** تابع smart_text_split با استفاده از max_chunk_size ورودی **
85
- def smart_text_split(text, max_chunk_size=3800):
86
- if not text: return []
87
- if len(text) <= max_chunk_size:
88
- return [text.strip()]
89
 
90
- chunks = []
91
- current_chunk = ""
92
- # تقسیم بر اساس جملات (با پشتیبانی از علائم نگارشی فارسی و انگلیسی)
93
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
94
-
95
  for sentence in sentences:
96
- sentence = sentence.strip()
97
- if not sentence:
98
- continue
99
-
100
- # اگر اضافه کردن جمله فعلی از حد مجاز بیشتر شود
101
- if len(current_chunk) + len(sentence) + (1 if current_chunk else 0) > max_chunk_size:
102
- # اگر current_chunk چیزی دارد، آن را اضافه کن
103
- if current_chunk:
104
- chunks.append(current_chunk)
 
 
 
 
 
 
105
  current_chunk = ""
106
-
107
- # اگر خود جمله هم از حد مجاز طولانی‌تر است، آن را بشکن
108
- if len(sentence) > max_chunk_size:
109
- # تقسیم جمله طولانی به قطعات کوچکتر
110
- for i in range(0, len(sentence), max_chunk_size):
111
- chunks.append(sentence[i:i + max_chunk_size].strip())
112
- else:
113
- current_chunk = sentence # شروع چانک جدید با این جمله
114
- else:
115
- # اضافه کردن جمله به چانک فعلی
116
- if current_chunk:
117
- current_chunk += " " + sentence
118
- else:
119
- current_chunk = sentence
120
-
121
- # اضافه کردن آخرین چانک اگر چیزی باقی مانده باشد
122
- if current_chunk:
123
- chunks.append(current_chunk)
124
-
125
- return [c for c in chunks if c] # حذف چانک‌های خالی احتمالی
126
-
127
 
128
  def merge_audio_files_func(file_paths, output_path):
129
  if not PYDUB_AVAILABLE: return False, "pydub در دسترس نیست. امکان ادغام فایل‌ها وجود ندارد.", None
130
- if not file_paths: return False, "هیچ فایل صوتی برای ادغام وجود ندارد.", None
131
  try:
132
  combined = AudioSegment.empty()
133
- for i, file_path in enumerate(file_paths):
134
- if os.path.exists(file_path):
135
  try:
136
- audio = AudioSegment.from_file(file_path)
137
  combined += audio
138
  if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
139
  except Exception as e_load:
140
- msg = f"خطا در بارگذاری فایل صوتی '{os.path.basename(file_path)}' با pydub: {e_load}"
141
- print(f"⚠️ {msg}"); return False, msg, None
142
- else:
143
- msg = f"فایل برای ادغام یافت نشد: {os.path.basename(file_path)}"
144
- print(f"⚠️ {msg}"); return False, msg, None
145
  abs_output_path = os.path.abspath(output_path)
146
  combined.export(abs_output_path, format="wav")
147
- return True, f"فایل ادغام شده با موفقیت در '{os.path.basename(abs_output_path)}' (فرمت WAV) ذخیره شد.", abs_output_path
148
- except Exception as e:
149
- msg = f"خطا در ادغام فایل‌ها: {e}"
150
- print(f"❌ {msg}"); return False, msg, None
151
  def create_zip_file(file_paths, zip_name):
152
  abs_zip_name = os.path.abspath(zip_name)
153
  try:
154
  with zipfile.ZipFile(abs_zip_name, 'w') as zipf:
155
- for file_path in file_paths:
156
- if os.path.exists(file_path):
157
- zipf.write(file_path, os.path.basename(file_path))
158
- return True, f"فایل ZIP با نام '{os.path.basename(abs_zip_name)}' ایجاد شد.", abs_zip_name
159
  except Exception as e: return False, f"خطا در ایجاد فایل ZIP: {e}", None
160
 
161
  # --- تابع اصلی تولید صدا ---
162
  def generate_audio_for_gradio(
163
- use_file_input_checkbox, text_file_obj, speech_prompt_input, text_to_speak_input,
164
- max_chunk_size_from_slider, # ** استفاده از مقدار اسلایدر **
165
- sleep_slider, temperature_slider, model_dropdown_key,
166
- speaker_dropdown, output_filename_base_input, merge_checkbox, delete_partials_checkbox,
 
167
  progress=gr.Progress(track_tqdm=True)
168
  ):
169
- status_messages = ["🚀 فرآیند تبدیل متن به گفتار آغاز شد..."]
170
  progress(0, desc="در حال آماده‌سازی...")
171
 
172
  api_key_to_use = HF_GEMINI_API_KEY
173
  if not api_key_to_use:
174
- status_messages.extend(["❌ خطا: کلید API جمینای (GEMINI_API_KEY) در تنظیمات Secret این Space یافت نشد.",
175
- "⬅️ لطفاً آن را در بخش Settings > Secrets مربوط به این Space تنظیم کنید."])
176
  return None, None, "\n".join(status_messages)
177
-
178
- genai.configure(api_key=api_key_to_use)
179
- status_messages.append("🔑 کلید API با موفقیت از Secrets بارگذاری و برای استفاده تنظیم شد.")
180
 
181
- actual_text_input = ""
182
  if use_file_input_checkbox:
183
  if text_file_obj is None:
184
- status_messages.append("❌ خطا: گزینه 'استفاده از فایل متنی' انتخاب شده، اما هیچ فایلی آپلود نشده است.")
185
  return None, None, "\n".join(status_messages)
186
  actual_text_input, msg = load_text_from_gr_file(text_file_obj)
187
- status_messages.append(msg)
188
- if not actual_text_input: return None, None, "\n".join(status_messages)
189
  else:
190
  actual_text_input = text_to_speak_input
191
- status_messages.append("⌨️ از متن وارد شده به صورت دستی استفاده می‌شود.")
 
 
192
 
193
- if not actual_text_input or actual_text_input.strip() == "":
194
- status_messages.append(" خطا: متن ورودی خالی است."); return None, None, "\n".join(status_messages)
195
-
196
- status_messages.append("✅ کلاینت جمینای (از طریق genai.configure) آماده است.")
 
 
 
 
197
 
198
- # ** استفاده از max_chunk_size_from_slider **
199
- text_chunks = smart_text_split(actual_text_input, int(max_chunk_size_from_slider))
200
- status_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد (حداکثر {max_chunk_size_from_slider} کاراکتر در هر قطعه).")
201
- for i, chunk_text_content in enumerate(text_chunks): status_messages.append(f" 📝 قطعه {i+1}: {len(chunk_text_content)} کاراکتر")
202
 
203
- generated_audio_files = []
204
- run_id = base64.urlsafe_b64encode(os.urandom(6)).decode()
205
- temp_output_dir = f"temp_audio_{run_id}"; os.makedirs(temp_output_dir, exist_ok=True)
206
  output_base_name_safe = re.sub(r'[\s\\\/\:\*\?\"\<\>\|\%]+', '_', output_filename_base_input)
207
 
 
 
 
 
208
  total_chunks = len(text_chunks)
209
  for i, chunk_text_content in enumerate(text_chunks):
210
- progress(0.1 + (0.7 * (i / total_chunks)), desc=f"در حال تولید قطعه {i+1} از {total_chunks}...")
211
- status_messages.append(f"\n🔊 در حال تولید صدا برای قطعه {i+1}/{total_chunks} با مدل '{model_dropdown_key}'...")
212
 
213
- text_for_tts_api = chunk_text_content
214
- if speech_prompt_input.strip():
215
- # نحوه صحیح ترکیب پرامپت با متن اصلی برای مدل‌های TTS باید طبق مستندات باشد.
216
- # این روش ساده ممکن است برای برخی مدل‌ها کار کند:
217
- text_for_tts_api = f"Prompt: \"{speech_prompt_input}\"\n\nText: \"{chunk_text_content}\""
218
- status_messages.append(f"ℹ️ اعمال پرامپت سبک: '{speech_prompt_input}'")
219
 
220
- # با بازگشت به مدل‌های اختصاصی TTS، ساختار contents و generation_config باید مطابق انتظار آن‌ها باشد.
221
- # ** این ساختار از کد اولیه شما گرفته شده است و باید با مدل‌های *-preview-tts کار کند **
222
- contents_for_api = [
223
- types.Content(
224
- role="user", # یا "model" اگر پرامپت سبک به عنوان بخشی از تاریخچه چت در نظر گرفته شود
225
- parts=[
226
- types.Part.from_text(text=text_for_tts_api),
227
- ],
228
- ),
229
- ]
230
- generation_config = types.GenerateContentConfig( # استفاده از GenerateContentConfig
231
  temperature=float(temperature_slider),
232
- response_modalities=["audio"], # درخواست خروجی صوتی
233
- speech_config=types.SpeechConfig( # ** ارسال speech_config **
234
  voice_config=types.VoiceConfig(
235
- prebuilt_voice_config=types.PrebuiltVoiceConfig(
236
- voice_name=speaker_dropdown
237
- )
238
  )
239
- ),
240
  )
241
- status_messages.append(f"ℹ️ تنظیمات گفتار: گوینده '{speaker_dropdown}'")
242
-
243
  try:
244
- chunk_filename_base = f"{output_base_name_safe}_part_{i+1:03d}"
245
- chunk_filepath_prefix = os.path.join(temp_output_dir, chunk_filename_base)
246
- audio_data_received = False
247
-
248
- model_instance = genai.GenerativeModel(model_dropdown_key)
249
-
250
- # استفاده از استریم با generation_config که شامل speech_config است
251
- for stream_response_chunk in model_instance.generate_content_stream(
252
- contents=contents_for_api, # ارسال contents ساخته شده
253
- generation_config=generation_config # ارسال generation_config ساخته شده
254
  ):
255
- if (stream_response_chunk.candidates and stream_response_chunk.candidates[0].content and
256
- stream_response_chunk.candidates[0].content.parts and
257
- stream_response_chunk.candidates[0].content.parts[0].inline_data):
258
- inline_data = stream_response_chunk.candidates[0].content.parts[0].inline_data
 
259
  data_buffer, api_mime_type = inline_data.data, inline_data.mime_type
260
  audio_data_received = True
261
  status_messages.append(f"ℹ️ MIME Type دریافتی از API: {api_mime_type}")
262
- file_extension = ".wav"
 
263
  if api_mime_type and ("mp3" in api_mime_type.lower() or "mpeg" in api_mime_type.lower()):
264
- file_extension = ".mp3"; status_messages.append(f"ℹ️ ذخیره با فرمت MP3: {api_mime_type}")
 
265
  elif api_mime_type and "wav" in api_mime_type.lower() and not ("audio/l16" in api_mime_type.lower() or "audio/l24" in api_mime_type.lower()):
266
- file_extension = ".wav"; status_messages.append(f"ℹ️ ذخیره با فرمت WAV: {api_mime_type}")
 
267
  else:
268
- status_messages.append(f"ℹ️ تبدیل به فرمت WAV برای MIME Type: {api_mime_type or 'نامشخص'}")
269
  data_buffer = convert_to_wav(data_buffer, api_mime_type)
270
- status_messages.append(f"ℹ️ پسوند فایل نهایی: {file_extension}")
271
- generated_file_path = save_binary_file(f"{chunk_filepath_prefix}{file_extension}", data_buffer)
272
- if generated_file_path:
273
- generated_audio_files.append(generated_file_path)
274
- status_messages.append(f"✅ قطعه {i+1} ذخیره شد: {os.path.basename(generated_file_path)}")
 
 
 
275
  else: status_messages.append(f"❌ عدم موفقیت در ذخیره قطعه {i+1}.")
276
  break
277
- elif stream_response_chunk.text: status_messages.append(f"ℹ️ پیام متنی از API (حین استریم): {stream_response_chunk.text}")
278
-
 
279
  if not audio_data_received:
280
- status_messages.append(f"❌ هیچ داده صوتی برای قطعه {i+1} دریافت نشد.")
281
- if 'stream_response_chunk' in locals() and stream_response_chunk and \
282
- hasattr(stream_response_chunk, 'prompt_feedback') and stream_response_chunk.prompt_feedback and \
283
- hasattr(stream_response_chunk.prompt_feedback, 'block_reason') and stream_response_chunk.prompt_feedback.block_reason:
284
- status_messages.append(f"🛑 دلیل مسدود شدن (از بازخورد پرامپت): "
285
- f"{stream_response_chunk.prompt_feedback.block_reason_message or stream_response_chunk.prompt_feedback.block_reason}")
286
- except Exception as e:
287
- is_quota_error = False
288
- # نام کلاس‌های خطا در کتابخانه google-generativeai ممکن است کمی متفاوت باشد.
289
- # BlockedPromptError و StopCandidateException معمولاً در types.generation_types یا مستقیماً types هستند.
290
- if hasattr(types, 'BlockedPromptError') and isinstance(e, types.BlockedPromptError):
291
- status_messages.append(f"❌ محتوای قطعه {i+1} توسط API مسدود شد: {e}")
292
- elif hasattr(types, 'StopCandidateException') and isinstance(e, types.StopCandidateException):
293
- status_messages.append(f"❌ تولید صدا برای قطعه {i+1} به دلیل پایان نامناسب متوقف شد: {e}")
294
- if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback'):
295
- status_messages.append(f" بازخورد API: {e.response.prompt_feedback}")
296
- elif isinstance(e, genai.errors.GoogleAPIError): # کلاس والد برای خطاهای API گوگل
297
- status_messages.append(f"❌ خطای API گوگل در قطعه {i+1} ({type(e).__name__}): {e}")
298
- # بررسی دقیق‌تر برای خطای سهمیه با استفاده از پیام خطا
299
- error_message_upper = str(getattr(e, 'message', '')).upper()
300
- if "QUOTA" in error_message_upper or "RESOURCE_EXHAUSTED" in error_message_upper:
301
- status_messages.append("🚫 شما از سهمیه رایگان/فعلی خود برای این مدل فراتر رفته‌اید. لطفاً طرح خود را بررسی کنید یا بعداً دوباره امتحان نمایید.")
302
- is_quota_error = True
303
- elif hasattr(e, 'message'):
304
- status_messages.append(f" پیام خطا از API: {e.message}")
305
- status_messages.append(traceback.format_exc())
306
- else:
307
- status_messages.extend([f"❌ خطا در تولید/پردازش قطعه {i+1}: {type(e).__name__} - {e}", traceback.format_exc()])
308
-
309
- if is_quota_error and model_dropdown_key.endswith("-pro-preview-tts"):
310
- status_messages.append("💡 پیشنهاد: از مدل 'جمینای فلش' که محدودیت کمتری دارد استفاده کنید یا برای استفاده از مدل پرو، طرح خود را در گوگل ارتقا دهید.")
311
 
312
- if not audio_data_received and i < total_chunks -1 :
313
- status_messages.append(f"⚠️ به دلیل خطا در قطعه {i+1}، ادامه تولید سایر قطعات ممکن است با مشکل مواجه شود.")
 
 
314
 
315
- if i < total_chunks - 1 and float(sleep_slider) > 0 :
316
- status_messages.append(f"⏱️ انتظار به مدت {sleep_slider} ثانیه..."); time.sleep(float(sleep_slider))
 
317
 
318
- progress(0.85, desc="پردازش فایل‌های نهایی...")
319
  if not generated_audio_files:
320
  status_messages.append("❌ هیچ فایل صوتی با موفقیت تولید یا ذخیره نشد!")
321
- progress(1, desc="پایان با خطا."); return None, None, "\n".join(status_messages)
322
- status_messages.append(f"\n🎉 {len(generated_audio_files)} فایل(های) صوتی تولید شد!")
323
- output_audio_path_for_player, output_path_for_download = None, None
324
- if merge_checkbox and len(generated_audio_files) > 1 and PYDUB_AVAILABLE:
325
- status_messages.append(f"🔗 در حال ادغام {len(generated_audio_files)} فایل صوتی...")
326
- merged_filename_path = os.path.join(temp_output_dir, f"{output_base_name_safe}_merged.wav")
327
- success_merge, msg_merge, merged_p = merge_audio_files_func(generated_audio_files, merged_filename_path)
328
- status_messages.append(msg_merge)
329
- if success_merge:
330
- output_audio_path_for_player, output_path_for_download = merged_p, merged_p
331
- if delete_partials_checkbox:
332
- status_messages.append("🗑️ در حال حذف فایل‌های جزئی...")
333
- for file_p in generated_audio_files:
334
- try: os.remove(file_p); status_messages.append(f" 🗑️ حذف شد: {os.path.basename(file_p)}")
335
- except Exception as e_del: status_messages.append(f" ⚠️ عدم موفقیت در حذف {os.path.basename(file_p)}: {e_del}")
336
  else:
337
- status_messages.append("⚠️ ادغام ناموفق بود. فایل ZIP از قطعات ارائه می‌شود.")
338
- success_zip, msg_zip, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
339
- status_messages.append(msg_zip)
340
- if success_zip: output_path_for_download = zip_p
 
 
 
 
 
 
 
 
 
 
 
 
341
  elif len(generated_audio_files) == 1:
342
- single_file_path = generated_audio_files[0]
343
- output_audio_path_for_player, output_path_for_download = single_file_path, single_file_path
344
- status_messages.append(f"🎵 فایل صوتی تکی: {os.path.basename(single_file_path)}")
345
- elif len(generated_audio_files) > 1:
346
- if not PYDUB_AVAILABLE and merge_checkbox: status_messages.append("⚠️ pydub در دسترس نیست، امکان ادغام وجود ندارد. فایل ZIP ارائه می‌شود.")
347
- status_messages.append("📦 چندین قطعه تولید شد. در حال ایجاد فایل ZIP...")
348
- success_zip, msg_zip, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
349
- status_messages.append(msg_zip)
350
- if success_zip: output_path_for_download = zip_p
351
  final_status = "\n".join(status_messages)
352
  print(final_status)
353
- progress(1, desc="انجام شد!")
354
- return output_audio_path_for_player, output_path_for_download, final_status
355
-
356
- # --- تعریف CSS سفارشی ---
357
- FLY_PRIMARY_COLOR_HEX = "#2563EB"; FLY_SECONDARY_COLOR_HEX = "#059669"; FLY_ACCENT_COLOR_HEX = "#D97706";
358
- FLY_TEXT_COLOR_HEX = "#111827"; FLY_SUBTLE_TEXT_HEX = "#4B5563"; FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB";
359
- FLY_WHITE_HEX = "#FFFFFF"; FLY_BORDER_COLOR_HEX = "#E5E7EB"; FLY_INPUT_BG_HEX = "#FFFFFF";
360
- custom_css_v2 = f"""
361
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
362
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
363
- :root {{
364
- --font-persian: 'Vazirmatn', 'Inter', sans-serif; --font-english: 'Inter', sans-serif;
365
- --primary-color: {FLY_PRIMARY_COLOR_HEX}; --secondary-color: {FLY_SECONDARY_COLOR_HEX};
366
- --accent-color: {FLY_ACCENT_COLOR_HEX}; --text-color: {FLY_TEXT_COLOR_HEX};
367
- --subtle-text-color: {FLY_SUBTLE_TEXT_HEX}; --light-bg-color: {FLY_LIGHT_BACKGROUND_HEX};
368
- --white-color: {FLY_WHITE_HEX}; --border-color: {FLY_BORDER_COLOR_HEX};
369
- --input-bg-color: {FLY_INPUT_BG_HEX};
370
- --radius-sm: 0.375rem; --radius-md: 0.625rem; --radius-lg: 0.875rem;
371
- --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
372
- --shadow-md: 0 4px 8px -2px rgba(0,0,0,0.08), 0 2px 4px -2px rgba(0,0,0,0.05);
373
- --shadow-lg: 0 12px 20px -4px rgba(0,0,0,0.08), 0 4px 8px -3px rgba(0,0,0,0.05);
374
- --transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
375
- }}
376
- body, .gradio-container {{ font-family: var(--font-persian); direction: rtl; background-color: var(--light-bg-color); color: var(--text-color); line-height: 1.7; font-size: 16px; scroll-behavior: smooth; }}
377
- .gradio-container {{ max-width: 100% !important; min-height: 100vh; margin:0 auto !important; padding:0 !important; border-radius:0 !important; box-shadow:none !important; }}
378
- .app-header-card {{ padding: 2.5rem 1.5rem; margin:0; background: linear-gradient(140deg, var(--primary-color) 10%, var(--secondary-color) 90%); color: var(--white-color); border-bottom-left-radius: var(--radius-lg); border-bottom-right-radius: var(--radius-lg); box-shadow: var(--shadow-lg); text-align: center; position:relative; overflow:hidden; }}
379
- .app-header-card::before {{ content:''; position:absolute; top:-60px; left:-60px; width:200px; height:200px; background:rgba(255,255,255,0.07); border-radius:50%; filter:blur(10px); animation: pulse-bubble 8s infinite ease-in-out; }}
380
- .app-header-card::after {{ content:''; position:absolute; bottom:-70px; right:-70px; width:250px; height:250px; background:rgba(255,255,255,0.05); border-radius:45% 55% 60% 40% / 40% 50% 50% 60% ; filter:blur(15px); animation: pulse-bubble 10s infinite ease-in-out reverse; }}
381
- @keyframes pulse-bubble {{ 0%, 100% {{ transform: scale(1); opacity: 0.05; }} 50% {{ transform: scale(1.1); opacity: 0.1; }} }}
382
- .app-header-card h1 {{ font-size: 2.2em !important; font-weight: 800 !important; margin-bottom: 0.6rem; text-shadow: 0 2px 5px rgba(0,0,0,0.15); animation: slideInDown 0.8s ease-out; }}
383
- .app-header-card .app-subtitle {{ font-size: 1.05em !important; opacity: 0.9; animation: fadeInUp 0.8s 0.2s ease-out backwards; }}
384
- .main-content-wrapper {{ padding: 1.5rem 1rem; width:100%; max-width: 960px; margin: -2.5rem auto 2.5rem auto; position:relative; z-index:10; }}
385
- .content-panel {{ background-color: var(--white-color); padding: 2rem 1.75rem; border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); animation: zoomIn 0.6s ease-out; }}
386
- .section-title {{ font-size: 1.3em; font-weight: 700; color: var(--primary-color); margin-bottom: 1.2rem; border-bottom: 3px solid var(--primary-color); padding-bottom: 0.6rem; display:inline-block; }}
387
- .gr-button.lg.primary, button[variant="primary"].generate-button-main {{ background: linear-gradient(135deg, var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 80%, #A15E00) 100%) !important; color: var(--white-color) !important; font-weight: 700 !important; border-radius: var(--radius-md) !important; border: none !important; box-shadow: 0 3px 6px rgba(0,0,0,0.1), 0 1px 3px rgba(0,0,0,0.08) !important; padding: 0.85rem 1.8rem !important; font-size: 1.05em !important; transition: var(--transition-ease); transform: perspective(1px) translateZ(0); }}
388
- .gr-button.lg.primary:hover, button[variant="primary"].generate-button-main:hover {{ background: linear-gradient(135deg, color-mix(in srgb, var(--accent-color) 90%, black) 0%, color-mix(in srgb, var(--accent-color) 70%, #A15E00) 100%) !important; transform: translateY(-2px) scale(1.02); box-shadow: 0 6px 12px rgba(0,0,0,0.12), 0 3px 6px rgba(0,0,0,0.1) !important; }}
389
- .gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-number > label + div > input[type="number"] {{ border-radius: var(--radius-md) !important; border: 1.5px solid var(--border-color) !important; background-color: var(--input-bg-color) !important; padding: 0.7rem 0.85rem !important; font-size: 0.98em !important; transition: var(--transition-ease); }}
390
- .gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-dropdown > label + div > div > select:focus, .gr-textbox > label + div > textarea:focus, .gr-number > label + div > input[type="number"]:focus {{ border-color: var(--primary-color) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 20%, transparent) !important; background-color: var(--white-color) !important; }}
391
- label > .label-text {{ font-weight: 600 !important; color: var(--subtle-text-color) !important; font-size: 0.92em !important; margin-bottom: 0.4rem !important; }}
392
- .gr-accordion > .gr-button {{ background-color: var(--light-bg-color) !important; border-radius: var(--radius-md) !important; font-weight: 600 !important; padding: 0.6rem 0.8rem !important; transition: var(--transition-ease); border: 1px solid var(--border-color) !important; }}
393
- .gr-accordion > .gr-button:hover {{ background-color: color-mix(in srgb, var(--light-bg-color) 90%, var(--border-color)) !important; }}
394
- .gr-accordion > .gr-panel {{ background-color: color-mix(in srgb, var(--light-bg-color) 97%, var(--border-color)) !important; border-radius: var(--radius-md) !important; padding: 1.2rem !important; margin-top:0.5rem; border: 1px solid var(--border-color); transition: var(--transition-ease); }}
395
- .status-log-panel {{ background-color: var(--input-bg-color); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 0.75rem 1rem; min-height: 180px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.04); }}
396
- .status-log-panel textarea {{ background-color: transparent !important; border: none !important; font-size: 0.88em !important; color: var(--subtle-text-color); line-height:1.6; }}
397
- .api-warning-box {{ background-color: color-mix(in srgb, var(--accent-color) 10%, #fff) !important; color: color-mix(in srgb, var(--accent-color) 85%, black) !important; padding: 1rem 1.2rem !important; border-radius: var(--radius-md) !important; border: 1.5px solid color-mix(in srgb, var(--accent-color) 40%, transparent) !important; text-align: center !important; margin-bottom: 1.5rem !important; font-size: 0.92em !important; box-shadow: var(--shadow-sm); }}
398
- .success-message-box {{ background-color: color-mix(in srgb, var(--secondary-color) 10%, #fff) !important; color: color-mix(in srgb, var(--secondary-color) 85%, black) !important; padding: 1rem 1.2rem !important; border-radius: var(--radius-md) !important; border: 1.5px solid color-mix(in srgb, var(--secondary-color) 40%, transparent) !important; text-align: center !important; margin-bottom: 1.5rem !important; font-size: 0.92em !important; box-shadow: var(--shadow-sm); }}
399
- .app-footer-text {{ text-align: center; font-size: 0.88em; color: var(--subtle-text-color); margin-top: 3rem; padding: 1.5rem 0; border-top: 1px solid var(--border-color); }}
400
- footer, .gradio-footer {{ display: none !important; visibility: hidden !important; }}
401
- #output_audio_col, #output_download_col {{ padding-top:1.2rem; }}
402
- .gr-form {{ gap: 1.5rem !important; }}
403
- .compact-group .gr-form {{ gap: 0.9rem !important; }}
404
- #examples-section .gr-sample-button {{ background-color: color-mix(in srgb, var(--secondary-color) 12%, transparent) !important; color: var(--secondary-color) !important; border-radius: var(--radius-sm) !important; font-size: 0.88em !important; padding: 0.4rem 0.7rem !important; border: 1.5px solid color-mix(in srgb, var(--secondary-color) 35%, transparent) !important; margin: 0.25rem !important; transition: var(--transition-ease); }}
405
- #examples-section .gr-sample-button:hover {{ background-color: color-mix(in srgb, var(--secondary-color) 22%, transparent) !important; transform: translateY(-1px); box-shadow: var(--shadow-sm); }}
406
- #examples-separator > div > hr, #examples-separator > div > p {{ margin-top: 2rem !important; margin-bottom: 1.5rem !important; height: 1.5px !important; background-color: var(--border-color) !important; border: none !important; opacity: 0.7; font-size:0 !important; }}
407
- @media (max-width: 768px) {{ .main-content-wrapper {{ margin-top: -1.5rem; padding: 0.75rem; }} .content-panel {{ padding: 1.2rem; }} .app-header-card h1 {{ font-size: 1.8em !important; }} .app-header-card .app-subtitle {{ font-size: 0.95em !important; }} .section-title {{ font-size:1.15em; }} }}
408
- @keyframes slideInDown {{ from {{ opacity: 0; transform: translateY(-20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
409
- @keyframes fadeInUp {{ from {{ opacity: 0; transform: translateY(20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
410
- @keyframes zoomIn {{ from {{ opacity: 0; transform: scale(0.95); }} to {{ opacity: 1; transform: scale(1); }} }}
411
- """
412
 
413
  # --- تعریف رابط کاربری Gradio ---
414
- with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "system-ui"]), css=custom_css_v2, title="آواگر جمینای - نسخه پلاس") as demo:
415
- gr.HTML(f"""
416
- <div class="app-header-card">
417
- <h1>💎 آواگر جمینای پلاس</h1>
418
- <p class="app-subtitle">تجربه نوین تبدیل متن به گفتار فارسی با هوش مصنوعی Gemini و ظاهری چشم‌نواز</p>
419
- </div>
420
- """)
421
-
422
- with gr.Column(elem_classes="main-content-wrapper"):
423
- with gr.Group(elem_classes="content-panel"):
424
- if not HF_GEMINI_API_KEY:
425
- gr.HTML(f"<div class='api-warning-box'>⚠️ <strong>هشدار حیاتی:</strong> کلید API جمینای (<code>GEMINI_API_KEY</code>) در Hugging Face Secrets یافت نشد. "
426
- "این ابزار برای کار کردن به این کلید نیاز دارد. لطفاً آن را در بخش 'Settings' > 'Secrets' این Space تنظیم کنید.</div>")
427
- else:
428
- gr.HTML(f"<div class='success-message-box'>"
429
- "🔑 کلید API جمینای با موفقیت از Secrets بارگذاری شد. آواگر جمینای پلاس آماده خدمت‌رسانی است!</div>")
430
-
431
- with gr.Row(equal_height=False):
432
- with gr.Column(scale=3, min_width=320):
433
- gr.Markdown("<h3 class='section-title'>۱. متن و سبک گفتار</h3>", elem_id="input-section")
434
- use_file_cb = gr.Checkbox(label="📁 استفاده از فایل متنی (.txt) برای متن اصلی", value=False, elem_id="use-file-checkbox")
435
- text_file_upload = gr.File(label="آپلود فایل متنی شما", file_types=['.txt'], visible=False, elem_id="text-file-uploader")
436
- text_to_speak_tb = gr.Textbox(
437
- label="📝 متن اصلی برای تبدیل به گفتار:", lines=8,
438
- placeholder="متن خود را در اینجا تایپ کنید، یا از گزینه بالا فایل خود را بارگذاری نمایید...",
439
- visible=True, text_align="right", elem_id="text-input-main"
440
- )
441
- use_file_cb.change(lambda x: (gr.update(visible=x), gr.update(visible=not x)), [use_file_cb], [text_file_upload, text_to_speak_tb])
442
-
443
- speech_prompt_tb = gr.Textbox(
444
- label="🗣️ فرمان سبک گفتار (اختیاری، برای هدایت لحن و احساسات صدا)",
445
- placeholder="مثال: «با صدایی گرم و دوستانه، مانند یک معلم مهربان صحبت کن»",
446
- text_align="right", elem_id="speech-prompt-input", info="این فرمان به هوش مصنوعی کمک می‌کند تا سبک گفتار را بهتر درک کند."
447
- )
448
-
449
- with gr.Column(scale=2, min_width=300):
450
- gr.Markdown("<h3 class='section-title'>۲. تنظیمات پیشرفته صدا</h3>", elem_id="settings-section")
451
- model_choices_farsi = [(MODEL_NAMES_FARSI.get(key, key), key) for key in MODELS]
452
- model_name_dd = gr.Dropdown(choices=model_choices_farsi, label="🤖 انتخاب مدل Gemini (TTS)", value=MODELS[0], elem_id="model-selector", info="مدل پرو کیفیت بالاتری دارد اما ممکن است محدودیت بیشتری داشته باشد.")
453
-
454
- speaker_choices_farsi = [(SPEAKER_VOICES_FARSI_SAMPLE.get(v, v) + f" ({v})", v) for v in SPEAKER_VOICES]
455
- speaker_voice_dd = gr.Dropdown(choices=speaker_choices_farsi, label="🎤 انتخاب صدای گوینده", value="Charon", elem_id="speaker-selector")
456
-
457
- temp_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.75, label="🌡️ دمای خلاقیت مدل", elem_id="temperature-slider", info="مقادیر بالاتر (نزدیک به ۱) تنوع بیشتری در صدا ایجاد می‌کنند.")
458
-
459
- with gr.Accordion("جزئیات بیشتر (تقسیم‌بندی متن و نام فایل)", open=False, elem_id="advanced-settings-accordion"):
460
- # ** اسلایدر max_chunk_size دوباره فعال شد **
461
- max_chunk_size_slider_ui = gr.Slider(minimum=1000, maximum=5000, step=100, value=3800, label="🧩 حداکثر کاراکتر در هر قطعه API (برای TTS)", elem_id="chunk-size-slider", info="مقدار پیشنهادی: ۳۰۰۰ تا ۴۰۰۰ کاراکتر.")
462
- sleep_slider = gr.Slider(minimum=0, maximum=5, step=0.25, value=0.5, label="⏱️ تاخیر بین درخواست‌ها به API (ثانیه)", elem_id="sleep-slider", info="برای جلوگیری از خطای محدودیت درخواست.")
463
- output_filename_tb = gr.Textbox(label="💾 نام پایه برای فایل‌های خروجی (انگلیسی، بدون پسوند)", value="gemini_voice_output", elem_id="output-filename-input")
464
-
465
- with gr.Group(elem_classes="compact-group", elem_id="merge-options-group"):
466
- gr.Markdown("گزینه‌های ادغام (در صورت تولید بیش از یک قطعه صوتی):", elem_id="merge-options-title")
467
- merge_cb = gr.Checkbox(label="🔗 ادغام خودکار قطعات صوتی به یک فایل WAV", value=True, visible=PYDUB_AVAILABLE, elem_id="merge-checkbox")
468
- delete_partials_cb = gr.Checkbox(label="🗑️ حذف فایل‌های قطعه‌بندی شده پس از ادغام موفق", value=True, visible=PYDUB_AVAILABLE, elem_id="delete-partials-checkbox")
469
- if PYDUB_AVAILABLE:
470
- merge_cb.change(lambda x: gr.update(visible=x), [merge_cb], [delete_partials_cb])
471
- else:
472
- gr.HTML("<div class='api-warning-box' style='background-color: #FEF3C7 !important; color: #92400E !important; border-color: #FDE68A !important; margin-top:0.5rem;'>⚠️ قابلیت ادغام فایل‌ها به دلیل عدم دسترسی به کتابخانه <code>pydub</code> غیرفعال است. صداها به صورت جداگانه ذخیره خواهند شد.</div>")
473
-
474
- submit_btn = gr.Button("✨ هم‌اکنون صدا را تولید کن! ✨", variant="primary", elem_id="generate-button-main", elem_classes=["generate-button-main"])
475
-
476
- with gr.Accordion("🎧 نتیجه و گزارش فرآیند 📊", open=True, elem_id="output-report-accordion"):
477
- with gr.Row():
478
- with gr.Column(scale=1, elem_id="output_audio_col"):
479
- output_audio_player = gr.Audio(label="🔊 فایل صوتی نهایی (قابل پخش)", type="filepath", autoplay=True, elem_id="audio-player-output", show_label=True)
480
- with gr.Column(scale=1, elem_id="output_download_col"):
481
- output_file_downloader = gr.File(label="📁 دانلود فایل نهایی (فرمت WAV یا ZIP)", type="filepath", elem_id="file-downloader-output", show_label=True)
482
-
483
- status_log_tb = gr.Textbox(label="📜 گزارش کامل وضعیت و پیام‌های سیستم:", lines=10, interactive=False, text_align="right", elem_id="status-log-textbox", elem_classes=["status-log-panel"], show_label=True)
484
-
485
- submit_btn.click(
486
- fn=generate_audio_for_gradio,
487
- inputs=[
488
- use_file_cb, text_file_upload, speech_prompt_tb, text_to_speak_tb,
489
- max_chunk_size_slider_ui, # ** ارسال مقدار اسلایدر به تابع **
490
- sleep_slider, temp_slider,
491
- model_name_dd, speaker_voice_dd, output_filename_tb,
492
- merge_cb, delete_partials_cb
493
- ],
494
- outputs=[output_audio_player, output_file_downloader, status_log_tb]
495
  )
 
 
 
 
 
496
 
497
- gr.Markdown("---", elem_id="examples-separator")
498
-
499
- gr.Examples(
500
- label="💡 چند نمونه برای شروع سریع (روی یکی کلیک کنید):",
501
- examples=[ # ** مقادیر max_chunk_size در مثال‌ها به‌روز شد **
502
- [False, None, "یک راوی با صدایی گرم و دلنشین، مناسب برای کتاب صوتی.", "در زمان‌های قدیم، در سرزمینی دور، پادشاهی عادل زندگی می‌کرد که مردمش او را بسیار دوست داشتند.", 3800, 0.5, 0.75, MODELS[0], "Charon", "داستان_پادشاه", True, True],
503
- [False, None, "با لحنی پرشور و هیجان‌انگیز، مانند یک گزارشگر ورزشی.", "و گل! یک گل باورنکردنی در دقیقه‌ی نود! تماشاگران به وجد آمده‌اند!", 3500, 0.5, 0.8, MODELS[1], "Achernar", "گزارش_فوتبال", True, True],
504
- ],
505
- fn=generate_audio_for_gradio,
506
- inputs=[ # ** ورودی max_chunk_size_slider_ui اضافه شد **
507
- use_file_cb, text_file_upload, speech_prompt_tb, text_to_speak_tb,
508
- max_chunk_size_slider_ui, sleep_slider, temp_slider,
509
- model_name_dd, speaker_voice_dd, output_filename_tb,
510
- merge_cb, delete_partials_cb
511
- ],
512
- outputs=[output_audio_player, output_file_downloader, status_log_tb],
513
- cache_examples=False, elem_id="examples-section"
514
  )
515
 
516
- gr.HTML("<p class='app-footer-text'>طراحی و توسعه با ❤️ توسط <a href='https://huggingface.co/Hamed744' target='_blank' style='color:var(--primary-color); text-decoration:none; font-weight:600;'>Hamed744 (AIGOLDEN)</a> | نسخه ۱.۳ آواگر جمینای پلاس</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
 
519
  if __name__ == "__main__":
520
- if not PYDUB_AVAILABLE: print("هشدار: کتابخانه pydub نصب نشده یا کار نمی‌کند.")
521
- if not HF_GEMINI_API_KEY: print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است.")
522
- demo.launch(debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true", share=False)
 
 
6
  import struct
7
  import time
8
  import zipfile
9
+ from google import genai
10
+ from google.genai import types
 
11
 
12
  # خواندن کلید API از Hugging Face Secrets
13
+ # این متغیر محیطی توسط Space در زمان اجرا اگر Secret تنظیم شده باشد، تزریق می‌شود.
14
  HF_GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
15
 
16
  try:
 
19
  except ImportError:
20
  PYDUB_AVAILABLE = False
21
  print("⚠️ کتابخانه pydub در دسترس نیست. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
22
+ print("اگر ادغام فایل‌ها مد نظر است، pydub را به requirements.txt اضافه کرده و از وجود ffmpeg در محیط اطمینان حاصل کنید.")
23
 
24
  # --- ثابت‌ها ---
25
+ SPEAKER_VOICES_FA = {
26
+ "آکیرد (زن)": "Achird", "زُبِن‌الجُنوبی (مرد)": "Zubenelgenubi", "ویندِمیاطریکس (زن)": "Vindemiatrix",
27
+ "سَعدالاَخبیه (مرد)": "Sadachbia", "سَعدالتَجر (زن)": "Sadaltager", "سولافات (مرد)": "Sulafat",
28
+ "لائومِدِیا (زن)": "Laomedeia", "آکِرنار (مرد)": "Achernar", "النِلام (زن)": "Alnilam",
29
+ "شِدار (مرد)": "Schedar", "گاکراکس (زن)": "Gacrux", "پولکِریما (مرد)": "Pulcherrima",
30
+ "آمبرِیِل (زن)": "Umbriel", "اَلجِیبا (مرد)": "Algieba", "دِسپینا (زن)": "Despina",
31
+ "اِرینومه (مرد)": "Erinome", "اَلجِنیب (زن)": "Algenib", "رأس‌الجاثی (مرد)": "Rasalthgeti",
32
+ "اوروس (زن)": "Orus", "آئوئِده (مرد)": "Aoede", "کالیرهوئه (زن)": "Callirrhoe",
33
+ "اوتونوئه (مرد)": "Autonoe", "اِنسِلادوس (زن)": "Enceladus", "یاپِتوس (مرد)": "Iapetus",
34
+ "زِفیر (زن)": "Zephyr", "پاک (مرد)": "Puck", "کارون (زن، پیش‌فرض)": "Charon",
35
+ "کوره (مرد)": "Kore", "فِنریر (زن)": "Fenrir", "لِدا (مرد)": "Leda"
 
36
  }
37
+ MODELS_FA = {
38
+ "جمینای ۲.۵ فلش (سریع‌تر، کیفیت خوب)": "gemini-2.5-flash-preview-tts",
39
+ "جمینای ۲.۵ پرو (کندتر، کیفیت بالاتر)": "gemini-2.5-pro-preview-tts"
 
 
40
  }
41
+ SPEAKER_VOICES_LIST = list(SPEAKER_VOICES_FA.keys())
42
+ MODELS_LIST = list(MODELS_FA.keys())
43
+
44
 
45
  # --- توابع کمکی ---
46
  def save_binary_file(file_name, data):
47
  abs_file_name = os.path.abspath(file_name)
48
  try:
49
+ with open(abs_file_name, "wb") as f:
50
+ f.write(data)
51
+ print(f" فایل در مسیر زیر ذخیره شد: {abs_file_name}")
52
+ return abs_file_name
53
+ except Exception as e:
54
+ print(f"❌ خطا در ذخیره فایل {abs_file_name}: {e}")
55
+ return None
56
 
57
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
58
  parameters = parse_audio_mime_type(mime_type)
59
+ bits_per_sample = parameters["bits_per_sample"]
60
+ sample_rate = parameters["rate"]
61
+ num_channels = 1
62
+ data_size = len(audio_data)
63
+ bytes_per_sample = bits_per_sample // 8
64
+ block_align = num_channels * bytes_per_sample
65
+ byte_rate = sample_rate * block_align
66
  chunk_size = 36 + data_size
67
+ header = struct.pack(
68
+ "<4sI4s4sIHHIIHH4sI",
69
+ b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
70
+ sample_rate, byte_rate, block_align, bits_per_sample, b"data", data_size
71
+ )
72
+ return header + audio_data
73
 
74
  def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
75
+ bits_per_sample = 16
76
+ rate = 24000
77
  if mime_type:
78
+ mime_type_lower = mime_type.lower()
79
+ parts = mime_type_lower.split(";")
80
  for param in parts:
81
  param = param.strip()
82
  if param.startswith("rate="):
 
84
  except: pass
85
  elif param.startswith("audio/l"):
86
  try:
87
+ potential_bits = param.split("l", 1)[1]
88
  if potential_bits.isdigit(): bits_per_sample = int(potential_bits)
89
  except: pass
90
  return {"bits_per_sample": bits_per_sample, "rate": rate}
91
 
92
  def load_text_from_gr_file(file_obj):
93
+ if file_obj is None:
94
+ return "", "فایلی برای ورودی متن انتخاب نشده است."
95
  try:
96
+ with open(file_obj.name, 'r', encoding='utf-8') as f:
97
+ content = f.read().strip()
98
+ if not content:
99
+ return "", "فایل متنی خالی است."
100
  return content, f"متن با موفقیت از فایل '{os.path.basename(file_obj.name)}' ({len(content)} کاراکتر) بارگذاری شد."
101
+ except Exception as e:
102
+ return "", f"خطا در خواندن فایل متنی: {e}"
 
 
 
 
 
103
 
104
+ def smart_text_split(text, max_size=3800):
105
+ if len(text) <= max_size: return [text]
106
+ chunks, current_chunk = [], ""
107
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
 
108
  for sentence in sentences:
109
+ if not sentence: continue
110
+ if len(current_chunk) + len(sentence) + 1 > max_size:
111
+ if current_chunk: chunks.append(current_chunk.strip())
112
+ if len(sentence) > max_size:
113
+ words = sentence.split(' ')
114
+ temp_part = ""
115
+ for word in words:
116
+ if len(temp_part) + len(word) + 1 > max_size:
117
+ if temp_part: chunks.append(temp_part.strip())
118
+ if len(word) > max_size:
119
+ for i in range(0, len(word), max_size): chunks.append(word[i:i+max_size])
120
+ temp_part = ""
121
+ else: temp_part = word
122
+ else: temp_part += (" " if temp_part else "") + word
123
+ if temp_part: chunks.append(temp_part.strip())
124
  current_chunk = ""
125
+ else: current_chunk = sentence
126
+ else: current_chunk += (" " if current_chunk else "") + sentence
127
+ if current_chunk: chunks.append(current_chunk.strip())
128
+ return chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  def merge_audio_files_func(file_paths, output_path):
131
  if not PYDUB_AVAILABLE: return False, "pydub در دسترس نیست. امکان ادغام فایل‌ها وجود ندارد.", None
132
+ if not file_paths: return False, "فایل صوتی برای ادغام وجود ندارد.", None
133
  try:
134
  combined = AudioSegment.empty()
135
+ for i, fp in enumerate(file_paths):
136
+ if os.path.exists(fp):
137
  try:
138
+ audio = AudioSegment.from_file(fp, format=fp.split('.')[-1]) # Guess format from extension
139
  combined += audio
140
  if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
141
  except Exception as e_load:
142
+ return False, f"خطا در بارگذاری فایل صوتی '{os.path.basename(fp)}': {e_load}", None
143
+ else: return False, f"فایل برای ادغام یافت نشد: {os.path.basename(fp)}", None
 
 
 
144
  abs_output_path = os.path.abspath(output_path)
145
  combined.export(abs_output_path, format="wav")
146
+ return True, f"فایل ادغام شده با موفقیت در '{os.path.basename(abs_output_path)}' ذخیره شد.", abs_output_path
147
+ except Exception as e: return False, f"خطا در ادغام فایل‌ها: {e}", None
148
+
 
149
  def create_zip_file(file_paths, zip_name):
150
  abs_zip_name = os.path.abspath(zip_name)
151
  try:
152
  with zipfile.ZipFile(abs_zip_name, 'w') as zipf:
153
+ for fp in file_paths:
154
+ if os.path.exists(fp): zipf.write(fp, os.path.basename(fp))
155
+ return True, f"فایل ZIP با موفقیت در '{os.path.basename(abs_zip_name)}' ایجاد شد.", abs_zip_name
 
156
  except Exception as e: return False, f"خطا در ایجاد فایل ZIP: {e}", None
157
 
158
  # --- تابع اصلی تولید صدا ---
159
  def generate_audio_for_gradio(
160
+ use_file_input_checkbox, text_file_obj,
161
+ speech_prompt_input, text_to_speak_input,
162
+ max_chunk_slider, sleep_slider, temperature_slider,
163
+ model_dropdown_fa, speaker_dropdown_fa, output_filename_base_input,
164
+ merge_checkbox, delete_partials_checkbox,
165
  progress=gr.Progress(track_tqdm=True)
166
  ):
167
+ status_messages = ["🚀 شروع فرآیند تبدیل متن به گفتار..."]
168
  progress(0, desc="در حال آماده‌سازی...")
169
 
170
  api_key_to_use = HF_GEMINI_API_KEY
171
  if not api_key_to_use:
172
+ status_messages.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) در Hugging Face Secrets یافت نشد.")
173
+ status_messages.append("⬅️ لطفاً آن را در بخش Settings > Secrets در تنظیمات Space خود اضافه کنید.")
174
  return None, None, "\n".join(status_messages)
175
+ os.environ["GEMINI_API_KEY"] = api_key_to_use
176
+ status_messages.append("🔑 کلید API با موفقیت از Secrets بارگذاری شد.")
 
177
 
178
+ actual_text_input, msg = ("", "")
179
  if use_file_input_checkbox:
180
  if text_file_obj is None:
181
+ status_messages.append("❌ خطا: گزینه 'استفاده از فایل متنی' انتخاب شده، اما فایلی آپلود نشده است.")
182
  return None, None, "\n".join(status_messages)
183
  actual_text_input, msg = load_text_from_gr_file(text_file_obj)
 
 
184
  else:
185
  actual_text_input = text_to_speak_input
186
+ msg = "⌨️ استفاده از متن وارد شده دستی."
187
+ status_messages.append(msg)
188
+ if not actual_text_input: return None, None, "\n".join(status_messages)
189
 
190
+ try:
191
+ status_messages.append("🛠️ در حال مقداردهی اولیه کلاینت جمینای...")
192
+ progress(0.1, desc="مقداردهی کلاینت جمینای...")
193
+ client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
194
+ status_messages.append("✅ کلاینت جمینای با موفقیت مقداردهی شد.")
195
+ except Exception as e:
196
+ status_messages.append(f"❌ خطا در مقداردهی کلاینت جمینای: {e}")
197
+ return None, None, "\n".join(status_messages)
198
 
199
+ text_chunks = smart_text_split(actual_text_input, int(max_chunk_slider))
200
+ status_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
201
+ for i, chunk_text in enumerate(text_chunks): status_messages.append(f" 📝 قطعه {i+1}: {len(chunk_text)} کاراکتر")
 
202
 
203
+ generated_audio_files, run_id = [], base64.urlsafe_b64encode(os.urandom(6)).decode()
204
+ temp_output_dir = f"temp_audio_{run_id}"
205
+ os.makedirs(temp_output_dir, exist_ok=True)
206
  output_base_name_safe = re.sub(r'[\s\\\/\:\*\?\"\<\>\|\%]+', '_', output_filename_base_input)
207
 
208
+ # Map selected FA names to actual API names
209
+ selected_model_api_name = MODELS_FA[model_dropdown_fa]
210
+ selected_speaker_api_name = SPEAKER_VOICES_FA[speaker_dropdown_fa]
211
+
212
  total_chunks = len(text_chunks)
213
  for i, chunk_text_content in enumerate(text_chunks):
214
+ progress_val = 0.1 + (0.7 * (i / total_chunks))
215
+ progress(progress_val, desc=f"در حال تولید قطعه {i+1} از {total_chunks}...")
216
 
217
+ status_messages.append(f"\n🔊 تولید صدا برای قطعه {i+1}/{total_chunks}...")
218
+ final_text_for_api = f'"{speech_prompt_input}"\n{chunk_text_content}' if speech_prompt_input.strip() else chunk_text_content
 
 
 
 
219
 
220
+ contents_for_api = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
221
+ generate_content_config = types.GenerateContentConfig(
 
 
 
 
 
 
 
 
 
222
  temperature=float(temperature_slider),
223
+ response_modalities=["audio"],
224
+ speech_config=types.SpeechConfig(
225
  voice_config=types.VoiceConfig(
226
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_speaker_api_name)
 
 
227
  )
228
+ )
229
  )
230
+ audio_data_received = False
 
231
  try:
232
+ for stream_resp_chunk in client.models.generate_content_stream(
233
+ model=selected_model_api_name, contents=contents_for_api, config=generate_content_config,
 
 
 
 
 
 
 
 
234
  ):
235
+ if (stream_resp_chunk.candidates and stream_resp_chunk.candidates[0].content and
236
+ stream_resp_chunk.candidates[0].content.parts and
237
+ stream_resp_chunk.candidates[0].content.parts[0].inline_data):
238
+
239
+ inline_data = stream_resp_chunk.candidates[0].content.parts[0].inline_data
240
  data_buffer, api_mime_type = inline_data.data, inline_data.mime_type
241
  audio_data_received = True
242
  status_messages.append(f"ℹ️ MIME Type دریافتی از API: {api_mime_type}")
243
+
244
+ file_ext = ".wav" # پیش‌فرض wav و تبدیل
245
  if api_mime_type and ("mp3" in api_mime_type.lower() or "mpeg" in api_mime_type.lower()):
246
+ file_ext = ".mp3"
247
+ status_messages.append(f"ℹ️ ذخیره به صورت MP3 بر اساس MIME: {api_mime_type}")
248
  elif api_mime_type and "wav" in api_mime_type.lower() and not ("audio/l16" in api_mime_type.lower() or "audio/l24" in api_mime_type.lower()):
249
+ file_ext = ".wav"
250
+ status_messages.append(f"ℹ️ ذخیره به صورت WAV بر اساس MIME: {api_mime_type}")
251
  else:
252
+ status_messages.append(f"ℹ️ تبدیل به WAV برای MIME: {api_mime_type or 'نامشخص'}")
253
  data_buffer = convert_to_wav(data_buffer, api_mime_type)
254
+
255
+ status_messages.append(f"ℹ️ پسوند فایل تعیین شده: {file_ext}")
256
+ chunk_fp_prefix = os.path.join(temp_output_dir, f"{output_base_name_safe}_part_{i+1:03d}")
257
+ gen_file_path = save_binary_file(f"{chunk_fp_prefix}{file_ext}", data_buffer)
258
+
259
+ if gen_file_path:
260
+ generated_audio_files.append(gen_file_path)
261
+ status_messages.append(f"✅ قطعه {i+1} ذخیره شد: {os.path.basename(gen_file_path)}")
262
  else: status_messages.append(f"❌ عدم موفقیت در ذخیره قطعه {i+1}.")
263
  break
264
+
265
+ elif stream_resp_chunk.text: status_messages.append(f"ℹ️ پیام متنی از API (حین استریم): {stream_resp_chunk.text}")
266
+
267
  if not audio_data_received:
268
+ status_messages.append(f"❌ داده صوتی برای قطعه {i+1} در استریم دریافت نشد.")
269
+ if stream_resp_chunk and stream_resp_chunk.prompt_feedback and stream_resp_chunk.prompt_feedback.block_reason:
270
+ status_messages.append(f"🛑 دلیل بلاک شدن توسط API: {stream_resp_chunk.prompt_feedback.block_reason_message or stream_resp_chunk.prompt_feedback.block_reason}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ except Exception as e:
273
+ status_messages.append(f" خطا در تولید/پردازش قطعه {i+1}: {e}")
274
+ import traceback; status_messages.append(traceback.format_exc())
275
+ continue
276
 
277
+ if i < total_chunks - 1:
278
+ status_messages.append(f"⏱️ انتظار به مدت {sleep_slider} ثانیه...")
279
+ time.sleep(float(sleep_slider))
280
 
281
+ progress(0.85, desc="پردازش فایل‌های تولید شده...")
282
  if not generated_audio_files:
283
  status_messages.append("❌ هیچ فایل صوتی با موفقیت تولید یا ذخیره نشد!")
284
+ final_status = "\n".join(status_messages)
285
+ print(final_status)
286
+ progress(1, desc="پایان با خطا.")
287
+ return None, None, final_status
288
+ status_messages.append(f"\n🎉 {len(generated_audio_files)} فایل صوتی تولید شد!")
289
+
290
+ out_audio_player_path, out_download_path = None, None
291
+ if merge_checkbox and len(generated_audio_files) > 1:
292
+ if not PYDUB_AVAILABLE:
293
+ status_messages.append("⚠️ pydub در دسترس نیست. امکان ادغام وجود ندارد. فایل ZIP قطعات ارائه می‌شود.")
294
+ success, msg, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
295
+ status_messages.append(msg)
296
+ if success: out_download_path = zip_p
 
 
297
  else:
298
+ status_messages.append(f"🔗 در حال ادغام {len(generated_audio_files)} فایل...")
299
+ merged_fp = os.path.join(temp_output_dir, f"{output_base_name_safe}_merged.wav")
300
+ success, msg, merged_p = merge_audio_files_func(generated_audio_files, merged_fp)
301
+ status_messages.append(msg)
302
+ if success:
303
+ out_audio_player_path, out_download_path = merged_p, merged_p
304
+ if delete_partials_checkbox:
305
+ status_messages.append("🗑️ در حال حذف فایل‌های جزئی...")
306
+ for fp in generated_audio_files:
307
+ try: os.remove(fp); status_messages.append(f" 🗑️ حذف شد: {os.path.basename(fp)}")
308
+ except Exception as e_del: status_messages.append(f" ⚠️ عدم موفقیت در حذف {os.path.basename(fp)}: {e_del}")
309
+ else:
310
+ status_messages.append("⚠️ ادغام ناموفق بود. فایل ZIP قطعات ارائه می‌شود.")
311
+ success_zip, msg_zip, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
312
+ status_messages.append(msg_zip)
313
+ if success_zip: out_download_path = zip_p
314
  elif len(generated_audio_files) == 1:
315
+ single_fp = generated_audio_files[0]
316
+ out_audio_player_path, out_download_path = single_fp, single_fp
317
+ status_messages.append(f"🎵 فایل صوتی تکی: {os.path.basename(single_fp)}")
318
+ else:
319
+ status_messages.append("📦 چندین قطعه تولید شده است. در حال ایجاد فایل ZIP.")
320
+ success, msg, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
321
+ status_messages.append(msg)
322
+ if success: out_download_path = zip_p
323
+
324
  final_status = "\n".join(status_messages)
325
  print(final_status)
326
+ print(f"DEBUG: مسیر فایل برای پخش‌کننده: {out_audio_player_path}")
327
+ print(f"DEBUG: مسیر فایل برای دانلود: {out_download_path}")
328
+ progress(1, desc="پایان!")
329
+ return out_audio_player_path, out_download_path, final_status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
  # --- تعریف رابط کاربری Gradio ---
332
+ css = """
333
+ body { font-family: 'Vazirmatn', 'Tahoma', sans-serif; direction: rtl; }
334
+ .gradio-container { max-width: 900px !important; margin: auto !important; }
335
+ footer { display: none !important; } /* Hide default Gradio footer */
336
+ .gr-button { font-weight: bold; }
337
+ .st-emotion-cache-1uj092c, .st-emotion-cache-1wnczdq { font-family: 'Vazirmatn', 'Tahoma', sans-serif !important; } /* Forcing font on some elements */
338
+ .rtl-override { direction: rtl !important; text-align: right !important; }
339
+ .rtl-override input, .rtl-override textarea, .rtl-override select { direction: rtl !important; text-align: right !important; }
340
+ label > .label-text { font-size: 1.1em !important; margin-bottom: 5px !important; }
341
+ .gr-input, .gr-dropdown, .gr-slider { margin-bottom: 10px !important; }
342
+ """
343
+
344
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky, font=[gr.themes.GoogleFont("Vazirmatn"), "Tahoma", "sans-serif"]), css=css, title="تبدیل متن به گفتار با جمینای") as demo:
345
+ gr.Markdown("<h1 style='text-align: center; color: #2A7AF2;'>🎵 تبدیل متن به گفتار با API جمینای 🗣️</h1>", elem_classes=["rtl-override"])
346
+
347
+ if not HF_GEMINI_API_KEY:
348
+ gr.Warning(
349
+ "کلید API جمینای (GEMINI_API_KEY) در Hugging Face Secrets یافت نشد. "
350
+ "برای کارکرد صحیح اپلیکیشن، لطفاً آن را در بخش 'Settings' > 'Secrets' این Space با نام `GEMINI_API_KEY` اضافه کنید."
351
+ )
352
+ else:
353
+ gr.Info("کلید API جمینای با موفقیت از Secrets بارگذاری شد. اپلیکیشن آماده تولید صدا است!")
354
+
355
+ gr.Markdown(
356
+ "این ابزار متن شما را با استفاده از مدل‌های پیشرفته جمینای گوگل به گفتار تبدیل می‌کند. "
357
+ "مطمئن شوید که کلید API جمینای خود را در بخش Secrets این Space تنظیم کرده‌اید."
358
+ "\n\nمی‌توانید کلید API خود را از [Google AI Studio](https://aistudio.google.com/app/apikey) دریافت کنید.",
359
+ elem_classes=["rtl-override"]
360
+ )
361
+
362
+ with gr.Row(elem_classes=["rtl-override"]):
363
+ with gr.Column(scale=2):
364
+ gr.Markdown("### ۱. ورودی متن", elem_classes=["rtl-override"])
365
+ use_file = gr.Checkbox(label="📁 استفاده از فایل متنی (.txt)", value=False, elem_classes=["rtl-override"])
366
+ text_file = gr.File(
367
+ label="بارگذاری فایل متنی", file_types=['.txt'], visible=False, elem_classes=["rtl-override"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  )
369
+ text_to_speak = gr.Textbox(
370
+ label="📝 متنی که می‌خواهید به گفتار تبدیل شود (یا از فایل بالا استفاده کنید):",
371
+ lines=8, placeholder="متن خود را اینجا وارد کنید...", visible=True, elem_classes=["rtl-override"]
372
+ )
373
+ use_file.change(lambda x: (gr.update(visible=x), gr.update(visible=not x)), [use_file], [text_file, text_to_speak])
374
 
375
+ speech_prompt = gr.Textbox(
376
+ label="🗣️ پرامپت راهنمای گفتار (اختیاری):",
377
+ placeholder="مثال: «با لحنی دوستانه و پرانرژی، مانند یک یوتیوبر»",
378
+ info="این پرامپت بر سبک، احساسات و ویژگی‌های صدای خروجی تأثیر می‌گذارد.", elem_classes=["rtl-override"]
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  )
380
 
381
+ with gr.Column(scale=1):
382
+ gr.Markdown("### ۲. تنظیمات تولید صدا", elem_classes=["rtl-override"])
383
+ model_name_fa = gr.Dropdown(
384
+ MODELS_LIST, label="🤖 انتخاب مدل:", value=MODELS_LIST[0], elem_classes=["rtl-override"]
385
+ )
386
+ speaker_voice_fa = gr.Dropdown(
387
+ SPEAKER_VOICES_LIST, label="🎤 انتخاب گوینده:", value="کارون (زن، پیش‌فرض)", elem_classes=["rtl-override"]
388
+ )
389
+ temperature = gr.Slider(
390
+ minimum=0.0, maximum=1.0, step=0.05, value=0.7, label="🌡️ دما (Temperature):",
391
+ info="میزان خلاقیت و تنوع صدا (0.0 تا 1.0). مقادیر بالاتر تنوع بیشتری ایجاد می‌کنند.", elem_classes=["rtl-override"]
392
+ )
393
+ max_chunk_size = gr.Slider(
394
+ minimum=1000, maximum=4000, step=100, value=3800, label="🧩 حداکثر کاراکتر در هر قطعه:",
395
+ info="متن برای ارسال به API به قطعات کوچکتر تقسیم می‌شود.", elem_classes=["rtl-override"]
396
+ )
397
+ sleep_between_requests = gr.Slider(
398
+ minimum=1, maximum=15, step=0.5, value=3, label="⏱️ تاخیر بین درخواست‌ها (ثانیه):",
399
+ info="برای مدیریت محدودیت‌های API (مثلاً جمینای فلش ۶۰ درخواست در دقیقه).", elem_classes=["rtl-override"]
400
+ )
401
+ output_filename_base = gr.Textbox(
402
+ label="💾 نام پایه فایل خروجی:", value="صدای_جمینای", elem_classes=["rtl-override"]
403
+ )
404
+
405
+ with gr.Group(visible=PYDUB_AVAILABLE):
406
+ merge_audio = gr.Checkbox(label="🔗 ادغام قطعات صوتی (در صورت وجود بیش از یک قطعه)", value=True, elem_classes=["rtl-override"])
407
+ delete_partials = gr.Checkbox(label="🗑️ حذف قطعات پس از ادغام", value=True, visible=True, elem_classes=["rtl-override"])
408
+ merge_audio.change(lambda x: gr.update(visible=x), [merge_audio], [delete_partials])
409
+
410
+ if not PYDUB_AVAILABLE:
411
+ gr.Markdown("<small style='color: orange;'>⚠️ قابلیت ادغام غیرفعال است: کتابخانه `pydub` یافت نشد.</small>", elem_classes=["rtl-override"])
412
+
413
+ submit_button = gr.Button("✨ تولید صدا ✨", variant="primary", elem_classes=["rtl-override"], scale=2)
414
+ gr.Markdown("---", elem_classes=["rtl-override"])
415
+ gr.Markdown("### ۳. خروجی و گزارش", elem_classes=["rtl-override"])
416
+
417
+ with gr.Row(elem_classes=["rtl-override"]):
418
+ with gr.Column(scale=1):
419
+ output_audio_player = gr.Audio(label="🎧 فایل صوتی تولید شده:", type="filepath", format="wav")
420
+ with gr.Column(scale=1):
421
+ output_file_download = gr.File(label="📥 دانلود فایل خروجی:", type="filepath")
422
+
423
+ status_textbox = gr.Textbox(label="📊 گزارش وضعیت:", lines=10, interactive=False, max_lines=20, elem_classes=["rtl-override"])
424
+
425
+ submit_button.click(
426
+ fn=generate_audio_for_gradio,
427
+ inputs=[
428
+ use_file, text_file, speech_prompt, text_to_speak,
429
+ max_chunk_size, sleep_between_requests, temperature,
430
+ model_name_fa, speaker_voice_fa, output_filename_base, # Use FA dropdowns
431
+ merge_audio, delete_partials
432
+ ],
433
+ outputs=[output_audio_player, output_file_download, status_textbox]
434
+ )
435
+
436
+ gr.Markdown("---", elem_classes=["rtl-override"])
437
+ # The encoded text part:
438
+ encoded_text_creator = "Q3JlYXRlIGJ5IDogYWlnb2xkZW4=" # "Created by : aigolden"
439
+ try:
440
+ decoded_text_creator = base64.b64decode(encoded_text_creator.encode('utf-8')).decode('utf-8')
441
+ gr.Markdown(f"<p style='text-align:center; font-size:small; color: #555;'><em>{decoded_text_creator} | ترجمه و بهبود توسط مدل هوش مصنوعی</em></p>", elem_classes=["rtl-override"])
442
+ except: pass
443
+
444
+ gr.Examples(
445
+ examples=[
446
+ [False, None, "راوی با لحنی دوستانه و آموزنده.", "سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با استفاده از جمینای و گرادیو است. امیدوارم به خوبی کار کند!", 3800, 3, 0.7, MODELS_LIST[0], "کارون (زن، پیش‌فرض)", "مثال_سلام", True, True],
447
+ [False, None, "گوینده خبر هیجان‌زده.", "خبر فوری! هوش مصنوعی اکنون می‌تواند گفتاری شبیه به انسان تولید کند. این فناوری به سرعت در حال پیشرفت است!", 3000, 3, 0.8, MODELS_LIST[1], "آکِرنار (مرد)", "مثال_خبر", True, True],
448
+ [True, "sample_text.txt", "داستان‌گویی با لحنی آرام.", "", 3500, 4, 0.6, MODELS_LIST[0], "ویندِمیاطریکس (زن)", "مثال_از_فایل", True, False]
449
+ ],
450
+ fn=generate_audio_for_gradio,
451
+ inputs=[
452
+ use_file, text_file, speech_prompt, text_to_speak,
453
+ max_chunk_size, sleep_between_requests, temperature,
454
+ model_name_fa, speaker_voice_fa, output_filename_base,
455
+ merge_audio, delete_partials
456
+ ],
457
+ outputs=[output_audio_player, output_file_download, status_textbox],
458
+ cache_examples=False, # API calls
459
+ label="نمونه‌های آماده (برای استفاده از مثال فایل، فایل sample_text.txt باید موجود باشد):",
460
+ elem_classes=["rtl-override"]
461
+ )
462
+ gr.Markdown("<small style='display: block; text-align: center;'>برای استفاده از مثال «فایل متنی نمونه»، ابتدا یک فایل با نام `sample_text.txt` حاوی متن دلخواه در ریشه این Space ایجاد کنید، یا فایل متنی خود را بارگذاری نمایید.</small>", elem_classes=["rtl-override"])
463
 
464
 
465
  if __name__ == "__main__":
466
+ if not PYDUB_AVAILABLE: print("هشدار: کتابخانه pydub نصب نشده یا کار نمی‌کند. ادغام فایل‌های صوتی غیرفعال خواهد بود.")
467
+ if not HF_GEMINI_API_KEY: print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است. اپلیکیشن در اجرای محلی ممکن است بدون کلید API کار نکند.")
468
+
469
+ demo.launch(debug=True, share=False)