Hamed744 commited on
Commit
4e27563
·
verified ·
1 Parent(s): e18004c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -145
app.py CHANGED
@@ -6,56 +6,43 @@ import re
6
  import struct
7
  import time
8
  import zipfile
9
- import importlib.metadata # برای بررسی نسخه پکیج
10
 
11
- # --- START: Import کتابخانه‌های گوگل با بررسی دقیق‌تر ---
12
  GOOGLE_LIBS_AVAILABLE = False
13
- GENAI_CLIENT_AVAILABLE = False
14
 
15
- def _log_startup(message): # تابع لاگ مخصوص این بخش
16
  print(f"[Startup Log] {message}")
17
 
18
  try:
19
- # ابتدا سعی در import کردن google.generativeai
20
  import google.generativeai as genai
21
  _log_startup("ماژول 'google.generativeai' با موفقیت به عنوان 'genai' وارد شد.")
22
-
23
- # بررسی نسخه نصب شده
24
  try:
25
  version = importlib.metadata.version('google-generativeai')
26
  _log_startup(f"نسخه نصب شده 'google-generativeai': {version}")
27
  except importlib.metadata.PackageNotFoundError:
28
  _log_startup("هشدار: پکیج 'google-generativeai' نصب شده، اما نسخه‌ی آن قابل تشخیص نیست.")
29
 
30
- # بررسی وجود Client
31
- if hasattr(genai, 'Client'):
32
- _log_startup("ویژگی 'Client' در ماژول 'genai' (google.generativeai) یافت شد.")
33
- GENAI_CLIENT_AVAILABLE = True
34
  else:
35
- _log_startup("⛔️ خطای مهم: ویژگی 'Client' در ماژول 'genai' (google.generativeai) یافت نشد.")
36
- _log_startup(f" محتویات ماژول genai: {dir(genai)}") # نمایش تمام محتویات برای دیباگ بیشتر
37
 
38
- # Import کردن types و exceptions
39
- from google.generativeai import types
40
  from google.api_core import exceptions as google_exceptions
41
  _log_startup("'types' و 'google_exceptions' با موفقیت وارد شدند.")
42
  GOOGLE_LIBS_AVAILABLE = True
43
 
44
  except ImportError as e:
45
  _log_startup(f"❌ خطای حیاتی در Import: {e}")
46
- _log_startup(" لطفاً از صحت 'google-generativeai' و 'google-api-core' در requirements.txt و ری‌استارت کامل Space مطمئن شوید.")
47
  except Exception as e_other:
48
  _log_startup(f"❌ خطای ناشناخته در حین import یا بررسی کتابخانه‌های گوگل: {e_other}")
49
-
50
  # --- END: Import کتابخانه‌های گوگل ---
51
 
52
- try:
53
- from pydub import AudioSegment
54
- PYDUB_AVAILABLE = True
55
- except ImportError:
56
- _log_startup("⚠️ کتابخانه pydub یافت نشد. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
57
- PYDUB_AVAILABLE = False
58
-
59
  # --- START: منطق چرخش API Key (بدون تغییر نسبت به قبل) ---
60
  GEMINI_API_KEYS = []
61
  i = 1
@@ -82,26 +69,13 @@ def advance_global_key_index_for_next_request():
82
  global CURRENT_KEY_INDEX_GLOBAL
83
  if NUM_API_KEYS > 0: CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
84
  # --- END: منطق چرخش API Key ---
 
 
 
 
 
 
85
 
86
- # ... (بقیه کد شامل SPEAKER_VOICES, FIXED_MODEL_NAME, توابع save_binary_file, convert_to_wav, etc. بدون تغییر باقی می‌ماند) ...
87
- # ... فقط مطمئن شوید که در core_generate_audio، قبل از استفاده از genai.Client، چک GENAI_CLIENT_AVAILABLE را اضافه کنید ...
88
-
89
- # تابع core_generate_audio با یک بررسی اضافه در ابتدا
90
- def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
91
- if not GOOGLE_LIBS_AVAILABLE or not GENAI_CLIENT_AVAILABLE:
92
- _log("❌ کتابخانه‌های گوگل یا genai.Client به درستی بارگذاری نشده‌اند. امکان تولید صدا وجود ندارد.")
93
- return None
94
- # ... (بقیه کد core_generate_audio که قبلاً داشتید، از اینجا شروع می‌شود) ...
95
- # ... و به جای if not GOOGLE_LIBS_AVAILABLE: در ابتدای تابع قبلی، حالا از GENAI_CLIENT_AVAILABLE هم چک می‌کنید ...
96
-
97
- _log("🚀 شروع فرآیند تولید صدا...")
98
- # ... (بقیه کد core_generate_audio بدون تغییر)
99
- # client = genai.Client(api_key=selected_api_key) # این خط باید بعد از بررسی GENAI_CLIENT_AVAILABLE باشد
100
-
101
- # ... (بقیه کد شامل SPEAKER_VOICES, FIXED_MODEL_NAME, توابع save_binary_file, convert_to_wav, etc. باید از نسخه کامل قبلی کپی شود)
102
- # این یک خلاصه است، کد کامل قبلی را با تغییرات بخش import ترکیب کنید.
103
-
104
- # --- کد کامل توابع کمکی (برای اطمینان از کامل بودن) ---
105
  SPEAKER_VOICES = [
106
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
107
  "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
@@ -109,12 +83,39 @@ SPEAKER_VOICES = [
109
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
110
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
111
  ]
112
- FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  DEFAULT_MAX_CHUNK_SIZE = 3800
114
  DEFAULT_SLEEP_BETWEEN_REQUESTS = 6
115
  RETRY_SLEEP_AFTER_QUOTA_ERROR = 2
116
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
117
 
 
118
  def save_binary_file(file_name, data):
119
  try:
120
  with open(file_name, "wb") as f: f.write(data)
@@ -122,7 +123,6 @@ def save_binary_file(file_name, data):
122
  except Exception as e:
123
  _log(f"❌ خطا در ذخیره فایل {file_name}: {e}")
124
  return None
125
-
126
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
127
  parameters = parse_audio_mime_type(mime_type)
128
  bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
@@ -131,23 +131,20 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
131
  byte_rate, chunk_size = rate * block_align, 36 + data_size
132
  header = 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)
133
  return header + audio_data
134
-
135
  def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
136
- bits, rate = 16, 24000
137
  for param in mime_type.split(";"):
138
  param = param.strip()
139
- if param.lower().startswith("rate="):
140
  try: rate = int(param.split("=", 1)[1])
141
  except: pass
142
  elif param.startswith("audio/L"):
143
  try: bits = int(param.split("L", 1)[1])
144
  except: pass
145
  return {"bits_per_sample": bits, "rate": rate}
146
-
147
  def smart_text_split(text, max_size=3800):
148
  if len(text) <= max_size: return [text]
149
- chunks, current_chunk = [], ""
150
- sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
151
  for sentence in sentences:
152
  if len(current_chunk) + len(sentence) + 1 > max_size:
153
  if current_chunk: chunks.append(current_chunk.strip())
@@ -155,63 +152,53 @@ def smart_text_split(text, max_size=3800):
155
  while len(current_chunk) > max_size:
156
  split_idx = -1
157
  for char_to_find in ['،', ',', ';', ':', ' ']:
158
- try:
159
- split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size)
160
- break
161
- except ValueError:
162
- continue
163
- if split_idx != -1:
164
- part = current_chunk[:split_idx+1]
165
- current_chunk = current_chunk[split_idx+1:]
166
- else:
167
- part = current_chunk[:max_size]
168
- current_chunk = current_chunk[max_size:]
169
  chunks.append(part.strip())
