Hamed744 commited on
Commit
47095d4
·
verified ·
1 Parent(s): 8a7ac92

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +244 -500
app.py CHANGED
@@ -1,76 +1,25 @@
1
  import gradio as gr
 
 
2
  import os
 
 
3
  import time
4
- import threading
5
- import sys
 
6
  import logging
7
- import traceback
8
- import asyncio # For first script's key rotation, though TTS part is sync
9
 
10
- # TTS specific imports from second script
11
- import base64 # Not directly used in final combined code, but was in original TTS parts
12
- import mimetypes
13
- import re
14
- import struct
15
- import zipfile # Not directly used in final combined code
16
- from google import genai # For TTS
17
- from google.genai import types as genai_types # For TTS
18
 
19
  try:
20
  from pydub import AudioSegment
21
  PYDUB_AVAILABLE = True
 
22
  except ImportError:
23
  PYDUB_AVAILABLE = False
24
- logging.warning("Pydub is not available. Audio merging will be disabled. Falling back to single file or ZIP.")
25
-
26
- # --- START: پیکربندی لاگینگ (From Alpha Translator) ---
27
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
28
- # --- END: پیکربندی لاگینگ ---
29
-
30
- # --- START: منطق چرخش API Key برای Gemini (Adapted from Alpha Translator for synchronous TTS client) ---
31
- API_KEYS_GEMINI = []
32
- i = 1
33
- while True:
34
- key = os.environ.get(f'GEMINI_API_KEY_{i}')
35
- if key:
36
- API_KEYS_GEMINI.append(key)
37
- i += 1
38
- else:
39
- break
40
 
