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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -109
app.py CHANGED
@@ -7,10 +7,10 @@ import struct
7
  import time
8
  import zipfile
9
  from google import genai
10
- from google.genai import types
11
  import threading
12
  import logging
13
- import uuid # [IMPROVEMENT] اضافه شد برای تولید شناسه منحصر به فرد
14
 
15
  try:
16
  from pydub import AudioSegment
@@ -57,9 +57,10 @@ SPEAKER_VOICES = [
57
  "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
58
  "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
59
  ]
60
- FIXED_MODEL_NAME = "gemini-1.5-flash-preview-0514" # استفاده از مدل جدیدتر و پایدارتر
 
61
  DEFAULT_MAX_CHUNK_SIZE = 3800
62
- DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
63
  DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
64
 
65
  def save_binary_file(file_name, data):
@@ -70,38 +71,23 @@ def save_binary_file(file_name, data):
70
  logging.error(f"❌ خطا در ذخیره فایل {file_name}: {e}")
71
  return None
72
 
73
- def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
74
- parameters = parse_audio_mime_type(mime_type)
75
- bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
76
- num_channels, data_size = 1, len(audio_data)
77
- bytes_per_sample, block_align = bits_per_sample // 8, num_channels * (bits_per_sample // 8)
78
- byte_rate, chunk_size = rate * block_align, 36 + data_size
79
- 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)
80
- return header + audio_data
81
-
82
- def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
83
- bits, rate = 16, 24000
84
- for param in mime_type.split(";"):
85
- param = param.strip()
86
- if param.lower().startswith("rate="):
87
- try: rate = int(param.split("=", 1)[1])
88
- except: pass
89
- elif param.startswith("audio/L"):
90
- try: bits = int(param.split("L", 1)[1])
91
- except: pass
92
- return {"bits_per_sample": bits, "rate": rate}
93
-
94
  def smart_text_split(text, max_size=3800):
95
  if len(text) <= max_size: return [text]
96
  chunks, current_chunk = [], ""
97
- sentences = re.split(r'(?<=[.!?؟])\s+', text)
 
98
  for sentence in sentences:
99
  if len(current_chunk) + len(sentence) + 1 > max_size:
100
  if current_chunk: chunks.append(current_chunk.strip())
101
  current_chunk = sentence
 
102
  while len(current_chunk) > max_size:
103
- split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
104
- part, current_chunk = (current_chunk[:split_idx+1], current_chunk[split_idx+1:]) if split_idx != -1 else (current_chunk[:max_size], current_chunk[max_size:])
 
 
 
 
105
  chunks.append(part.strip())
106
  else: current_chunk += (" " if current_chunk else "") + sentence
107
  if current_chunk: chunks.append(current_chunk.strip())
@@ -114,10 +100,13 @@ def merge_audio_files_func(file_paths, output_path, request_id=""):
114
  combined = AudioSegment.empty()
115
  for i, fp in enumerate(file_paths):
116
  if os.path.exists(fp):
117
- combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
 
 
118
  else:
119
  logging.warning(f"[{request_id}] ⚠️ فایل برای ادغام پیدا نشد: {fp}")
120
- combined.export(output_path, format="wav")
 
121
  logging.info(f"[{request_id}] ✅ فایل‌ها با موفقیت در {output_path} ادغام شدند.")
122
  return True
123
  except Exception as e:
@@ -131,45 +120,32 @@ def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp, reques
131
  for _ in range(len(ALL_API_KEYS)):
132
  selected_api_key, key_idx_display = get_next_api_key()
133
  if not selected_api_key:
134
- logging.warning(f"[{request_id}] ⚠️ get_next_api_key هیچ کلیدی برنگرداند. تلاش‌های باقیمانده نادیده گرفته می‌شوند.")
135
  break
136
 
137
  logging.info(f"[{request_id}] ⚙️ تلاش برای تولید قطعه با کلید API شماره {key_idx_display} (...{selected_api_key[-4:]})")
138
 
139
  try:
140
- # [IMPROVEMENT] استفاده از client جدید در هر تلاش برای اطمینان از تنظیمات صحیح
141
  genai.configure(api_key=selected_api_key)
142
  model = genai.GenerativeModel(FIXED_MODEL_NAME)
143
 
144
  final_text = f'"{prompt_text}"\n{chunk_text}' if prompt_text and prompt_text.strip() else chunk_text
145
 
