Hamed744 commited on
Commit
42a6235
·
verified ·
1 Parent(s): d9cad98

Update app.py

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