Hamed744 commited on
Commit
e2d623f
·
verified ·
1 Parent(s): afeabe2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -143
app.py CHANGED
@@ -6,14 +6,28 @@ import re
6
  import struct
7
  import time
8
  import zipfile
9
- from google import genai
10
- from google.genai import types
11
- from google.api_core import exceptions as google_exceptions # برای تشخیص دقیق‌تر خطای سهمیه
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  try:
14
  from pydub import AudioSegment
15
  PYDUB_AVAILABLE = True
16
  except ImportError:
 
17
  PYDUB_AVAILABLE = False
18
 
19
  # --- START: منطق چرخش API Key ---
@@ -24,40 +38,29 @@ while os.environ.get(f"GEMINI_API_KEY_{i}"):
24
  i += 1
25
 
26
  NUM_API_KEYS = len(GEMINI_API_KEYS)
27
- # CURRENT_KEY_INDEX_GLOBAL: نشان دهنده *اولین* کلیدی است که برای یک درخواست کامل از Gradio باید امتحان شود.
28
- # این متغیر پس از هر درخواست کامل (موفق یا ناموفق) پیش می‌رود.
29
  CURRENT_KEY_INDEX_GLOBAL = 0
30
 
31
  def _log(message):
32
  print(f"[لاگ آلفا TTS] {message}")
33
 
 
 
 
34
  if NUM_API_KEYS == 0:
35
- _log("⛔️ خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!")
36
  else:
37
  _log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
38
 
39
  def get_api_key_for_attempt(attempt_within_request):
40
- """
41
- کلید API را برای یک تلاش خاص *درون یک درخواست Gradio* برمی‌گرداند.
42
- attempt_within_request از 0 شروع می‌شود.
43
- """
44
  if NUM_API_KEYS == 0:
45
- return None, -1, -1 # key, display_num, actual_index_in_list
46
 
47
- # اندیس کلیدی که باید در لیست GEMINI_API_KEYS استفاده شود:
48
- # (اندیس کلید شروع سراسری + تعداد تلاش‌های این درخواست) % تعداد کل کلیدها
49
  actual_key_index_in_list = (CURRENT_KEY_INDEX_GLOBAL + attempt_within_request) % NUM_API_KEYS
50
-
51
  key_to_use = GEMINI_API_KEYS[actual_key_index_in_list]
52
- key_display_number = actual_key_index_in_list + 1 # برای نمایش به کاربر (1-based)
53
-
54
  return key_to_use, key_display_number, actual_key_index_in_list
55
 
56
  def advance_global_key_index_for_next_request():
57
- """
58
- پس از اتمام یک درخواست کامل Gradio (تمام قطعات آن)،
59
- اندیس سراسری را برای *درخواست Gradio بعدی* پیش می‌برد.
60
- """
61
  global CURRENT_KEY_INDEX_GLOBAL
62
  if NUM_API_KEYS > 0:
63
  CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
@@ -72,8 +75,8 @@ SPEAKER_VOICES = [
72
  ]
73
  FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
74
  DEFAULT_MAX_CHUNK_SIZE = 3800
75
- DEFAULT_SLEEP_BETWEEN_REQUESTS = 8 # کاهش زمان انتظار بین قطعات اگر از یک کلید موفق استفاده می‌کنیم
76
- RETRY_SLEEP_AFTER_QUOTA_ERROR = 2 # زمان کوتاه انتظار قبل از تلاش با کلید بعدی
77
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
78
 
79
  def save_binary_file(file_name, data):
