Hamed744 commited on
Commit
e4cf6e1
·
verified ·
1 Parent(s): 9e97ca0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -228
app.py CHANGED
@@ -1,7 +1,4 @@
1
  import gradio as gr
2
- # این خط برای بررسی نسخه Gradio در لاگ‌ها اضافه شده است
3
- print(f"Gradio version at startup: {gr.__version__}")
4
-
5
  import base64
6
  import mimetypes
7
  import os
@@ -9,64 +6,49 @@ import re
9
  import struct
10
  import time
11
  import zipfile
12
- import google.generativeai as genai
13
- from google.generativeai import types
14
  import threading
15
  import logging
16
- import io
17
- import numpy as np # جدید: این خط را اضافه کنید
18
 
19
  try:
20
  from pydub import AudioSegment
21
  PYDUB_AVAILABLE = True
22
  except ImportError:
23
  PYDUB_AVAILABLE = False
24
- logging.warning("⚠️ pydub نصب نشده است. قابلیت ادغام فایل‌های صوتی و تبدیل به NumPy غیرفعال خواهد بود.")
25
 
26
  # --- START: پیکربندی لاگینگ ---
27
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
28
  # --- END: پیکربندی لاگینگ ---
29
 
30
- # --- START: منطق جدید مدیریت API Key (مشابه کد قبلی شما) ---
31
  ALL_API_KEYS: list[str] = []
32
  NEXT_KEY_INDEX: int = 0
33
- KEY_LOCK: threading.Lock = threading.Lock() # برای اطمینان از ایمنی تردها هنگام به روز رسانی ایندکس
34
 
35
  def _init_api_keys():
36
- """
37
- کلیدهای API را از یک متغیر محیطی واحد شناسایی و مرتب می‌کند.
38
- این تابع باید یک بار هنگام شروع برنامه اجرا شود.
39
- """
40
  global ALL_API_KEYS
41
-
42
- all_keys_string = os.environ.get("ALL_GEMINI_API_KEYS")
43
-
44
  if all_keys_string:
45
  ALL_API_KEYS = [key.strip() for key in all_keys_string.split(',') if key.strip()]
46
-
47
  logging.info(f"✅ تعداد {len(ALL_API_KEYS)} کلید API جیمینای بارگذاری شد.")
48
  if not ALL_API_KEYS:
49
  logging.warning("⛔️ خطای حیاتی: هیچ Secret با نام ALL_GEMINI_API_KEYS یافت نشد!")
50
  logging.warning(" لطفاً Secret را به عنوان یک رشته با کاما جدا شده (مثال: key1,key2,key3) در تنظیمات Space خود اضافه کنید.")
51
 
52
- # فراخوانی تابع شناسایی کلیدها در ابتدای برنامه
53
  _init_api_keys()
54
 
55
  def get_next_api_key():
56
- """
57
- کلید API بعدی را به صورت چرخشی برمی‌گرداند.
58
- """
59
  global NEXT_KEY_INDEX, ALL_API_KEYS, KEY_LOCK
60
-
61
- with KEY_LOCK: # اطمینان از اینکه تنها یک ترد در هر زمان به ایندکس دسترسی دارد
62
  if not ALL_API_KEYS:
63
  return None, None
64
-
65
  key_to_use = ALL_API_KEYS[NEXT_KEY_INDEX % len(ALL_API_KEYS)]
66
  key_display_index = (NEXT_KEY_INDEX % len(ALL_API_KEYS)) + 1
67
  NEXT_KEY_INDEX += 1
68
  return key_to_use, key_display_index
69
- # --- END: منطق جدید مدیریت API Key ---
70
 
