Hamed744 commited on
Commit
d9cad98
·
verified ·
1 Parent(s): 20924da

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -116
app.py CHANGED
@@ -1,17 +1,16 @@
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 # این دیگر برای مدل 1.5 نیاز نیست
11
  import threading
12
  import logging
13
  import uuid
14
 
 
 
 
15
  try:
16
  from pydub import AudioSegment
17
  PYDUB_AVAILABLE = True
@@ -50,17 +49,18 @@ def get_next_api_key():
50
  return key_to_use, key_display_index
51
  # --- END: منطق مدیریت API Key ---
52
 
 
53
  SPEAKER_VOICES = [
54
- "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 = 3800
63
- DEFAULT_SLEEP_BETWEEN_REQUESTS = 5 # کاهش زمان انتظار
64
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
65
 
66
  def save_binary_file(file_name, data):
@@ -71,28 +71,57 @@ 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=3800):
75
- if len(text) <= max_size: return [text]
76
- chunks, current_chunk = [], ""
77
- # بهبود تقسیم‌بندی برای زبان فارسی
78
- sentences = re.split(r'(?<=[.!?؟\n])\s+', text)
79
- for sentence in sentences:
80
- if len(current_chunk) + len(sentence) + 1 > max_size:
81
- if current_chunk: chunks.append(current_chunk.strip())
82
- current_chunk = sentence
83
- # اگر یک جمله به تنهایی بزرگتر از حد مجاز باشد
84
- while len(current_chunk) > max_size:
85
- split_idx = current_chunk.rfind('،', 0, max_size)
86
- if split_idx == -1:
87
- split_idx = current_chunk.rfind(' ', 0, max_size)
88
- if split_idx == -1: # اگر هیچ جداکننده ای پیدا نشد
89
- split_idx = max_size
90
- part, current_chunk = current_chunk[:split_idx], current_chunk[split_idx:]
91
- chunks.append(part.strip())
92
- else: current_chunk += (" " if current_chunk else "") + sentence
93
- if current_chunk: chunks.append(current_chunk.strip())
94
- final_chunks = [c for c in chunks if c]
95
- return final_chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  def merge_audio_files_func(file_paths, output_path, request_id=""):
98
  if not PYDUB_AVAILABLE: logging.warning(f"[{request_id}] ⚠️ pydub برای ادغام در دسترس نیست."); return False
@@ -100,24 +129,24 @@ def merge_audio_files_func(file_paths, output_path, request_id=""):
100
  combined = AudioSegment.empty()
101
  for i, fp in enumerate(file_paths):
102
  if os.path.exists(fp):
103
- combined += AudioSegment.from_file(fp, format="mp3") # فرمت را به mp3 تغییر می‌دهیم
 
104
  if i < len(file_paths) - 1:
105
- combined += AudioSegment.silent(duration=150)
106
  else:
107
  logging.warning(f"[{request_id}] ⚠️ فایل برای ادغام پیدا نشد: {fp}")
108
- # خروجی نهایی را هم mp3 ذخیره می‌کنیم
109
  combined.export(output_path, format="mp3")
110
  logging.info(f"[{request_id}] ✅ فایل‌ها با موفقیت در {output_path} ادغام شدند.")
111
  return True
112
  except Exception as e:
113
  logging.error(f"[{request_id}] ❌ خطا در ادغام فایل‌های صوتی: {e}"); return False
114
 
115
- def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp, request_id=""):
116
  if not ALL_API_KEYS:
117
  logging.error(f"[{request_id}] ❌ هیچ کلید API برای تولید صدا در دسترس نیست.")
118
  return None
119
 
120
- for _ in range(len(ALL_API_KEYS)):
121
  selected_api_key, key_idx_display = get_next_api_key()
122
  if not selected_api_key:
123
  logging.warning(f"[{request_id}] ⚠️ get_next_api_key هیچ کلیدی برنگرداند.")
@@ -129,16 +158,13 @@ def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp, reques
129
  genai.configure(api_key=selected_api_key)
130
  model = genai.GenerativeModel(FIXED_MODEL_NAME)
131
 
132
- final_text = f'"{prompt_text}"\n{chunk_text}' if prompt_text and prompt_text.strip() else chunk_text
133
-
134
  response = model.generate_content(
135
- final_text,
136
- generation_config=genai.GenerationConfig(temperature=temp)
137
- # ساختار جدید API برای TTS ساده‌تر شده و نیازی به tools ندارد
138
  )