170
- else:
171
- if current_chunk:
172
- current_chunk += " " + sentence
173
- else:
174
- current_chunk = sentence
175
  if current_chunk: chunks.append(current_chunk.strip())
176
- final_chunks = [c for c in chunks if c]
177
- return final_chunks
178
-
179
  def merge_audio_files_func(file_paths, output_path):
180
- if not PYDUB_AVAILABLE: _log("⚠️ pydub برای ادغام در دسترس نیست."); return False
181
  try:
182
  combined = AudioSegment.empty()
183
  for i, fp in enumerate(file_paths):
184
  if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
185
- else: _log(f"⚠️ فایل برای ادغام پیدا نشد: {fp}")
186
- combined.export(output_path, format="wav")
187
- return True
188
- except Exception as e: _log(f"❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
189
 
190
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
191
- if not GOOGLE_LIBS_AVAILABLE: # اولین بررسی
192
- _log("❌ کتابخانه‌های گوگل به طور کلی بارگذاری نشده‌اند.")
193
- return None
194
- if not GENAI_CLIENT_AVAILABLE: # بررسی وجود Client
195
- _log("❌ ویژگی 'Client' در کتابخانه 'google.generativeai' یافت نشد. لطفاً نسخه کتابخانه را بررسی کنید.")
196
  return None
197
  if NUM_API_KEYS == 0:
198
- _log("❌ هیچ کلید API برای استفاده موجود نیست.")
199
  return None
200
 
201
- _log("🚀 شروع فرآیند تولید صدا...")
202
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
203
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  if not text_input or not text_input.strip():
206
- _log("❌ متن ورودی خالی است.")
207
- advance_global_key_index_for_next_request()
208
- return None
209
-
210
  text_chunks = smart_text_split(text_input, max_chunk)
211
  if not text_chunks:
212
- _log("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
213
- advance_global_key_index_for_next_request()
214
- return None
215
 
216
  generated_files = []
217
  all_chunks_processed = True
@@ -222,58 +209,203 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
222
  max_attempts_for_chunk = NUM_API_KEYS
223
 
224
  for attempt_num_for_chunk in range(max_attempts_for_chunk):
225
- selected_api_key, key_display_num, actual_key_idx = get_api_key_for_attempt(attempt_num_for_chunk)
226
- _log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} برای قطعه {chunk_idx+1} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
227
 
228
  try:
229
- client = genai.Client(api_key=selected_api_key)
 
 
 
 
 
 
 
 
 
 
 
 
230
  if prompt_input and prompt_input.strip():
231
  processed_prompt = prompt_input.strip()
232
  if not re.search(r'[.!?؟،:۔]$', processed_prompt): processed_prompt += "،"
233
  final_text_for_api = f"{processed_prompt} {chunk_text.strip()}"
234
  else: final_text_for_api = chunk_text.strip()
235
 
236
- contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
237
- config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
239
- prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
 
 
 
 
 
240
 
241
- fname_base = f"{output_base_name}_part{chunk_idx+1:03d}"
242
- response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
 
 
 
 
 
 
 
 
 
 
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
245
- inline_data = response.candidates[0].content.parts[0].inline_data; data_buffer = inline_data.data
246
- ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
247
- if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
 
 
 
 
 
 
248
  if not ext.startswith("."): ext = "." + ext
 
 
249
  temp_fpath_for_chunk = f"{fname_base}{ext}"
250
  if os.path.exists(temp_fpath_for_chunk):
251
  try: os.remove(temp_fpath_for_chunk)
252
  except OSError: pass
 
253
  fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
254
  if fpath:
255
  generated_files.append(fpath); chunk_processed_successfully = True
256
  _log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
257
  if chunk_idx < len(text_chunks) - 1: time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
258
  break
259
- else: _log(f" ⚠️ پاسخ API برای قطعه {chunk_idx+1} با کلید {key_display_num} بدون داده صوتی بود.")
 
 
260
  except google_exceptions.ResourceExhausted as e_quota:
261
  _log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
262
  if attempt_num_for_chunk < max_attempts_for_chunk - 1:
263
  _log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه."); time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
264
  else: _log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند (خطای سهمیه)."); all_chunks_processed = False
 
265
  except Exception as e_general:
266
- _log(f" ❌ خطای عمومی در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {type(e_general).__name__} - {str(e_general)[:150]}")
 
 
 
 
 
 
267
  if attempt_num_for_chunk < max_attempts_for_chunk - 1: time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
268
  else: all_chunks_processed = False
 
269
  if chunk_processed_successfully: break
 
270
  if not chunk_processed_successfully:
271
  _log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود."); all_chunks_processed = False; break
 
272
  advance_global_key_index_for_next_request()
 
273
  if not all_chunks_processed or not generated_files:
274
  _log("❌ هیچ فایل صوتی معتبری تولید نشد.")
275
- for fp in generated_files:
276
- try: os.remove(fp)
277
  except: pass
278
  return None
279
  final_audio_file = None; final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
@@ -289,9 +421,9 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
289
  if os.path.exists(renamed_first_chunk): os.remove(renamed_first_chunk)
290
  os.rename(generated_files[0], renamed_first_chunk); final_audio_file = renamed_first_chunk
291
  except Exception as e_rename: _log(f"خطا در تغییر نام اولین قطعه: {e_rename}"); final_audio_file = generated_files[0]
292
- for fp_cleanup in generated_files:
293
- if final_audio_file and os.path.abspath(fp_cleanup) == os.path.abspath(final_audio_file): continue
294
- try: os.remove(fp_cleanup)
295
  except: pass
296
  else:
297
  _log("⚠️ pydub نیست. اولین قطعه ارائه می‌شود.")
@@ -303,18 +435,20 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
303
  for i_gf in range(1, len(generated_files)):
304
  try: os.remove(generated_files[i_gf])
305
  except: pass
306
- except Exception as e_rename_single: _log(f"خطا در تغییر نام (بدون pydub): {e_rename_single}"); final_audio_file = generated_files[0]
307
  elif len(generated_files) == 1:
308
  try:
309
  target_ext = os.path.splitext(generated_files[0])[1]; final_single_fn = f"{final_output_path_base}{target_ext}"
310
  if os.path.exists(final_single_fn): os.remove(final_single_fn)
311
  os.rename(generated_files[0], final_single_fn); final_audio_file = final_single_fn
312
- except Exception as e_rename_single_final: _log(f"خطا در تغییر نام فایل تکی: {e_rename_single_final}"); final_audio_file = generated_files[0]
313
  if final_audio_file and os.path.exists(final_audio_file): _log(f"✅ فایل نهایی: {os.path.basename(final_audio_file)}")
314
  elif final_audio_file: _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!"); return None
315
  else: _log(f"❓ وضعیت نامشخص برای فایل نهایی."); return None
316
  return final_audio_file
317
 
 
 
318
  def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
319
  actual_text = ""
320
  if use_file_input:
@@ -327,47 +461,21 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
327
  else:
328
  actual_text = text_to_speak
329
  if not actual_text or not actual_text.strip(): _log("❌ متن ورودی خالی."); return None
330
- if not GOOGLE_LIBS_AVAILABLE or not GENAI_CLIENT_AVAILABLE:
331
- gr.Warning("خطای سیستمی: کتابخانه‌های مورد نیاز بارگذاری نشده‌اند.")
332
  return None
333
  if NUM_API_KEYS == 0:
334
  gr.Warning("خطای سیستمی: کلید API موجود نیست.")
335
  return None
336
- final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
337
  if final_path is None:
338
  gr.Info("امکان تولید صدا وجود ندارد. لطفاً دقایقی دیگر یا با متن کوتاه‌تری امتحان کنید.")
339
  return final_path
340
 
341
- # --- CSS و UI (بدون تغییر نسبت به نسخه کامل قبلی) ---
342
- custom_css_inspired_by_image = f"""
343
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
344
- :root {{ --app-font: 'Vazirmatn', sans-serif; --app-header-grad-start: #2980b9; --app-header-grad-end: #2ecc71; --app-panel-bg: #FFFFFF; --app-input-bg: #F7F7F7; --app-button-bg: #2979FF; --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); --app-text-primary: #333; --app-text-secondary: #555; --app-border-color: #E0E0E0; --radius-card: 20px; --radius-input: 8px; --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1); --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);}}
345
- body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 16px; line-height: 1.65; }}
346
- .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
347
- .app-header-alpha {{ padding: 3rem 1.5rem 4rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2); }}
348
- .app-header-alpha h1 {{ font-size: 2.4em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }}
349
- .app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}
350
- .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; }}
351
- @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;}} }}
352
- footer {{display:none !important;}}
353
- .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; }}
354
- .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);}}
355
- .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; }}
356
- .gr-file > label + div {{ text-align:center; border-style: dashed !important; }}
357
- .gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-textbox > label + div > textarea:focus {{ border-color: var(--app-button-bg) !important; box-shadow: 0 0 0 3px rgba(41,121,255,0.2) !important; }}
358
- label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}
359
- .section-title-main-alpha {{ font-size: 1.1em; color: var(--app-text-secondary); margin-bottom:1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--app-border-color); font-weight:500; text-align:right; }}
360
- label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}
361
- label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}
362
- label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
363
- label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
364
- label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
365
- #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
366
- .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
367
- .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);}}"""
368
  alpha_header_html_v3 = """<div class='app-header-alpha'><h1>Alpha TTS</h1><p>جادوی تبدیل متن به صدا در دستان شما</p></div>"""
369
 
370
- if GOOGLE_LIBS_AVAILABLE and GENAI_CLIENT_AVAILABLE:
371
  with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
372
  gr.HTML(alpha_header_html_v3)
373
  with gr.Column(elem_classes=["main-content-panel-alpha"]):
@@ -376,24 +484,24 @@ if GOOGLE_LIBS_AVAILABLE and GENAI_CLIENT_AVAILABLE:
376
  text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
377
  use_file_input_cb.change(fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
378
  speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
379
- speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3")
380
  temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
381
  gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
382
  generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
383
  output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
384
  generate_button.click(fn=gradio_tts_interface, inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio])
385
  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")
386
- gr.Examples(examples=[[False,None,"سلام بر شما، امیدوارم روز خوبی داشته باشید.","با لحنی گرم و صمیمی.","Zephyr",0.85],[False,None,"این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.","با صدایی طبیعی و روان.","Charon",0.9],[False,None,"آیا می‌توانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.","با کنجکاوی","Puck",0.95],[False,None,"این یک متن بسیار طولانی است که به احتمال زیاد به چندین قطعه تقسیم خواهد شد. هدف از این نمونه، بررسی عملکرد صحیح تقسیم متن و همچنین آزمایش مکانیزم چرخش کلید API در صورتی که سهمیه یک کلید در حین پردازش تمام شود، می‌باشد. امیدواریم که برنامه بتواند به طور خودکار به کلید بعدی سوئیچ کرده و فرآیند تولید صدا را با موفقیت به اتمام برساند. این بخش اول است. این بخش دوم است. و این هم بخش سوم برای طولانی‌تر کردن متن.","با لحنی آرام و واضح","Achird",0.8],], inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio], fn=gradio_tts_interface, cache_examples=False )
387
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
388
 
