Hamed744 commited on
Commit
18059a6
·
verified ·
1 Parent(s): e2b4736

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +418 -298
app.py CHANGED
@@ -1,4 +1,3 @@
1
- import gradio as gr
2
  import base64
3
  import mimetypes
4
  import os
@@ -6,10 +5,10 @@ import re
6
  import struct
7
  import time
8
  import zipfile
9
- import traceback # For detailed error logging if needed
10
- from google import genai
11
- from google.genai import types as genai_types
12
 
 
 
13
  try:
14
  from pydub import AudioSegment
15
  PYDUB_AVAILABLE = True
@@ -17,360 +16,481 @@ except ImportError:
17
  PYDUB_AVAILABLE = False
18
  print("⚠️ pydub در دسترس نیست. فایل‌های صوتی به صورت جداگانه ذخیره می‌شوند.")
19
 
20
- # --- Constants ---
21
- SPEAKER_VOICES_LIST = [
22
- "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
23
- "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
24
- "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib",
25
- "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
26
- "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
27
- ]
28
- MODELS_LIST = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
29
-
30
- OUTPUT_DIR = "generated_audio"
31
- if not os.path.exists(OUTPUT_DIR):
32
- os.makedirs(OUTPUT_DIR)
33
-
34
- # --- Helper functions (unchanged) ---
35
- def log_message(msg, current_logs):
36
- print(msg)
37
- return f"{current_logs}\n{msg}".strip()
38
-
39
- def save_binary_file(file_name, data, log_func, current_logs):
40
- full_path = os.path.join(OUTPUT_DIR, file_name)
41
- try:
42
- with open(full_path, "wb") as f: f.write(data)
43
- current_logs = log_func(f"✅ فایل: {full_path}", current_logs)
44
- return full_path, current_logs
45
- except Exception as e:
46
- current_logs = log_func(f"❌ خطا ذخیره {file_name}: {e}", current_logs)
47
- return None, current_logs
48
 
49
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
50
  parameters = parse_audio_mime_type(mime_type)
51
- bits_per_sample, sample_rate = parameters["bits_per_sample"], parameters["rate"]
52
- num_channels, data_size = 1, len(audio_data)
 
 
53
  bytes_per_sample = bits_per_sample // 8
54
  block_align = num_channels * bytes_per_sample
55
  byte_rate = sample_rate * block_align
56
  chunk_size = 36 + data_size
57
- header = struct.pack("<4sI4s4sIHHIIHH4sI", b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels, sample_rate, byte_rate, block_align, bits_per_sample, b"data", data_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return header + audio_data
59
 
60
  def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
61
- bits_per_sample, rate = 16, 24000
62
- for param in mime_type.split(";"):
 
 
63
  param = param.strip()
64
  if param.lower().startswith("rate="):
65
- try: rate = int(param.split("=", 1)[1])
66
- except: pass
 
 
 
67
  elif param.startswith("audio/L"):
68
- try: bits_per_sample = int(param.split("L", 1)[1])
69
- except: pass
 
 
70
  return {"bits_per_sample": bits_per_sample, "rate": rate}
71
 
72
- def load_text_from_file(file_obj, log_func, current_logs):
73
- if file_obj is None: return "", log_func("❌ فایل آپلود نشد.", current_logs)
74
- file_path = file_obj.name
75
- current_logs = log_func(f"✅ فایل '{os.path.basename(file_path)}' دریافت.", current_logs)
76
  try:
77
- with open(file_path, 'r', encoding='utf-8') as f: content = f.read().strip()
78
- current_logs = log_func(f"📖 متن: {len(content)} کاراکتر. نمونه: '{content[:100]}{'...' if len(content) > 100 else ''}'", current_logs)
79
- return content, current_logs
80
- except Exception as e: return "", log_func(f"❌ خطا خواندن فایل: {e}", current_logs)
 
 
 
 
81
 
82
  def smart_text_split(text, max_size=3800):
83
- if len(text) <= max_size: return [text]
84
- chunks, current_chunk = [], ""
85
- sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
 
 
86
  for sentence in sentences:
87
  if len(current_chunk) + len(sentence) + 1 > max_size:
88
- if current_chunk: chunks.append(current_chunk.strip())
89
- if len(sentence) > max_size:
90
- words, temp_word_chunk = sentence.split(), ""
91
- for word in words:
92
- if len(temp_word_chunk) + len(word) + 1 > max_size:
93
- if temp_word_chunk: chunks.append(temp_word_chunk.strip())
94
- if len(word) > max_size:
95
- for i in range(0, len(word), max_size): chunks.append(word[i:i+max_size])
96
- temp_word_chunk = ""
97
- else: temp_word_chunk = word
98
- else: temp_word_chunk += (" " if temp_word_chunk else "") + word
99
- if temp_word_chunk: chunks.append(temp_word_chunk.strip())
100
- current_chunk = ""
101
- else: current_chunk = sentence
102
- else: current_chunk += (" " if current_chunk else "") + sentence
103
- if current_chunk: chunks.append(current_chunk.strip())
104
- return [c for c in chunks if c]
105
-
106
- def merge_audio_files_func(file_paths, output_filename, log_func, current_logs):
107
- if not PYDUB_AVAILABLE: return None, log_func("❌ pydub نیست.", current_logs)
108
- output_path = os.path.join(OUTPUT_DIR, output_filename)
 
 
 
 
 
 
 
 
 
109
  try:
110
- current_logs = log_func(f"🔗 ادغام {len(file_paths)} فایل...", current_logs)
 
 
111
  combined = AudioSegment.empty()
 
112
  for i, file_path in enumerate(file_paths):
113
- if os.path.exists(file_path):
114
- current_logs = log_func(f"📎 فایل {i+1}: {file_path}", current_logs)
115
  try:
 
 
116
  audio = AudioSegment.from_file(file_path)
117
  combined += audio
118
- if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=500)
119
- except Exception as e_pydub:
120
- current_logs = log_func(f"⚠️ خطا pydub {file_path}: {e_pydub}. رد شد.", current_logs)
121
- continue
122
- else: current_logs = log_func(f"⚠️ فایل نیست: {file_path}", current_logs)
123
- if not combined: return None, log_func("❌ فایل معتبری برای ادغام نبود.", current_logs)
 
 
 
 
 
 
 
124
  combined.export(output_path, format="wav")
125
- return output_path, log_func(f"✅ ادغام شد: {output_path}", current_logs)
126
- except Exception as e: return None, log_func(f"❌ خطا ادغام: {e}", current_logs)
 
 
 
 
 
127
 
128
- def create_zip_file(file_paths, zip_name_base, log_func, current_logs):
129
- zip_filename = os.path.join(OUTPUT_DIR, f"{zip_name_base}.zip")
 
 
 
