Hamed744 commited on
Commit
5b42498
·
verified ·
1 Parent(s): 5f5b8e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -87
app.py CHANGED
@@ -8,6 +8,7 @@ import time
8
  import zipfile
9
  from google import genai
10
  from google.genai import types
 
11
 
12
  try:
13
  from pydub import AudioSegment
@@ -23,28 +24,43 @@ while os.environ.get(f"GEMINI_API_KEY_{i}"):
23
  i += 1
24
 
25
  NUM_API_KEYS = len(GEMINI_API_KEYS)
26
- CURRENT_KEY_INDEX = 0
 
 
27
 
28
  def _log(message):
29
- """تابع ساده شده برای لاگ کردن پیام‌ها به کنسول."""
30
  print(f"[لاگ آلفا TTS] {message}")
31
 
32
  if NUM_API_KEYS == 0:
33
  _log("⛔️ خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n یافت نشد!")
34
- _log(" لطفاً Secret ها را مانند GEMINI_API_KEY_1, GEMINI_API_KEY_2, ... در تنظیمات Space خود اضافه کنید.")
35
  else:
36
  _log(f"✅ تعداد {NUM_API_KEYS} کلید API جیمینای بارگذاری شد.")
37
 
38
- def get_next_api_key():
39
- global CURRENT_KEY_INDEX
 
 
 
40
  if NUM_API_KEYS == 0:
41
- _log("⚠️ تلاش برای گرفتن کلید API در حالی که هیچ کلیدی بارگذاری نشده است.")
42
- return None, -1
43
 
44
- key_to_use = GEMINI_API_KEYS[CURRENT_KEY_INDEX % NUM_API_KEYS]
45
- key_display_index = (CURRENT_KEY_INDEX % NUM_API_KEYS) + 1
46
- CURRENT_KEY_INDEX += 1
47
- return key_to_use, key_display_index
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  # --- END: منطق چرخش API Key ---
49
 
50
  SPEAKER_VOICES = [
@@ -56,7 +72,8 @@ SPEAKER_VOICES = [
56
  ]
57
  FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
58
  DEFAULT_MAX_CHUNK_SIZE = 3800
59
- DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
 
60
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
61
 
62
  def save_binary_file(file_name, data):
@@ -91,33 +108,28 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
91
  def smart_text_split(text, max_size=3800):
92
  if len(text) <= max_size: return [text]
93
  chunks, current_chunk = [], ""
94
- # بهبود regex برای پشتیبانی بهتر از جداکننده‌های فارسی و انگلیسی
95
  sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
96
  for sentence in sentences:
97
- if len(current_chunk) + len(sentence) + 1 > max_size: # +1 for space
98
  if current_chunk: chunks.append(current_chunk.strip())
99
  current_chunk = sentence
100
- while len(current_chunk) > max_size: # If a single sentence is too long
101
- # Try to split at sensible places like comma, semicolon, or space
102
  split_idx = -1
103
- # Prefer splitting at punctuation, then space
104
  for char_to_find in ['،', ',', ';', ':', ' ']:
105
  try:
106
- # Search backwards from max_size towards middle
107
  split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size)
108
  break
109
  except ValueError:
110
  continue
111
-
112
  if split_idx != -1:
113
  part = current_chunk[:split_idx+1]
114
  current_chunk = current_chunk[split_idx+1:]
115
- else: # Force split if no ideal character found
116
  part = current_chunk[:max_size]
117
  current_chunk = current_chunk[max_size:]
118
  chunks.append(part.strip())
119
  else:
120
- if current_chunk: # Add space if it's not the first part of the chunk
121
  current_chunk += " " + sentence
122
  else:
123
  current_chunk = sentence
@@ -142,77 +154,124 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
142
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
143
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
144
 
145
- selected_api_key, key_idx_display = get_next_api_key()
146
-
147
- if not selected_api_key:
148
- _log("❌ کلید API برای این درخواست در دسترس نیست. لطفاً از تنظیمات Secrets مطمئن شوید.")
149
- return None
150
-
151
- _log(f"⚙️ استفاده از کلید API شماره {key_idx_display} (پایان یافته با: ...{selected_api_key[-4:]})")
152
-
153
- try:
154
- client = genai.Client(api_key=selected_api_key)
155
- except Exception as e:
156
- _log(f"❌ خطا در مقداردهی اولیه کلاینت Gemini با کلید شماره {key_idx_display}: {e}")
157
- return None
158
-
159
  if not text_input or not text_input.strip():
160
  _log("❌ متن ورودی خالی است.")
 