41
- NUM_GEMINI_KEYS = len(API_KEYS_GEMINI)
42
- current_gemini_key_index = 0
43
- gemini_key_lock = threading.Lock() # Use threading.Lock for synchronous operations
44
-
45
- if NUM_GEMINI_KEYS == 0:
46
- logging.error(
47
- 'خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n (مثلاً GEMINI_API_KEY_1) یافت نشد! ' +
48
- 'قابلیت تبدیل متن به گفتار غیرفعال خواهد بود. لطفاً Secret ها را در تنظیمات Space خود اضافه کنید.'
49
- )
50
- else:
51
- logging.info(f"تعداد {NUM_GEMINI_KEYS} کلید API جیمینای بارگذاری شد.")
52
-
53
- def get_gemini_api_key_sync():
54
- if NUM_GEMINI_KEYS == 0:
55
- return None
56
- with gemini_key_lock:
57
- global current_gemini_key_index
58
- selected_api_key = API_KEYS_GEMINI[current_gemini_key_index]
59
- current_gemini_key_index = (current_gemini_key_index + 1) % NUM_GEMINI_KEYS
60
- logging.info(f"TTS Gemini: استفاده از کلید API با اندیس چرخشی: ...{selected_api_key[-4:]}")
61
- return selected_api_key
62
- # --- END: منطق چرخش API Key ---
63
-
64
- # --- START: تابع ری‌استارت خودکار (From Alpha Translator) ---
65
- def auto_restart_service():
66
- RESTART_INTERVAL_SECONDS = 24 * 60 * 60 # 24 ساعت
67
- logging.info(f"سرویس برای ری‌استارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمان‌بندی شده است.")
68
- time.sleep(RESTART_INTERVAL_SECONDS)
69
- logging.info(f"زمان ری‌استارت خودکار ({RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت) فرا رسیده است. برنامه خارج می‌شود تا توسط پلتفرم ری‌استارت شود...")
70
- os._exit(1) # خروج فوری برای تحریک ری‌استارت توسط پلتفرم
71
- # --- END: تابع ری‌استارت خودکار ---
72
-
73
- # --- START: TTS Core Logic (From Alpha TTS, adapted for key rotation) ---
74
  SPEAKER_VOICES = [
75
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
76
  "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
@@ -78,22 +27,22 @@ SPEAKER_VOICES = [
78
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
79
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
80
  ]
81
- FIXED_MODEL_NAME = "gemini-1.5-flash-preview-tts"
82
  DEFAULT_MAX_CHUNK_SIZE = 3800
83
- DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
84
- DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
85
 
86
- def _log_tts(message, log_list_ref): # Renamed to avoid conflict if other _log exists
87
  log_list_ref.append(message)
88
- logging.info(f"[TTS_CORE] {message}")
89
 
90
  def save_binary_file(file_name, data, log_list_ref):
91
  try:
92
  with open(file_name, "wb") as f: f.write(data)
93
- _log_tts(f"فایل ذخیره شد: {file_name}", log_list_ref)
94
  return file_name
95
  except Exception as e:
96
- _log_tts(f"خطا در ذخیره فایل {file_name}: {e}", log_list_ref)
97
  return None
98
 
99
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
@@ -120,481 +69,276 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
120
  def smart_text_split(text, max_size=3800, log_list_ref=None):
121
  if len(text) <= max_size: return [text]
122
  chunks, current_chunk = [], ""
123
- sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
124
  for sentence in sentences:
125
  if len(current_chunk) + len(sentence) + 1 > max_size:
126
  if current_chunk: chunks.append(current_chunk.strip())
127
  current_chunk = sentence
128
- while len(current_chunk) > max_size:
129
- split_idx = -1
130
- for punc in ['،', ',', ';', ':', ' ']:
131
- idx = current_chunk.rfind(punc, max_size // 2, max_size)
132
- if idx > split_idx : split_idx = idx
133
-
134
- if split_idx != -1:
135
- part, current_chunk = current_chunk[:split_idx+1], current_chunk[split_idx+1:]
136
- else:
137
- part, current_chunk = current_chunk[:max_size], current_chunk[max_size:]
138
  chunks.append(part.strip())
139
- else:
140
- current_chunk += (" " if current_chunk and sentence else "") + sentence
141
  if current_chunk: chunks.append(current_chunk.strip())
142
- final_chunks = [c for c in chunks if c]
143
- if log_list_ref: _log_tts(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list_ref)
144
  return final_chunks
145
 
146
-
147
  def merge_audio_files_func(file_paths, output_path, log_list_ref):
148
- if not PYDUB_AVAILABLE:
149
- _log_tts("❌ Pydub در دسترس نیست. ادغام فایل انجام نشد.", log_list_ref)
150
- return False
151
  try:
152
- _log_tts(f"🔗 شروع ادغام {len(file_paths)} فایل صوتی...", log_list_ref)
153
  combined = AudioSegment.empty()
154
  for i, fp in enumerate(file_paths):
155
- if os.path.exists(fp):
156
- segment = AudioSegment.from_file(fp)
157
- combined += segment
158
- if i < len(file_paths) - 1:
159
- combined += AudioSegment.silent(duration=150)
160
- else:
161
- _log_tts(f"⚠️ فایل صوتی برای ادغام یافت نشد: {fp}", log_list_ref)
162
-
163
- combined.export(output_path, format="wav")
164
- _log_tts(f"✅ فایل صوتی با موفقیت در '{output_path}' ادغام و ذخیره شد.", log_list_ref)
165
- return True
166
- except Exception as e:
167
- _log_tts(f"❌ خطا در هنگام ادغام فایل‌های صوتی: {e}\n{traceback.format_exc()}", log_list_ref)
168
- return False
169
 
170
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list_ref):
171
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
172
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
173
-
174
- _log_tts("🚀 شروع فرآیند تولید صدا...", log_list_ref)
175
-
176
- api_key = get_gemini_api_key_sync()
177
- if not api_key:
178
- _log_tts("❌ کلید API جیمینای معتبری یافت نشد یا دریافت نشد. عملیات متوقف شد.", log_list_ref)
179
- return None, "خطا: کلید API جیمینای برای سرویس TTS در دسترس نیست."
180
-
181
- try:
182
- genai.configure(api_key=api_key)
183
- except Exception as e:
184
- _log_tts(f"❌ خطا در مقداردهی اولیه کلاینت Gemini: {e}", log_list_ref)
185
- return None, f"خطا در ارتباط با Gemini: {e}"
186
-
187
- if not text_input or not text_input.strip():
188
- _log_tts("❌ متن ورودی برای تبدیل به گفتار خالی است.", log_list_ref)
189
- return None, "خطا: متن ورودی خالی است."
190
 
 
 
 
191
  text_chunks = smart_text_split(text_input, max_chunk, log_list_ref)
192
- if not text_chunks:
193
- _log_tts("❌ متن قابل پردازش برای تبدیل به گفتار نیست.", log_list_ref)
194
- return None, "خطا: متن قابل پردازش نیست."
195
 
196
- generated_files = []
197
  for i, chunk in enumerate(text_chunks):
198
- _log_tts(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...", log_list_ref)
199
-
200
- final_text_for_tts = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
201
-
 
 
 
 
202
  try:
203
- # The `selected_voice` from the dropdown is not currently used in this call.
204
- # This would require knowing the specific API parameter for voice selection with this model.
205
- # For now, the model will use its default voice or whatever behavior is programmed.
206
- # A more advanced implementation would pass `selected_voice` to the API if possible.
207
-
208
- # This is where the SyntaxError occurred. The `custom_config_for_tts` variable was an
209
- # incomplete assignment. It's removed/commented out. The actual config is inline below.
210
- #
211
- # # custom_config_for_tts = genai_types.GenerationConfig( # This seems to be the new way # THIS LINE CAUSED SyntaxError
212
- # temperature=temperature_val,
213
- # # ... (rest of the commented out block) ...
214
- # # )
215
-
216
- tts_model = genai.GenerativeModel(FIXED_MODEL_NAME)
217
-
218
- # Note: `selected_voice` is not used here yet. This means the dropdown for voice selection
219
- # will not have an effect until this part is updated to correctly pass the voice
220
- # to the Gemini API for the `FIXED_MODEL_NAME`.
221
- # The `final_text_for_tts` includes the `prompt_input` for style.
222
- response = tts_model.generate_content(
223
- final_text_for_tts,
224
- generation_config=genai_types.GenerationConfig(
225
- temperature=temperature_val,
226
- response_mime_type="audio/wav"
227
- ),
228
- )
229
-
230
- fname_base = f"{output_base_name}_part{i+1:03d}"
231
-
232
- audio_bytes = None
233
- mime_type = None
234
-
235
- if response.parts and hasattr(response.parts[0], 'blob') and response.parts[0].blob.mime_type.startswith("audio/"): # More common for new SDK
236
- audio_bytes = response.parts[0].blob.data
237
- mime_type = response.parts[0].blob.mime_type
238
- elif response.candidates and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data: # AlphaTTS way
239
- inline_data = response.candidates[0].content.parts[0].inline_data
240
- audio_bytes = inline_data.data
241
- mime_type = inline_data.mime_type
242
- else:
243
- audio_part = None
244
- if response.parts:
245
- for part in response.parts:
246
- if hasattr(part, 'mime_type') and part.mime_type.startswith("audio/"): # Check for mime_type attr
247
- audio_part = part
248
- break
249
- if audio_part and hasattr(audio_part, 'data'):
250
- audio_bytes = audio_part.data
251
- mime_type = audio_part.mime_type
252
- elif audio_part and hasattr(audio_part, '_blob'):
253
- audio_bytes = audio_part._blob.data
254
- mime_type = audio_part._blob.mime_type
255
-
256
- if not audio_bytes:
257
- _log_tts(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی معتبر دریافت شد.", log_list_ref)
258
- _log_tts(f"ساختار پاسخ (Response structure): {response}", log_list_ref)
259
- continue
260
-
261
- if not mime_type: # Safety net if mime_type wasn't extracted
262
- _log_tts(f"⚠️ MIME type برای قطعه {i+1} یافت نشد. پیش‌فرض wav.", log_list_ref)
263
- mime_type = "audio/wav"
264
-
265
-
266
- ext = mimetypes.guess_extension(mime_type) or ".wav"
267
- if "audio/L" in mime_type and ext == ".wav":
268
- audio_bytes = convert_to_wav(audio_bytes, mime_type)
269
- if not ext.startswith("."): ext = "." + ext
270
-
271
- fpath = save_binary_file(f"{fname_base}{ext}", audio_bytes, log_list_ref)
272
- if fpath:
273
- generated_files.append(fpath)
274
-
275
- except Exception as e:
276
- _log_tts(f"❌ خطا در تولید قطعه صوتی {i+1} با Gemini: {e}\n{traceback.format_exc()}", log_list_ref)
277
- if hasattr(e, 'response') and e.response:
278
- _log_tts(f"جزئیات خطای Gemini API: {e.response}", log_list_ref)
279
- continue
280
-
281
- if i < len(text_chunks) - 1 and len(text_chunks) > 1:
282
- _log_tts(f"💤 توقف کوتاه ({sleep_time} ثانیه) قبل از پردازش قطعه بعدی...", log_list_ref)
283
- time.sleep(sleep_time)
284
-
285
- if not generated_files:
286
- _log_tts("❌ هیچ فایل صوتی تولید نشد.", log_list_ref)
287
- return None, "تولید صدا ناموفق بود. هیچ فایلی ایجاد نشد."
288
-
289
- _log_tts(f"🎉 {len(generated_files)} فایل(های) صوتی با موفقیت تولید شد.", log_list_ref)
290
 
291
- final_audio_file = None
292
- final_output_path_base = f"{output_base_name}_final"
 
 
293
 
294
- if len(generated_files) > 1:
 
295
  if PYDUB_AVAILABLE:
296
- merged_fn = f"{final_output_path_base}.wav"
297
- if os.path.exists(merged_fn):
298
- try: os.remove(merged_fn)
299
- except OSError as e: _log_tts(f"⚠️ عدم امکان حذف فایل ادغام شده قبلی '{merged_fn}': {e}", log_list_ref)
300
-
301
- if merge_audio_files_func(generated_files, merged_fn, log_list_ref):
302
- final_audio_file = merged_fn
303
- for fp in generated_files:
304
- if os.path.abspath(fp) != os.path.abspath(merged_fn):
305
- try: os.remove(fp)
306
- except OSError as e_del: _log_tts(f"⚠️ عدم امکان حذف فایل موقت '{fp}': {e_del}", log_list_ref)
307
  else:
308
- _log_tts("⚠️ ادغام فایل‌های صوتی ناموفق بود. اولین قطعه ارائه می‌شود.", log_list_ref)
309
- if generated_files:
310
- try:
311
- first_chunk_path = generated_files[0]
312
- target_ext = os.path.splitext(first_chunk_path)[1]
313
- fallback_fn = f"{final_output_path_base}_fallback{target_ext}"
314
- if os.path.exists(fallback_fn): os.remove(fallback_fn)
315
- os.rename(first_chunk_path, fallback_fn)
316
- final_audio_file = fallback_fn
317
- for i_gf in range(1, len(generated_files)):
318
- try: os.remove(generated_files[i_gf])
319
- except OSError as e_del: _log_tts(f"⚠️ عدم امکان حذف فایل موقت '{generated_files[i_gf]}': {e_del}", log_list_ref)
320
- except Exception as e_rename_fallback:
321
- _log_tts(f"خطا در تغییر نام فایل اولین قطعه (fallback): {e_rename_fallback}", log_list_ref)
322
- final_audio_file = generated_files[0]
323
  else:
324
- _log_tts("⚠️ Pydub برای ادغام در دسترس نیست. اولین قطعه صوتی ارائه می‌شود.", log_list_ref)
325
- if generated_files:
326
- try:
327
- first_chunk_path = generated_files[0]
328
- target_ext = os.path.splitext(first_chunk_path)[1]
329
- single_fallback_fn = f"{final_output_path_base}_single{target_ext}"
330
- if os.path.exists(single_fallback_fn): os.remove(single_fallback_fn)
331
- os.rename(first_chunk_path, single_fallback_fn)
332
- final_audio_file = single_fallback_fn
333
- for i_gf in range(1, len(generated_files)):
334
- _log_tts(f"قطعه اضافی موجود: {generated_files[i_gf]} (ادغام نشده)", log_list_ref)
335
-
336
- except Exception as e_rename_nopydub:
337
- _log_tts(f"خطا در تغییر نام اولین قطعه (بدون pydub): {e_rename_nopydub}", log_list_ref)
338
- final_audio_file = generated_files[0]
339
-
340
- elif len(generated_files) == 1:
341
  try:
342
- single_file_path = generated_files[0]
343
- target_ext = os.path.splitext(single_file_path)[1]
344
- final_single_fn = f"{final_output_path_base}{target_ext}"
345
- if os.path.exists(final_single_fn) and os.path.abspath(single_file_path) != os.path.abspath(final_single_fn):
346
- os.remove(final_single_fn)
347
-
348
- if os.path.abspath(single_file_path) != os.path.abspath(final_single_fn):
349
- os.rename(single_file_path, final_single_fn)
350
-
351
- final_audio_file = final_single_fn
352
- except Exception as e_rename_single:
353
- _log_tts(f"خطا در تغییر نام فایل تکی نهایی: {e_rename_single}", log_list_ref)
354
- final_audio_file = generated_files[0]
355
-
356
- if final_audio_file and not os.path.exists(final_audio_file):
357
- _log_tts(f"⚠️ فایل صوتی نهایی '{final_audio_file}' پس از پردازش وجود ندارد!", log_list_ref)
358
- return None, "خطا: فایل صوتی نهایی یافت نشد."
 
 
 
 
 
359
 
360
- return final_audio_file, "موفق"
361
 
362
- def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature):
363
- logs_for_this_run = []
364
  actual_text = ""
365
- status_message = "شروع پردازش..."
366
- final_audio_path = None
367
-
368
- if NUM_GEMINI_KEYS == 0:
369
- return None, "خطای پیکربندی: هیچ کلید API جیمینای برای سرویس TTS تنظیم نشده است."
370
-
371
  if use_file_input:
372
- if uploaded_file and hasattr(uploaded_file, 'name'):
373
  try:
374
- with open(uploaded_file.name, 'r', encoding='utf-8') as f:
375
- actual_text = f.read().strip()
376
- if not actual_text:
377
- return None, "خطا: فایل متنی انتخاب شده خالی است."
378
- _log_tts(f"خوانش متن از فایل: {uploaded_file.name}", logs_for_this_run)
379
- except Exception as e:
380
- _log_tts(f"❌ خطا در خواندن فایل متنی: {e}", logs_for_this_run)
381
- return None, f"خطا در خواندن فایل: {e}"
382
- else:
383
- return None, "خطا: فایل متنی انتخاب نشده است در حالی که گزینه استفاده از فایل فعال است."
384
  else:
385
  actual_text = text_to_speak
386
- if not actual_text or not actual_text.strip():
387
- return None, "خطا: لطفاً متنی را برای تبدیل به گفتار وارد کنید."
388
 
389
- _log_tts(f"متن ورودی برای TTS (اولین 50 کاراکتر): '{actual_text[:50]}...'", logs_for_this_run)
390
- _log_tts(f"تنظیمات: Speaker={speaker_voice}, Temp={temperature}, Prompt='{speech_prompt[:30]}...'", logs_for_this_run)
391
-
392
- try:
393
- final_audio_path, generation_status_msg = core_generate_audio(
394
- actual_text, speech_prompt, speaker_voice, temperature, logs_for_this_run
395
- )
396
-
397
- if final_audio_path and generation_status_msg == "موفق":
398
- status_message = "✅ تبدیل متن به گفتار با موفقیت انجام شد."
399
- _log_tts(status_message, logs_for_this_run)
400
- return final_audio_path, status_message
401
- elif final_audio_path and generation_status_msg != "موفق":
402
- status_message = f"⚠️ {generation_status_msg}. فایل صوتی ممکن است ناقص باشد: {final_audio_path}"
403
- _log_tts(status_message, logs_for_this_run)
404
- return final_audio_path, status_message
405
- else:
406
- status_message = f"❌ {generation_status_msg}"
407
- _log_tts(status_message, logs_for_this_run)
408
- return None, status_message
409
-
410
- except Exception as e:
411
- _log_tts(f"❌ خطای پیش‌بینی نشده در gradio_tts_interface: {e}\n{traceback.format_exc()}", logs_for_this_run)
412
- return None, f"خطای داخلی سرویس: {e}"
413
-
414
- # --- END: TTS Core Logic ---
415
-
416
-
417
- # --- START: بخش UI و Gradio (Adapted from Alpha Translator, content from Alpha TTS) ---
418
- FLY_PRIMARY_COLOR_HEX = "#4F46E5"
419
- FLY_SECONDARY_COLOR_HEX = "#10B981"
420
- FLY_ACCENT_COLOR_HEX = "#D97706"
421
- FLY_TEXT_COLOR_HEX = "#1F2937"
422
- FLY_SUBTLE_TEXT_HEX = "#6B7280"
423
- FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
424
- FLY_WHITE_HEX = "#FFFFFF"
425
- FLY_BORDER_COLOR_HEX = "#D1D5DB"
426
- FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6"
427
- FLY_PANEL_BG_SIMPLE = "#E0F2FE"
428
-
429
- app_theme_outer = gr.themes.Base(
430
- font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
431
- ).set(
432
- body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
433
- )
434
 
435
- custom_css = f"""
436
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
437
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
438
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  :root {{
440
- --fly-primary: {FLY_PRIMARY_COLOR_HEX}; --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
441
- --fly-accent: {FLY_ACCENT_COLOR_HEX}; --fly-text-primary: {FLY_TEXT_COLOR_HEX};
442
- --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX}; --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
443
- --fly-bg-white: {FLY_WHITE_HEX}; --fly-border-color: {FLY_BORDER_COLOR_HEX};
444
- --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE}; --fly-panel-bg-simple: {FLY_PANEL_BG_SIMPLE};
445
- --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
446
- --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
447
- --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; --radius-full: 9999px;
448
- --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05); --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1);
449
- --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);
450
- --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1);
451
- --fly-primary-rgb: 79,70,229; --fly-accent-rgb: 217,119,6;
 
 
 
 
 
 
 
452
  }}
453
- body {{font-family:var(--font-global);direction:rtl;background-color:var(--fly-bg-light);color:var(--fly-text-primary);line-height:1.7;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:16px;}}
454
- .gradio-container {{max-width:100% !important;width:100% !important;min-height:100vh;margin:0 auto !important;padding:0 !important;border-radius:0 !important;box-shadow:none !important;background:linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%);display:flex;flex-direction:column;}}
455
- .app-title-card {{text-align:center;padding:2.5rem 1rem;margin:0;background:linear-gradient(135deg,var(--fly-primary) 0%,var(--fly-secondary) 100%);color:var(--fly-bg-white);border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl);box-shadow:var(--shadow-lg);position:relative;overflow:hidden;}}
456
- .app-title-card::before {{content:'';position:absolute;top:-50px;right:-50px;width:150px;height:150px;background:rgba(255,255,255,0.1);border-radius:var(--radius-full);opacity:0.5;transform:rotate(45deg);}}
457
- .app-title-card h1 {{font-size:2.25em !important;font-weight:800 !important;margin:0 0 0.5rem 0;font-family:var(--font-english);letter-spacing:-0.5px;text-shadow:0 2px 4px rgba(0,0,0,0.1);}}
458
- .app-title-card p {{font-size:1em !important;margin-top:0.25rem;font-weight:400;color:rgba(255,255,255,0.85) !important;}}
459
- .app-footer-fly {{text-align:center;font-size:0.85em;color:var(--fly-text-secondary);margin-top:2.5rem;padding:1rem 0;background-color:rgba(255,255,255,0.3);backdrop-filter:blur(5px);border-top:1px solid var(--fly-border-color);}}
460
- footer,.gradio-footer,.flagging-container,.flex.row.gap-2.absolute.bottom-2.right-2.gr-compact.gr-box.gr-text-gray-500,div[data-testid="flag"],button[title="Flag"],button[aria-label="Flag"],.footer-utils {{display:none !important;visibility:hidden !important;}}
461
- .main-content-area {{flex-grow:1;padding:0.75rem;width:100%;margin:0 auto;box-sizing:border-box;}}
462
- .content-panel-simple {{background-color:var(--fly-bg-white);padding:1rem;border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);margin-top:-2rem;position:relative;z-index:10;margin-bottom:2rem;width:100%;box-sizing:border-box;}}
463
- .content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{background:var(--fly-accent) !important;margin-top:1rem !important;padding:12px 20px !important;transition:all 0.25s ease-in-out !important;color:white !important;font-weight:600 !important;border-radius:10px !important;border:none !important;box-shadow:0 3px 8px -1px rgba(var(--fly-accent-rgb),0.3) !important;width:100% !important;font-size:1em !important;display:flex;align-items:center;justify-content:center;}}
464
- .content-panel-simple .gr-button.lg.primary:hover,.content-panel-simple button[variant="primary"]:hover {{background:#B45309 !important;transform:translateY(-1px) !important;box-shadow:0 5px 10px -1px rgba(var(--fly-accent-rgb),0.4) !important;}}
465
- .content-panel-simple .gr-input > label + div > textarea,.content-panel-simple .gr-dropdown > label + div > div > input,.content-panel-simple .gr-dropdown > label + div > div > select,.content-panel-simple .gr-textbox > label + div > textarea, .content-panel-simple .gr-file > label + div {{border-radius:8px !important;border:1.5px solid var(--fly-border-color) !important;font-size:0.95em !important;background-color:var(--fly-input-bg-simple) !important;padding:10px 12px !important;color:var(--fly-text-primary) !important;}}
466
- .content-panel-simple .gr-input > label + div > textarea:focus,.content-panel-simple .gr-dropdown > label + div > div > input:focus,.content-panel-simple .gr-dropdown > label + div > div > select:focus,.content-panel-simple .gr-textbox > label + div > textarea:focus, .content-panel-simple .gr-file > label + div:focus-within {{border-color:var(--fly-primary) !important;box-shadow:0 0 0 3px rgba(var(--fly-primary-rgb),0.12) !important;background-color:var(--fly-bg-white) !important;}}
467
- .content-panel-simple .gr-file > label + div {{ text-align:center; border-style: dashed !important; }}
468
- .content-panel-simple .gr-dropdown select {{font-family:var(--font-global) !important;width:100%;cursor:pointer;}}
469
- .content-panel-simple .gr-textbox[label*="وضعیت"] > label + div > textarea {{background-color:var(--fly-panel-bg-simple) !important;border-color:#A5D5FE !important;min-height:80px;font-family:var(--font-global);font-size:0.9em !important;line-height:1.5;padding:10px !important;}}
470
- .content-panel-simple .gr-panel,.content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > .gr-panel {{border-radius:8px !important;border:1px solid var(--fly-border-color) !important;background-color:var(--fly-input-bg-simple) !important;padding:0.8rem 1rem !important;margin-top:0.6rem;box-shadow:none;}}
471
- .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > button.gr-button {{font-weight:500 !important;padding:8px 10px !important;border-radius:6px !important;background-color:#E5E7EB !important;color:var(--fly-text-primary) !important;border:1px solid #D1D5DB !important;}}
472
- .content-panel-simple label > span.label-text {{font-weight:500 !important;color:#4B5563 !important;font-size:0.88em !important;margin-bottom:6px !important;display:inline-block;}}
473
- .content-panel-simple .gr-slider label span {{font-size:0.82em !important;color:var(--fly-text-secondary);}}
474
- .temp-description-tts {{ font-size: 0.82em !important; color: var(--fly-text-secondary) !important; margin-top: -0.5rem; margin-bottom: 1rem; padding-right: 5px; }}
475
- .content-panel-simple div[label*="نمونه"] {{margin-top:1.5rem;}}
476
- .content-panel-simple div[label*="نمونه"] .gr-button.gr-button-tool,.content-panel-simple div[label*="نمونه"] .gr-sample-button {{background-color:#E0E7FF !important;color:var(--fly-primary) !important;border-radius:6px !important;font-size:0.78em !important;padding:4px 8px !important;}}
477
- .content-panel-simple .custom-hr {{height:1px;background-color:var(--fly-border-color);margin:1.5rem 0;border:none;}}
478
- .api-warning-message {{background-color:#FFFBEB !important;color:#92400E !important;padding:10px 12px !important;border-radius:8px !important;border:1px solid #FDE68A !important;text-align:center !important;margin:0 0.2rem 1rem 0.2rem !important;font-size:0.85em !important;}}
479
- .content-panel-simple #output_audio_tts audio {{ width: 100%; border-radius: var(--radius-md); margin-top:0.5rem; }}
480
- @media (min-width:640px) {{.main-content-area {{padding:1.5rem;max-width:700px;}} .content-panel-simple {{padding:1.5rem;}} .app-title-card h1 {{font-size:2.5em !important;}} .app-title-card p {{font-size:1.05em !important;}} }}
481
- @media (min-width:768px) {{
482
- .main-content-area {{max-width:780px;}} .content-panel-simple {{padding:2rem;}}
483
- .content-panel-simple .main-content-row {{display:flex !important;flex-direction:row !important;gap:1.5rem !important;}}
484
- .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{flex-basis:60%; min-width:0;}}
485
- .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{flex-basis:40%; min-width:0;}}
486
- .content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{width:auto !important;align-self:flex-start;}}
487
- .app-title-card h1 {{font-size:2.75em !important;}} .app-title-card p {{font-size:1.1em !important;}}
488
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  """
490
- logging.info(f"Gradio version: {gr.__version__}")
491
- if not PYDUB_AVAILABLE:
492
- logging.warning("Pydub (برای ادغام فایل‌های صوتی) یافت نشد. لطفاً با `pip install pydub` نصب کنید. در غیر این صورت، فقط اولین قطعه صوتی ارائه خواهد شد.")
493
-
494
- with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آلفا TTS") as demo:
495
- gr.HTML(f"""
496
- <div class="app-title-card">
497
- <h1>🚀 Alpha TTS</h1>
498
- <p>جادوی تبدیل متن به صدا با هوش مصنوعی Gemini</p>
499
- </div>
500
- """)
501
-
502
- with gr.Column(elem_classes=["main-content-area"]):
503
- with gr.Group(elem_classes=["content-panel-simple"]):
504
- if NUM_GEMINI_KEYS == 0:
505
- missing_key_msg = (
506
- "⚠️ هشدار: قابلیت تبدیل متن به گفتار غیرفعال است. "
507
- "هیچ کلید API جیمینای (با فرمت GEMINI_API_KEY_1, ...) "
508
- "در بخش Secrets این Space یافت نشد. "
509
- "لطفاً حداقل یک کلید با نام GEMINI_API_KEY_1 تنظیم کنید."
510
- )
511
- gr.Markdown(f"<div class='api-warning-message'>{missing_key_msg}</div>")
512
 
513
- status_message_output = gr.Textbox(label="وضعیت پردازش", interactive=False, lines=1, placeholder="پیام‌های وضعیت اینجا نمایش داده می‌شوند...")
514
-
515
- with gr.Row(elem_classes=["main-content-row"]):
516
- with gr.Column(scale=3):
517
- use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False)
518
- uploaded_file_input = gr.File(
519
- label="آپلود فایل متنی",
520
- file_types=['.txt'],
521
- visible=False
522
- )
523
- text_to_speak_tb = gr.Textbox(
524
- label="📝 متن فارسی برای تبدیل به گفتار",
525
- placeholder="مثال: سلام، به پروژه آلفا خوش آمدید.",
526
- lines=5,
527
- value=""
528
- )
529
- speech_prompt_tb = gr.Textbox(
530
- label="🗣️ سبک و زمینه گفتار (اختیاری)",
531
- placeholder="مثال: با لحنی شاد و پرانرژی",
532
- value="با لحنی دوستانه و رسا صحبت کن.",
533
- lines=2
534
- )
535
- with gr.Column(scale=2):
536
- speaker_voice_dd = gr.Dropdown(
537
- SPEAKER_VOICES,
538
- label="🎤 انتخاب گوینده",
539
- value="Charon"
540
- )
541
- temperature_slider = gr.Slider(
542
- minimum=0.1, maximum=1.5, step=0.05, value=0.9,
543
- label="🌡️ میزان خلاقیت صدا (دما)"
544
- )
545
- gr.Markdown("<p class='temp-description-tts'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>", elem_classes=["temp-description-tts-container"])
546
-
547
- output_audio = gr.Audio(label="🎧 فایل صوتی خروجی", type="filepath", elem_id="output_audio_tts")
548
-
549
- generate_button = gr.Button("🚀 تولید و پخش صدا", variant="primary", elem_classes=["lg"])
550
-
551
- gr.HTML("<hr class='custom-hr'>")
552
-
553
- gr.Examples(
554
- examples=[
555
- [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید. این یک نمونه صدای تولید شده توسط آلفا است.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
556
- [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی پیشرفته جیمینای است.", "با صدایی طبیعی، روان و کمی رسمی.", "Charon", 0.9],
557
- [False, None, "آیا می‌توانم یک پیتزای پپرونی سفارش دهم؟", "پرسشی و مودبانه.", "Achird", 0.75],
558
- ],
559
- inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
560
- outputs=[output_audio, status_message_output],
561
- fn=gradio_tts_interface,
562
- cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
563
- label="💡 نمونه‌های کاربردی"
564
- )
565
-
566
- gr.Markdown("<p class='app-footer-fly'>Alpha TTS © 2024</p>")
567
-
568
- def toggle_file_input(use_file):
569
- if use_file:
570
- return gr.update(visible=True, label=" "), gr.update(visible=False)
571
- else:
572
- return gr.update(visible=False), gr.update(visible=True, label="📝 متن فارسی برای تبدیل به گفتار")
573
 
574
- use_file_input_cb.change(
575
- fn=toggle_file_input,
576
- inputs=use_file_input_cb,
577
- outputs=[uploaded_file_input, text_to_speak_tb]
578
- )
579
-
580
- if generate_button is not None:
581
- generate_button.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  fn=gradio_tts_interface,
583
- inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
584
- outputs=[output_audio, status_message_output]
585
  )
586
- else:
587
- logging.error("دکمه تولید صدا (generate_button) به درستی مقداردهی اولیه نشده است.")
588
-
589
 
590
  if __name__ == "__main__":
591
- if os.getenv("AUTO_RESTART_ENABLED", "true").lower() == "true":
592
- restart_scheduler_thread = threading.Thread(target=auto_restart_service, daemon=True)
593
- restart_scheduler_thread.start()
594
-
595
- demo.launch(
596
- server_name="0.0.0.0",
597
- server_port=int(os.getenv("PORT", 7860)),
598
- debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true",
599
- show_error=True
600
- )
 
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
+ from google import genai
10
+ from google.genai import types
11
  import logging
 
 
12
 
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
 
 
 
 
14
 
15
  try:
16
  from pydub import AudioSegment
17
  PYDUB_AVAILABLE = True
18
+ logging.info("pydub با موفقیت ایمپورت شد.")
19
  except ImportError:
20
  PYDUB_AVAILABLE = False
21
+ logging.warning("pydub یافت نشد. قابلیت ادغام فایل‌های صوتی غیرفعال خواهد بود.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  SPEAKER_VOICES = [
24
  "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
25
  "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
 
27
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
28
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
29
  ]
30
+ FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
31
  DEFAULT_MAX_CHUNK_SIZE = 3800
32
+ DEFAULT_SLEEP_BETWEEN_REQUESTS = 7
33
+ DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio_gtts"
34
 
35
+ def _log_internal(message, log_list_ref):
36
  log_list_ref.append(message)
37
+ logging.info(f"[CORE_LOG] {message}")
38
 
39
  def save_binary_file(file_name, data, log_list_ref):
40
  try:
41
  with open(file_name, "wb") as f: f.write(data)
42
+ _log_internal(f"فایل ذخیره شد: {os.path.basename(file_name)}", log_list_ref)
43
  return file_name
44
  except Exception as e:
45
+ _log_internal(f"خطا در ذخیره فایل {os.path.basename(file_name)}: {e}", log_list_ref)
46
  return None
47
 
48
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
 
69
  def smart_text_split(text, max_size=3800, log_list_ref=None):
70
  if len(text) <= max_size: return [text]
71
  chunks, current_chunk = [], ""
72
+ sentences = re.split(r'(?<=[.!?؟])\s+', text)
73
  for sentence in sentences:
74
  if len(current_chunk) + len(sentence) + 1 > max_size:
75
  if current_chunk: chunks.append(current_chunk.strip())
76
  current_chunk = sentence
77
+ while len(current_chunk) > max_size:
78
+ split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
79
+ 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:])
 
 
 
 
 
 
 