71
  SPEAKER_VOICES = [
72
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
@@ -75,12 +57,20 @@ SPEAKER_VOICES = [
75
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
76
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
77
  ]
78
- FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
79
  DEFAULT_MAX_CHUNK_SIZE = 3800
80
  DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
 
 
 
 
 
 
 
 
 
81
 
82
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
83
- """تبدیل داده‌های صوتی LPCM به فرمت WAV استاندارد (در حافظه)."""
84
  parameters = parse_audio_mime_type(mime_type)
85
  bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
86
  num_channels, data_size = 1, len(audio_data)
@@ -90,238 +80,236 @@ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
90
  return header + audio_data
91
 
92
  def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
93
- """تجزیه MIME Type صوتی برای استخراج اطلاعات نمونه‌برداری."""
94
- bits, rate = 16, 24000 # مقادیر پیش‌فرض
95
  for param in mime_type.split(";"):
96
  param = param.strip()
97
  if param.lower().startswith("rate="):
98
  try: rate = int(param.split("=", 1)[1])
99
- except ValueError: pass
100
  elif param.startswith("audio/L"):
101
  try: bits = int(param.split("L", 1)[1])
102
- except ValueError: pass
103
  return {"bits_per_sample": bits, "rate": rate}
104
 
105
  def smart_text_split(text, max_size=3800):
106
- """تقسیم متن به قطعات کوچکتر بر اساس علائم نگارشی برای سازگاری با محدودیت API."""
107
  if len(text) <= max_size: return [text]
108
  chunks, current_chunk = [], ""
109
- # سعی می‌کنیم بر اساس جملات یا نقاط مکث منطقی تقسیم کنیم
110
- sentences = re.split(r'(?<=[.!?؟])\s+', text)
111
- if not sentences: # اگر هیچ نقطه نگارشی نبود یا متن کوتاه بود
112
- sentences = re.split(r'(?<=[,؛:،])\s*|\s+', text) # بر اساس کاما یا فاصله تقسیم کن
113
-
114
  for sentence in sentences:
115
  if len(current_chunk) + len(sentence) + 1 > max_size:
116
  if current_chunk: chunks.append(current_chunk.strip())
117
  current_chunk = sentence
118
- # اگر یک جمله به تنهایی بزرگتر از max_size بود، آن را هم تقسیم کنید
119
  while len(current_chunk) > max_size:
120
- # سعی کنید در نقطه ای منطقی مانند کاما، نقطه ویرگول یا فاصله بزرگ تقسیم کنید
121
  split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
122
  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:])
123
  chunks.append(part.strip())
124
- else:
125
- current_chunk += (" " if current_chunk else "") + sentence
126
  if current_chunk: chunks.append(current_chunk.strip())
127
  final_chunks = [c for c in chunks if c]
128
  return final_chunks
129
 
130
- def merge_audio_bytes_to_numpy(audio_data_list: list[bytes]) -> tuple[int, np.ndarray] | None:
131
- """
132
- لیستی از بایت‌های صوتی WAV را ادغام کرده و یک تاپل (sample_rate, numpy_array) برمی‌گرداند.
133
- """
134
- if not PYDUB_AVAILABLE:
135
- logging.warning("⚠️ pydub برای ادغام و تبدیل به NumPy در دسترس نیست.")
136
- return None # در این حالت نمی‌توانیم خروجی NumPy بدهیم
137
-
138
  try:
139
- combined_audio_segment = AudioSegment.empty()
140
- for i, audio_bytes in enumerate(audio_data_list):
141
- audio_segment = AudioSegment.from_file(io.BytesIO(audio_bytes), format="wav")
142
- combined_audio_segment += audio_segment
143
- if i < len(audio_data_list) - 1:
144
- combined_audio_segment += AudioSegment.silent(duration=150) # 150 میلی‌ثانیه سکوت
145
-
146
- # استخراج نرخ نمونه و داده‌های صوتی به عنوان آرایه NumPy
147
- sample_rate = combined_audio_segment.frame_rate
148
- # pydub به صورت پیش‌فرض داده‌ها را به int16 تبدیل می‌کند، مناسب برای NumPy
149
- audio_array = np.array(combined_audio_segment.get_array_of_samples())
150
-
151
- return (sample_rate, audio_array)
152
  except Exception as e:
153
- logging.error(f"❌ خطا در ادغام بایت‌های صوتی و تبدیل به NumPy: {e}")
154
- return None
155
 
156
- # --- START: منطق تولید صدا با قابلیت تلاش مجدد با کلیدهای چرخشی ---
157
-
158
- def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp):
159
- """
160
- یک قطعه صوتی را با قابلیت تلاش مجدد با کلیدهای مختلف API تولید می‌کند.
161
- اگر یک کلید ناموفق بود، به طور خودکار کلید بعدی را امتحان می‌کند تا تمام کلیدها بررسی شوند.
162
- """
163
  if not ALL_API_KEYS:
164
- logging.error("❌ هیچ کلید API برای تولید صدا در دسترس نیست.")
165
  return None
166
 
167
- for _ in range(len(ALL_API_KEYS)):
168
  selected_api_key, key_idx_display = get_next_api_key()
169
-
170
  if not selected_api_key:
171
- logging.warning("⚠️ get_next_api_key هیچ کلیدی برنگرداند. تلاش‌های باقیمانده نادیده گرفته می‌شوند.")
172
- break
173
 
174
- logging.info(f"⚙️ تلاش برای تولید قطعه با کلید API شماره {key_idx_display} (...{selected_api_key[-4:]})")
175
 
176
  try:
177
- client = genai.Client(api_key=selected_api_key)
 
 
 
178
  final_text = f'"{prompt_text}"\n{chunk_text}' if prompt_text and prompt_text.strip() else chunk_text