@@ -149,14 +152,21 @@ def merge_audio_files_func(file_paths, output_path):
149
  except Exception as e: _log(f"❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
150
 
151
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
 
 
 
 
 
 
 
 
152
  _log("🚀 شروع فرآیند تولید صدا...")
153
-
154
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
155
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
156
 
157
  if not text_input or not text_input.strip():
158
  _log("❌ متن ورودی خالی است.")
159
- advance_global_key_index_for_next_request() # اطمینان از اینکه درخواست بعدی با کلید بعدی شروع شود
160
  return None
161
 
162
  text_chunks = smart_text_split(text_input, max_chunk)
@@ -166,28 +176,22 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
166
  return None
167
 
168
  generated_files = []
169
- all_chunks_processed = True # پرچمی برای بررسی اینکه آیا تمام قطعات موفقیت آمیز بوده اند
170
 
171
- # --- START: منطق تلاش مجدد با کلیدهای مختلف برای هر قطعه ---
172
  for chunk_idx, chunk_text in enumerate(text_chunks):
173
  chunk_processed_successfully = False
174
  _log(f" 🔊 پردازش قطعه {chunk_idx + 1}/{len(text_chunks)}...")
175
-
176
- # تعداد تلاش‌ها برای این قطعه خاص، حداکثر به تعداد کلیدهای موجود یا 1 اگر کلیدی نیست
177
- max_attempts_for_chunk = NUM_API_KEYS if NUM_API_KEYS > 0 else 1
178
 
179
  for attempt_num_for_chunk in range(max_attempts_for_chunk):
180
  selected_api_key, key_display_num, actual_key_idx = get_api_key_for_attempt(attempt_num_for_chunk)
181
-
182
- if not selected_api_key: # اگر هیچ کلیدی موجود نباشد (نباید اینجا اتفاق بیفتد اگر NUM_API_KEYS > 0)
183
- _log("❌ هیچ کلید API معتبری برای تلاش وجود ندارد.")
184
- all_chunks_processed = False
185
- break # خروج از حلقه تلاش برای این قطعه
186
-
187
  _log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} برای قطعه {chunk_idx+1} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
188
 
189
  try:
190
- client = genai.Client(api_key=selected_api_key)
 
191
 
192
  if prompt_input and prompt_input.strip():
193
  processed_prompt = prompt_input.strip()
@@ -197,15 +201,13 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
197
  else:
198
  final_text_for_api = chunk_text.strip()
199
 
200
- # _log(f" متن ارسالی به API: '{final_text_for_api[:70]}...'")
201
-
202
  contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
203
  config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
204
  speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
205
  prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
206
 
207
- fname_base = f"{output_base_name}_part{chunk_idx+1:03d}" # نام فایل موقت برای این قطعه
208
-
209
  response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
210
 
211
  if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
@@ -215,61 +217,55 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
215
  if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
216
  if not ext.startswith("."): ext = "." + ext
217
 
218
- # اطمینان از اینکه فایل قبلی (اگر از تلاش قبلی برای همین قطعه مانده) حذف شود
219
  temp_fpath_for_chunk = f"{fname_base}{ext}"
220
  if os.path.exists(temp_fpath_for_chunk):
221
  try: os.remove(temp_fpath_for_chunk)
222
- except OSError as e_rem: _log(f" ⚠️ نتوانست فایل موقت قبلی را حذف کند: {e_rem}")
223
 
224
  fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
225
  if fpath:
226
  generated_files.append(fpath)
227
  chunk_processed_successfully = True
228
  _log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
229
- if chunk_idx < len(text_chunks) - 1: # اگر قطعات دیگری هم هستند
230
- time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS) # صبر قبل از پردازش قطعه بعدی (با همین کلید)
231
- break # خروج از حلقه تلاش (attempt_num_for_chunk) چون این قطعه موفق بود
232
  else:
233
  _log(f" ⚠️ پاسخ API برای قطعه {chunk_idx+1} با کلید {key_display_num} بدون داده صوتی بود.")
234
 
235
- except google_exceptions.ResourceExhausted as e_quota:
236
- _log(f" ❌ خطای سهمیه (RESOURCE_EXHAUSTED) برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {e_quota.message[:100]}...")
237
- if attempt_num_for_chunk < max_attempts_for_chunk - 1: # اگر کلیدهای دیگری برای امتحان باقی مانده
238
  _log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه.")
239
  time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
240
  else:
241
  _log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند و ناموفق بودند (خطای سهمیه).")
242
- all_chunks_processed = False # یک قطعه ناموفق بود
243
 
244
  except Exception as e_general:
245
- _log(f" ❌ خطای عمومی در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {e_general}")
246
- # برای خطاهای عمومی، معمولاً تلاش مجدد با کلید دیگر کمکی نمی‌کند، مگر اینکه خطای شبکه موقتی باشد
247
- # اما برای سادگی، اجازه می‌دهیم حلقه تلاش ادامه یابد
248
  if attempt_num_for_chunk < max_attempts_for_chunk - 1:
249
- time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR) # کمی صبر قبل از تلاش با کلید بعدی
250
  else:
251
- all_chunks_processed = False # یک قطعه ناموفق بود
252
 
253
  if chunk_processed_successfully:
254
- break # از حلقه تلاش برای این قطعه خارج شو
255
 
256
  if not chunk_processed_successfully:
257
  _log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود.")
258
  all_chunks_processed = False
259
- break # خروج از حلقه اصلی پردازش قطعات (chunk_idx) چون یک قطعه حیاتی ناموفق بود
260
- # --- END: منطق تلاش مجدد ---
261
-
262
- advance_global_key_index_for_next_request() # برای درخواست Gradio بعدی، از کلید بعدی شروع کن
263
 
264
  if not all_chunks_processed or not generated_files:
265
- _log("❌ هیچ فایل صوتی معتبری تولید نشد (ممکن است برخی قطعات ناموفق بوده باشند یا سهمیه تمام کلیدها تمام شده باشد).")
266
- # پاک کردن فایل‌های جزئی ایجاد شده اگر فرآیند کامل نشده
267
- for fp in generated_files:
268
  try: os.remove(fp)
269
  except: pass
270
  return None
271
 
272
- # _log(f"🎉 {len(generated_files)} فایل(های) صوتی خام تولید شد.") # لاگ کمتر
273
  final_audio_file = None
274
  final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
275
 
@@ -291,7 +287,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
291
  _log(f"خطا در تغییر نام اولین قطعه (پس از ادغام ناموفق): {e_rename}")
292
  final_audio_file = generated_files[0]
293
 
294
- for fp_cleanup in generated_files:
295
  if final_audio_file and os.path.abspath(fp_cleanup) == os.path.abspath(final_audio_file):
296
  continue
297
  try: os.remove(fp_cleanup)
@@ -309,7 +305,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
309
  try: os.remove(generated_files[i_gf])
310
  except: pass
311
  except Exception as e_rename_single:
312
- _log(f"خطا در تغییر نام فایل اولین قطعه (بدون pydub): {e_rename_single}")
313
  final_audio_file = generated_files[0]
314
  elif len(generated_files) == 1:
315
  try:
@@ -328,7 +324,6 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
328
  _log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!")
329
  return None
330
  else:
331
- # این حالت نباید رخ دهد اگر generated_files خالی نباشد و خطایی در تغییر نام رخ ندهد
332
  _log(f"❓ وضعیت نامشخص برای فایل نهایی.")
333
  return None
334
 
@@ -347,20 +342,27 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
347
  actual_text = text_to_speak
348
  if not actual_text or not actual_text.strip(): _log("❌ متن ورودی برای تبدیل خالی است."); return None
349
 
350
- if NUM_API_KEYS == 0: # بررسی اولیه قبل از فراخوانی core_generate_audio
351
- _log(" هیچ کلید API برای پردازش موجود نیست. لطفاً Secrets را بررسی کنید.")
 
 
 
352
  return None
353
 
354
  final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
 
 
 
 
355
  return final_path
356
 