80
  chunks.append(part.strip())
81
+ else: current_chunk += (" " if current_chunk else "") + sentence
 
82
  if current_chunk: chunks.append(current_chunk.strip())
83
+ final_chunks = [c for c in chunks if c]
84
+ if log_list_ref: _log_internal(f"متن به {len(final_chunks)} قطعه تقسیم شد.", log_list_ref)
85
  return final_chunks
86
 
 
87
  def merge_audio_files_func(file_paths, output_path, log_list_ref):
88
+ if not PYDUB_AVAILABLE: _log_internal("pydub در دسترس نیست، ادغام انجام نشد.", log_list_ref); return False
 
 
89
  try:
90
+ _log_internal(f"ادغام {len(file_paths)} فایل صوتی...", log_list_ref)
91
  combined = AudioSegment.empty()
92
  for i, fp in enumerate(file_paths):
93
+ if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
94
+ else: _log_internal(f"فایل پیدا نشد برای ادغام: {fp}", log_list_ref)
95
+ combined.export(output_path, format="wav")
96
+ _log_internal(f"فایل با موفقیت در {os.path.basename(output_path)} ادغام شد.", log_list_ref); return True
97
+ except Exception as e: _log_internal(f"خطا در ادغام فایل‌ها: {e}", log_list_ref); return False
 
 
 
 
 
 
 
 
 