179
- contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
180
- config = types.GenerateContentConfig(temperature=temp, response_modalities=["audio"],
181
- speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
182
- prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=voice))))
183
-
184
- response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
185
-
186
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
187
- logging.info(f"✅ قطعه با موفقیت توسط کلید شماره {key_idx_display} تولید شد.")
188
- # همیشه داده را به صورت بایت WAV برمی‌گرداند.
189
- data_buffer = response.candidates[0].content.parts[0].inline_data.data
190
- mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
191
- if "audio/L" in mime_type:
192
- data_buffer = convert_to_wav(data_buffer, mime_type)
193
- return data_buffer
 
 
 
 
 
 
 
194
  else:
195
- logging.warning(f"⚠️ پاسخ API برای قطعه با کلید شماره {key_idx_display} بدون داده صوتی بود. تلاش با کلید بعدی...")
196
 
197
  except Exception as e:
198
- logging.error(f"❌ خطا در تولید قطعه با کلید شماره {key_idx_display}: {e}. تلاش با کلید بعدی...")
199
-
200
- logging.error("❌ تمام کلیدهای API امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
 
 
201
  return None
202
 
203
- def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val):
204
- logging.info("🚀 شروع فرآیند تولید صدا با قابلیت تعویض کلید خودکار...")
205
 
 
 
206
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
207
 
208
- if not text_input or not text_input.strip():
209
- logging.error("❌ متن ورودی خالی است.")
210
- return None
211
-
212
  text_chunks = smart_text_split(text_input, max_chunk)
213
  if not text_chunks:
214
- logging.error("❌ متن قابل پردازش به قطعات کوچکتر نیست.")
215
  return None
216
 