357
- # --- CSS (بدون تغییر نسبت به کد شما) ---
358
  custom_css_inspired_by_image = f"""
359
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
360
  :root {{
361
  --app-font: 'Vazirmatn', sans-serif;
362
- --app-header-grad-start: #2980b9; /* آبی */
363
- --app-header-grad-end: #2ecc71; /* سبز */
364
  --app-panel-bg: #FFFFFF;
365
  --app-input-bg: #F7F7F7;
366
  --app-button-bg: #2979FF;
@@ -381,7 +383,6 @@ body, .gradio-container {{ font-family: var(--app-font); direction: rtl; backgro
381
  .main-content-panel-alpha {{ padding: 1.8rem 1.5rem; max-width: 680px; margin: -2.5rem auto 2rem auto; width: 90%; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); position:relative; z-index:10; }}
382
  @media (max-width: 768px) {{ .main-content-panel-alpha {{ width: 95%; padding: 1.5rem 1rem; margin-top: -2rem; }} .app-header-alpha h1 {{font-size:2em;}} .app-header-alpha p {{font-size:1em;}} }}
383
  footer {{display:none !important;}}
384
-
385
  .gr-button.generate-button-final {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.8rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.3s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.5rem !important; }}
386
  .gr-button.generate-button-final:hover {{ filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba(41,121,255,0.6);}}
387
  .gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-file > label + div {{ border-radius: var(--radius-input) !important; border: 1px solid var(--app-border-color) !important; background-color: var(--app-input-bg) !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); padding: 0.75rem !important; }}
@@ -394,7 +395,6 @@ label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝';
394
  label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
395
  label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
396
  label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
397
-
398
  #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
399
  .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
400
  .app-footer-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.8; margin-top:3rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}
@@ -407,76 +407,88 @@ alpha_header_html_v3 = """
407
  </div>