98
 
99
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list_ref):
100
  output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
101
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
102
+ _log_internal("شروع فرآیند تولید صدا...", log_list_ref)
103
+ api_key = os.environ.get("GEMINI_API_KEY_1")
104
+ if not api_key: api_key = os.environ.get("GEMINI_API_KEY")
105
+ if not api_key:
106
+ _log_internal("خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_1 یا GEMINI_API_KEY یافت نشد!", log_list_ref)
107
+ return None
108
+ _log_internal(f"استفاده از کلید API جمینای (...{api_key[-4:] if api_key else 'N/A'})", log_list_ref)
 
 
 
 
 
 
 
 
 
 
109
 
110
+ try: client = genai.Client(api_key=api_key)
111
+ except Exception as e: _log_internal(f"خطا در ایجاد کلاینت جمینای: {e}", log_list_ref); return None
112
+ if not text_input or not text_input.strip(): _log_internal("متن ورودی خالی است.", log_list_ref); return None
113
  text_chunks = smart_text_split(text_input, max_chunk, log_list_ref)
114
+ if not text_chunks: _log_internal("پس از تقسیم‌بندی، متنی برای پردازش وجود ندارد.", log_list_ref); return None
 
 
115
 
116
+ generated_files_temp = []
117
  for i, chunk in enumerate(text_chunks):