389
  if __name__ == "__main__":
390
- if GOOGLE_LIBS_AVAILABLE and GENAI_CLIENT_AVAILABLE and NUM_API_KEYS > 0:
391
  demo.launch()
392
  else:
 
393
  if not GOOGLE_LIBS_AVAILABLE: msg = "کتابخانه‌های گوگل بارگذاری نشدند."
394
- elif not GENAI_CLIENT_AVAILABLE: msg = "'genai.Client' یافت نشد. نسخه کتابخانه را بررسی کنید."
395
  elif NUM_API_KEYS == 0: msg = "هیچ کلید API یافت نشد."
396
- else: msg = "خطای ناشناخته در شروع برنامه."
397
  _log(f"🔴 برنامه به دلیل '{msg}' اجرا نشد.")
398
  with gr.Blocks(title="خطا") as error_demo:
399
  gr.Markdown(f"# خطای اجرای برنامه\n\n**دلیل:** {msg}\n\nلطفاً لاگ‌های برنامه یا تنظیمات Space را بررسی کنید.")
 
6
  import struct
7
  import time
8
  import zipfile
9
+ import importlib.metadata
10
 
11
+ # --- START: Import کتابخانه‌های گوگل ---
12
  GOOGLE_LIBS_AVAILABLE = False
13
+ GENAI_MODEL_ACCESS_CONFIGURED = False # برای اطمینان از اینکه configure قبل از model استفاده می‌شود
14
 
15
+ def _log_startup(message):
16
  print(f"[Startup Log] {message}")
17
 
18
  try:
 