146
- # [IMPROVEMENT] ساختار جدید API برای مدل های 1.5
147
  response = model.generate_content(
148
  final_text,
149
- generation_config=genai.types.GenerationConfig(temperature=temp),
150
- request_options={"timeout": 60},
151
- # مدل های 1.5 از این ساختار جدید برای TTS استفاده می کنند
152
- tools=[genai.protos.Tool(
153
- google_search_retrieval=genai.protos.GoogleSearchRetrieval(),
154
- tool_code=genai.protos.ToolCode(
155
- function_call=genai.protos.FunctionCall(
156
- name="text-to-speech",
157
- args={"text": final_text, "voice_name": voice}
158
- )
159
- )
160
- )]
161
  )
162
 
163
- audio_part = response.candidates[0].content.parts[0]
164
- if audio_part.file_data:
165
  logging.info(f"[{request_id}] ✅ قطعه با موفقیت توسط کلید شماره {key_idx_display} تولید شد.")
166
- return audio_part.file_data
167
  else:
168
  logging.warning(f"[{request_id}] ⚠️ پاسخ API برای قطعه با کلید شماره {key_idx_display} بدون داده صوتی بود. تلاش با کلید بعدی...")
169
 
170
  except Exception as e:
171
  logging.error(f"[{request_id}] ❌ خطا در تولید قطعه با کلید شماره {key_idx_display}: {e}. تلاش با کلید بعدی...")
172
- # برخی خطاها نیاز به یک تاخیر کوتاه قبل از تلاش مجدد دارند
173
  time.sleep(2)
174
 