130
  try:
131
- with zipfile.ZipFile(zip_filename, 'w') as zipf:
132
  for file_path in file_paths:
133
- if os.path.exists(file_path): zipf.write(file_path, os.path.basename(file_path))
134
- return zip_filename, log_func(f"📦 ZIP شد: {zip_filename}", current_logs)
135
- except Exception as e: return None, log_func(f" خطا ZIP: {e}", current_logs)
136
-
137
- # --- Main generation function ---
138
- def generate_audio_from_text_gradio(
139
- api_key_hf_secret, input_method, text_to_speak_ui, uploaded_file_ui,
140
- speech_prompt_ui, model_name_ui, speaker_voice_ui, temperature_ui,
141
- max_chunk_size_ui, sleep_between_requests_ui, output_filename_base_ui,
142
- merge_audio_files_ui, delete_partial_files_ui
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  ):
144
- logs = " شروع فرآیند..."
145
- if not api_key_hf_secret:
146
- return log_message("❌ کلید API جمینای در Secrets نیست.", logs), None, None, gr.update(visible=False)
147
 
148
- os.environ["GEMINI_API_KEY"] = api_key_hf_secret
149
- logs = log_message("🔑 کلید API از Secrets بارگذاری شد.", logs)
150
-
151
- client = None
152
- try:
153
- logs = log_message("🛠️ ایجاد کلاینت `genai.Client()`...", logs)
154
- client = genai.Client(api_key=api_key_hf_secret)
155
- logs = log_message("✅ کلاینت ایجاد شد.", logs)
156
- except Exception as e:
157
- return log_message(f"❌ خطا ایجاد کلاینت: {type(e).__name__} - {e}", logs), None, None, gr.update(visible=False)
158
 
159
- text_input_content = ""
160
- if input_method == "آپلود فایل":
161
- text_input_content, logs = load_text_from_file(uploaded_file_ui, log_message, logs)
162
- if not text_input_content: return logs, None, None, gr.update(visible=False)
 
 
 
 
 
 
 
163
  else:
164
- text_input_content = text_to_speak_ui
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- if not text_input_content or not text_input_content.strip():
167
- return log_message(" متن ورودی خالی است.", logs), None, None, gr.update(visible=False)
168
 