161
  return None
162
 
163
  text_chunks = smart_text_split(text_input, max_chunk)
164
  if not text_chunks:
165
  _log("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
 
166
  return None
167
 
168
  generated_files = []
169
- for i, chunk in enumerate(text_chunks):
170
- # --- START: تغییر نحوه ترکیب prompt و chunk ---
171
- if prompt_input and prompt_input.strip():
172
- processed_prompt = prompt_input.strip()
173
- # اگر سبک گفتار با نقطه گذاری تمام نمی‌شود، یک ویرگول یا نقطه اضافه می‌کنیم
174
- # این به مدل کمک می‌کند تا آن را به عنوان یک عبارت راهنما قبل از متن اصلی تشخیص دهد
175
- if not re.search(r'[.!?؟،:۔]$', processed_prompt):
176
- processed_prompt += "،" # افزودن ویرگول فارسی به عنوان جداکننده ملایم
177
- final_text = f"{processed_prompt} {chunk.strip()}"
178
- else:
179
- final_text = chunk.strip()
180
- # --- END: تغییر نحوه ترکیب prompt و chunk ---
181
 
182
- _log(f" متن ارسالی به API (قطعه {i+1}): '{final_text[:100]}...'") # نمایش بخش کوچکی از متن نهایی برای دیباگ
 
 
 
183
 
184
- contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
185
- config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
186
- speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
187
- prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
188
- fname_base = f"{output_base_name}_part{i+1:03d}"
189
- try:
190
- response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
191
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
192
- inline_data = response.candidates[0].content.parts[0].inline_data
193
- data_buffer = inline_data.data
194
- ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
195
- if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
196
- if not ext.startswith("."): ext = "." + ext
197
- fpath = save_binary_file(f"{fname_base}{ext}", data_buffer)
198
- if fpath: generated_files.append(fpath)
199
- else: _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی بود (با کلید شماره {key_idx_display}).")
200
- except Exception as e:
201
- _log(f"❌ خطا در تولید قطعه {i+1} با کلید شماره {key_idx_display}: {e}")
202
- # بررسی جزئیات خطا از Gemini، اگر موجود باشد
203
- if hasattr(e, 'message') and "API key" in e.message:
204
- _log(f" این خطا ممکن است مربوط به کلید API (شماره {key_idx_display}) یا محدودیت‌های آن باشد.")
205
- elif hasattr(e, 'message') and "resource has been exhausted" in e.message.lower():
206
- _log(f" احتمالاً به محدودیت استفاده از کلید API (شماره {key_idx_display}) رسیده‌اید.")
207
- continue
208
- if i < len(text_chunks) - 1 and len(text_chunks) > 1: time.sleep(sleep_time)
209
-
210
- if not generated_files:
211
- _log(f"❌ هیچ فایل صوتی با کلید شماره {key_idx_display} تولید نشد.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  return None
213
 
 
214
  final_audio_file = None
215
- final_output_path_base = f"{output_base_name}_final"
216
 
217
  if len(generated_files) > 1:
218
  if PYDUB_AVAILABLE:
@@ -229,7 +288,7 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
229
  os.rename(generated_files[0], renamed_first_chunk)
230
  final_audio_file = renamed_first_chunk
231
  except Exception as e_rename:
232
- _log(f"خطا در تغییر نام فایل اولین قطعه (پس از ادغام ناموفق): {e_rename}")
233
  final_audio_file = generated_files[0]
234
 
235
  for fp_cleanup in generated_files:
@@ -264,13 +323,13 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
264
  final_audio_file = generated_files[0]
265
 
266
  if final_audio_file and os.path.exists(final_audio_file):
267
- _log(f"✅ فایل صوتی نهایی با موفقیت با کلید شماره {key_idx_display} تولید شد: {os.path.basename(final_audio_file)}")
268
  elif final_audio_file:
269
- _log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد! (با کلید شماره {key_idx_display})")
270
  return None
271
  else:
272
  # این حالت نباید رخ دهد اگر generated_files خالی نباشد و خطایی در تغییر نام رخ ندهد
273
- _log(f"❓ وضعیت نامشخص برای فایل نهایی پس از پردازش تمام قطعات. (با کلید شماره {key_idx_display})")
274
  return None
275
 
276
  return final_audio_file
@@ -280,7 +339,6 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
280
  if use_file_input:
281
  if uploaded_file:
282
  try:
283
- # استفاده از uploaded_file.name که مسیر فایل موقت در Gradio است
284
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
285
  if not actual_text: _log("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
286
  except Exception as e: _log(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
@@ -289,6 +347,10 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
289
  actual_text = text_to_speak
290
  if not actual_text or not actual_text.strip(): _log("❌ متن ورودی برای تبدیل خالی است."); return None
291
 
 
 
 
 
292
  final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
293
  return final_path
294
 
@@ -373,7 +435,7 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
373
  speech_prompt_tb = gr.Textbox(
374
  label="سبک گفتار (اختیاری)",
375
  placeholder="مثال: با لحنی شاد و پرانرژی",
376
- value="با لحنی دوستانه و رسا صحبت کن.", # مقدار پیش‌فرض
377
  lines=2, elem_id="speech_prompt_alpha_v3"
378
  )
379
  speaker_voice_dd = gr.Dropdown(
@@ -387,7 +449,7 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
387
 
388
  generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
389
 
390
- output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3") # لیبل خالی برای تطابق با ظاهر
391
 
392
  generate_button.click(
393
  fn=gradio_tts_interface,
@@ -399,13 +461,17 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
399
  gr.Examples(
400
  examples=[
401
  [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
402
- [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبیعی و روان.", "Charon", 0.9],
403
- [False, None, "آیا می‌توانم سوالی از شما بپرسم؟", "با کنجکاوی", "Puck", 0.95],
 
 
 
 
404
  ],
405
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
406
  outputs=[output_audio],
407
  fn=gradio_tts_interface,
408
- cache_examples=False # برای اینکه همیشه با API تماس بگیرد و از کش استفاده نکند
409
  )
410
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
411
 
 
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
 
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
64
  # --- END: منطق چرخش API Key ---
65
 
66
  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):
 
108
  def smart_text_split(text, max_size=3800):
109
  if len(text) <= max_size: return [text]
110
  chunks, current_chunk = [], ""
 
111
  sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
112
  for sentence in sentences:
113
+ if len(current_chunk) + len(sentence) + 1 > max_size:
114
  if current_chunk: chunks.append(current_chunk.strip())
115
  current_chunk = sentence
116
+ while len(current_chunk) > max_size:
 
117
  split_idx = -1
 
118
  for char_to_find in ['،', ',', ';', ':', ' ']:
119
  try:
 
120
  split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size)
121
  break
122
  except ValueError:
123
  continue
 
124
  if split_idx != -1:
125
  part = current_chunk[:split_idx+1]
126
  current_chunk = current_chunk[split_idx+1:]
127
+ else:
128
  part = current_chunk[:max_size]
129
  current_chunk = current_chunk[max_size:]
130
  chunks.append(part.strip())
131
  else:
132
+ if current_chunk:
133
  current_chunk += " " + sentence
134
  else:
135
  current_chunk = sentence
 
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)
163
  if not text_chunks:
164
  _log("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
165
+ advance_global_key_index_for_next_request()
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()
194
+ if not re.search(r'[.!?؟،:۔]$', processed_prompt):
195
+ processed_prompt += "،"
196
+ final_text_for_api = f"{processed_prompt} {chunk_text.strip()}"
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:
212
+ inline_data = response.candidates[0].content.parts[0].inline_data
213
+ data_buffer = inline_data.data
214
+ ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
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
 
276
  if len(generated_files) > 1:
277
  if PYDUB_AVAILABLE:
 
288
  os.rename(generated_files[0], renamed_first_chunk)
289
  final_audio_file = renamed_first_chunk
290
  except Exception as e_rename:
291
+ _log(f"خطا در تغییر نام اولین قطعه (پس از ادغام ناموفق): {e_rename}")
292
  final_audio_file = generated_files[0]
293
 
294
  for fp_cleanup in generated_files:
 
323
  final_audio_file = generated_files[0]
324
 
325
  if final_audio_file and os.path.exists(final_audio_file):
326
+ _log(f"✅ فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_audio_file)}")
327
  elif final_audio_file:
328
+ _log(f"⚠️ فایل نهایی '{final_audio_file}' پس از پردازش وجود ندارد!")
329
  return None
330
  else:
331
  # این حالت نباید رخ دهد اگر generated_files خالی نباشد و خطایی در تغییر نام رخ ندهد
332
+ _log(f"❓ وضعیت نامشخص برای فایل نهایی.")
333
  return None
334
 
335
  return final_audio_file
 
339
  if use_file_input:
340
  if uploaded_file:
341
  try:
 
342
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
343
  if not actual_text: _log("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
344
  except Exception as e: _log(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
 
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
 
 
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(
 
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,
 
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