408
  """
409
 
410
- with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
411
- gr.HTML(alpha_header_html_v3)
412
-
413
- with gr.Column(elem_classes=["main-content-panel-alpha"]):
414
- use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
415
- uploaded_file_input = gr.File(
416
- label=" ",
417
- file_types=['.txt'],
418
- visible=False,
419
- elem_id="file_uploader_alpha_main_v3"
420
- )
421
- text_to_speak_tb = gr.Textbox(
422
- label="متن فارسی برای تبدیل",
423
- placeholder="مثال: سلام، فردا هوا چطور است؟",
424
- lines=5,
425
- value="",
426
- visible=True,
427
- elem_id="text_input_main_alpha_v3"
428
- )
429
- use_file_input_cb.change(
430
- fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)),
431
- inputs=use_file_input_cb,
432
- outputs=[uploaded_file_input, text_to_speak_tb]
433
- )
434
-
435
- speech_prompt_tb = gr.Textbox(
436
- label="سبک گفتار (اختیاری)",
437
- placeholder="مثال: با لحنی شاد و پرانرژی",
438
- value="با لحنی دوستانه و رسا صحبت کن.",
439
- lines=2, elem_id="speech_prompt_alpha_v3"
440
- )
441
- speaker_voice_dd = gr.Dropdown(
442
- SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3"
443
- )
444
- temperature_slider = gr.Slider(
445
- minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا",
446
- elem_id="temperature_slider_alpha_v3"
447
- )
448
- gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
449
-
450
- generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
 
 
 
 
 
451
 
452
- output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
453
-
454
- generate_button.click(
455
- fn=gradio_tts_interface,
456
- inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
457
- outputs=[output_audio]
458
- )
459
-
460
- gr.Markdown("<h3 class='section-title-main-alpha' style='margin-top:2.5rem; text-align:center; border-bottom:none;'>نمونه‌های کاربردی</h3>", elem_id="examples_section_title_v3")
461
- gr.Examples(
462
- examples=[
463
- [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
464
- [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.", "با صدایی طبیعی و روان.", "Charon", 0.9],
465
- [False, None, "آیا می‌توانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.", "با کنجکاوی", "Puck", 0.95],
466
- # یک نمونه طولانی‌تر برای تست تقسیم به چند قطعه و چرخش کلید
467
- [False, None,
468
- "این یک متن بسیار طولانی است که به احتمال زیاد به چندین قطعه تقسیم خواهد شد. هدف از این نمونه، بررسی عملکرد صحیح تقسیم متن و همچنین آزمایش مکانیزم چرخش کلید API در صورتی که سهمیه یک کلید در حین پردازش تمام شود، می‌باشد. امیدواریم که برنامه بتواند به طور خودکار به کلید بعدی سوئیچ کرده و فرآیند تولید صدا را با موفقیت به اتمام برساند. این بخش اول است. این بخش دوم است. و این هم بخش سوم برای طولانی‌تر کردن متن.",
469
- "با لحنی آرام و واضح", "Achird", 0.8],
470
- ],
471
- inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
472
- outputs=[output_audio],
473
- fn=gradio_tts_interface,
474
- cache_examples=False
475
- )
476
- gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
477
-
478
  if __name__ == "__main__":
479
- if NUM_API_KEYS > 0 :
480
  demo.launch()
481
- else:
482
- _log("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")
 
 
 
 
 
 
 
 
 
 
6
  import struct
7
  import time
8
  import zipfile
9
+
10
+ # --- START: تغییر نحوه Import برای google-generativeai ---
11
+ try:
12
+ import google.generativeai as genai # روش پیشنهادی برای import
13
+ from google.generativeai import types # اگر از types استفاده می‌کنید، به همین شکل باقی بماند
14
+ from google.api_core import exceptions as google_exceptions
15
+ GOOGLE_LIBS_AVAILABLE = True
16
+ except ImportError as e:
17
+ print(f"❌ خطای حیاتی: عدم موفقیت در import کتابخانه‌های گوگل: {e}")
18
+ print(" لطفاً از صحت نصب 'google-generativeai' و 'google-api-core' در requirements.txt و ری‌استارت Space مطمئن شوید.")
19
+ GOOGLE_LIBS_AVAILABLE = False
20
+ # در ادامه برنامه، قبل از استفاده از genai و ... باید GOOGLE_LIBS_AVAILABLE را چک کنیم
21
+ # یا برنامه را همینجا متوقف کنیم اگر این کتابخانه‌ها حیاتی هستند.
22
+ # برای سادگی فعلی، فرض می‌کنیم اگر import نشوند، در ادامه با خطا مواجه می‌شویم.
23
+ # --- END: تغییر نحوه Import ---
24
+
25
 
26
  try:
27
  from pydub import AudioSegment
28
  PYDUB_AVAILABLE = True
29
  except ImportError:
30
+ print("⚠️ کتابخانه pydub یافت نشد. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
31
  PYDUB_AVAILABLE = False
32
 
33
  # --- START: منطق چرخش API Key ---
 
38
  i += 1
39
 
40
  NUM_API_KEYS = len(GEMINI_API_KEYS)
 
 
41
  CURRENT_KEY_INDEX_GLOBAL = 0
42
 
43
  def _log(message):
44
  print(f"[لاگ آلفا TTS] {message}")
45
 
46
+ if not GOOGLE_LIBS_AVAILABLE:
47
+ _log("🔴 به دلیل عدم بارگذاری کتابخانه‌های اصلی گوگل، عملکرد برنامه مختل خواهد شد.")
48
+
49
  if NUM_API_KEYS == 0:
50
+ _log("⛔️ هشدار: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد! برنامه بدون کلید API کار نخواهد کرد.")
51
  else:
52
  _log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
53
 
54
  def get_api_key_for_attempt(attempt_within_request):
 
 
 
 
55
  if NUM_API_KEYS == 0:
56
+ return None, -1, -1
57
 
 
 
58
  actual_key_index_in_list = (CURRENT_KEY_INDEX_GLOBAL + attempt_within_request) % NUM_API_KEYS
 
59
  key_to_use = GEMINI_API_KEYS[actual_key_index_in_list]
60
+ key_display_number = actual_key_index_in_list + 1
 
61
  return key_to_use, key_display_number, actual_key_index_in_list
62
 
63
  def advance_global_key_index_for_next_request():
 
 
 
 
64
  global CURRENT_KEY_INDEX_GLOBAL
65
  if NUM_API_KEYS > 0:
66
  CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
 
75
  ]
76
  FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
77
  DEFAULT_MAX_CHUNK_SIZE = 3800
78
+ DEFAULT_SLEEP_BETWEEN_REQUESTS = 6 # کمی کاهش یافته
79
+ RETRY_SLEEP_AFTER_QUOTA_ERROR = 2
80
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
81
 
82
  def save_binary_file(file_name, data):
 
152
  except Exception as e: _log(f"❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
153
 
154
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
155
+ if not GOOGLE_LIBS_AVAILABLE:
156
+ _log("❌ کتابخانه‌های گوگل بارگذاری نشده‌اند. امکان تولید صدا وجود ندارد.")
157
+ return None
158
+ if NUM_API_KEYS == 0:
159
+ _log("❌ هیچ کلید API برای استفاده موجود نیست.")
160
+ # advance_global_key_index_for_next_request() # حتی اگر کلیدی نیست، برای یکنواختی فراخوانی می‌شود
161
+ return None
162
+
163
  _log("🚀 شروع فرآیند تولید صدا...")
 
164
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
165
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
166
 
167
  if not text_input or not text_input.strip():
168
  _log("❌ متن ورودی خالی است.")
169
+ advance_global_key_index_for_next_request()
170
  return None
171
 
172
  text_chunks = smart_text_split(text_input, max_chunk)
 
176
  return None
177
 
178
  generated_files = []
179
+ all_chunks_processed = True
180
 
 
181
  for chunk_idx, chunk_text in enumerate(text_chunks):
182
  chunk_processed_successfully = False
183
  _log(f" 🔊 پردازش قطعه {chunk_idx + 1}/{len(text_chunks)}...")
184
+ max_attempts_for_chunk = NUM_API_KEYS # اگر NUM_API_KEYS صفر باشد، این حلقه اجرا نمی‌شود (بالاتر هندل شده)
 
 
185
 
186
  for attempt_num_for_chunk in range(max_attempts_for_chunk):
187
  selected_api_key, key_display_num, actual_key_idx = get_api_key_for_attempt(attempt_num_for_chunk)
188
+ # selected_api_key در این نقطه نباید None باشد چون NUM_API_KEYS > 0 است
189
+
 
 
 
 
190
  _log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} برای قطعه {chunk_idx+1} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
191
 
192
  try:
193
+ # استفاده از `genai` که در ابتدای فایل import شده
194
+ client = genai.Client(api_key=selected_api_key)
195
 
196
  if prompt_input and prompt_input.strip():
197
  processed_prompt = prompt_input.strip()
 
201
  else:
202
  final_text_for_api = chunk_text.strip()
203
 
204
+ # استفاده از `types` که در ابتدای فایل import شده
 
205
  contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
206
  config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
207
  speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
208
  prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
209
 
210
+ fname_base = f"{output_base_name}_part{chunk_idx+1:03d}"
 
211
  response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
212
 
213
  if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
 
217
  if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
218
  if not ext.startswith("."): ext = "." + ext
219
 
 
220
  temp_fpath_for_chunk = f"{fname_base}{ext}"
221
  if os.path.exists(temp_fpath_for_chunk):
222
  try: os.remove(temp_fpath_for_chunk)
223
+ except OSError: pass
224
 
225
  fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
226
  if fpath:
227
  generated_files.append(fpath)
228
  chunk_processed_successfully = True
229
  _log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
230
+ if chunk_idx < len(text_chunks) - 1:
231
+ time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
232
+ break
233
  else:
234
  _log(f" ⚠️ پاسخ API برای قطعه {chunk_idx+1} با کلید {key_display_num} بدون داده صوتی بود.")
235
 
236
+ except google_exceptions.ResourceExhausted as e_quota: # استفاده از google_exceptions
237
+ _log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
238
+ if attempt_num_for_chunk < max_attempts_for_chunk - 1:
239
  _log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه.")
240
  time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
241
  else:
242
  _log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند و ناموفق بودند (خطای سهمیه).")
243
+ all_chunks_processed = False
244
 
245
  except Exception as e_general:
246
+ _log(f" ❌ خطای عمومی در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {str(e_general)[:150]}")
 
 
247
  if attempt_num_for_chunk < max_attempts_for_chunk - 1:
248
+ time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
249
  else:
250
+ all_chunks_processed = False
251
 
252
  if chunk_processed_successfully:
253
+ break
254
 
255
  if not chunk_processed_successfully:
256
  _log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود.")
257
  all_chunks_processed = False
258
+ break
259
+
260
+ advance_global_key_index_for_next_request()
 
261
 
262
  if not all_chunks_processed or not generated_files:
263
+ _log("❌ هیچ فایل صوتی معتبری تولید نشد.")
264
+ for fp in generated_files: # پاک کردن فایل‌های جزئی ایجاد شده
 
265
  try: os.remove(fp)
266
  except: pass
267
  return None
268
 
 
269
  final_audio_file = None
270
  final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
271
 
 
287
  _log(f"خطا در تغییر نام اولین قطعه (پس از ادغام ناموفق): {e_rename}")
288
  final_audio_file = generated_files[0]
289
 
290
+ for fp_cleanup in generated_files: # پاک کردن فایل‌های جزئی
291
  if final_audio_file and os.path.abspath(fp_cleanup) == os.path.abspath(final_audio_file):
292
  continue
293
  try: os.remove(fp_cleanup)
 
305
  try: os.remove(generated_files[i_gf])
306
  except: pass
307
  except Exception as e_rename_single:
308
+ _log(f"خطا در تغییر نام اولین قطعه (بدون pydub): {e_rename_single}")
309
  final_audio_file = generated_files[0]
310
  elif len(generated_files) == 1:
311
  try:
 
324
  _log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!")
325
  return None
326
  else:
 
327
  _log(f"❓ وضعیت نامشخص برای فایل نهایی.")
328
  return None
329
 
 
342
  actual_text = text_to_speak
343
  if not actual_text or not actual_text.strip(): _log("❌ متن ورودی برای تبدیل خالی است."); return None
344
 
345
+ if not GOOGLE_LIBS_AVAILABLE: # بررسی اولیه
346
+ gr.Warning("خطای سیستمی: کتابخانه‌های مورد نیاز گوگل بارگذاری نشده‌اند. لطفاً با پشتیبانی تماس بگیرید.")
347
+ return None
348
+ if NUM_API_KEYS == 0:
349
+ gr.Warning("خطای سیستمی: هیچ کلید API برای پردازش موجود نیست. لطفاً تنظیمات را بررسی کنید.")
350
  return None
351
 
352
  final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
353
+
354
+ if final_path is None:
355
+ gr.Info("متاسفانه در حال حاضر امکان تولید صدا وجود ندارد. لطفاً دقایقی دیگر مجدداً تلاش کنید یا با متن کوتاه‌تری امتحان کنید.")
356
+
357
  return final_path
358
 
359
+ # --- CSS ---
360
  custom_css_inspired_by_image = f"""