118
+ _log_internal(f"پردازش قطعه {i+1} از {len(text_chunks)}...", log_list_ref)
119
+ final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
120
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
121
+ config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
122
+ speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
123
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
124
+ timestamp = int(time.time() * 1000)
125
+ temp_fname_base = f"temp_audio_{timestamp}_part{i+1:03d}"
126
  try:
127
+ response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
128
+ if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
129
+ inline_data = response.candidates[0].content.parts[0].inline_data
130
+ data_buffer = inline_data.data
131
+ ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
132
+ if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
133
+ if not ext.startswith("."): ext = "." + ext
134
+ temp_fpath = save_binary_file(f"{temp_fname_base}{ext}", data_buffer, log_list_ref)
135
+ if temp_fpath: generated_files_temp.append(temp_fpath)
136
+ else: _log_internal(f"پاسخ API برای قطعه {i+1} بدون داده صوتی معتبر.", log_list_ref)
137
+ except Exception as e:
138
+ _log_internal(f"خطای بحرانی در تولید قطعه {i+1} با جمینای: {e}\n{traceback.format_exc()}", log_list_ref);
139
+ for fp_clean in generated_files_temp:
140
+ if os.path.exists(fp_clean):
141
+ try: os.remove(fp_clean)
142
+ except: _log_internal(f"خطا در پاک کردن فایل موقت {fp_clean} پس از خطا", log_list_ref)
143
+ return None
144
+ if i < len(text_chunks) - 1 and len(text_chunks) > 1: time.sleep(sleep_time)
145
+
146
+ if not generated_files_temp: _log_internal("هیچ فایل صوتی موقتی تولید نشد.", log_list_ref); return None
147
+ _log_internal(f"{len(generated_files_temp)} قطعه صوتی با موفقیت تولید شد.", log_list_ref)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ final_output_path = f"{output_base_name}_final.wav"
150
+ if os.path.exists(final_output_path):
151
+ try: os.remove(final_output_path)
152
+ except Exception as e_del: _log_internal(f"خطا در حذف فایل خروجی قبلی {final_output_path}: {e_del}", log_list_ref)
153
 