139
 
140
- # مدل جدید مستقیما بایت‌های mp3 را برمی‌گرداند
141
- if response.audio_content:
142
  logging.info(f"[{request_id}] ✅ قطعه با موفقیت توسط کلید شماره {key_idx_display} تولید شد.")
143
  return response.audio_content
144
  else:
@@ -146,38 +172,35 @@ def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp, reques
146
 
147
  except Exception as e:
148
  logging.error(f"[{request_id}] ❌ خطا در تولید قطعه با کلید شماره {key_idx_display}: {e}. تلاش با کلید بعدی...")
149
- time.sleep(2)
150
 
151
  logging.error(f"[{request_id}] ❌ تمام کلیدهای API امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
152
  return None
153
 
154
- def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, request_id):
155
  logging.info(f"[{request_id}] 🚀 شروع فرآیند تولید صدا.")
156
 
157
  output_base_name = f"{DEFAULT_OUTPUT_FILENAME_BASE}_{request_id}"
158
- max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
159
-
160
- text_chunks = smart_text_split(text_input, max_chunk)
161
  if not text_chunks:
162
- logging.error(f"[{request_id}] ❌ متن قابل پردازش به قطعات کوچکتر نیست.")
163
  return None
164
 
165
  generated_files = []
166
  final_audio_file = None
167
 
168
  try:
 
169
  for i, chunk in enumerate(text_chunks):
170
- logging.info(f"[{request_id}] 🔊 پردازش قطعه {i+1}/{len(text_chunks)}...")
171
 
172
- # مدل جدید دیگر از voice و prompt پشتیبانی نمی‌کند، این پارامترها نادیده گرفته می‌شوند
173
- audio_bytes = generate_audio_chunk_with_retry(chunk, "", "default", temperature_val, request_id)
174
 
175
  if audio_bytes:
176
- # خروجی همیشه mp3 است
177
  ext = ".mp3"
178
- fname_base = f"{output_base_name}_part{i+1:03d}"
179
- fpath = save_binary_file(f"{fname_base}{ext}", audio_bytes)
180
- if fpath:
181
  generated_files.append(fpath)
182
  else:
183
  logging.error(f"[{request_id}] ❌ موفق به ذخیره فایل برای قطعه {i+1} نشدیم.")
@@ -186,26 +209,17 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
186
  logging.error(f"[{request_id}] 🛑 فرآیند متوقف شد زیرا تولید قطعه {i+1} ناموفق بود.")
187
  raise Exception(f"Failed to generate chunk {i+1}")
188
 
189
- if i < len(text_chunks) - 1 and len(text_chunks) > 1:
190
- time.sleep(sleep_time)
191
 
192
  if not generated_files:
193
  logging.error(f"[{request_id}] ❌ هیچ فایل صوتی تولید نشد.")
194
  return None
195
 
196
- final_output_path_base = f"{output_base_name}_final"
197
-
198
  if len(generated_files) > 1:
199
- if PYDUB_AVAILABLE:
200
- merged_fn = f"{final_output_path_base}.mp3"
201
- if os.path.exists(merged_fn): os.remove(merged_fn)
202
- if merge_audio_files_func(generated_files, merged_fn, request_id):
203
- final_audio_file = merged_fn
204
- else:
205
- logging.warning(f"[{request_id}] ⚠️ ادغام ناموفق بود. فقط قطعه اول برگردانده می‌شود.")
206
- final_audio_file = generated_files[0]
207
- else:
208
- logging.warning(f"[{request_id}] ⚠️ pydub در دسترس نیست. اولین قطعه صوتی ارائه می‌شود.")
209
  final_audio_file = generated_files[0]
210
  else:
211
  final_audio_file = generated_files[0]
@@ -232,12 +246,9 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
232
  logging.error(f"[{request_id}] ❌ خطا در حذف فایل موقت {fp}: {e_clean}")
233
 
234
 
235
- def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
236
  request_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
237
-
238
- # === FIX: این خط اصلاح شده است ===
239
  logging.info(f"✅ درخواست جدید با شناسه دریافت شد: {request_id}")
240
- # ================================
241
 
242
  actual_text = ""
243
  if use_file_input:
@@ -251,13 +262,13 @@ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_pr
251
  actual_text = text_to_speak
252
  if not actual_text or not actual_text.strip(): logging.warning(f"[{request_id}] ❌ متن ورودی خالی است."); return None
253
 
254
- # توجه: پارامترهای prompt و voice دیگر در مدل جدید استفاده نمی‌شوند ولی برای سازگاری در تابع باقی می‌مانند
255
- final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, request_id)
256
 
257
  if final_path:
258
  logging.info(f"[{request_id}] ✅ فرآیند با موفقیت به پایان رسید.")
259
  else:
260
  logging.error(f"[{request_id}] ❌ فرآیند ناموفق بود.")
 
261
 
262
  return final_path
263
 
@@ -268,74 +279,72 @@ def auto_restart_service():
268
  logging.info(f"زمان ری‌استارت خودکار فرا رسیده است. برنامه برای ری‌استارت خارج می‌شود...")
269
  os._exit(1)
270
 
271
- custom_css_inspired_by_image = f"""
272
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
273
- :root {{ --app-font: 'Vazirmatn', sans-serif; --app-header-grad-start: #2980b9; --app-header-grad-end: #2ecc71; --app-panel-bg: #FFFFFF; --app-input-bg: #F7F7F7; --app-button-bg: #2979FF; --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); --app-text-primary: #333; --app-text-secondary: #555; --app-border-color: #E0E0E0; --radius-card: 20px; --radius-input: 8px; --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1); --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5); }}
274
- 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; }}
275
- .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
276
- .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); }}
277
- .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); }}
278
- .app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}
279
- .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; }}
280
- @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;}} }}
281
- footer {{display:none !important;}}
282
- .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; }}
283
- .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);}}
284
- .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; }}
285
- .gr-file > label + div {{ text-align:center; border-style: dashed !important; }}
286
- .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; }}
287
- label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}
288
- .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; }}
289
- #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
290
- .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
291
- .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);}}
292
- .gradio-container .gr-form {{gap: 1.25rem !important;}}
293
  """
294
 
295
- alpha_header_html_v3 = """
296
  <div class='app-header-alpha'>
297
  <h1>Alpha TTS</h1>
298
- <p>جادوی تبدیل متن به صدا در دستان شما</p>
299
  </div>
300
  """
301
 
302
- with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
303
- gr.HTML(alpha_header_html_v3)
304
 
305
  with gr.Column(elem_classes=["main-content-panel-alpha"]):
306
- with gr.Row():
307
- use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
 
 
308
 
309
- uploaded_file_input = gr.File(label="فایل .txt خود را اینجا بکشید یا آپلود کنید", file_types=['.txt'], visible=False, elem_id="file_uploader_alpha_main_v3")
310
- text_to_speak_tb = gr.Textbox(label="📝 متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
311
 
312
  use_file_input_cb.change(
313
- fn=lambda x: (gr.update(visible=x), gr.update(visible=not x)),
314
  inputs=use_file_input_cb,
315
  outputs=[uploaded_file_input, text_to_speak_tb]
316
  )
 
 
317
 
318
- with gr.Accordion("⚙️ تنظیمات پیشرفته (اختیاری)", open=False):
319
- speech_prompt_tb = gr.Textbox(label="🗣️ سبک گفتار (Prompt)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3", visible=False) # مخفی شد
320
- speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="🎤 انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3", visible=False) # مخفی شد
321
- temperature_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.7, label="🌡️ میزان خلاقیت (Temperature)", elem_id="temperature_slider_alpha_v3")
322
- gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
323
-
324
- generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
325
  output_audio = gr.Audio(label="خروجی صدا", type="filepath", elem_id="output_audio_player_alpha_v3")
326
 
 
327
  generate_button.click(
328
  fn=gradio_tts_interface,
329
- inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
330
  outputs=[output_audio]
331
  )
332
 
333
  gr.Examples(
334
  examples=[
335
- [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "", "", 0.7],
336
- [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "", "", 0.7],
337
  ],
338
- inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
339
  outputs=[output_audio],
340
  fn=gradio_tts_interface,
341
  cache_examples=False,
 
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
16
  PYDUB_AVAILABLE = True
 
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):
 
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
 
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 هیچ کلیدی برنگرداند.")
 
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:
 
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} نشدیم.")
 
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]
 
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:
 
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
 
 
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,