19
  import google.generativeai as genai
20
  _log_startup("ماژول 'google.generativeai' با موفقیت به عنوان 'genai' وارد شد.")
 
 
21
  try:
22
  version = importlib.metadata.version('google-generativeai')
23
  _log_startup(f"نسخه نصب شده 'google-generativeai': {version}")
24
  except importlib.metadata.PackageNotFoundError:
25
  _log_startup("هشدار: پکیج 'google-generativeai' نصب شده، اما نسخه‌ی آن قابل تشخیص نیست.")
26
 
27
+ # در نسخه‌های جدید، Client وجود ندارد، به جای آن GenerativeModel و configure استفاده می‌شود
28
+ if hasattr(genai, 'GenerativeModel') and hasattr(genai, 'configure'):
29
+ _log_startup("ویژگی‌های 'GenerativeModel' و 'configure' در ماژول 'genai' یافت شدند.")
30
+ GENAI_MODEL_ACCESS_CONFIGURED = True # نشان‌دهنده آمادگی برای استفاده از API جدید
31
  else:
32
+ _log_startup("⛔️ خطای مهم: 'GenerativeModel' یا 'configure' در 'genai' یافت نشد. سازگاری نسخه کتابخانه بررسی شود.")
 
33
 
34
+ from google.generativeai import types # انواع هنوز به همین شکل هستند
 
35
  from google.api_core import exceptions as google_exceptions
36
  _log_startup("'types' و 'google_exceptions' با موفقیت وارد شدند.")
37
  GOOGLE_LIBS_AVAILABLE = True
38
 
39
  except ImportError as e:
40
  _log_startup(f"❌ خطای حیاتی در Import: {e}")
 
41
  except Exception as e_other:
42
  _log_startup(f"❌ خطای ناشناخته در حین import یا بررسی کتابخانه‌های گوگل: {e_other}")
 
43
  # --- END: Import کتابخانه‌های گوگل ---
44
 
45
+ # ... (بقیه import های pydub و منطق چرخش کلید API و توابع کمکی بدون تغییر باقی می‌مانند) ...
 
 
 
 
 
 
46
  # --- START: منطق چرخش API Key (بدون تغییر نسبت به قبل) ---
47
  GEMINI_API_KEYS = []
48
  i = 1
 
69
  global CURRENT_KEY_INDEX_GLOBAL
70
  if NUM_API_KEYS > 0: CURRENT_KEY_INDEX_GLOBAL = (CURRENT_KEY_INDEX_GLOBAL + 1) % NUM_API_KEYS
71
  # --- END: منطق چرخش API Key ---
72
+ try:
73
+ from pydub import AudioSegment
74
+ PYDUB_AVAILABLE = True
75
+ except ImportError:
76
+ _log_startup("⚠️ کتابخانه pydub یافت نشد. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
77
+ PYDUB_AVAILABLE = False
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  SPEAKER_VOICES = [
80
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
81
  "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
 
83
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
84
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
85
  ]
86
+ FIXED_MODEL_NAME = "models/gemini-1.5-flash-latest" # نام مدل برای API جدید ممکن است متفاوت باشد، این را چک کنید. برای TTS باید از مدل مخصوص TTS استفاده کرد.
87
+ # نام مدل صحیح برای TTS در API جدید: "gemini-1.5-flash" و استفاده از response_mime_type="audio/ogg" یا "audio/wav" در generation_config
88
+ # یا استفاده از مدل خاص TTS اگر موجود باشد. فعلا "models/tts-alpha" یا مشابه را در نظر می‌گیریم
89
+ # بر اساس داکیومنت جدید، مدل‌های TTS ممکن است به صورت "models/text-to-speech" یا نام‌های خاص دیگر باشند.
90
+ # برای Gemini 1.5 Flash و قابلیت TTS، باید مدل درست را پیدا کنیم.
91
+ # اگر از مدل پایه Flash استفاده می‌کنیم، باید قابلیت TTS آن را فعال کنیم.
92
+ # فعلاً از نام مدل قبلی استفاده می‌کنیم و امیدواریم با configure کار کند.
93
+ # ** مهم: نام مدل TTS در API جدید ممکن است "models/tts-1" یا چیزی شبیه به این باشد. باید داکیومنت API v1beta را برای TTS بررسی کرد.
94
+ # با توجه به اینکه قبلا از "gemini-2.5-flash-preview-tts" استفاده می‌کردید، احتمالاً برای API جدید
95
+ # باید از "models/gemini-1.5-flash" (یا مشابه) به همراه تنظیمات TTS استفاده کنید.
96
+ # فعلاً "models/gemini-1.5-flash" را فرض می‌کنیم و generation_config را برای TTS تنظیم می‌کنیم.
97
+ # **اصلاح مهم: مدل TTS هنوز در API اصلی به طور عمومی در دسترس نیست و ممکن است نیاز به endpoint خاصی داشته باشد یا از طریق Vertex AI قابل دسترس باشد.**
98
+ # **با فرض اینکه مدل TTS مانند قبل در دسترس است، اما با API جدید:**
99
+ # **مدل صحیح برای TTS با API جدیدتر احتمالاً چیزی شبیه به این است:**
100
+ # FIXED_MODEL_NAME_TTS = "models/tts-1" # یا نامی که در داکیومنت جدید برای TTS آمده
101
+ # یا استفاده از مدل پایه با قابلیت‌های خاص:
102
+ FIXED_MODEL_NAME_FOR_TTS_API_V1 = "gemini-1.5-flash" # یا "gemini-pro" اگر TTS دارند
103
+ # فعلا با همان نام مدل قبلی شما پیش می‌رویم و امیدواریم با API جدید کار کند:
104
+ # FIXED_MODEL_NAME_ACTUAL = "gemini-2.5-flash-preview-tts" # این نام برای API قبلی بود
105
+ # برای API جدید (که Client ندارد)، باید از نام مدل‌های استاندارد استفاده کرد.
106
+ # **به نظر می‌رسد مستقیم‌ترین راه برای TTS با API جدید از طریق `genai.GenerativeModel('models/gemini-1.5-flash-latest')` و تنظیم `response_mime_type='audio/ogg'` در `generation_config` است.**
107
+ # یا یک مدل خاص TTS اگر تعریف شده.
108
+ # بیایید فرض کنیم یک مدل به نام "tts-model" یا مشابه در دسترس است.
109
+ # فعلاً از نام مدل قبلی شما استفاده می‌کنیم و به جای client.models... از model... استفاده خواهیم کرد.
110
+ # **اگر مدل "gemini-2.5-flash-preview-tts" با API جدید (که Client ندارد) کار نکند، باید به دنبال نام مدل TTS معادل در API جدید بگردید.**
111
+ # **مهمترین تغییر این است که `client.models.generate_content` به `model.generate_content` تبدیل می‌شود.**
112
+
113
  DEFAULT_MAX_CHUNK_SIZE = 3800
114
  DEFAULT_SLEEP_BETWEEN_REQUESTS = 6
115
  RETRY_SLEEP_AFTER_QUOTA_ERROR = 2
116
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
117
 
118
+ # ... (توابع save_binary_file, convert_to_wav, parse_audio_mime_type, smart_text_split, merge_audio_files_func بدون تغییر) ...
119
  def save_binary_file(file_name, data):
120
  try:
121
  with open(file_name, "wb") as f: f.write(data)
 
123
  except Exception as e:
124
  _log(f"❌ خطا در ذخیره فایل {file_name}: {e}")
125
  return None
 
126
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
127
  parameters = parse_audio_mime_type(mime_type)
128
  bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
 
131
  byte_rate, chunk_size = rate * block_align, 36 + data_size
132
  header = 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)