169
- text_chunks = smart_text_split(text_input_content, max_chunk_size_ui)
170
- logs = log_message(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.", logs)
171
- for i, chunk_text in enumerate(text_chunks):
172
- logs = log_message(f"📝 قطعه {i+1}: {len(chunk_text)} کاراکتر", logs)
 
 
 
 
 
 
 
 
173
 
174
  generated_files = []
175
- for i, chunk_for_api in enumerate(text_chunks):
176
- logs = log_message(f"\n🔊 تولید صدا قطعه {i+1}/{len(text_chunks)}...", logs)
 
 
177
 
178
- # REVERTING to adding speech_prompt to the text, as per Colab's presumed successful logic
179
- # Using a simple concatenation. The Colab might have had a more specific format.
180
- # If speech_prompt_ui is "شاد و پر انرژی" and chunk_for_api is "سلام دنیا"
181
- # final_text_for_api will be "شاد و پر انرژی\nسلام دنیا"
182
- if speech_prompt_ui and speech_prompt_ui.strip():
183
- final_text_for_api = f"{speech_prompt_ui.strip()}\n{chunk_for_api}"
184
- logs = log_message(f"ℹ️ پرامپت سبک '{speech_prompt_ui.strip()}' به متن اضافه شد.", logs)
185
- else:
186
- final_text_for_api = chunk_for_api
187
 
188
- api_contents = [
189
- genai_types.Content(
190
- role="user",
191
- parts=[genai_types.Part.from_text(text=final_text_for_api)],
 
 
 
 
192
  ),
193
- ]
194
-
195
- genai_speech_config = genai_types.SpeechConfig(
196
- voice_config=genai_types.VoiceConfig(
197
- prebuilt_voice_config=genai_types.PrebuiltVoiceConfig(voice_name=speaker_voice_ui)
198
- )
199
  )
200
 
201
- stream_generation_config = genai_types.GenerateContentConfig(
202
- temperature=temperature_ui,
203
- response_modalities=["audio"],
204
- speech_config=genai_speech_config
205
- )
206
 
207
  try:
208
- if not hasattr(client, 'models') or not hasattr(client.models, 'generate_content_stream'): # type: ignore
209
- logs = log_message(f"❌ کلاینت (`{type(client)}`) متد `models.generate_content_stream` ندارد.", logs)
210
- continue
211
-
212
- stream_iterator = client.models.generate_content_stream( # type: ignore
213
- model=model_name_ui, contents=api_contents, config=stream_generation_config,
214
  )
 
 
 
215
 
216
- chunk_filename_base = f"{output_filename_base_ui}_part_{i+1:03d}"
217
- audio_data_buffer, mime_type_from_api = b"", "audio/wav"
218
-
219
- for chunk_response in stream_iterator:
220
- if (chunk_response.candidates and chunk_response.candidates[0].content and
221
- chunk_response.candidates[0].content.parts and
222
- chunk_response.candidates[0].content.parts[0].inline_data):
223
  inline_data = chunk_response.candidates[0].content.parts[0].inline_data
224
- audio_data_buffer += inline_data.data
225
- mime_type_from_api = inline_data.mime_type
226
  elif chunk_response.text:
227
- log_text = f"💬 پیام API قطعه {i+1}: {chunk_response.text}"
228
- # Check if it's an error that might indicate the prompt was misunderstood or caused an issue
229
- if "error" in chunk_response.text.lower() or "failed" in chunk_response.text.lower() or "invalid input" in chunk_response.text.lower():
230
- logs = log_message(f"❌ {log_text} (ممکن است به دلیل پرامپت سبک باشد)", logs)
231
- else:
232
- logs = log_message(f"ℹ️ {log_text}", logs)
233
-
234
-
235
- if audio_data_buffer:
236
- file_extension = mimetypes.guess_extension(mime_type_from_api)
237
- final_audio_data = audio_data_buffer
238
- if file_extension is None or file_extension.lower() not in ['.wav', '.mp3', '.ogg', '.aac']:
239
- if "audio/L" in mime_type_from_api or "audio/raw" in mime_type_from_api:
240
- logs = log_message(f"ℹ️ Mime: {mime_type_from_api}. تبدیل به WAV...", logs)
241
- final_audio_data = convert_to_wav(audio_data_buffer, mime_type_from_api)
242
- file_extension = ".wav"
243
- else:
244
- logs = log_message(f"ℹ️ Mime ناشناخته: {mime_type_from_api}. ذخیره .bin.", logs)
245
- file_extension = ".bin"
246
- if mime_type_from_api == "audio/wav" and (file_extension != ".wav" and file_extension != ".wave"): file_extension = ".wav"
247
- elif mime_type_from_api == "audio/mpeg" and file_extension != ".mp3": file_extension = ".mp3"
248
- elif mime_type_from_api == "audio/ogg" and file_extension != ".ogg": file_extension = ".ogg"
249
- if file_extension is None: file_extension = ".audio"
250
-
251
- saved_file_path, logs = save_binary_file(f"{chunk_filename_base}{file_extension}", final_audio_data, log_message, logs)
252
- if saved_file_path:
253
- generated_files.append(saved_file_path)
254
- logs = log_message(f"✅ قطعه {i+1} تولید شد: {saved_file_path}", logs)
255
  else:
256
- if not f"❌ پیام API قطعه {i+1}" in logs:
257
- logs = log_message(f"❌ قطعه {i+1} بدون داده صوتی.", logs)
258
-
259
  except Exception as e:
260
- error_msg = f"❌ خطا تولید قطعه {i+1}: {type(e).__name__} - {e}"
261
- # Check if the error message from API (if any in e.args) mentions input format or similar
262
- if hasattr(e, 'args') and e.args and isinstance(e.args[0], str) and ("input" in e.args[0].lower() or "parse" in e.args[0].lower()):
263
- error_msg += "\n (ممکن است خطا به دلیل فرمت پرامپت سبک الحاق شده به متن باشد)"
264
-
265
- if "API_KEY_INVALID" in str(e): error_msg += "\n🔑 کلید API نامعتبر."
266
- elif "permission" in str(e).lower() or "403" in str(e): error_msg += f"\n🚫 عدم دسترسی به {model_name_ui}."
267
- elif "429" in str(e) or "quota" in str(e).lower(): error_msg += f"\n🐢 محدودیت Quota."
268
- elif "DeadlineExceeded" in str(e) or "504" in str(e): error_msg += f"\n⏱️ Timeout."
269
- logs = log_message(error_msg, logs)
270
- # logs = log_message(traceback.format_exc(), logs) # DEBUG
271
- continue
272
-
273
- if i < len(text_chunks) - 1 and sleep_between_requests_ui > 0:
274
- logs = log_message(f"⏱️ انتظار {sleep_between_requests_ui} ثانیه...", logs)
275
- time.sleep(sleep_between_requests_ui)
276
 
 
 
 
 
277
  if not generated_files:
278
- return log_message("❌ هیچ فایل صوتی تولید نشد!", logs), None, None, gr.update(visible=False)
 
279
 
280
- logs = log_message(f"\n🎉 {len(generated_files)} فایل صوتی تولید شد!", logs)
281
- final_audio_path, zip_file_path, zip_visible = None, None, False
282
 
283
- if merge_audio_files_ui and len(generated_files) > 1:
284
- if not PYDUB_AVAILABLE:
285
- logs = log_message("⚠️ pydub نیست. ارائه ZIP.", logs)
286
- zip_file_path, logs = create_zip_file(generated_files, f"{output_filename_base_ui}_all", log_message, logs)
287
- if zip_file_path: zip_visible = True
288
- if generated_files: final_audio_path = generated_files[0]
 
 
 
 
 
 
 
 
289
  else:
290
- final_audio_path, logs = merge_audio_files_func(generated_files, f"{output_filename_base_ui}_merged.wav", log_message, logs)
291
- if final_audio_path:
292
- logs = log_message(f"🎵 ادغام شده: {final_audio_path}", logs)
293
- if delete_partial_files_ui:
294
- for fp_del in generated_files:
295
- if fp_del != final_audio_path:
296
- try: os.remove(fp_del); logs = log_message(f"🗑️ حذف: {fp_del}", logs)
297
- except Exception as e_del: logs = log_message(f"⚠️ خطا حذف {fp_del}: {e_del}", logs)
298
- else:
299
- logs = log_message("⚠️ ادغام ناموفق. ارائه ZIP.", logs)
300
- zip_file_path, logs = create_zip_file(generated_files, f"{output_filename_base_ui}_all", log_message, logs)
301
- if zip_file_path: zip_visible = True
302
- if generated_files: final_audio_path = generated_files[0]
303
  elif len(generated_files) == 1:
304
- final_audio_path = generated_files[0]
305
- logs = log_message(f"🎵 فایل نهایی: {final_audio_path}", logs)
306
- elif len(generated_files) > 1: # Not merging
307
- zip_file_path, logs = create_zip_file(generated_files, f"{output_filename_base_ui}_all", log_message, logs)
308
- if zip_file_path: zip_visible = True
309
- if generated_files: final_audio_path = generated_files[0]
310
-
311
- if not final_audio_path and not zip_file_path:
312
- return log_message("🛑 خروجی صوتی نیست.", logs), None, None, gr.update(visible=False)
313
- return logs, final_audio_path, zip_file_path, gr.update(visible=zip_visible)
314
-
315
- # --- Gradio UI (unchanged) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  css = """
317
- body { direction: rtl; }
318
- .rtl_override { direction: rtl !important; text-align: right !important; }
319
- .gr-input, .gr-output, .gr-radio label span { text-align: right !important; direction: rtl !important;}
320
- .gr-checkbox label span { text-align: right !important; direction: rtl !important; margin-right: 0.5em;}
321
- footer { display: none !important; }
322
  .gradio-container { max-width: 800px !important; margin: auto !important; }
 
 
 
323
  """
324
- API_KEY_FROM_ENV = os.environ.get("GEMINI_API_KEY")
325
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange"), css=css) as demo:
326
- gr.Markdown(
327
- """
328
- <div style='text-align: center; font-family: "Arial", sans-serif;'>
329
- <h1 class='rtl_override'>تبدیل متن به گفتار با Gemini API</h1>
330
- <p class='rtl_override'>توجه: تاثیر "پرامپت سبک گفتار" به نحوه تفسیر مدل بستگی دارد.</p>
331
- </div>
332
- """
333
- )
334
- api_key_status_text = "⚠️ کلید API جمینای (GEMINI_API_KEY) در Secrets این اسپیس تنظیم نشده است."
335
- if API_KEY_FROM_ENV: api_key_status_text = "✅ کلید API جمینای از Secrets بارگذاری شد."
336
- gr.Markdown(f"<p style='text-align:center; color: {'green' if API_KEY_FROM_ENV else 'red'};' class='rtl_override'>{api_key_status_text}</p>")
 
 
337
  with gr.Row():
338
  with gr.Column(scale=2):
339
- gr.Markdown("<h3 class='rtl_override'>تنظیمات ورودی</h3>", elem_classes="rtl_override")
340
- input_method_radio = gr.Radio(["ورودی متنی", "آپلود فایل"], label="روش ورودی", value="ورودی متنی", elem_classes="rtl_override")
341
- text_to_speak_area = gr.Textbox(label="متن مورد نظر", placeholder="متن خود را اینجا وارد کنید...", lines=5, visible=True, elem_classes="rtl_override")
342
- uploaded_file_input = gr.File(label="فایل متنی (.txt)", file_types=[".txt"], visible=False, elem_classes="rtl_override") # type: ignore
343
- speech_prompt_area = gr.Textbox(label="پرامپت سبک گفتار (اختیاری)", placeholder="مثال: شاد و پر انرژی", lines=2, elem_classes="rtl_override")
344
- gr.Markdown("<h3 class='rtl_override'>تنظیمات مدل و خروجی</h3>", elem_classes="rtl_override")
345
- model_name_dropdown = gr.Dropdown(MODELS_LIST, label="مدل", value=MODELS_LIST[0], elem_classes="rtl_override")
346
- speaker_voice_dropdown = gr.Dropdown(SPEAKER_VOICES_LIST, label="گوینده", value="Charon", elem_classes="rtl_override")
347
- temperature_slider = gr.Slider(minimum=0, maximum=2, step=0.05, value=1.0, label="دما", elem_classes="rtl_override")
348
- output_filename_base_input = gr.Textbox(value="gemini_tts_output", label="نام پایه فایل خروجی", elem_classes="rtl_override")
 
 
 
 
 
 
 
 
 
 
 
 
349
  with gr.Column(scale=1):
350
- gr.Markdown("<h3 class='rtl_override'>تنظیمات پیشرفته</h3>", elem_classes="rtl_override")
351
- max_chunk_size_slider = gr.Slider(minimum=2000, maximum=4000, step=100, value=3800, label="حداکثر کاراکتر در قطعه", elem_classes="rtl_override")
352
- sleep_between_requests_slider = gr.Slider(minimum=0, maximum=20, step=0.5, value=14, label="فاصله بین درخواست‌ها (ثانیه)", info="برای جلوگیری از Rate Limit (0 برای بدون تاخیر).", elem_classes="rtl_override")
353
- merge_audio_files_checkbox = gr.Checkbox(value=True, label="ادغام فایل‌های صوتی", elem_classes="rtl_override")
354
- pydub_warn_lbl = " (pydub نیست!)" if not PYDUB_AVAILABLE else ""
355
- del_partial_lbl = f"حذف فایل‌های جزئی{pydub_warn_lbl}"
356
- delete_partial_files_checkbox = gr.Checkbox(value=False, label=del_partial_lbl, interactive=PYDUB_AVAILABLE, elem_classes="rtl_override")
357
- submit_button = gr.Button("🎤 تولید صدا", variant="primary", elem_id="submit_button_custom")
358
- gr.Markdown("<h3 class='rtl_override'>خروجی</h3>", elem_classes="rtl_override")
359
- status_output_area = gr.Textbox(label="پیام‌های وضعیت", lines=10, interactive=False, elem_classes="rtl_override")
360
- with gr.Row():
361
- audio_player_output = gr.Audio(label="فایل صوتی نهایی/اولین قطعه", type="filepath", elem_classes="rtl_override") # type: ignore
362
- zip_file_output = gr.File(label="دانلود همه قطعات (ZIP)", type="filepath", visible=False, elem_classes="rtl_override") # type: ignore
363
- def toggle_input_method_visibility(method): return (gr.update(visible=True), gr.update(visible=False)) if method == "ورودی متنی" else (gr.update(visible=False), gr.update(visible=True))
364
- input_method_radio.change(fn=toggle_input_method_visibility, inputs=input_method_radio, outputs=[text_to_speak_area, uploaded_file_input])
365
- def update_delete_partials_interactive(merge_checked): return gr.update(interactive=merge_checked and PYDUB_AVAILABLE)
366
- merge_audio_files_checkbox.change(fn=update_delete_partials_interactive, inputs=merge_audio_files_checkbox, outputs=delete_partial_files_checkbox)
367
- def trigger_generation_with_api_key(*args_from_ui):
368
- hf_secret_key = os.environ.get("GEMINI_API_KEY")
369
- return generate_audio_from_text_gradio(hf_secret_key, *args_from_ui)
370
- submit_inputs = [input_method_radio, text_to_speak_area, uploaded_file_input, speech_prompt_area, model_name_dropdown, speaker_voice_dropdown, temperature_slider, max_chunk_size_slider, sleep_between_requests_slider, output_filename_base_input, merge_audio_files_checkbox, delete_partial_files_checkbox]
371
- submit_outputs = [status_output_area, audio_player_output, zip_file_output, zip_file_output]
372
- submit_button.click(fn=trigger_generation_with_api_key, inputs=submit_inputs, outputs=submit_outputs)
373
- def initial_delete_partials_state(merge_init): return gr.update(interactive=PYDUB_AVAILABLE and merge_init)
374
- demo.load(fn=initial_delete_partials_state, inputs=[merge_audio_files_checkbox], outputs=delete_partial_files_checkbox)
375
-
376
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import base64
2
  import mimetypes
3
  import os
 
5
  import struct
6
  import time
7
  import zipfile
8
+ import shutil # برای حذف فایل‌ها و پوشه‌ها
 
 
9
 
10
+ from google import genai
11
+ from google.genai import types
12
  try:
13
  from pydub import AudioSegment
14
  PYDUB_AVAILABLE = True
 
16
  PYDUB_AVAILABLE = False
17
  print("⚠️ pydub در دسترس نیست. فایل‌های صوتی به صورت جداگانه ذخیره می‌شوند.")
18
 
19
+ import gradio as gr
20
+
21
+ # --- Helper functions (mostly from your Colab notebook) ---
22
+
23
+ def save_binary_file(file_name, data):
24
+ # Ensure output directory exists
25
+ output_dir = "outputs"
26
+ os.makedirs(output_dir, exist_ok=True)
27
+ full_file_path = os.path.join(output_dir, file_name)
28
+ with open(full_file_path, "wb") as f:
29
+ f.write(data)
30
+ print(f"✅ فایل در مسیر زیر ذخیره شد: {full_file_path}")
31
+ return full_file_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
34
  parameters = parse_audio_mime_type(mime_type)
35
+ bits_per_sample = parameters["bits_per_sample"]
36
+ sample_rate = parameters["rate"]
37
+ num_channels = 1
38
+ data_size = len(audio_data)
39
  bytes_per_sample = bits_per_sample // 8
40
  block_align = num_channels * bytes_per_sample
41
  byte_rate = sample_rate * block_align
42
  chunk_size = 36 + data_size
43
+
44
+ header = struct.pack(
45
+ "<4sI4s4sIHHIIHH4sI",
46
+ b"RIFF",
47
+ chunk_size,
48
+ b"WAVE",
49
+ b"fmt ",
50
+ 16,
51
+ 1,
52
+ num_channels,
53
+ sample_rate,
54
+ byte_rate,
55
+ block_align,
56
+ bits_per_sample,
57
+ b"data",
58
+ data_size
59
+ )
60
  return header + audio_data
61
 
62
  def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
63
+ bits_per_sample = 16
64
+ rate = 24000
65
+ parts = mime_type.split(";")
66
+ for param in parts:
67
  param = param.strip()
68
  if param.lower().startswith("rate="):
69
+ try:
70
+ rate_str = param.split("=", 1)[1]
71
+ rate = int(rate_str)
72
+ except (ValueError, IndexError):
73
+ pass
74
  elif param.startswith("audio/L"):
75
+ try:
76
+ bits_per_sample = int(param.split("L", 1)[1])
77
+ except (ValueError, IndexError):
78
+ pass
79
  return {"bits_per_sample": bits_per_sample, "rate": rate}
80
 
81
+ def load_text_from_gradio_file(file_obj):
82
+ if file_obj is None:
83
+ return ""
 
84
  try:
85
+ with open(file_obj.name, 'r', encoding='utf-8') as f:
86
+ content = f.read().strip()
87
+ print(f"📖 متن بارگذاری شده: {len(content)} کاراکتر")
88
+ print(f"📝 نمونه متن: '{content[:100]}{'...' if len(content) > 100 else ''}'")
89
+ return content
90
+ except Exception as e:
91
+ print(f"❌ خطا در خواندن فایل: {e}")
92
+ return ""
93
 
94
  def smart_text_split(text, max_size=3800):
95
+ if len(text) <= max_size:
96
+ return [text]
97
+ chunks = []
98
+ current_chunk = ""
99
+ sentences = re.split(r'(?<=[.!?])\s+', text)
100
  for sentence in sentences:
101
  if len(current_chunk) + len(sentence) + 1 > max_size:
102
+ if current_chunk:
103
+ chunks.append(current_chunk.strip())
104
+ current_chunk = sentence
105
+ # Handle very long sentences by splitting words/characters
106
+ while len(current_chunk) > max_size:
107
+ # Find a good split point (e.g., space) within max_size
108
+ split_at = current_chunk[:max_size].rfind(' ')
109
+ if split_at == -1 or split_at < max_size // 2 : # if no space or space is too early, force split
110
+ split_at = max_size
111
+ chunks.append(current_chunk[:split_at].strip())
112
+ current_chunk = current_chunk[split_at:].strip()
113
+ else:
114
+ current_chunk += (" " if current_chunk else "") + sentence
115
+ if current_chunk:
116
+ chunks.append(current_chunk.strip())
117
+ return chunks
118
+
119
+
120
+ def merge_audio_files_func(file_paths, output_path):
121
+ if not PYDUB_AVAILABLE:
122
+ print("❌ pydub در دسترس نیست. نمی‌توان فایل‌ها را ادغام کرد.")
123
+ return None, "خطا: کتابخانه pydub برای ادغام فایل‌ها در دسترس نیست."
124
+ if not file_paths:
125
+ return None, "خطا: هیچ فایلی برای ادغام وجود ندارد."
126
+
127
+ # Ensure output directory exists
128
+ output_dir = os.path.dirname(output_path)
129
+ if output_dir: # If output_path includes a directory
130
+ os.makedirs(output_dir, exist_ok=True)
131
+
132
  try:
133
+ print(f"🔗 در حال ادغام {len(file_paths)} فایل صوتی...")
134
+ log_messages = [f"🔗 در حال ادغام {len(file_paths)} فایل صوتی..."]
135
+
136
  combined = AudioSegment.empty()
137
+ valid_files_merged = 0
138
  for i, file_path in enumerate(file_paths):
139
+ if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
 
140
  try:
141
+ print(f"📎 اضافه کردن فایل {i+1}: {file_path}")
142
+ log_messages.append(f"📎 اضافه کردن فایل {i+1}: {os.path.basename(file_path)}")
143
  audio = AudioSegment.from_file(file_path)
144
  combined += audio
145
+ if i < len(file_paths) - 1: # Add silence between segments, except for the last one
146
+ combined += AudioSegment.silent(duration=200) # 200ms silence
147
+ valid_files_merged += 1
148
+ except Exception as e:
149
+ print(f"⚠️ خطا در خواندن یا اضافه کردن فایل {file_path}: {e}")
150
+ log_messages.append(f"⚠️ خطا در خواندن یا اضافه کردن فایل {os.path.basename(file_path)}: {e}")
151
+ else:
152
+ print(f"⚠️ فایل پیدا نشد یا خالی است: {file_path}")
153
+ log_messages.append(f"⚠️ فایل پیدا نشد یا خالی است: {os.path.basename(file_path)}")
154
+
155
+ if valid_files_merged == 0:
156
+ return None, "\n".join(log_messages) + "\n❌ هیچ فایل معتبری برای ادغام پیدا نشد."
157
+
158
  combined.export(output_path, format="wav")
159
+ print(f"✅ فایل ادغام شده ذخیره شد: {output_path}")
160
+ log_messages.append(f"✅ فایل ادغام شده ذخیره شد: {os.path.basename(output_path)}")
161
+ return output_path, "\n".join(log_messages)
162
+ except Exception as e:
163
+ print(f"❌ خطا در ادغام فایل‌ها: {e}")
164
+ log_messages.append(f"❌ خطا در ادغام فایل‌ها: {e}")
165
+ return None, "\n".join(log_messages)
166
 
167
+ def create_zip_file(file_paths, zip_name_base):
168
+ output_dir = "outputs"
169
+ os.makedirs(output_dir, exist_ok=True)
170
+ zip_path = os.path.join(output_dir, f"{zip_name_base}.zip")
171
+ log_messages = []
172
  try:
173
+ with zipfile.ZipFile(zip_path, 'w') as zipf:
174
  for file_path in file_paths:
175
+ if os.path.exists(file_path):
176
+ zipf.write(file_path, os.path.basename(file_path))
177
+ log_messages.append(f"📦 فایل ZIP ایجاد شد: {os.path.basename(zip_path)}")
178
+ return zip_path, "\n".join(log_messages)
179
+ except Exception as e:
180
+ log_messages.append(f"❌ خطا در ایجاد فایل ZIP: {e}")
181
+ return None, "\n".join(log_messages)
182
+
183
+ def cleanup_temp_files(files_to_delete):
184
+ if not files_to_delete:
185
+ return
186
+ print("🧹 در حال پاکسازی فایل‌های موقت...")
187
+ for f_path in files_to_delete:
188
+ if f_path and os.path.exists(f_path):
189
+ try:
190
+ os.remove(f_path)
191
+ print(f"🗑️ فایل موقت حذف شد: {f_path}")
192
+ except Exception as e:
193
+ print(f"⚠️ خطا در حذف فایل {f_path}: {e}")
194
+ # Clean up the 'outputs' directory content as well, but not the directory itself
195
+ output_dir_content = "outputs"
196
+ if os.path.exists(output_dir_content):
197
+ for item in os.listdir(output_dir_content):
198
+ item_path = os.path.join(output_dir_content, item)
199
+ try:
200
+ if os.path.isfile(item_path) or os.path.islink(item_path):
201
+ os.unlink(item_path)
202
+ elif os.path.isdir(item_path):
203
+ shutil.rmtree(item_path) # Danger: be careful with rmtree
204
+ print(f"🗑️ آیتم حذف شده از outputs: {item_path}")
205
+ except Exception as e:
206
+ print(f"⚠️ خطا در حذف {item_path} از outputs: {e}")
207
+
208
+
209
+ # --- Main generation function for Gradio ---
210
+ def generate_audio_gradio(
211
+ use_file_input, text_file_obj, text_to_speak, speech_prompt,
212
+ selected_voice, output_filename_base, model_name, temperature,
213
+ max_chunk_size, sleep_between_requests, merge_audio_files, delete_partial_files,
214
+ progress=gr.Progress(track_tqdm=True)
215
  ):
216
+ log_messages = ["🚀 شروع فرآیند تبدیل متن به گفتار...\n"]
 
 
217
 
218
+ # Cleanup previous run files from 'outputs' if any, except for permanent ones.
219
+ # It's better to generate files in a unique temp dir per request or clean up specifically.
220
+ # For simplicity here, we'll clean up 'outputs' at the start of each run.
221
+ # Ensure 'outputs' directory exists for this run
222
+ os.makedirs("outputs", exist_ok=True)
223
+
224
+ # It's safer to delete specific files from previous runs rather than wiping the whole dir
225
+ # For now, let's skip aggressive auto-deletion of 'outputs' and rely on specific file deletion.
 
 
226
 
227
+ text_input = ""
228
+ if use_file_input:
229
+ log_messages.append("📁 حالت فایل فعال است. در حال خواندن فایل...")
230
+ if text_file_obj is None:
231
+ log_messages.append("❌ خطا: هیچ فایلی آپلود نشده است.")
232
+ return None, None, "\n".join(log_messages)
233
+ text_input = load_text_from_gradio_file(text_file_obj)
234
+ if not text_input:
235
+ log_messages.append("❌ خطا: متن استخراج شده از فایل خالی است.")
236
+ return None, None, "\n".join(log_messages)
237
+ log_messages.append("✅ متن از فایل با موفقیت بارگذاری شد.")
238
  else:
239
+ log_messages.append("⌨️ حالت ورودی دستی فعال است.")
240
+ text_input = text_to_speak
241
+
242
+ if not text_input or text_input.strip() == "":
243
+ log_messages.append("❌ خطا: متن ورودی برای تبدیل به گفتار خالی است.")
244
+ return None, None, "\n".join(log_messages)
245
+
246
+ api_key = os.environ.get("GEMINI_API_KEY")
247
+ if not api_key:
248
+ log_messages.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) پیدا نشد.")
249
+ log_messages.append("لطفاً کلید API خود را در بخش Secrets این Space تنظیم کنید.")
250
+ return None, None, "\n".join(log_messages)
251
 
252
+ # os.environ["GEMINI_API_KEY"] = api_key # Not needed if genai client picks it up directly
253
+ log_messages.append("🔑 کلید API از Hugging Face Secrets بارگذاری شد.")
254
 
255
+ try:
256
+ log_messages.append("🛠️ در حال ایجاد کلاینت جمینای...")
257
+ client = genai.Client(api_key=api_key)
258
+ log_messages.append(" کلاینت جمینای با موفقیت ایجاد شد.")
259
+ except Exception as e:
260
+ log_messages.append(f"❌ خطا در ایجاد کلاینت جمینای: {e}")
261
+ return None, None, "\n".join(log_messages)
262
+
263
+ text_chunks = smart_text_split(text_input, int(max_chunk_size))
264
+ log_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
265
+ for i, chunk in enumerate(text_chunks):
266
+ log_messages.append(f"📝 قطعه {i+1}: {len(chunk)} کاراکتر")
267
 
268
  generated_files = []
269
+ total_chunks = len(text_chunks)
270
+
271
+ for i, chunk in enumerate(progress.tqdm(text_chunks, desc="تولید قطعات صوتی")):
272
+ current_log = [f"\n🔊 تولید صدا برای قطعه {i+1}/{total_chunks}..."]
273
 
274
+ final_text_for_api = f'"{speech_prompt}"\n{chunk}' if speech_prompt and speech_prompt.strip() else chunk
 
 
 
 
 
 
 
 
275
 
276
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
277
+ generate_content_config = types.GenerateContentConfig(
278
+ temperature=float(temperature),
279
+ response_modalities=["audio"],
280
+ speech_config=types.SpeechConfig(
281
+ voice_config=types.VoiceConfig(
282
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)
283
+ )
284
  ),
 
 
 
 
 
 
285
  )
286
 
287
+ chunk_filename_base = f"{output_filename_base}_part_{i+1:03d}"
 
 
 
 
288
 
289
  try:
290
+ response_stream = client.models.generate_content_stream(
291
+ model=model_name,
292
+ contents=contents,
293
+ config=generate_content_config,
 
 
294
  )
295
+
296
+ audio_data_buffer = b""
297
+ final_mime_type = None
298
 
299
+ for chunk_response in response_stream:
300
+ if (
301
+ chunk_response.candidates and chunk_response.candidates[0].content and
302
+ chunk_response.candidates[0].content.parts and
303
+ chunk_response.candidates[0].content.parts[0].inline_data
304
+ ):
 
305
  inline_data = chunk_response.candidates[0].content.parts[0].inline_data
306
+ audio_data_buffer += inline_data.data # Accumulate data if streamed in parts
307
+ final_mime_type = inline_data.mime_type # Mime type should be consistent
308
  elif chunk_response.text:
309
+ current_log.append(f"ℹ️ پیام متنی از API: {chunk_response.text}")
310
+
311
+ if audio_data_buffer and final_mime_type:
312
+ file_extension = mimetypes.guess_extension(final_mime_type)
313
+ if file_extension is None or file_extension == ".bin": # .bin is often default for unknown binary
314
+ file_extension = ".wav" # Force wav if unknown
315
+ processed_data_buffer = convert_to_wav(audio_data_buffer, final_mime_type)
316
+ else:
317
+ processed_data_buffer = audio_data_buffer
318
+
319
+ generated_file_path = save_binary_file(f"{chunk_filename_base}{file_extension}", processed_data_buffer)
320
+ generated_files.append(generated_file_path)
321
+ current_log.append(f" قطعه {i+1} تولید شد: {os.path.basename(generated_file_path)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  else:
323
+ current_log.append(f"❌ داده صوتی برای قطعه {i+1} دریافت نشد.")
324
+
325
+
326
  except Exception as e:
327
+ current_log.append(f"❌ خطا در تولید قطعه {i+1}: {e}")
328
+ # Attempt to get more details from the error if it's a GenAI specific one
329
+ if hasattr(e, 'message'):
330
+ current_log.append(f" جزئیات خطا: {e.message}")
331
+ if "API key not valid" in str(e):
332
+ current_log.append(" 🔑 به نظر می‌رسد کلید API نامعتبر است. لطفاً آن را در بخش Secrets بررسی کنید.")
333
+
334
+ log_messages.extend(current_log)
 
 
 
 
 
 
 
 
335
 
336
+ if i < total_chunks - 1:
337
+ log_messages.append(f"⏱️ انتظار {sleep_between_requests} ثانیه...")
338
+ time.sleep(float(sleep_between_requests))
339
+
340
  if not generated_files:
341
+ log_messages.append("\n❌ هیچ فایل صوتی تولید نشد!")
342
+ return None, None, "\n".join(log_messages)
343
 
344
+ log_messages.append(f"\n🎉 {len(generated_files)} فایل صوتی با موفقیت تولید شد!")
 
345
 
346
+ final_audio_output_path = None
347
+ zip_file_output_path = None
348
+
349
+ if merge_audio_files and len(generated_files) > 1:
350
+ merged_filename_path = os.path.join("outputs", f"{output_filename_base}_merged.wav")
351
+ merged_path, merge_log = merge_audio_files_func(generated_files, merged_filename_path)
352
+ log_messages.append(merge_log)
353
+ if merged_path:
354
+ final_audio_output_path = merged_path
355
+ log_messages.append(f"🎵 فایل نهایی ادغام شده: {os.path.basename(final_audio_output_path)}")
356
+ if delete_partial_files:
357
+ log_messages.append("🗑️ حذف فایل‌های جزئی فعال است...")
358
+ files_to_remove = [f for f in generated_files if f != final_audio_output_path]
359
+ cleanup_temp_files(files_to_remove)
360
  else:
361
+ log_messages.append("⚠️ ادغام ممکن نبود یا موفقیت آمیز نبود. فایل‌های جداگانه در صورت وجود حفظ شدند.")
362
+ # If merge fails, offer zip of parts
363
+ zip_path, zip_log = create_zip_file(generated_files, f"{output_filename_base}_all_parts")
364
+ log_messages.append(zip_log)
365
+ zip_file_output_path = zip_path
366
+
 
 
 
 
 
 
 
367
  elif len(generated_files) == 1:
368
+ final_audio_output_path = generated_files[0]
369
+ log_messages.append(f"🎵 فقط یک فایل تولید شد: {os.path.basename(final_audio_output_path)}")
370
+
371
+ else: # Multiple files but merge_audio_files is False
372
+ log_messages.append("📦 چون ادغام فایل‌ها انتخاب نشده، فایل‌ها به صورت ZIP ارائه می‌شوند.")
373
+ zip_path, zip_log = create_zip_file(generated_files, f"{output_filename_base}_all_parts")
374
+ log_messages.append(zip_log)
375
+ zip_file_output_path = zip_path
376
+ # Optionally, provide the first audio file for direct listening if no zip
377
+ if not zip_file_output_path and generated_files:
378
+ final_audio_output_path = generated_files[0]
379
+
380
+
381
+ # Determine what to return for audio and file download components
382
+ # Priority: Merged Audio > First Chunk Audio (if no merge/zip)
383
+ # Zip file is always offered if multiple parts exist and not merged, or if merge fails
384
+
385
+ primary_audio_to_play = final_audio_output_path
386
+ downloadable_file = None
387
+
388
+ if final_audio_output_path: # Merged or single file
389
+ downloadable_file = final_audio_output_path
390
+ elif zip_file_output_path: # Zip of multiple parts
391
+ downloadable_file = zip_file_output_path
392
+ # If we have a zip, maybe don't auto-play anything or play the first part?
393
+ # For now, let's not auto-play if only zip is available.
394
+ # primary_audio_to_play = None
395
+ if generated_files: # Still offer first part for playing
396
+ primary_audio_to_play = generated_files[0]
397
+
398
+
399
+ if not primary_audio_to_play and not downloadable_file:
400
+ log_messages.append("🛑 هیچ خروجی صوتی یا فایل دانلودی برای ارائه وجود ندارد.")
401
+
402
+ # Cleanup non-primary generated files if delete_partial_files is True AND merge was successful
403
+ # This part is tricky because `generated_files` might be what `downloadable_file` or `primary_audio_to_play` points to.
404
+ # The `merge_audio_files_func` already handles deletion if `delete_partial_files` is true AND merge is successful.
405
+ # If not merging, and delete_partial_files is true, it means we shouldn't delete anything as they are the final product (in a zip).
406
+ # So, the logic inside merge_audio_files_func for deletion is probably sufficient.
407
+
408
+ return primary_audio_to_play, downloadable_file, "\n".join(log_messages)
409
+
410
+
411
+ # --- Gradio Interface ---
412
  css = """
413
+ body { font-family: 'Tahoma', sans-serif; }
 
 
 
 
414
  .gradio-container { max-width: 800px !important; margin: auto !important; }
415
+ footer { display: none !important; }
416
+ .gr-button { background-color: #4CAF50 !important; color: white !important; }
417
+ .gr-button:hover { background-color: #45a049 !important; }
418
  """
419
+
420
+ # استخراج نام گویندگان از کد شما
421
+ speaker_choices = ["Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"]
422
+ model_choices = ["gemini-1.5-flash-latest", "gemini-1.5-pro-latest"] # Updated model names
423
+
424
+ # لوگوی Base64 (اختیاری)
425
+ aigolden_logo_encoded = "Q3JlYXRlIGJ5IDogYWlnb2xkZW4="
426
+ aigolden_logo_decoded = base64.b64decode(aigolden_logo_encoded.encode()).decode()
427
+
428
+
429
+ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="green", secondary_hue="lightGreen")) as demo:
430
+ gr.Markdown(f"<h1 style='text-align: center; color: #2E7D32;'>تبدیل متن به گفتار با Gemini گوگل</h1>")
431
+ gr.Markdown(f"<p style='text-align: center; color: gray;'>{aigolden_logo_decoded}</p>")
432
+ gr.Markdown("⚠️ **توجه:** برای استفاده از این ابزار، نیاز به یک کلید API از Google AI Studio دارید. لطفاً کلید خود را در بخش `Settings` -> `Secrets` این Space با نام `GEMINI_API_KEY` ذخیره کنید.")
433
+
434
  with gr.Row():
435
  with gr.Column(scale=2):
436
+ gr.Markdown("### ⚙️ تنظیمات اصلی")
437
+ use_file_input_cb = gr.Checkbox(label="استفاده از فایل متنی ورودی (.txt)", value=False, info="اگر فعال شود، متن از فایل آپلود شده خوانده می‌شود.")
438
+
439
+ # نمایش شرطی برای آپلود فایل یا ورود متن دستی
440
+ text_file_upload = gr.File(label="آپلو�� فایل متنی (.txt)", file_types=[".txt"], visible=False)
441
+ text_to_speak_input = gr.Textbox(lines=7, label="متن برای تبدیل به گفتار (اگر فایل آپلود نشده باشد)", placeholder="متن خود را اینجا وارد کنید...", visible=True)
442
+
443
+ def toggle_input_method(use_file):
444
+ return {
445
+ text_file_upload: gr.update(visible=use_file),
446
+ text_to_speak_input: gr.update(visible=not use_file)
447
+ }
448
+ use_file_input_cb.change(toggle_input_method, inputs=use_file_input_cb, outputs=[text_file_upload, text_to_speak_input])
449
+
450
+ speech_prompt_input = gr.Textbox(label="پرامپت راهنمای سبک گفتار (اختیاری)", placeholder="مثال: از زبان یک یوتوبر پر انرژی و حرفه ای", info="این پرامپت به مدل کمک می‌کند تا سبک و لحن گفتار را تنظیم کند.")
451
+ output_filename_base_input = gr.Textbox(label="نام پایه فایل خروجی (بدون پسوند)", value="gemini_tts_output", info="برای نامگذاری فایل‌های صوتی تولید شده استفاده می‌شود.")
452
+
453
+ gr.Markdown("### 🗣️ تنظیمات مدل و گوینده")
454
+ model_name_dd = gr.Dropdown(model_choices, label="انتخاب مدل Gemini", value=model_choices[0], info="مدل‌های جدیدتر ممکن است کیفیت بهتری داشته باشند.")
455
+ speaker_voice_dd = gr.Dropdown(speaker_choices, label="انتخاب گوینده", value="Charon", info="گوینده مورد نظر را برای تولید صدا انتخاب کنید.")
456
+ temperature_slider = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, value=0.7, label="دمای مدل (Temperature)", info="مقادیر بالاتر منجر به خروجی خلاقانه‌تر و متنوع‌تر می‌شود، مقادیر پایین‌تر خروجی قابل پیش‌بینی‌تری دارد.")
457
+
458
  with gr.Column(scale=1):
459
+ gr.Markdown("### 分割 و خروجی") # Corrected title
460
+ max_chunk_size_slider = gr.Slider(minimum=2000, maximum=4000, step=100, value=3800, label="حداکثر کاراکتر در هر قطعه", info="متن‌های طولانی به قطعات کوچکتر تقسیم می‌شوند.")
461
+ sleep_between_requests_slider = gr.Slider(minimum=5, maximum=20, step=0.5, value=12, label="فاصله زمانی بین درخواست‌ها (ثانیه)", info="برای جلوگیری از خطاهای مربوط به محدودیت تعداد درخواست به API.")
462
+
463
+ merge_audio_files_cb = gr.Checkbox(label="ادغام فایل‌های صوتی جزئی", value=True, info="اگر متن طولانی باشد و به چند بخش تقسیم شود، فایل‌های صوتی این بخش‌ها ادغام می‌شوند.")
464
+ delete_partial_files_cb = gr.Checkbox(label="حذف فایل‌های جزئی پس از ادغام (در صورت انتخاب ادغام)", value=False, info="فقط در صورتی اعمال می‌شود که ادغام فایل‌ها فعال باشد.")
465
+
466
+ submit_button = gr.Button("🎧 تبدیل متن به گفتار 🎧", variant="primary")
467
+
468
+ gr.Markdown("### 🔊 خروجی و گزارش")
469
+ audio_output = gr.Audio(label="فایل صوتی نهایی", type="filepath")
470
+ file_download_output = gr.File(label="دانلود فایل (ادغام شده یا ZIP)", type="filepath")
471
+ status_output = gr.Textbox(label="وضعیت و گزارش‌ها", lines=10, interactive=False)
472
+
473
+ # اتصال دکمه به تابع اصلی
474
+ submit_button.click(
475
+ generate_audio_gradio,
476
+ inputs=[
477
+ use_file_input_cb, text_file_upload, text_to_speak_input, speech_prompt_input,
478
+ speaker_voice_dd, output_filename_base_input, model_name_dd, temperature_slider,
479
+ max_chunk_size_slider, sleep_between_requests_slider, merge_audio_files_cb, delete_partial_files_cb
480
+ ],
481
+ outputs=[audio_output, file_download_output, status_output]
482
+ )
483
+
484
+ gr.Markdown("---")
485
+ gr.Markdown("ℹ️ **نکات مهم:**")
486
+ gr.Markdown("- **کلید API:** مطمئن شوید که کلید API جمینای شما معتبر است و در بخش Secrets این Space به درستی تنظیم شده است.")
487
+ gr.Markdown("- **محدودیت‌های API:** گوگل ممکن است محدودیت‌هایی برای تعداد درخواست‌ها در دقیقه اعمال کند. اگر با خطا مواجه شدید، کمی صبر کنید و دوباره تلاش کنید یا فاصله زمانی بین درخواست‌ها را افزایش دهید.")
488
+ gr.Markdown("- **فایل‌های حجیم:** پردازش متن‌های بسیار طولانی ممکن است زمان‌بر باشد و به دلیل محدودیت‌های منابع در Spaces با مشکل مواجه شود.")
489
+ gr.Markdown("- **پاکسازی:** فایل‌های تولید شده در هر اجرا در پوشه `outputs` در سرور ذخیره می‌شوند. برای جلوگیری از پر شدن فضا، بهتر است فایل‌ها را پس از دانلود، از بخش Files and versions اسپیس خود مدیریت کنید یا از قابلیت حذف فایل‌های جزئی استفاده کنید.")
490
+
491
+
492
+ if __name__ == "__main__":
493
+ # ایجاد پوشه outputs اگر وجود ندارد (برای اجرای محلی)
494
+ if not os.path.exists("outputs"):
495
+ os.makedirs("outputs")
496
+ demo.launch(debug=True) # share=True برای اشتراک گذاری لینک عمومی در اجرای محلی