361
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
362
  :root {{
363
  --app-font: 'Vazirmatn', sans-serif;
364
+ --app-header-grad-start: #2980b9;
365
+ --app-header-grad-end: #2ecc71;
366
  --app-panel-bg: #FFFFFF;
367
  --app-input-bg: #F7F7F7;
368
  --app-button-bg: #2979FF;
 
383
  .main-content-panel-alpha {{ padding: 1.8rem 1.5rem; max-width: 680px; margin: -2.5rem auto 2rem auto; width: 90%; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); position:relative; z-index:10; }}
384
  @media (max-width: 768px) {{ .main-content-panel-alpha {{ width: 95%; padding: 1.5rem 1rem; margin-top: -2rem; }} .app-header-alpha h1 {{font-size:2em;}} .app-header-alpha p {{font-size:1em;}} }}
385
  footer {{display:none !important;}}
 
386
  .gr-button.generate-button-final {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.8rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.3s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.5rem !important; }}
387
  .gr-button.generate-button-final:hover {{ filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba(41,121,255,0.6);}}
388
  .gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-file > label + div {{ border-radius: var(--radius-input) !important; border: 1px solid var(--app-border-color) !important; background-color: var(--app-input-bg) !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); padding: 0.75rem !important; }}
 
395
  label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