217
- generated_wav_bytes_list = [] # لیست حاوی داده‌های صوتی هر قطعه (بایت WAV)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- for i, chunk in enumerate(text_chunks):
220
- logging.info(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- # generate_audio_chunk_with_retry اکنون مستقیماً بایت‌های WAV را برمی‌گرداند
223
- wav_data_for_chunk = generate_audio_chunk_with_retry(chunk, prompt_input, selected_voice, temperature_val)
 
 
 
 
 
 
 
 
 
 
224
 
225
- if wav_data_for_chunk:
226
- generated_wav_bytes_list.append(wav_data_for_for_chunk)
227
- else:
228
- logging.error(f"🛑 فرآیند متوقف شد زیرا تولید قطعه {i+1} با تمام کلیدهای موجود ناموفق بود.")
229
- break
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- if i < len(text_chunks) - 1 and len(text_chunks) > 1:
232
- time.sleep(sleep_time)
233
 
234
- if not generated_wav_bytes_list:
235
- logging.error(f"❌ هیچ داده صوتی تولید نشد.")
236
- return None
 
237
 
238
- final_audio_output = None
239
-
240
- if len(generated_wav_bytes_list) > 1:
241
- logging.info("♻️ ادغام قطعات صوتی و تبدیل به NumPy...")
242
- final_audio_output = merge_audio_bytes_to_numpy(generated_wav_bytes_list)
243
- if final_audio_output:
244
- logging.info("✅ ادغام و تبدیل به NumPy با موفقیت انجام شد.")
245
- else:
246
- logging.warning("⚠️ ادغام ناموفق بود یا pydub در دسترس نیست. تلاش برای بازگرداندن اولین قطعه به عنوان NumPy...")
247
- if generated_wav_bytes_list and PYDUB_AVAILABLE:
248
- try:
249
- # اگر ادغام به مشکل خورد، سعی می‌کنیم حداقل اولین قطعه را به NumPy تبدیل کنیم
250
- single_audio_segment = AudioSegment.from_file(io.BytesIO(generated_wav_bytes_list[0]), format="wav")
251
- final_audio_output = (single_audio_segment.frame_rate, np.array(single_audio_segment.get_array_of_samples()))
252
- except Exception as e:
253
- logging.error(f"❌ خطا در تبدیل اولین قطعه به NumPy: {e}")
254
- return None
255
- else:
256
- return None # هیچ راهی برای بازگرداندن NumPy بدون pydub/داده وجود ندارد
257
-
258
- elif len(generated_wav_bytes_list) == 1:
259
- logging.info("✅ تنها یک قطعه صوتی تولید شد. تبدیل مستقیم به NumPy.")
260
- if PYDUB_AVAILABLE:
261
- try:
262
- single_audio_segment = AudioSegment.from_file(io.BytesIO(generated_wav_bytes_list[0]), format="wav")
263
- final_audio_output = (single_audio_segment.frame_rate, np.array(single_audio_segment.get_array_of_samples()))
264
- except Exception as e:
265
- logging.error(f"❌ خطا در تبدیل قطعه تکی به NumPy: {e}")
266
- return None
267
- else:
268
- logging.error("❌ pydub برای تبدیل قطعه تکی به NumPy در دسترس نیست.")
269
- return None # نمی‌توانیم خروجی numpy بدهیم
270
 
271
- if final_audio_output:
272
- logging.info("✅ عملیات تولید صدا با موفقیت کامل شد.")
273
- return final_audio_output
274
- else:
275
- logging.error("❓ وضعیت نامشخص برای خروجی نهایی صدا.")
276
- return None
277
-
278
- # --- END: منطق جدید تولید صدا ---
279
 
280
- def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
281
  actual_text = ""
282
  if use_file_input:
283
- if uploaded_file and uploaded_file.name:
284
  try:
285
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
286
- if not actual_text: logging.error("❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
287
- except Exception as e: logging.error(f"❌ خطا در خواندن فایل آپلود شده: {e}"); return None
288
- else: logging.warning("❌ گزینه استفاده از فایل انتخاب شده اما فایلی آپلود نشده."); return None
289
  else:
290
  actual_text = text_to_speak
291
- if not actual_text or not actual_text.strip(): logging.warning("❌ متن ورودی برای تبدیل خالی است."); return None
 
 
292
 
293
- # core_generate_audio اکنون یک تاپل (sample_rate, numpy_array) برمی‌گرداند
294
- output_audio_data_numpy = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature)
295
- return output_audio_data_numpy
 
 
 
296
 
297
- # --- تابع جدید برای ریست خودکار هر 24 ساعت ---
298
  def auto_restart_service():
299
  RESTART_INTERVAL_SECONDS = 24 * 60 * 60
300
  logging.info(f"سرویس برای ری‌استارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمان‌بندی شده است.")
301
  time.sleep(RESTART_INTERVAL_SECONDS)
302
  logging.info(f"زمان ری‌استارت خودکار فرا رسیده است. برنامه برای ری‌استارت خارج می‌شود...")
303
  os._exit(1)
304
- # --- END: تابع ری‌استارت خودکار ---
305
-
306
 
307
  # --- CSS و Gradio UI (بدون تغییر) ---
308
  custom_css_inspired_by_image = f"""
309
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
310
  :root {{
311
- --app-font: 'Vazirmatn', sans-serif;
312
- --app-header-grad-start: #2980b9;
313
- --app-header-grad-end: #2ecc71;
314
- --app-panel-bg: #FFFFFF;
315
- --app-input-bg: #F7F7F7;
316
- --app-button-bg: #2979FF;
317
- --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%);
318
- --app-text-primary: #333;
319
- --app-text-secondary: #555;
320
- --app-border-color: #E0E0E0;
321
- --radius-card: 20px;
322
- --radius-input: 8px;
323
- --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1);
324
- --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);
325
  }}
326
  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; }}
327
  .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
@@ -331,7 +319,6 @@ body, .gradio-container {{ font-family: var(--app-font); direction: rtl; backgro
331
  .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; }}
332
  @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;}} }}
333
  footer {{display:none !important;}}
334
-
335
  .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; }}
336
  .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);}}
337
  .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; }}
@@ -344,7 +331,6 @@ label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝';
344
  label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
345
  label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
346
  label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
347
-
348
  #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
349
  .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
350
  .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);}}
@@ -357,56 +343,27 @@ alpha_header_html_v3 = """
357
  </div>
358
  """
359
 
360
- # --- رابط کاربری Gradio ---
361
  with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
362
  gr.HTML(alpha_header_html_v3)
363
 
364
  with gr.Column(elem_classes=["main-content-panel-alpha"]):
365
  use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
366
- uploaded_file_input = gr.File(
367
- label=" ",
368
- file_types=['.txt'],
369
- visible=False,
370
- elem_id="file_uploader_alpha_main_v3"
371
- )
372
- text_to_speak_tb = gr.Textbox(
373
- label="متن فارسی برای تبدیل",
374
- placeholder="مثال: سلام، فردا هوا چطور است؟",
375
- lines=5,
376
- value="",
377
- visible=True,
378
- elem_id="text_input_main_alpha_v3"
379
- )
380
- use_file_input_cb.change(
381
- fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)),
382
- inputs=use_file_input_cb,
383
- outputs=[uploaded_file_input, text_to_speak_tb]
384
- )
385
 
386
- speech_prompt_tb = gr.Textbox(
387
- label="سبک گفتار (اختیاری)",
388
- placeholder="مثال: با لحنی شاد و پرانرژی",
389
- value="با لحنی دوستانه و رسا صحبت کن.",
390
- lines=2, elem_id="speech_prompt_alpha_v3"
391
- )
392
- speaker_voice_dd = gr.Dropdown(
393
- SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3"
394
- )
395
- temperature_slider = gr.Slider(
396
- minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا",
397
- elem_id="temperature_slider_alpha_v3"
398
- )
399
  gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
400
 
401
  generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
402
-
403
- # مهم: type="numpy" را برای خروجی صو��ی تنظیم کنید
404
- output_audio = gr.Audio(label=" ", type="numpy", elem_id="output_audio_player_alpha_v3")
405
-
406
  generate_button.click(
407
  fn=gradio_tts_interface,
408
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
409
- outputs=[output_audio]
410
  )
411
 
412
  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")
@@ -418,7 +375,7 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
418
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
419
  outputs=[output_audio],
420
  fn=gradio_tts_interface,
421
- cache_examples=False
422
  )
423
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
424
 
@@ -426,6 +383,7 @@ if __name__ == "__main__":
426
  threading.Thread(target=auto_restart_service, daemon=True, name="AutoRestartThread").start()
427
 
428
  if len(ALL_API_KEYS) > 0 :
429
- demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
 
430
  else:
431
  logging.critical("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")
 
1
  import gradio as gr
 
 
 
2
  import base64
3
  import mimetypes
4
  import os
 
6
  import struct
7
  import time
8
  import zipfile
9
+ from google import genai
10
+ from google.genai import types
11
  import threading
12
  import logging
13
+ import uuid # [IMPROVEMENT] اضافه شد برای تولید شناسه منحصر به فرد
 
14
 
15
  try:
16
  from pydub import AudioSegment
17
  PYDUB_AVAILABLE = True
18
  except ImportError:
19
  PYDUB_AVAILABLE = False
 
20
 
21
  # --- START: پیکربندی لاگینگ ---
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
23
  # --- END: پیکربندی لاگینگ ---
24
 
25
+ # --- START: منطق مدیریت API Key ---
26
  ALL_API_KEYS: list[str] = []
27
  NEXT_KEY_INDEX: int = 0
28
+ KEY_LOCK: threading.Lock = threading.Lock()
29
 
30
  def _init_api_keys():
 
 
 
 
31
  global ALL_API_KEYS
32
+ all_keys_string = os.environ.get("ALL_GEMINI_API_KEYS")
 
 
33
  if all_keys_string:
34
  ALL_API_KEYS = [key.strip() for key in all_keys_string.split(',') if key.strip()]
 
35
  logging.info(f"✅ تعداد {len(ALL_API_KEYS)} کلید API جیمینای بارگذاری شد.")
36
  if not ALL_API_KEYS:
37
  logging.warning("⛔️ خطای حیاتی: هیچ Secret با نام ALL_GEMINI_API_KEYS یافت نشد!")
38
  logging.warning(" لطفاً Secret را به عنوان یک رشته با کاما جدا شده (مثال: key1,key2,key3) در تنظیمات Space خود اضافه کنید.")
39
 
 
40
  _init_api_keys()
41
 
42
  def get_next_api_key():
 
 
 
43
  global NEXT_KEY_INDEX, ALL_API_KEYS, KEY_LOCK
44
+ with KEY_LOCK:
 
45
  if not ALL_API_KEYS:
46
  return None, None
 
47
  key_to_use = ALL_API_KEYS[NEXT_KEY_INDEX % len(ALL_API_KEYS)]
48
  key_display_index = (NEXT_KEY_INDEX % len(ALL_API_KEYS)) + 1
49
  NEXT_KEY_INDEX += 1
50
  return key_to_use, key_display_index
51
+ # --- END: منطق مدیریت API Key ---
52
 
53
  SPEAKER_VOICES = [
54
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
 
57
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
58
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
59
  ]
60
+ FIXED_MODEL_NAME = "gemini-1.5-flash-preview-0514" # استفاده از مدل جدیدتر و پایدارتر
61
  DEFAULT_MAX_CHUNK_SIZE = 3800
62
  DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
63
+ DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
64
+
65
+ def save_binary_file(file_name, data):
66
+ try:
67
+ with open(file_name, "wb") as f: f.write(data)
68
+ return file_name
69
+ except Exception as e:
70
+ logging.error(f"❌ خطا در ذخیره فایل {file_name}: {e}")
71
+ return None
72
 
73
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
 
74
  parameters = parse_audio_mime_type(mime_type)
75
  bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
76
  num_channels, data_size = 1, len(audio_data)
 
80
  return header + audio_data
81
 
82
  def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
83
+ bits, rate = 16, 24000
 
84
  for param in mime_type.split(";"):
85
  param = param.strip()
86
  if param.lower().startswith("rate="):
87
  try: rate = int(param.split("=", 1)[1])
88
+ except: pass
89
  elif param.startswith("audio/L"):
90
  try: bits = int(param.split("L", 1)[1])
91
+ except: pass
92
  return {"bits_per_sample": bits, "rate": rate}
93
 
94
  def smart_text_split(text, max_size=3800):
 
95
  if len(text) <= max_size: return [text]
96
  chunks, current_chunk = [], ""
97
+ sentences = re.split(r'(?<=[.!?؟])\s+', text)
 
 
 
 
98
  for sentence in sentences:
99
  if len(current_chunk) + len(sentence) + 1 > max_size:
100
  if current_chunk: chunks.append(current_chunk.strip())
101
  current_chunk = sentence
 
102
  while len(current_chunk) > max_size:
 
103
  split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
104
  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:])
105
  chunks.append(part.strip())
106
+ else: current_chunk += (" " if current_chunk else "") + sentence
 
107
  if current_chunk: chunks.append(current_chunk.strip())
108
  final_chunks = [c for c in chunks if c]
109
  return final_chunks
110
 
111
+ def merge_audio_files_func(file_paths, output_path, request_id=""):
112
+ if not PYDUB_AVAILABLE: logging.warning(f"[{request_id}] ⚠️ pydub برای ادغام در دسترس نیست."); return False
 
 
 
 
 
 
113
  try:
114
+ combined = AudioSegment.empty()
115
+ for i, fp in enumerate(file_paths):
116
+ if os.path.exists(fp):
117
+ combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
118
+ else:
119
+ logging.warning(f"[{request_id}] ⚠️ فایل برای ادغام پیدا نشد: {fp}")
120
+ combined.export(output_path, format="wav")
121
+ logging.info(f"[{request_id}] فایل‌ها با موفقیت در {output_path} ادغام شدند.")
122
+ return True
 
 
 
 
123
  except Exception as e:
124
+ logging.error(f"[{request_id}] ❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
 
125
 
126
+ def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp, request_id=""):
 
 
 
 
 
 
127
  if not ALL_API_KEYS:
128
+ logging.error(f"[{request_id}] ❌ هیچ کلید API برای تولید صدا در دسترس نیست.")
129
  return None
130
 
131
+ for _ in range(len(ALL_API_KEYS)):
132
  selected_api_key, key_idx_display = get_next_api_key()
 
133
  if not selected_api_key:
134
+ logging.warning(f"[{request_id}] ⚠️ get_next_api_key هیچ کلیدی برنگرداند. تلاش‌های باقیمانده نادیده گرفته می‌شوند.")
135
+ break
136
 
137
+ logging.info(f"[{request_id}] ⚙️ تلاش برای تولید قطعه با کلید API شماره {key_idx_display} (...{selected_api_key[-4:]})")
138
 
139
  try:
140
+ # [IMPROVEMENT] استفاده از client جدید در هر تلاش برای اطمینان از تنظیمات صحیح
141
+ genai.configure(api_key=selected_api_key)
142
+ model = genai.GenerativeModel(FIXED_MODEL_NAME)
143
+
144
  final_text = f'"{prompt_text}"\n{chunk_text}' if prompt_text and prompt_text.strip() else chunk_text
145
+
146
+ # [IMPROVEMENT] ساختار جدید API برای مدل های 1.5
147
+ response = model.generate_content(
148
+ final_text,
149
+ generation_config=genai.types.GenerationConfig(temperature=temp),
150
+ request_options={"timeout": 60},
151
+ # مدل های 1.5 از این ساختار جدید برای TTS استفاده می کنند
152
+ tools=[genai.protos.Tool(
153
+ google_search_retrieval=genai.protos.GoogleSearchRetrieval(),
154
+ tool_code=genai.protos.ToolCode(
155
+ function_call=genai.protos.FunctionCall(
156
+ name="text-to-speech",
157
+ args={"text": final_text, "voice_name": voice}
158
+ )
159
+ )
160
+ )]
161
+ )
162
+
163
+ audio_part = response.candidates[0].content.parts[0]
164
+ if audio_part.file_data:
165
+ logging.info(f"[{request_id}] ✅ قطعه با موفقیت توسط کلید شماره {key_idx_display} تولید شد.")
166
+ return audio_part.file_data
167
  else:
168
+ logging.warning(f"[{request_id}] ⚠️ پاسخ API برای قطعه با کلید شماره {key_idx_display} بدون داده صوتی بود. تلاش با کلید بعدی...")
169
 
170
  except Exception as e:
171
+ logging.error(f"[{request_id}] ❌ خطا در تولید قطعه با کلید شماره {key_idx_display}: {e}. تلاش با کلید بعدی...")
172
+ # برخی خطاها نیاز به یک تاخیر کوتاه قبل از تلاش مجدد دارند
173
+ time.sleep(2)
174
+
175
+ logging.error(f"[{request_id}] ❌ تمام کلیدهای API امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
176
  return None
177
 
178
+ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, request_id):
179
+ logging.info(f"[{request_id}] 🚀 شروع فرآیند تولید صدا.")
180
 
181
+ # [IMPROVEMENT] استفاده از شناسه منحصر به فرد برای نام‌گذاری فایل‌ها
182
+ output_base_name = f"{DEFAULT_OUTPUT_FILENAME_BASE}_{request_id}"
183
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
184
 
 
 
 
 
185
  text_chunks = smart_text_split(text_input, max_chunk)
186
  if not text_chunks:
187
+ logging.error(f"[{request_id}] ❌ متن قابل پردازش به قطعات کوچکتر نیست.")
188
  return None
189
 
190
+ generated_files = []
191
+ final_audio_file = None
192
+
193
+ # [IMPROVEMENT] استفاده از try/finally برای تضمین پاک‌سازی فایل‌های موقت
194
+ try:
195
+ for i, chunk in enumerate(text_chunks):
196
+ logging.info(f"[{request_id}] 🔊 پردازش قطعه {i+1}/{len(text_chunks)}...")
197
+
198
+ file_data = generate_audio_chunk_with_retry(chunk, prompt_input, selected_voice, temperature_val, request_id)
199
+
200
+ if file_data:
201
+ data_buffer = file_data.data
202
+ ext = mimetypes.guess_extension(file_data.mime_type) or ".wav"
203
+
204
+ fname_base = f"{output_base_name}_part{i+1:03d}"
205
+ fpath = save_binary_file(f"{fname_base}{ext}", data_buffer)
206
+ if fpath:
207
+ generated_files.append(fpath)
208
+ else:
209
+ logging.error(f"[{request_id}] ❌ موفق به ذخیره فایل برای قطعه {i+1} نشدیم. این قطعه نادیده گرفته می‌شود.")
210
+ continue
211
+ else:
212
+ logging.error(f"[{request_id}] 🛑 فرآیند متوقف شد زیرا تولید قطعه {i+1} با تمام کلیدهای موجود ناموفق بود.")
213
+ # اگر یک قطعه شکست بخورد، ادامه نده
214
+ raise Exception(f"Failed to generate chunk {i+1}")
215
 
216
+ if i < len(text_chunks) - 1 and len(text_chunks) > 1:
217
+ time.sleep(sleep_time)
218
+
219
+ if not generated_files:
220
+ logging.error(f"[{request_id}] ❌ هیچ فایل صوتی تولید نشد.")
221
+ return None
222
+
223
+ final_output_path_base = f"{output_base_name}_final"
224
+
225
+ if len(generated_files) > 1:
226
+ if PYDUB_AVAILABLE:
227
+ merged_fn = f"{final_output_path_base}.wav"
228
+ if os.path.exists(merged_fn): os.remove(merged_fn)
229
+ if merge_audio_files_func(generated_files, merged_fn, request_id):
230
+ final_audio_file = merged_fn
231
+ else:
232
+ logging.warning(f"[{request_id}] ⚠️ ادغام ناموفق بود. فقط قطعه اول برگردانده می‌شود.")
233
+ # اگر ادغام شکست خورد، به عنوان جایگزین، فایل اول را برگردان
234
+ final_audio_file = generated_files[0]
235
+ else:
236
+ logging.warning(f"[{request_id}] ⚠️ pydub در دسترس نیست. اولین قطعه صوتی ارائه می‌شود.")
237
+ final_audio_file = generated_files[0]
238
+ else: # فقط یک فایل تولید شده است
239
+ final_audio_file = generated_files[0]
240
 
241
+ # [IMPROVEMENT] تغییر نام فایل نهایی برای سازگاری بهتر و پاک کردن شناسه
242
+ if final_audio_file:
243
+ target_ext = os.path.splitext(final_audio_file)[1]
244
+ final_renamed_path = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final_{request_id}{target_ext}"
245
+ os.rename(final_audio_file, final_renamed_path)
246
+ # فایل نهایی را در لیست فایل های تولید شده به روز می کن��م تا در finally حذف نشود
247
+ generated_files.remove(final_audio_file)
248
+ generated_files.append(final_renamed_path)
249
+ logging.info(f"[{request_id}] ✅ فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_renamed_path)}")
250
+ return final_renamed_path
251
+
252
+ return None # اگر هیچ فایلی نهایی نشد
253
 
254
+ except Exception as e:
255
+ logging.error(f"[{request_id}] ❌ خطای کلی در حین پردازش: {e}")
256
+ return None
257
+ finally:
258
+ # [IMPROVEMENT] پاک‌سازی تمام فایل‌های موقت به جز فایل نهایی که برگردانده شده
259
+ logging.info(f"[{request_id}] 🧹 شروع پاک‌سازی فایل‌های موقت...")
260
+ for fp in generated_files:
261
+ # اگر fp همان فایل نهایی است که به کاربر ارسال می شود، آن را حذف نکن
262
+ if final_audio_file and os.path.abspath(fp) == os.path.abspath(final_audio_file):
263
+ continue
264
+ try:
265
+ if os.path.exists(fp):
266
+ os.remove(fp)
267
+ logging.info(f"[{request_id}] 🗑️ فایل موقت حذف شد: {fp}")
268
+ except Exception as e_clean:
269
+ logging.error(f"[{request_id}] ❌ خطا در حذف فایل موقت {fp}: {e_clean}")
270
 
 
 
271
 
272
+ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
273
+ # [IMPROVEMENT] تولید شناسه منحصر به فرد برای هر درخواست
274
+ request_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
275
+ logging.info(f"
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
+ درخواست جدید با شناسه دریافت شد: {request_id}")
 
 
 
 
 
 
 
279
 
 
280
  actual_text = ""
281
  if use_file_input:
282
+ if uploaded_file:
283
  try:
284
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
285
+ if not actual_text: logging.error(f"[{request_id}] ❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
286
+ except Exception as e: logging.error(f"[{request_id}] ❌ خطا در خواندن فایل آپلود شده: {e}"); return None
287
+ else: logging.warning(f"[{request_id}] ❌ گزینه استفاده از فایل انتخاب شده اما فایلی آپلود نشده."); return None
288
  else:
289
  actual_text = text_to_speak
290
+ if not actual_text or not actual_text.strip(): logging.warning(f"[{request_id}] ❌ متن ورودی برای تبدیل خالی است."); return None
291
+
292
+ final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, request_id)
293
 
294
+ if final_path:
295
+ logging.info(f"[{request_id}] فرآیند با موفقیت به پایان رسید. فایل صوتی برای کاربر ارسال می‌شود.")
296
+ else:
297
+ logging.error(f"[{request_id}] ❌ فرآیند ناموفق بود. هیچ فایلی برای کاربر ارسال نمی‌شود.")
298
+
299
+ return final_path
300
 
 
301
  def auto_restart_service():
302
  RESTART_INTERVAL_SECONDS = 24 * 60 * 60
303
  logging.info(f"سرویس برای ری‌استارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمان‌بندی شده است.")
304
  time.sleep(RESTART_INTERVAL_SECONDS)
305
  logging.info(f"زمان ری‌استارت خودکار فرا رسیده است. برنامه برای ری‌استارت خارج می‌شود...")
306
  os._exit(1)
 
 
307
 
308
  # --- CSS و Gradio UI (بدون تغییر) ---
309
  custom_css_inspired_by_image = f"""
310
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
311
  :root {{
312
+ --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);
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  }}
314
  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; }}