175
  logging.error(f"[{request_id}] ❌ تمام کلیدهای API امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
@@ -178,7 +154,6 @@ def generate_audio_chunk_with_retry(chunk_text, prompt_text, voice, temp, reques
178
  def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, request_id):
179
  logging.info(f"[{request_id}] 🚀 شروع فرآیند تولید صدا.")
180
 
181
- # [IMPROVEMENT] استفاده از شناسه منحصر به فرد برای نام‌گذاری فایل‌ها
182
  output_base_name = f"{DEFAULT_OUTPUT_FILENAME_BASE}_{request_id}"
183
  max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
184
 
@@ -190,27 +165,25 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
190
  generated_files = []
191
  final_audio_file = None
192
 
193
- # [IMPROVEMENT] استفاده از try/finally برای تضمین پاک‌سازی فایل‌های موقت
194
  try:
195
  for i, chunk in enumerate(text_chunks):
196
  logging.info(f"[{request_id}] 🔊 پردازش قطعه {i+1}/{len(text_chunks)}...")
197
 
198
- file_data = generate_audio_chunk_with_retry(chunk, prompt_input, selected_voice, temperature_val, request_id)
 
199
 
200
- if file_data:
201
- data_buffer = file_data.data
202
- ext = mimetypes.guess_extension(file_data.mime_type) or ".wav"
203
-
204
  fname_base = f"{output_base_name}_part{i+1:03d}"
205
- fpath = save_binary_file(f"{fname_base}{ext}", data_buffer)
206
  if fpath:
207
  generated_files.append(fpath)
208
  else:
209
- logging.error(f"[{request_id}] ❌ موفق به ذخیره فایل برای قطعه {i+1} نشدیم. این قطعه نادیده گرفته می‌شود.")
210
  continue
211
  else:
212
- logging.error(f"[{request_id}] 🛑 فرآیند متوقف شد زیرا تولید قطعه {i+1} با تمام کلیدهای موجود ناموفق بود.")
213
- # اگر یک قطعه شکست بخورد، ادامه نده
214
  raise Exception(f"Failed to generate chunk {i+1}")
215
 
216
  if i < len(text_chunks) - 1 and len(text_chunks) > 1:
@@ -224,41 +197,31 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
224
 
225
  if len(generated_files) > 1:
226
  if PYDUB_AVAILABLE:
227
- merged_fn = f"{final_output_path_base}.wav"
228
  if os.path.exists(merged_fn): os.remove(merged_fn)
229
  if merge_audio_files_func(generated_files, merged_fn, request_id):
230
  final_audio_file = merged_fn
231
  else:
232
  logging.warning(f"[{request_id}] ⚠️ ادغام ناموفق بود. فقط قطعه اول برگردانده می‌شود.")
233
- # اگر ادغام شکست خورد، به عنوان جایگزین، فایل اول را برگردان
234
  final_audio_file = generated_files[0]
235
  else:
236
  logging.warning(f"[{request_id}] ⚠️ pydub در دسترس نیست. اولین قطعه صوتی ارائه می‌شود.")
237
  final_audio_file = generated_files[0]
238
- else: # فقط یک فایل تولید شده است
239
  final_audio_file = generated_files[0]
240
 
241
- # [IMPROVEMENT] تغییر نام فایل نهایی برای سازگاری بهتر و پاک کردن شناسه
242
  if final_audio_file:
243
- target_ext = os.path.splitext(final_audio_file)[1]
244
- final_renamed_path = f"{DEFAULT_OUTPUT_FILENAME_BASE}_final_{request_id}{target_ext}"
245
- os.rename(final_audio_file, final_renamed_path)
246
- # فایل نهایی را در لیست فایل های تولید شده به روز می کنیم تا در finally حذف نشود
247
- generated_files.remove(final_audio_file)
248
- generated_files.append(final_renamed_path)
249
- logging.info(f"[{request_id}] ✅ فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_renamed_path)}")
250
- return final_renamed_path
251
 
252
- return None # اگر هیچ فایلی نهایی نشد
253
 
254
  except Exception as e:
255
  logging.error(f"[{request_id}] ❌ خطای کلی در حین پردازش: {e}")
256
  return None
257
  finally:
258
- # [IMPROVEMENT] پاک‌سازی تمام فایل‌های موقت به جز فایل نهایی که برگردانده شده
259
  logging.info(f"[{request_id}] 🧹 شروع پاک‌سازی فایل‌های موقت...")
260
  for fp in generated_files:
261
- # اگر fp همان فایل نهایی است که به کاربر ارسال می شود، آن را حذف نکن
262
  if final_audio_file and os.path.abspath(fp) == os.path.abspath(final_audio_file):
263
  continue
264
  try:
@@ -270,31 +233,31 @@ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_va
270
 
271
 
272
  def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
273
- # [IMPROVEMENT] تولید شناسه منحصر به فرد برای هر درخواست
274
  request_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
275
- logging.info(f"
276
 
277
-
278
- ✅ درخواست جدید با شناسه دریافت شد: {request_id}")
 
279
 
280
  actual_text = ""
281
  if use_file_input:
282
  if uploaded_file:
283
  try:
284
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
285
- if not actual_text: logging.error(f"[{request_id}] ❌ فایل آپلود شده خالی است یا خوانده نشد."); return None
286
  except Exception as e: logging.error(f"[{request_id}] ❌ خطا در خواندن فایل آپلود شده: {e}"); return None
287
- else: logging.warning(f"[{request_id}] ❌ گزینه استفاده از فایل انتخاب شده اما فایلی آپلود نشده."); return None
288
  else:
289
  actual_text = text_to_speak
290
- if not actual_text or not actual_text.strip(): logging.warning(f"[{request_id}] ❌ متن ورودی برای تبدیل خالی است."); return None
291
 
 
292
  final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, request_id)
293
 
294
  if final_path:
295
- logging.info(f"[{request_id}] ✅ فرآیند با موفقیت به پایان رسید. فایل صوتی برای کاربر ارسال می‌شود.")
296
  else:
297
- logging.error(f"[{request_id}] ❌ فرآیند ناموفق بود. هیچ فایلی برای کاربر ارسال نمی‌شود.")
298
 
299
  return final_path
300
 
@@ -305,12 +268,9 @@ def auto_restart_service():
305
  logging.info(f"زمان ری‌استارت خودکار فرا رسیده است. برنامه برای ری‌استارت خارج می‌شود...")
306
  os._exit(1)
307
 
308
- # --- CSS و Gradio UI (بدون تغییر) ---
309
  custom_css_inspired_by_image = f"""
310
  @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
311
- :root {{
312
- --app-font: 'Vazirmatn', sans-serif; --app-header-grad-start: #2980b9; --app-header-grad-end: #2ecc71; --app-panel-bg: #FFFFFF; --app-input-bg: #F7F7F7; --app-button-bg: #2979FF; --app-main-bg: linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%); --app-text-primary: #333; --app-text-secondary: #555; --app-border-color: #E0E0E0; --radius-card: 20px; --radius-input: 8px; --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1); --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);
313
- }}
314
  body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 16px; line-height: 1.65; }}
315
  .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
316
  .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); }}
@@ -326,14 +286,10 @@ footer {{display:none !important;}}
326
  .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; }}
327
  label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}
328
  .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; }}
329
- label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}
330
- label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}
331
- label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
332
- label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
333
- label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
334
  #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
335
  .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
336
  .app-footer-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.8; margin-top:3rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}
 
337
  """
338
 
339
  alpha_header_html_v3 = """
@@ -347,35 +303,43 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), c
347
  gr.HTML(alpha_header_html_v3)
348
 
349
  with gr.Column(elem_classes=["main-content-panel-alpha"]):
350
- use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
351
- uploaded_file_input = gr.File(label=" ", file_types=['.txt'], visible=False, elem_id="file_uploader_alpha_main_v3")
352
- text_to_speak_tb = gr.Textbox(label="متن فارسی برای تبدیل", placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_main_alpha_v3")
353
- use_file_input_cb.change(fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
 
 
 
 
 
 
 
354
 
355
- speech_prompt_tb = gr.Textbox(label="سبک گفتار (اختیاری)", placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_v3")
356
- speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3")
357
- temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا", elem_id="temperature_slider_alpha_v3")
358
- gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
 
359
 
360
  generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
361
- output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
362
-
363
  generate_button.click(
364
  fn=gradio_tts_interface,
365
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
366
- outputs=[output_audio]
367
  )
368
 
369
- gr.Markdown("<h3 class='section-title-main-alpha' style='margin-top:2.5rem; text-align:center; border-bottom:none;'>نمونه‌های کاربردی</h3>", elem_id="examples_section_title_v3")
370
  gr.Examples(
371
  examples=[
372
- [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
373
- [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبی��ی و روان.", "Charon", 0.9],
374
  ],
375
  inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
376
  outputs=[output_audio],
377
  fn=gradio_tts_interface,
378
- cache_examples=False
 
379
  )
380
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
381
 
@@ -383,7 +347,6 @@ if __name__ == "__main__":
383
  threading.Thread(target=auto_restart_service, daemon=True, name="AutoRestartThread").start()
384
 
385
  if len(ALL_API_KEYS) > 0 :
386
- # [IMPROVEMENT] افزایش تعداد تردها در صف برای مدیریت بهتر کاربران همزمان
387
  demo.queue(default_concurrency_limit=10).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
388
  else:
389
  logging.critical("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")
 
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
 
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
  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())
 
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:
 
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 هیچ کلیدی برنگرداند.")
124
  break
125
 
126
  logging.info(f"[{request_id}] ⚙️ تلاش برای تولید قطعه با کلید API شماره {key_idx_display} (...{selected_api_key[-4:]})")
127
 
128
  try:
 
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:
145
  logging.warning(f"[{request_id}] ⚠️ پاسخ API برای قطعه با کلید شماره {key_idx_display} بدون داده صوتی بود. تلاش با کلید بعدی...")
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 امتحان شدند اما هیچ‌کدام موفق به تولید قطعه نشدند.")
 
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
 
 
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} نشدیم.")
184
  continue
185
  else:
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:
 
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]
212
 
 
213
  if final_audio_file:
214
+ logging.info(f"[{request_id}] فایل صوتی نهایی با موفقیت تولید شد: {os.path.basename(final_audio_file)}")
215
+ return final_audio_file
 
 
 
 
 
 
216
 
217
+ return None
218
 
219
  except Exception as e:
220
  logging.error(f"[{request_id}] ❌ خطای کلی در حین پردازش: {e}")
221
  return None
222
  finally:
 
223
  logging.info(f"[{request_id}] 🧹 شروع پاک‌سازی فایل‌های موقت...")
224
  for fp in generated_files:
 
225
  if final_audio_file and os.path.abspath(fp) == os.path.abspath(final_audio_file):
226
  continue
227
  try:
 
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:
244
  if uploaded_file:
245
  try:
246
  with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
247
+ if not actual_text: logging.error(f"[{request_id}] ❌ فایل آپلود شده خالی است."); return None
248
  except Exception as e: logging.error(f"[{request_id}] ❌ خطا در خواندن فایل آپلود شده: {e}"); return None
249
+ else: logging.warning(f"[{request_id}] ❌ گزینه فایل انتخاب شده اما فایلی آپلود نشده."); return None
250
  else:
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
  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); }}
 
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 = """
 
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,
342
+ label="چند نمونه برای تست"
343
  )
344
  gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>")
345
 
 
347
  threading.Thread(target=auto_restart_service, daemon=True, name="AutoRestartThread").start()
348
 
349
  if len(ALL_API_KEYS) > 0 :
 
350
  demo.queue(default_concurrency_limit=10).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
351
  else:
352
  logging.critical("🔴 برنامه به دلیل عدم وجود کلید API جیمینای اجرا نشد. لطفاً Secrets را بررسی کنید.")