396
  label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
397
  label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
 
398
  #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
399
  .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
400
  .app-footer-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.8; margin-top:3rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}
 
407
  </div>
408
  """
409
 
410
+ # --- Gradio UI ---
411
+ # فقط در صورتی که کتابخانه اصلی گوگل بارگذاری شده باشد، UI را بساز
412
+ if GOOGLE_LIBS_AVAILABLE:
413
+ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
414
+ gr.HTML(alpha_header_html_v3)
415
+
416
+ with gr.Column(elem_classes=["main-content-panel-alpha"]):
417
+ use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
418
+ uploaded_file_input = gr.File(
419
+ label=" ",
420
+ file_types=['.txt'],
421
+ visible=False,
422
+ elem_id="file_uploader_alpha_main_v3"
423
+ )
424
+ text_to_speak_tb = gr.Textbox(
425
+ label="متن فارسی برای تبدیل",
426
+ placeholder="مثال: سلام، فردا هوا چطور است؟",
427
+ lines=5,
428
+ value="",
429
+ visible=True,
430
+ elem_id="text_input_main_alpha_v3"
431
+ )
432
+ use_file_input_cb.change(
433
+ fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)),
434
+ inputs=use_file_input_cb,
435
+ outputs=[uploaded_file_input, text_to_speak_tb]
436
+ )
437
+
438
+ speech_prompt_tb = gr.Textbox(
439
+ label="سبک گفتار (اختیاری)",
440
+ placeholder="مثال: ب�� لحنی شاد و پرانرژی",
441
+ value="با لحنی دوستانه و رسا صحبت کن.",
442
+ lines=2, elem_id="speech_prompt_alpha_v3"
443
+ )
444
+ speaker_voice_dd = gr.Dropdown(
445
+ SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3"
446
+ )
447
+ temperature_slider = gr.Slider(
448
+ minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا",
449
+ elem_id="temperature_slider_alpha_v3"
450
+ )
451
+ gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
452
+
453
+ generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
454
+
455
+ output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
456
 
457
+ generate_button.click(
458
+ fn=gradio_tts_interface,
459
+ inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
460
+ outputs=[output_audio]
461
+ )
462
+
463
+ gr.Markdown("<h3 class='section-title-main-alpha' style='margin-top:2.5rem; text-align:center; border-bottom:none;'>نمونه‌های کاربردی</h3>", elem_id="examples_section_title_v3")
464
+ gr.Examples(
465
+ examples=[
466
+ [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
467
+ [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.", "با صدایی طبیعی و روان.", "Charon", 0.9],
468
+ [False, None, "آیا می‌توانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.", "با کنجکاوی", "Puck", 0.95],
469
+ [False, None,
470
+ "این یک متن بسیار طولانی است که به احتمال زیاد به چندین قطعه تقسیم خواهد شد. هدف از این نمونه، بررسی عملکرد صحیح تقسیم متن و همچنین آزمایش مک��نیزم چرخش کلید API در صورتی که سهمیه یک کلید در حین پردازش تمام شود، می‌باشد. امیدواریم که برنامه بتواند به طور خودکار به کلید بعدی سوئیچ کرده و فرآیند تولید صدا را با موفقیت به اتمام برساند. این بخش اول است. این بخش دوم است. و این هم بخش سوم برای طولانی‌تر کردن متن.",
471
+ "با لحنی آرام و واضح", "Achird", 0.8],
472
+ ],
473
+ inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
474
+ outputs=[output_audio],
475
+ fn=gradio_tts_interface,
476
+ cache_examples=False
477
+ )
478
+ gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
479
+
480
+ # --- Launch ---
 
 
481
  if __name__ == "__main__":
482
+ if GOOGLE_LIBS_AVAILABLE and NUM_API_KEYS > 0: # فقط در صورتی که کتابخانه و کلیدها موجود باشند اجرا کن
483
  demo.launch()
484
+ elif not GOOGLE_LIBS_AVAILABLE:
485
+ _log("🔴 برنامه به دلیل عدم بارگذاری کتابخانه‌های گوگل اجرا نشد. requirements.txt و لاگ‌های Build را بررسی کنید.")
486
+ # می‌توانید یک UI ساده با پیام خطا با Gradio نمایش دهید
487
+ with gr.Blocks() as error_demo:
488
+ gr.Markdown("# خطای سیستمی \n\n متاسفانه برنامه به دلیل مشکلات فنی در بارگذاری کتابخانه‌های اصلی قادر به اجرا نیست. لطفاً با مدیر سیستم تماس بگیرید.")
489
+ error_demo.launch()
490
+ elif NUM_API_KEYS == 0:
491
+ _log("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")
492
+ with gr.Blocks() as error_demo:
493
+ gr.Markdown("# خطای پیکربندی \n\n هیچ کلید API معتبری برای سرویس Gemini یافت نشد. لطفاً از تنظیم صحیح Secrets مطمئن شوید.")
494
+ error_demo.launch()