315
  .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
 
319
  .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; }}
320
  @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;}} }}
321
  footer {{display:none !important;}}
 
322
  .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; }}
323
  .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);}}
324
  .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; }}
 
331
  label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
332
  label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
333
  label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
 
334
  #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
335
  .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
336
  .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);}}
 
343
  </div>
344
  """
345
 
 
346
  with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
347
  gr.HTML(alpha_header_html_v3)
348
 
349
  with gr.Column(elem_classes=["main-content-panel-alpha"]):
350
  use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
351
+ uploaded_file_input = gr.File(label=" ", file_types=['.txt'], visible=False, elem_id="file_uploader_alpha_main_v3")
352
+ text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
353
+ 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])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
+ speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
356
+ speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3")
357
+ temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
 
 
 
 
 
 
 
 
 
 
358
  gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
359
 
360
  generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
361
+ output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
362
+
 
 
363
  generate_button.click(
364
  fn=gradio_tts_interface,
365
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
366
+ outputs=[output_audio]
367
  )
368
 
369
  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")
 
375
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
376
  outputs=[output_audio],
377
  fn=gradio_tts_interface,
378
+ cache_examples=False
379
  )
380
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
381
 
 
383
  threading.Thread(target=auto_restart_service, daemon=True, name="AutoRestartThread").start()
384
 
385
  if len(ALL_API_KEYS) > 0 :
386
+ # [IMPROVEMENT] افزایش تعداد تردها در صف برای مدیریت بهتر کاربران همزمان
387
+ demo.queue(default_concurrency_limit=10).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
388
  else:
389
  logging.critical("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")