154
+ final_audio_file_to_return = None
155
+ if len(generated_files_temp) > 1:
156
  if PYDUB_AVAILABLE:
157
+ if merge_audio_files_func(generated_files_temp, final_output_path, log_list_ref):
158
+ final_audio_file_to_return = final_output_path
 
 
 
 
 
 
 
 
 
159
  else:
160
+ _log_internal("ادغام ناموفق بود.", log_list_ref)
161
+ final_audio_file_to_return = None
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  else:
163
+ _log_internal("pydub برای ادغام چند قطعه در دسترس نیست.", log_list_ref)
164
+ final_audio_file_to_return = None
165
+ elif len(generated_files_temp) == 1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  try:
167
+ os.rename(generated_files_temp[0], final_output_path)
168
+ final_audio_file_to_return = final_output_path
169
+ except Exception as e_rename:
170
+ _log_internal(f"خطا در انتقال فایل تکی به مسیر نهایی: {e_rename}", log_list_ref)
171
+ final_audio_file_to_return = None
172
+ if os.path.exists(generated_files_temp[0]):
173
+ try: os.remove(generated_files_temp[0])
174
+ except: pass
175
+
176
+ # پاک کردن تمام فایل‌های موقت که در generated_files_temp لیست شده‌اند
177
+ # این حلقه باید در این سطح تورفتگی باشد
178
+ for temp_f in generated_files_temp:
179
+ if os.path.exists(temp_f) and (not final_audio_file_to_return or os.path.abspath(temp_f) != os.path.abspath(final_audio_file_to_return)):
180
+ try:
181
+ os.remove(temp_f)
182
+ _log_internal(f"فایل موقت {os.path.basename(temp_f)} پاک شد.", log_list_ref)
183
+ except Exception as e_clean:
184
+ _log_internal(f"خطا در پاک کردن فایل موقت {os.path.basename(temp_f)}: {e_clean}", log_list_ref)
185
+
186
+ if final_audio_file_to_return and not os.path.exists(final_audio_file_to_return):
187
+ _log_internal(f"فایل نهایی '{final_audio_file_to_return}' پس از پردازش وجود ندارد!", log_list_ref)
188
+ return None
189
 