133
  return header + audio_data
 
134
  def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
135
+ bits, rate = 16, 24000; param_lower_startswith = lambda p, s: p.lower().startswith(s)
136
  for param in mime_type.split(";"):
137
  param = param.strip()
138
+ if param_lower_startswith(param, "rate="):
139
  try: rate = int(param.split("=", 1)[1])
140
  except: pass
141
  elif param.startswith("audio/L"):
142
  try: bits = int(param.split("L", 1)[1])
143
  except: pass
144
  return {"bits_per_sample": bits, "rate": rate}
 
145
  def smart_text_split(text, max_size=3800):
146
  if len(text) <= max_size: return [text]
147
+ chunks, current_chunk = [], ""; sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
 
148
  for sentence in sentences:
149
  if len(current_chunk) + len(sentence) + 1 > max_size:
150
  if current_chunk: chunks.append(current_chunk.strip())
 
152
  while len(current_chunk) > max_size:
153
  split_idx = -1
154
  for char_to_find in ['،', ',', ';', ':', ' ']:
155
+ try: split_idx = current_chunk.rindex(char_to_find, max_size // 2, max_size); break
156
+ except ValueError: continue
157
+ part, current_chunk = (current_chunk[:split_idx+1], current_chunk[split_idx+1:]) if split_idx != -1 else (current_chunk[:max_size], current_chunk[max_size:])
 
 
 
 
 
 
 
 
158
  chunks.append(part.strip())
159
+ else: current_chunk += (" " if current_chunk else "") + sentence
 
 
 
 
160
  if current_chunk: chunks.append(current_chunk.strip())
161
+ return [c for c in chunks if c]
 
 
162
  def merge_audio_files_func(file_paths, output_path):
163
+ if not PYDUB_AVAILABLE: _log("⚠️ pydub نیست."); return False
164
  try:
165
  combined = AudioSegment.empty()
166
  for i, fp in enumerate(file_paths):
167
  if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
168
+ else: _log(f"⚠️ فایل {fp} نیست.")
169
+ combined.export(output_path, format="wav"); return True
170
+ except Exception as e: _log(f"❌ خطا در ادغام: {e}"); return False
171
+
172
 
173
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
174
+ if not GOOGLE_LIBS_AVAILABLE or not GENAI_MODEL_ACCESS_CONFIGURED:
175
+ _log("❌ کتابخانه‌های گوگل یا تنظیمات مدل به درستی بارگذاری نشده‌اند.")
 
 
 
176
  return None
177
  if NUM_API_KEYS == 0:
178
+ _log("❌ هیچ کلید API موجود نیست.")
179
  return None
180
 
181
+ _log("🚀 شروع فرآیند تولید صدا (با API جدید)...")
182
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
183
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
184
+
185
+ # ** مهم: نام مدل برای TTS با API جدید باید بررسی شود. **
186
+ # در اینجا از FIXED_MODEL_NAME_FOR_TTS_API_V1 استفاده می‌کنیم که باید نام یک مدل پایه باشد.
187
+ # اگر یک مدل خاص TTS مانند "models/text-to-speech" وجود دارد، از آن استفاده کنید.
188
+ # در حال حاضر، از نام مدل قبلی شما ("gemini-2.5-flash-preview-tts") استفاده می‌کنیم،
189
+ # و امیدواریم که با ساختار API جدید (بدون Client) کار کند.
190
+ # اگر کار نکرد، باید نام مدل را به یکی از مدل‌های استاندارد مانند "gemini-1.5-flash-latest" تغییر دهید
191
+ # و generation_config را برای خروجی صوتی تنظیم کنید.
192
+ # **برای سادگی و تست اولیه، فرض می‌کنیم نام مدل قبلی هنوز معتبر است.**
193
+ model_name_to_use = "gemini-1.5-flash-latest" # این یک مدل پایه است، برای TTS باید config خاصی داشته باشد.
194
+ # یا اگر مدل قبلی شما هنوز کار می‌کند:
195
+ # model_name_to_use = "gemini-2.5-flash-preview-tts" # این نام از API قبلی است
196
 
197
  if not text_input or not text_input.strip():
198
+ _log("❌ متن ورودی خالی است."); advance_global_key_index_for_next_request(); return None
 
 
 
199
  text_chunks = smart_text_split(text_input, max_chunk)
200
  if not text_chunks:
201
+ _log("❌ متن قابل پردازش نیست."); advance_global_key_index_for_next_request(); return None
 
 
202
 
203
  generated_files = []
204
  all_chunks_processed = True
 
209
  max_attempts_for_chunk = NUM_API_KEYS
210
 
211
  for attempt_num_for_chunk in range(max_attempts_for_chunk):
212
+ selected_api_key, key_display_num, _ = get_api_key_for_attempt(attempt_num_for_chunk)
213
+ _log(f" प्रयास {attempt_num_for_chunk + 1}/{max_attempts_for_chunk} با کلید شماره {key_display_num} (...{selected_api_key[-4:]})")
214
 
215
  try:
216
+ # --- تغییر کلیدی: تنظیم API Key و ایجاد مدل ---
217
+ genai.configure(api_key=selected_api_key)
218
+ # model = genai.GenerativeModel(model_name_to_use) # برای مدل‌های پایه
219
+ # برای TTS ممکن است نیاز به مدل خاص یا تنظیمات خاص باشد.
220
+ # فعلا از نام مدل قبلی استفاده می‌کنیم، اگر با API جدید کار کند:
221
+ # ** اگر "gemini-2.5-flash-preview-tts" با API جدید کار نمی‌کند، این بخش باید تغییر کند **
222
+ # ** به احتمال زیاد باید از یک مدل پایه (مانند gemini-1.5-flash-latest) و GenerationConfig برای TTS استفاده کرد **
223
+
224
+ # استفاده از نام مدل قبلی شما با این فرض که با API جدید هم کار می‌کند
225
+ # این بخش نیاز به تست و احتمالا اصلاح نام مدل دارد.
226
+ model_instance = genai.GenerativeModel(model_name_to_use)
227
+
228
+
229
  if prompt_input and prompt_input.strip():
230
  processed_prompt = prompt_input.strip()
231
  if not re.search(r'[.!?؟،:۔]$', processed_prompt): processed_prompt += "،"
232
  final_text_for_api = f"{processed_prompt} {chunk_text.strip()}"
233
  else: final_text_for_api = chunk_text.strip()
234
 
235
+ # تنظیمات برای خروجی صوتی (این بخش ممکن است نیاز به تنظیم دقیق‌تر بر اساس داکیومنت API جدید داشته باشد)
236
+ # این config از کد قبلی شما می‌آید و برای API جدید باید سازگار باشد.
237
+ # ** مهم: `response_modalities` در API جدید با `response_mime_type` جایگزین شده است. **
238
+ generation_config_tts = types.GenerationConfig(
239
+ temperature=temperature_val,
240
+ # response_modalities=["audio"], # این برای API قدیمی بود
241
+ response_mime_type="audio/wav", # یا audio/ogg - برای API جدید
242
+ candidate_count=1 # معمولا برای TTS یک کاندید کافی است
243
+ )
244
+ # speech_config هنوز ممکن است معتبر باشد یا به generation_config منتقل شده باشد.
245
+ # فعلا فرض می‌کنیم SpeechConfig جداگانه هنوز استفاده می‌شود.
246
+ # ** این بخش نیاز به بررسی داکیومنت API v1 (یا جدیدتر) دارد. **
247
+ # ** به نظر می‌رسد SpeechConfig دیگر به این شکل مستقیم در generate_content نیست **
248
+ # ** و تنظیمات صدا باید بخشی از prompt یا generation_config باشند. **
249
+ # ** برای سادگی، فعلا speech_config را حذف می‌کنیم و به تنظیمات پایه اکتفا می‌کنیم **
250
+ # ** و امیدواریم مدل TTS به طور پیش‌فرض صدای مناسبی تولید کند یا بتوانیم با prompt آن را کنترل کنیم. **
251
+
252
+ # response = model_instance.generate_content(
253
+ # contents=final_text_for_api, # API جدید معمولا contents را به عنوان رشته یا لیست رشته‌ها می‌پذیرد
254
+ # generation_config=generation_config_tts,
255
+ # # speech_config=types.SpeechConfig(...) # این احتمالا دیگر کار نمی‌کند
256
+ # )
257
+ # ** روش قدیمی‌تر ارسال content با types.Content **
258
+ contents_payload = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
259
+ # ** این generation_config از کد قبلی شما می‌آید **
260
+ original_config_from_your_code = types.GenerateContentConfig(
261
+ temperature=temperature_val,
262
+ response_modalities=["audio"], # این باید به response_mime_type تغییر کند
263
  speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
264
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
265
+ )
266
+ # ** تطبیق با API جدید **
267
+ # در API جدید، `response_modalities` وجود ندارد. به جای آن `response_mime_type` در `GenerationConfig` اصلی قرار می‌گیرد.
268
+ # `speech_config` هم ممکن است مستقیماً در `generate_content` نباشد.
269
+ # فعلاً از ساختار قبلی شما با کمی تغییر برای `generate_content` مدل جدید استفاده می‌کنیم.
270
 
271
+ # --- روش جدیدتر برای TTS با مدل پایه (مثل Flash) ---
272
+ # این روش استانداردتر برای API جدید است اگر مدل پایه قابلیت TTS دارد.
273
+ # شما باید voice و سایر تنظیمات را از طریق prompt یا تنظیمات خاص مدل انجام دهید.
274
+ # فعلاً فرض می‌کنیم که مدل `model_name_to_use` به طور مستقیم از TTS پشتیبانی می‌کند
275
+ # و `speech_config` هنوز معتبر است.
276
+ # این بخش بحرانی است و نیاز به تطبیق با داکیومنت دقیق API جدید دارد.
277
+ # ** اگر از مدل پایه مثل gemini-1.5-flash-latest استفاده می‌کنید، به احتمال زیاد speech_config مستقیم کار نمی‌کند **
278
+ # ** و باید از طریق prompt یا generation_config خاص TTS عمل کنید. **
279
+
280
+ # ** تلاش برای استفاده از ساختار قبلی شما با مدل جدید، با این امید که کار کند **
281
+ # این فقط یک حدس است و ممکن است نیاز به تغییرات اساسی داشته باشد.
282
+ # مهمترین تغییر `client.models.generate_content` به `model_instance.generate_content` است.
283
 
284
+ # --- START: تلاش برای تطبیق با ساختار قبلی generate_content ---
285
+ # این بخش بحرانی است و ممکن است با API جدید کار نکند اگر مدل و configها تغییر کرده باشند
286
+ # ** این GenerationConfig از کد قبلی شما می‌آید **
287
+ generation_config_for_tts = types.GenerationConfig( # در API جدید، این معمولاً GenerationConfig ساده است
288
+ temperature=temperature_val,
289
+ # response_modalities=["audio"], # حذف شد
290
+ response_mime_type="audio/wav", # یا audio/ogg
291
+ # speech_config ممکن است دیگر اینجا نباشد
292
+ )
293
+ # اگر speech_config هنوز کار می‌کند:
294
+ # tools = [types.Tool(speech_config=types.SpeechConfig(voice_config=...))]
295
+ # اما به احتمال زیاد این تغییر کرده.
296
+
297
+ # ** یک فرض ساده‌تر: مدل خودش می‌داند که TTS است و فقط متن و config پایه را می‌خواهد **
298
+ # response = model_instance.generate_content(
299
+ # final_text_for_api,
300
+ # generation_config=generation_config_for_tts
301
+ # )
302
+ # ** بازگشت به تلاش برای استفاده از ساختار config قبلی شما، با تغییرات جزئی **
303
+ # این بخش بسیار آزمایشی است
304
+ final_config_attempt = types.GenerateContentConfig( # این GenerateContentConfig از google.generativeai.types است
305
+ temperature=temperature_val,
306
+ # response_modalities=["audio"], # این دیگر وجود ندارد
307
+ # speech_config هنوز ممکن است در برخی موارد خاص کار کند، اما بعید است
308
+ # speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
309
+ # prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
310
+ )
311
+ # برای TTS با مدل‌های جدید، معمولاً به این شکل است:
312
+ # model = genai.GenerativeModel('models/gemini-1.5-flash') یا مدل TTS خاص
313
+ # response = model.generate_content(
314
+ # "متن شما",
315
+ # generation_config=genai.types.GenerationConfig(
316
+ # response_mime_type="audio/wav", # یا ogg
317
+ # # سایر پارامترهای دما و ...
318
+ # ),
319
+ # # برای کنترل صدا، ممکن است نیاز به prompt engineering باشد
320
+ # # یا اگر مدلی با قابلیت‌های voice وجود دارد، از طریق آن.
321
+ # )
322
+ # ** با توجه به اینکه شما speech_config داشتید، باید ببینیم معادل آن در API جدید چیست **
323
+ # ** فعلا فرض می‌کنیم مدل TTS هوشمند است و فقط با متن کار می‌کند و تنظیمات پایه **
324
+ # ** این یک ساده‌سازی بزرگ است و احتمالاً کار نخواهد کرد بدون تنظیمات دقیق TTS **
325
+
326
+ # ** برای تست، فعلاً speech_config را حذف می‌کنیم و فقط متن و config پایه را ارسال می‌کنیم **
327
+ # ** و امیدواریم مدل پیش‌فرض TTS صدای مناسبی بدهد. **
328
+ # ** این احتمالاً درست نیست و نیاز به بررسی داکیومنت API جدید برای TTS دارد. **
329
+
330
+ # *** مهمترین تغییر: ***
331
+ # از `model_instance.generate_content` استفاده می‌کنیم.
332
+ # `contents` باید یک لیست از `Part` یا رشته باشد.
333
+ # `generation_config` باید `types.GenerationConfig` باشد.
334
+ # `speech_config` در اینجا دیگر مستقیم نیست.
335
+
336
+ # ساختار ساده‌تر برای generate_content با API جدید:
337
+ response = model_instance.generate_content(
338
+ contents=final_text_for_api, # یا [final_text_for_api]
339
+ generation_config=types.GenerationConfig( # استفاده از types.GenerationConfig
340
+ temperature=temperature_val,
341
+ response_mime_type="audio/wav" # درخواست خروجی صوتی
342
+ # candidate_count=1 # معمولا برای TTS
343
+ )
344
+ # پارامتر voice_name و speech_prompt باید به نحو دیگری به مدل منتقل شوند،
345
+ # احتمالاً از طریق خود متن (prompt engineering) یا تنظیمات خاص مدل اگر وجود داشته باشد.
346
+ # این یک چالش با API جدید برای TTS است اگر تنظیمات صدا پیچیده باشند.
347
+ )
348
+
349
+ # --- END: تلاش برای تطبیق ---
350
+
351
+ # پردازش پاسخ (این بخش باید با ساختار پاسخ جدید API تطابق داشته باشد)
352
+ # در API جدید، معمولاً پاسخ مستقیم حاوی داده باینری نیست، بلکه یک URI به فایل است یا داده در Candidate.
353
+ # با فرض اینکه ساختار Candidate.content.parts[0].inline_data هنوز معتبر است:
354
  if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
355
+ inline_data = response.candidates[0].content.parts[0].inline_data
356
+ data_buffer = inline_data.data
357
+ # mime_type از پاسخ هم باید بررسی شود
358
+ # mime_type_from_response = inline_data.mime_type
359
+ # ext = mimetypes.guess_extension(mime_type_from_response) or ".wav"
360
+ ext = ".wav" # چون درخواست wav کرده‌ایم
361
+
362
+ # if "audio/L" in mime_type_from_response and ext == ".wav": # این برای فرمت خاص قبلی بود
363
+ # data_buffer = convert_to_wav(data_buffer, mime_type_from_response)
364
  if not ext.startswith("."): ext = "." + ext
365
+
366
+ fname_base = f"{output_base_name}_part{chunk_idx+1:03d}"
367
  temp_fpath_for_chunk = f"{fname_base}{ext}"
368
  if os.path.exists(temp_fpath_for_chunk):
369
  try: os.remove(temp_fpath_for_chunk)
370
  except OSError: pass
371
+
372
  fpath = save_binary_file(temp_fpath_for_chunk, data_buffer)
373
  if fpath:
374
  generated_files.append(fpath); chunk_processed_successfully = True
375
  _log(f" ✅ قطعه {chunk_idx+1} با کلید شماره {key_display_num} موفقیت آمیز بود.")
376
  if chunk_idx < len(text_chunks) - 1: time.sleep(DEFAULT_SLEEP_BETWEEN_REQUESTS)
377
  break
378
+ else:
379
+ _log(f" ⚠️ پاسخ API برای قطعه {chunk_idx+1} با کلید {key_display_num} بدون داده صوتی معتبر بود. پاسخ: {response.text if hasattr(response, 'text') else str(response)[:200]}")
380
+
381
  except google_exceptions.ResourceExhausted as e_quota:
382
  _log(f" ❌ خطای سهمیه برای قطعه {chunk_idx+1} با کلید شماره {key_display_num}: {str(e_quota)[:100]}...")
383
  if attempt_num_for_chunk < max_attempts_for_chunk - 1:
384
  _log(f" ... تلاش با کلید بعدی پس از {RETRY_SLEEP_AFTER_QUOTA_ERROR} ثانیه."); time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
385
  else: _log(f" ⛔️ تمام کلیدهای API برای قطعه {chunk_idx+1} امتحان شدند (خطای سهمیه)."); all_chunks_processed = False
386
+
387
  except Exception as e_general:
388
+ error_type_name = type(e_general).__name__
389
+ _log(f" ❌ خطای عمومی ({error_type_name}) در تولید قطعه {chunk_idx+1} با کلید {key_display_num}: {str(e_general)[:200]}")
390
+ if "response_mime_type" in str(e_general).lower() or "modality" in str(e_general).lower():
391
+ _log(" این خطا ممکن است مربوط به عدم پشتیبانی ��دل از خروجی صوتی یا تنظیمات نادرست response_mime_type باشد.")
392
+ if "model" in str(e_general).lower() and "not found" in str(e_general).lower():
393
+ _log(f" مدل '{model_name_to_use}' یافت نشد یا برای این کلید API در دسترس نیست.")
394
+
395
  if attempt_num_for_chunk < max_attempts_for_chunk - 1: time.sleep(RETRY_SLEEP_AFTER_QUOTA_ERROR)
396
  else: all_chunks_processed = False
397
+
398
  if chunk_processed_successfully: break
399
+
400
  if not chunk_processed_successfully:
401
  _log(f" ⛔️ پردازش قطعه {chunk_idx+1} پس از {max_attempts_for_chunk} تلاش ناموفق بود."); all_chunks_processed = False; break
402
+
403
  advance_global_key_index_for_next_request()
404
+ # ... (بقیه کد core_generate_audio برای ادغام و بازگرداندن فایل، بدون تغییر) ...
405
  if not all_chunks_processed or not generated_files:
406
  _log("❌ هیچ فایل صوتی معتبری تولید نشد.")
407
+ for fp_cleanup in generated_files: # پاک کردن فایل‌های جزئی ایجاد شده
408
+ try: os.remove(fp_cleanup)
409
  except: pass
410
  return None
411
  final_audio_file = None; final_output_path_base = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final"
 
421
  if os.path.exists(renamed_first_chunk): os.remove(renamed_first_chunk)
422
  os.rename(generated_files[0], renamed_first_chunk); final_audio_file = renamed_first_chunk
423
  except Exception as e_rename: _log(f"خطا در تغییر نام اولین قطعه: {e_rename}"); final_audio_file = generated_files[0]
424
+ for fp_cleanup_merge in generated_files: # پاک کردن فایل‌های جزئی
425
+ if final_audio_file and os.path.abspath(fp_cleanup_merge) == os.path.abspath(final_audio_file): continue
426
+ try: os.remove(fp_cleanup_merge)
427
  except: pass
428
  else:
429
  _log("⚠️ pydub نیست. اولین قطعه ارائه می‌شود.")
 
435
  for i_gf in range(1, len(generated_files)):
436
  try: os.remove(generated_files[i_gf])
437
  except: pass
438
+ except Exception as e_rename_single_no_pydub: _log(f"خطا در تغییر نام (بدون pydub): {e_rename_single_no_pydub}"); final_audio_file = generated_files[0]
439
  elif len(generated_files) == 1:
440
  try:
441
  target_ext = os.path.splitext(generated_files[0])[1]; final_single_fn = f"{final_output_path_base}{target_ext}"
442
  if os.path.exists(final_single_fn): os.remove(final_single_fn)
443
  os.rename(generated_files[0], final_single_fn); final_audio_file = final_single_fn
444
+ except Exception as e_rename_single_final_file: _log(f"خطا در تغییر نام فایل تکی: {e_rename_single_final_file}"); final_audio_file = generated_files[0]
445
  if final_audio_file and os.path.exists(final_audio_file): _log(f"✅ فایل نهایی: {os.path.basename(final_audio_file)}")
446
  elif final_audio_file: _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!"); return None
447
  else: _log(f"❓ وضعیت نامشخص برای فایل نهایی."); return None
448
  return final_audio_file
449
 
450
+
451
+ # ... (تابع gradio_tts_interface و UI و launch بدون تغییر نسبت به نسخه کامل قبلی) ...
452
  def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
453
  actual_text = ""
454
  if use_file_input:
 
461
  else:
462
  actual_text = text_to_speak
463
  if not actual_text or not actual_text.strip(): _log("❌ متن ورودی خالی."); return None
464
+ if not GOOGLE_LIBS_AVAILABLE or not GENAI_MODEL_ACCESS_CONFIGURED : # بررس�� جدید
465
+ gr.Warning("خطای سیستمی: کتابخانه‌های مورد نیاز یا تنظیمات مدل به درستی بارگذاری نشده‌اند.")
466
  return None
467
  if NUM_API_KEYS == 0:
468
  gr.Warning("خطای سیستمی: کلید API موجود نیست.")
469
  return None
470
+ final_path = core_generate_audio(actual_text, prompt_input, speaker_voice, temperature)
471
  if final_path is None:
472
  gr.Info("امکان تولید صدا وجود ندارد. لطفاً دقایقی دیگر یا با متن کوتاه‌تری امتحان کنید.")
473
  return final_path
474
 
475
+ custom_css_inspired_by_image = f"""@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');:root {{ --app-font: 'Vazirmatn', sans-serif; --app-header-grad-start: #2980b9; --app-header-grad-end: #2ecc71; --app-panel-bg: #FFFFFF; --app-input-bg: #F7F7F7; --app-button-bg: #2979FF; --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); --app-text-primary: #333; --app-text-secondary: #555; --app-border-color: #E0E0E0; --radius-card: 20px; --radius-input: 8px; --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1); --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);}}body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 16px; line-height: 1.65; }}.gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}.app-header-alpha {{ padding: 3rem 1.5rem 4rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2); }}.app-header-alpha h1 {{ font-size: 2.4em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }}.app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}.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; }}@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;}} }}footer {{display:none !important;}}.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; }}.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);}}.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; }}.gr-file > label + div {{ text-align:center; border-style: dashed !important; }}.gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-textbox > label + div > textarea:focus {{ border-color: var(--app-button-bg) !important; box-shadow: 0 0 0 3px rgba(41,121,255,0.2) !important; }}label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}.section-title-main-alpha {{ font-size: 1.1em; color: var(--app-text-secondary); margin-bottom:1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--app-border-color); font-weight:500; text-align:right; }}label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}#output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}.temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}.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);}}"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  alpha_header_html_v3 = """<div class='app-header-alpha'><h1>Alpha TTS</h1><p>جادوی تبدیل متن به صدا در دستان شما</p></div>"""
477
 
478
+ if GOOGLE_LIBS_AVAILABLE and GENAI_MODEL_ACCESS_CONFIGURED:
479
  with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
480
  gr.HTML(alpha_header_html_v3)
481
  with gr.Column(elem_classes=["main-content-panel-alpha"]):
 
484
  text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
485
  use_file_input_cb.change(fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
486
  speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
487
+ speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3") # speaker_voice دیگر به طور مستقیم به API ارسال نمی‌شود، اما در UI باقی می‌ماند
488
  temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
489
  gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
490
  generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
491
  output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
492
  generate_button.click(fn=gradio_tts_interface, inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio])
493
  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")
494
+ gr.Examples(examples=[[False,None,"سلام بر شما، امیدوارم روز خوبی داشته باشید.","با لحنی گرم و صمیمی.","Zephyr",0.85],[False,None,"این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است. امیدوارم از نتیجه راضی باشید.","با صدایی طبیعی و روان.","Charon",0.9],[False,None,"آیا می‌توانم سوالی از شما بپرسم؟ لطفاً راهنمایی کنید.","با کنجکاوی","Puck",0.95],[False,None,"این یک متن بسیار طولانی است.","با لحنی آرام و واضح","Achird",0.8],], inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio], fn=gradio_tts_interface, cache_examples=False )
495
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
496
 
497
  if __name__ == "__main__":
498
+ if GOOGLE_LIBS_AVAILABLE and GENAI_MODEL_ACCESS_CONFIGURED and NUM_API_KEYS > 0:
499
  demo.launch()
500
  else:
501
+ msg = "خطای ناشناخته در شروع برنامه."
502
  if not GOOGLE_LIBS_AVAILABLE: msg = "کتابخانه‌های گوگل بارگذاری نشدند."
503
+ elif not GENAI_MODEL_ACCESS_CONFIGURED: msg = "تنظیمات مدل API جدید (GenerativeModel/configure) یافت نشد."
504
  elif NUM_API_KEYS == 0: msg = "هیچ کلید API یافت نشد."
 
505
  _log(f"🔴 برنامه به دلیل '{msg}' اجرا نشد.")
506
  with gr.Blocks(title="خطا") as error_demo:
507
  gr.Markdown(f"# خطای اجرای برنامه\n\n**دلیل:** {msg}\n\nلطفاً لاگ‌های برنامه یا تنظیمات Space را بررسی کنید.")