190
+ return final_audio_file_to_return
191
 
192
+ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
193
+ logs = []
194
  actual_text = ""
 
 
 
 
 
 
195
  if use_file_input:
196
+ if uploaded_file:
197
  try:
198
+ with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
199
+ if not actual_text: _log_internal("فایل آپلود شده خالی است.", logs); return None
200
+ except Exception as e: _log_internal(f"خطا در خواندن فایل آپلود شده: {e}", logs); return None
201
+ else: _log_internal("گزینه فایل انتخاب شده اما فایلی آپلود نشده.", logs); return None
 
 
 
 
 
 
202
  else:
203
  actual_text = text_to_speak
204
+ if not actual_text or not actual_text.strip(): _log_internal("متن ورودی برای تبدیل خالی است.", logs); return None
 
205
 
206
+ final_audio_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, logs)
207
+ if final_audio_path:
208
+ logging.info(f"فایل صوتی نهایی برای ارسال به کاربر: {final_audio_path}")
209
+ return final_audio_path
210
+ else:
211
+ logging.warning("هیچ فایل صوتی نهایی برای ارسال به کاربر تولید نشد.")
212
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
+ APP_HEADER_GRADIENT_START = "#4F46E5"
215
+ APP_HEADER_GRADIENT_END = "#10B981"
216
+ PANEL_BACKGROUND = "#FFFFFF"
217
+ TEXT_INPUT_BG = "#F3F4F6"
218
+ BUTTON_BG = "#2979FF"
219
+ MAIN_BACKGROUND = "linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%)"
220
+ TEXT_PRIMARY = "#1F2937"
221
+ TEXT_SECONDARY = "#6B7280"
222
+ BORDER_COLOR = "#D1D5DB"
223
+ RADIUS_CARD = "20px"
224
+ RADIUS_INPUT = "10px"
225
+ SHADOW_CARD = "0 10px 30px -5px rgba(0,0,0,0.1)"
226
+ SHADOW_BUTTON = f"0 4px 10px -2px rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)},0.5)"
227
+
228
+ LABEL_TEXT_INPUT = "📝 متن فارسی برای تبدیل"
229
+ LABEL_SPEECH_PROMPT = "🗣️ سبک گفتار (اختیاری)"
230
+ LABEL_SPEAKER_VOICE = "🎤 انتخاب گوینده و لهجه"
231
+ LABEL_TEMPERATURE = "🌡️ خلاقیت و تنوع صدا"
232
+ LABEL_FILE_UPLOAD = "📄 استفاده از فایل متنی (.txt)"
233
+
234
+ custom_css_final_attempt = f"""
235
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
236
  :root {{
237
+ --app-font: 'Vazirmatn', sans-serif;
238
+ --app-header-grad-start: {APP_HEADER_GRADIENT_START}; --app-header-grad-end: {APP_HEADER_GRADIENT_END};
239
+ --app-panel-bg: {PANEL_BACKGROUND}; --app-input-bg: {TEXT_INPUT_BG};
240
+ --app-button-bg: {BUTTON_BG}; --app-main-bg: {MAIN_BACKGROUND};
241
+ --app-text-primary: {TEXT_PRIMARY}; --app-text-secondary: {TEXT_SECONDARY};
242
+ --app-border-color: {BORDER_COLOR};
243
+ --radius-card: {RADIUS_CARD}; --radius-input: {RADIUS_INPUT};
244
+ --shadow-card: {SHADOW_CARD}; --shadow-button: {SHADOW_BUTTON};
245
+ }}
246
+ body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 15px; line-height: 1.6; }}
247
+ .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
248
+ .app-header-container {{ padding: 2.8rem 1.5rem 3.5rem 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.25); }}
249
+ .app-header-container h1 {{ font-size: 2.3em; font-weight: 800; margin:0 0 0.4rem 0; text-shadow: 0 1px 3px rgba(0,0,0,0.2); }}
250
+ .app-header-container p {{ font-size: 1.05em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.95; }}
251
+ .main-content-wrapper-alpha {{ padding: 1.8rem 1.5rem; max-width: 650px; 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; }}
252
+ @media (max-width: 768px) {{
253
+ .main-content-wrapper-alpha {{ width: 92%; padding: 1.5rem 1.2rem; margin-top: -2rem; }}
254
+ .app-header-container h1 {{font-size:2em;}}
255
+ .app-header-container p {{font-size:1em;}}
256
  }}
257
+ footer {{display:none !important;}}
258
+ .gradio-button.generate-button-final-alpha {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.85rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.25s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.8rem !important; }}
259
+ .gradio-button.generate-button-final-alpha:hover {{ filter: brightness(1.15); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)},0.65);}}
260
+ .gradio-textbox > label + div > textarea,
261
+ .gradio-dropdown > label + div > div > input,
262
+ .gradio-dropdown select,
263
+ .gradio-file > label + div {{
264
+ border-radius: var(--radius-input) !important;
265
+ border: 1px solid var(--app-border-color) !important;
266
+ background-color: var(--app-input-bg) !important;
267
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.06);
268
+ padding: 0.8rem !important;
269
+ font-size: 0.95em !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }}
271
+ .gradio-dropdown .wrap-inner {{ border-radius: var(--radius-input) !important; }}
272
+ .gradio-file > label + div {{ text-align:center; border-style: dashed !important; padding: 1.2rem !important; }}
273
+ .gradio-file span[data-testid="block-title"] {{ font-weight:500; color: var(--app-text-secondary); font-size:0.9em; }}
274
+ .gradio-file button.svelte-116rqfv {{ background: var(--app-button-bg) !important; color:white !important; border-radius:6px !important; padding: 0.4rem 0.8rem !important; font-size:0.85em !important; }}
275
+ .gradio-textbox > label + div > textarea:focus,
276
+ .gradio-dropdown > label + div > div > input:focus {{
277
+ border-color: var(--app-button-bg) !important;
278
+ box-shadow: 0 0 0 3.5px rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)},0.25) !important;
279
+ }}
280
+ label.gradio-label > .label-text {{ font-weight: 500 !important; color: var(--app-text-primary) !important; font-size: 0.98em !important; margin-bottom: 0.6rem !important; display: block; }}
281
+ .gradio-textbox[elem_id="text_input_alpha_final"] > label > .label-text::before,
282
+ .gradio-checkbox[elem_id="use_file_cb_alpha_final"] > label > .label-text > span::before,
283
+ .gradio-textbox[elem_id="speech_prompt_alpha_final"] > label > .label-text::before,
284
+ .gradio-dropdown[elem_id="speaker_voice_alpha_final"] > label > .label-text::before,
285
+ .gradio-slider[elem_id="temperature_slider_alpha_final"] > label > .label-text > span::before {{
286
+ margin-left: 10px; vertical-align: -2px; font-size: 1.1em; opacity: 0.8;
287
+ }}
288
+ .gradio-textbox[elem_id="text_input_alpha_final"] > label > .label-text::before {{ content: '📝'; }}
289
+ .gradio-checkbox[elem_id="use_file_cb_alpha_final"] > label > .label-text > span::before {{ content: '📄'; }}
290
+ .gradio-textbox[elem_id="speech_prompt_alpha_final"] > label > .label-text::before {{ content: '🗣️'; }}
291
+ .gradio-dropdown[elem_id="speaker_voice_alpha_final"] > label > .label-text::before {{ content: '🎤'; }}
292
+ .gradio-slider[elem_id="temperature_slider_alpha_final"] > label > .label-text > span::before {{ content: '🌡️'; }}
293
+ #output_audio_player_alpha_final audio {{ width: 100%; border-radius: var(--radius-input); margin-top:1rem; box-shadow: 0 2px 5px rgba(0,0,0,0.08); }}
294
+ .temp_description_class_alpha_final {{ font-size: 0.88em; color: var(--app-text-secondary); margin-top: -0.3rem; margin-bottom: 1.2rem; }}
295
+ .app-footer-container-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.9; margin-top:3.5rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}
296
+ .gradio-examples {{ margin-top: 2.5rem !important; }}
297
+ .gradio-examples > .gradio-label > .label-text {{ font-size: 1.1em !important; font-weight: 700 !important; color: var(--app-text-primary) !important; text-align:center; margin-bottom: 1rem !important; }}
298
+ .gradio-examples table th {{ background-color: var(--app-input-bg) !important; font-weight:700 !important; font-size:0.9em !important; padding: 0.6rem 0.5rem !important; text-align:right !important; }}
299
+ .gradio-examples table td {{ padding: 0.6rem 0.5rem !important; font-size:0.9em !important; }}
300
+ .gradio-examples .gr-sample-button {{ background-color: rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)}, 0.1) !important; color: var(--app-button-bg) !important; border: 1px solid rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)}, 0.3) !important; font-weight:500 !important; }}
301
+ #output_audio_player_alpha_final > .gradio-label {{ display: none !important; }}
302
+ #file_uploader_alpha_final > .gradio-label {{ display: none !important; }}
303
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
+ alpha_header_html_final = """
306
+ <div class='app-header-container'>
307
+ <h1>Alpha Translator</h1>
308
+ <p>جادوی ترجمه و تلفظ در دستان شما</p>
309
+ </div>
310
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_final_attempt, title="Alpha TTS") as demo:
313
+ gr.HTML(alpha_header_html_final)
314
+ with gr.Column(elem_classes=["main-content-wrapper-alpha"]):
315
+ use_file_input_cb = gr.Checkbox(label=LABEL_FILE_UPLOAD, value=False, elem_id="use_file_cb_alpha_final")
316
+ uploaded_file_input = gr.File(label=" ", file_types=['.txt'], visible=False, elem_id="file_uploader_alpha_final" )
317
+ text_to_speak_tb = gr.Textbox(label=LABEL_TEXT_INPUT, placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_alpha_final")
318
+ use_file_input_cb.change(fn=lambda x: (gr.update(visible=x), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
319
+ speech_prompt_tb = gr.Textbox(label=LABEL_SPEECH_PROMPT, placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_final")
320
+ speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label=LABEL_SPEAKER_VOICE, value="Charon", elem_id="speaker_voice_alpha_final")
321
+ temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label=LABEL_TEMPERATURE, elem_id="temperature_slider_alpha_final")
322
+ gr.Markdown("<p class='temp_description_class_alpha_final'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
323
+ generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final-alpha"], elem_id="generate_button_alpha_final")
324
+ output_audio = gr.Audio(type="filepath", elem_id="output_audio_player_alpha_final", label=" ")
325
+ 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] )
326
+ gr.Examples(
327
+ label="نمونه‌های کاربردی",
328
+ examples=[
329
+ [False, None, "قیمت این لباس چقدر است؟", "با لحنی مودبانه و سوالی.", "Zubenelgenubi", 0.75],
330
+ [False, None, "می‌توانید آدرس را روی نقشه به من نشان دهید؟", "واضح و با سرعت متوسط.", "Achird", 0.8],
331
+ [False, None, "ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", "کمی آهسته‌تر و شمرده.", "Vindemiatrix", 0.6],
332
+ ],
333
+ inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
334
+ outputs=[output_audio],
335
  fn=gradio_tts_interface,
336
+ cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true"
 
337
  )
338
+ gr.Markdown("<p class='app-footer-container-final'>Alpha Language Learning © 2025</p>")
 
 
339
 
340
  if __name__ == "__main__":
341
+ logging.info("اپلیکیشن Alpha TTS در حال راه‌اندازی است...")
342
+ if not os.environ.get("GEMINI_API_KEY_1") and not os.environ.get("GEMINI_API_KEY"):
343
+ logging.warning("هشدار: هیچ کلید API جمینای (GEMINI_API_KEY_1 یا GEMINI_API_KEY) در متغیرهای محیطی یافت نشد. اپلیکیشن ممکن است کار نکند.")
344
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)), debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true", show_error=True )