Update app.py
Browse files
app.py
CHANGED
@@ -24,14 +24,20 @@ SPEAKER_VOICES = [
|
|
24 |
]
|
25 |
FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
|
26 |
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
log_list.append(message)
|
29 |
|
30 |
def save_binary_file(file_name, data, log_list):
|
31 |
try:
|
32 |
with open(file_name, "wb") as f:
|
33 |
f.write(data)
|
34 |
-
_log(f"✅ فایل
|
35 |
return file_name
|
36 |
except Exception as e:
|
37 |
_log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
|
@@ -61,74 +67,51 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
|
61 |
for param in parts:
|
62 |
param = param.strip()
|
63 |
if param.lower().startswith("rate="):
|
64 |
-
try:
|
65 |
-
|
66 |
-
rate = int(rate_str)
|
67 |
-
except (ValueError, IndexError): pass
|
68 |
elif param.startswith("audio/L"):
|
69 |
-
try:
|
70 |
-
|
71 |
-
except (ValueError, IndexError): pass
|
72 |
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
73 |
|
74 |
def smart_text_split(text, max_size=3800, log_list=None):
|
75 |
-
if len(text) <= max_size:
|
76 |
-
|
77 |
-
chunks = []
|
78 |
-
current_chunk = ""
|
79 |
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
80 |
for sentence in sentences:
|
81 |
-
|
82 |
-
|
83 |
-
if current_chunk:
|
84 |
-
chunks.append(current_chunk.strip())
|
85 |
current_chunk = sentence
|
86 |
while len(current_chunk) > max_size:
|
87 |
-
split_idx = -1
|
88 |
-
|
89 |
-
|
90 |
-
if current_chunk[char_idx] in possible_split_chars:
|
91 |
-
split_idx = char_idx + 1
|
92 |
-
break
|
93 |
-
if split_idx != -1:
|
94 |
-
chunks.append(current_chunk[:split_idx].strip())
|
95 |
-
current_chunk = current_chunk[split_idx:].strip()
|
96 |
-
else:
|
97 |
-
chunks.append(current_chunk[:max_size].strip())
|
98 |
-
current_chunk = current_chunk[max_size:].strip()
|
99 |
else:
|
100 |
current_chunk += (" " if current_chunk else "") + sentence
|
101 |
-
if current_chunk:
|
102 |
-
chunks.append(current_chunk.strip())
|
103 |
-
|
104 |
final_chunks = [c for c in chunks if c]
|
105 |
-
if log_list:
|
106 |
-
_log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
|
107 |
return final_chunks
|
108 |
|
109 |
def merge_audio_files_func(file_paths, output_path, log_list):
|
110 |
if not PYDUB_AVAILABLE:
|
111 |
-
_log("❌ pydub در دسترس نیست.
|
112 |
return False
|
113 |
try:
|
114 |
-
_log(f"🔗
|
115 |
combined = AudioSegment.empty()
|
116 |
for i, file_path in enumerate(file_paths):
|
117 |
if os.path.exists(file_path):
|
118 |
-
|
119 |
-
|
120 |
-
if i < len(file_paths) - 1:
|
121 |
-
combined += AudioSegment.silent(duration=200)
|
122 |
-
else:
|
123 |
-
_log(f"⚠️ فایل پیدا نشد: {file_path}", log_list)
|
124 |
combined.export(output_path, format="wav")
|
125 |
-
_log(f"✅ فایل ادغام
|
126 |
return True
|
127 |
except Exception as e:
|
128 |
-
_log(f"❌ خطا در
|
129 |
return False
|
130 |
|
131 |
-
def create_zip_file(file_paths, zip_name, log_list):
|
132 |
try:
|
133 |
with zipfile.ZipFile(zip_name, 'w') as zipf:
|
134 |
for file_path in file_paths:
|
@@ -137,245 +120,212 @@ def create_zip_file(file_paths, zip_name, log_list):
|
|
137 |
_log(f"📦 فایل ZIP ایجاد شد: {zip_name}", log_list)
|
138 |
return True
|
139 |
except Exception as e:
|
140 |
-
_log(f"❌ خطا در ایجاد
|
141 |
return False
|
142 |
|
143 |
def core_generate_audio(
|
144 |
text_input, prompt_input, selected_voice, output_base_name,
|
145 |
-
temperature_val,
|
146 |
-
log_list
|
147 |
):
|
148 |
-
|
|
|
|
|
|
|
149 |
api_key = os.environ.get("GEMINI_API_KEY")
|
150 |
if not api_key:
|
151 |
-
_log("❌
|
152 |
-
|
|
|
|
|
153 |
|
154 |
try:
|
155 |
-
_log("🛠️ در حال ایجاد کلاینت هوش مصنوعی آلفا...", log_list)
|
156 |
client = genai.Client(api_key=api_key)
|
157 |
-
_log("✅ کلاینت با موفقیت ایجاد شد.", log_list)
|
158 |
except Exception as e:
|
159 |
-
_log(f"❌ خطا در
|
160 |
-
return None
|
161 |
|
162 |
-
if not text_input or text_input.strip()
|
163 |
-
_log("❌
|
164 |
-
return None
|
165 |
|
166 |
text_chunks = smart_text_split(text_input, max_chunk, log_list)
|
167 |
if not text_chunks:
|
168 |
-
_log("❌
|
169 |
-
return None
|
170 |
|
171 |
generated_files = []
|
172 |
model_to_use = FIXED_MODEL_NAME
|
173 |
|
174 |
for i, chunk in enumerate(text_chunks):
|
175 |
-
_log(f"
|
176 |
final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
|
177 |
contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
|
178 |
generate_content_config = types.GenerateContentConfig(
|
179 |
-
temperature=temperature_val,
|
180 |
-
|
181 |
-
|
182 |
-
voice_config=types.VoiceConfig(
|
183 |
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)
|
184 |
-
)
|
185 |
-
),
|
186 |
)
|
187 |
current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
|
188 |
try:
|
189 |
-
response = client.models.generate_content(
|
190 |
-
model=model_to_use, contents=contents, config=generate_content_config,
|
191 |
-
)
|
192 |
if (response.candidates and response.candidates[0].content and
|
193 |
-
response.candidates[0].content.parts and
|
194 |
-
response.candidates[0].content.parts[0].inline_data):
|
195 |
inline_data = response.candidates[0].content.parts[0].inline_data
|
196 |
data_buffer = inline_data.data
|
197 |
-
file_extension = mimetypes.guess_extension(inline_data.mime_type)
|
198 |
-
if
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_list)
|
205 |
-
if generated_file_path:
|
206 |
-
|
207 |
-
_log(f"✅ قطعه {i+1} تولید شد.", log_list)
|
208 |
-
elif response.text:
|
209 |
-
_log(f"ℹ️ پیام API برای قطعه {i+1}: {response.text}", log_list)
|
210 |
-
if "rate limit" in response.text.lower() or "quota" in response.text.lower():
|
211 |
-
_log(f"⏳ محدودیت درخواست API.", log_list)
|
212 |
-
else:
|
213 |
-
_log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی/متنی. باز��ورد: {response.prompt_feedback if response else 'No response'}", log_list)
|
214 |
-
except types.generation_types.BlockedPromptException as bpe:
|
215 |
-
_log(f"❌ محتوای قطعه {i+1} مسدود شد: {bpe}", log_list)
|
216 |
-
return None, None, "خطا: محتوای ورودی شما توسط سیستم ایمنی مسدود شد. لطفاً متن را تغییر دهید."
|
217 |
except Exception as e:
|
218 |
_log(f"❌ خطا در تولید قطعه {i+1}: {e}", log_list)
|
219 |
-
|
220 |
-
|
221 |
continue
|
222 |
-
if i < len(text_chunks) - 1 and len(text_chunks) > 1
|
223 |
-
_log(f"⏱️ انتظار {sleep_time} ثانیه...", log_list)
|
224 |
time.sleep(sleep_time)
|
225 |
|
226 |
if not generated_files:
|
227 |
-
_log("❌ هیچ
|
228 |
-
return None
|
229 |
|
230 |
-
_log(f"
|
231 |
-
|
232 |
-
download_file = None
|
233 |
-
user_message = "صدا با موفقیت تولید شد."
|
234 |
|
235 |
if len(generated_files) > 1:
|
236 |
if PYDUB_AVAILABLE:
|
237 |
-
merged_filename = f"{output_base_name}_final_audio.wav"
|
238 |
if merge_audio_files_func(generated_files, merged_filename, log_list):
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
zip_filename = f"{output_base_name}_all_parts.zip"
|
250 |
-
if create_zip_file(generated_files, zip_filename, log_list): download_file = zip_filename
|
251 |
-
if generated_files: playback_file = generated_files[0]
|
252 |
-
else:
|
253 |
-
user_message = "فایلهای صوتی به صورت جداگانه در یک فایل ZIP آماده شدند (امکان ادغام خودکار فراهم نبود)."
|
254 |
-
zip_filename = f"{output_base_name}_all_parts.zip"
|
255 |
-
if create_zip_file(generated_files, zip_filename, log_list): download_file = zip_filename
|
256 |
-
if generated_files: playback_file = generated_files[0]
|
257 |
elif len(generated_files) == 1:
|
258 |
-
|
259 |
-
|
|
|
|
|
|
|
|
|
|
|
260 |
|
261 |
-
if playback_file and not os.path.exists(playback_file): playback_file = None
|
262 |
-
if download_file and not os.path.exists(download_file): download_file = None
|
263 |
-
if not playback_file and not download_file and generated_files:
|
264 |
-
user_message = "خطا در آمادهسازی فایل نهایی. ممکن است قطعات جداگانه تولید شده باشند اما ادغام یا فشردهسازی ناموفق بوده."
|
265 |
-
return playback_file, download_file, user_message
|
266 |
|
267 |
def gradio_tts_interface(
|
268 |
use_file_input, uploaded_file, text_to_speak,
|
269 |
speech_prompt, speaker_voice, output_filename_base_in,
|
270 |
-
temperature,
|
271 |
progress=gr.Progress(track_tqdm=True)
|
272 |
):
|
273 |
-
internal_logs = []
|
274 |
actual_text_input = ""
|
275 |
if use_file_input:
|
276 |
if uploaded_file is not None:
|
277 |
try:
|
278 |
with open(uploaded_file.name, 'r', encoding='utf-8') as f:
|
279 |
actual_text_input = f.read().strip()
|
280 |
-
|
281 |
-
if not actual_text_input:
|
282 |
-
return None, None, "خطا: فایل آپلود شده خالی است."
|
283 |
except Exception as e:
|
284 |
-
_log(f"❌ خطا در خواندن
|
285 |
-
return None
|
286 |
-
else:
|
287 |
-
return None, None, "خطا: گزینه فایل انتخاب شده اما فایلی آپلود نشده."
|
288 |
else:
|
289 |
actual_text_input = text_to_speak
|
290 |
-
if not actual_text_input or not actual_text_input.strip():
|
291 |
-
return None, None, "خطا: لطفاً متنی را وارد کنید."
|
292 |
|
293 |
output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "alpha_tts_output")
|
294 |
if not output_filename_base: output_filename_base = "alpha_tts_output"
|
295 |
|
296 |
-
|
|
|
297 |
actual_text_input, speech_prompt, speaker_voice, output_filename_base,
|
298 |
-
temperature,
|
299 |
)
|
300 |
-
# for log_entry in internal_logs: # For debugging in HF Spaces console
|
301 |
-
# print(log_entry)
|
302 |
-
return playback_path, download_path, user_message_from_core
|
303 |
-
|
304 |
-
def format_user_message(message_text):
|
305 |
-
if not message_text:
|
306 |
-
return "<div class='user_message_output'></div>"
|
307 |
|
308 |
-
#
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
escaped_message = gr.utils.escape_html(message_text)
|
313 |
-
|
314 |
-
if "خطا:" in message_text or "متاسفانه" in message_text or "مسدود شد" in message_text or "نامعتبر" in message_text:
|
315 |
-
return f"<div class='user_message_output error'>{escaped_message}</div>"
|
316 |
-
elif "موفقیت" in message_text or "آماده شد" in message_text:
|
317 |
-
return f"<div class='user_message_output success'>{escaped_message}</div>"
|
318 |
-
else:
|
319 |
-
return f"<div class='user_message_output info'>{escaped_message}</div>"
|
320 |
|
321 |
css = """
|
322 |
-
body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; background-color: #
|
323 |
-
.gradio-container { max-width:
|
324 |
-
@media (min-width: 768px) { .gradio-container { max-width:
|
325 |
footer { display: none !important; }
|
326 |
-
.gr-button { font-weight: bold; background
|
327 |
-
.gr-button:hover {
|
328 |
-
.gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius:
|
329 |
-
.gr-
|
330 |
-
h1
|
331 |
-
|
332 |
-
|
333 |
-
label { font-weight:
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
.
|
338 |
-
.
|
339 |
-
.
|
340 |
-
.user_message_output.info { background-color: #cff4fc; color: #055160; border-color: #b6effb; }
|
341 |
-
#api_key_accordion details { border: 1px solid #ddd; border-radius: 6px; margin-bottom: 15px; }
|
342 |
-
#api_key_accordion summary { font-weight: bold; padding: 10px; cursor: pointer; background-color: #f7f7f7; border-radius: 6px 6px 0 0;}
|
343 |
-
#api_key_accordion div[class^="prose"] { padding: 10px; border-top: 1px solid #ddd;}
|
344 |
"""
|
345 |
|
346 |
-
|
347 |
-
<div
|
348 |
-
<img src='https://img.icons8.com/fluency/96/artificial-intelligence.png' alt='AI Icon'
|
349 |
<h1>تبدیل متن به صدا با هوش مصنوعی آلفا</h1>
|
350 |
-
<p style='font-size:1.1em; color:#555;'
|
351 |
</div>
|
352 |
"""
|
353 |
|
354 |
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
|
355 |
-
gr.HTML(
|
356 |
-
|
357 |
-
with gr.Accordion("⚠️ راهنمای مهم: تنظیم کلید API", open=False, elem_id="api_key_accordion"):
|
358 |
-
gr.Markdown(
|
359 |
-
"**برای استفاده از این ابزار، نیاز به تنظیم یک کلید API در تنظیمات این Space دارید:**\n"
|
360 |
-
"1. به صفحه اصلی این Space بروید.\n"
|
361 |
-
"2. روی نام Space و سپس 'Settings' (⚙️) کلیک کنید.\n"
|
362 |
-
"3. در منوی سمت چپ، به 'Secrets' بروید.\n"
|
363 |
-
"4. روی '+ New secret' کلیک کنید.\n"
|
364 |
-
"5. نام Secret را `GEMINI_API_KEY` (با حروف بزرگ) وارد کنید.\n"
|
365 |
-
"6. کلید API خود را در فیلد 'Value' وارد کنید.\n"
|
366 |
-
"7. 'Save secret' را بزنید و در صورت نیاز Space را Restart کنید."
|
367 |
-
)
|
368 |
|
369 |
with gr.Row(elem_classes="gr-form"):
|
370 |
-
with gr.Column(scale=3
|
371 |
-
gr.Markdown("### ۱. متن و
|
372 |
-
use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی
|
373 |
uploaded_file_input = gr.File(label="📂 آپلود فایل متنی (UTF-8)", file_types=['.txt'], visible=False)
|
374 |
text_to_speak_tb = gr.Textbox(
|
375 |
-
label="⌨️
|
376 |
placeholder="اینجا بنویسید...",
|
377 |
-
lines=
|
378 |
-
value="سلام! من هوش مصنوعی آلفا
|
379 |
visible=True
|
380 |
)
|
381 |
use_file_input_cb.change(
|
@@ -389,84 +339,56 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue,
|
|
389 |
value="با لحنی دوستانه و واضح صحبت کن.",
|
390 |
lines=2
|
391 |
)
|
392 |
-
|
393 |
-
with gr.Column(scale=2, min_width=250):
|
394 |
-
gr.Markdown("### ۲. تنظیمات صدا")
|
395 |
speaker_voice_dd = gr.Dropdown(
|
396 |
-
SPEAKER_VOICES, label="🎤 انتخاب
|
397 |
)
|
398 |
temperature_slider = gr.Slider(
|
399 |
-
minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="🌡️ خلاقیت
|
400 |
)
|
|
|
|
|
|
|
401 |
output_filename_base_tb = gr.Textbox(
|
402 |
-
label="📛 نام
|
403 |
-
)
|
404 |
-
|
405 |
-
gr.Markdown("#### تنظیمات فنی (پیشرفته)")
|
406 |
-
max_chunk_size_slider = gr.Slider(
|
407 |
-
minimum=1500, maximum=4000, step=100, value=3800, label="📏 حداکثر کاراکتر هر بخش:"
|
408 |
-
)
|
409 |
-
sleep_between_requests_slider = gr.Slider(
|
410 |
-
minimum=3, maximum=20, step=0.5, value=10, label="⏱️ تاخیر بین بخشها (ثانیه):"
|
411 |
)
|
412 |
|
413 |
-
generate_button = gr.Button("🎧 تولید
|
414 |
|
415 |
-
|
416 |
-
|
417 |
|
418 |
-
user_message_display = gr.HTML(value="<div class='user_message_output'>پیام وضعیت اینجا نمایش داده میشود...</div>")
|
419 |
-
|
420 |
-
gr.HTML("<hr style='margin: 20px 0;'>")
|
421 |
-
gr.Markdown("<h3 style='text-align:center; margin-bottom:10px;'>📢 نتیجه و دانلود 📢</h3>")
|
422 |
-
with gr.Row():
|
423 |
-
with gr.Column(scale=1):
|
424 |
-
output_audio = gr.Audio(label="🔊 فایل صوتی تولید شده:", type="filepath", elem_id="output_audio_player")
|
425 |
-
with gr.Column(scale=1):
|
426 |
-
download_file_output = gr.File(label="💾 دانلود فایل نهایی (WAV یا ZIP):", elem_id="download_file_link")
|
427 |
-
|
428 |
-
# رویداد کلیک دکمه
|
429 |
generate_button.click(
|
430 |
fn=gradio_tts_interface,
|
431 |
inputs=[
|
432 |
use_file_input_cb, uploaded_file_input, text_to_speak_tb,
|
433 |
speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
|
434 |
-
temperature_slider
|
435 |
],
|
436 |
-
outputs=[output_audio
|
437 |
-
).then( # سپس، کامپوننت مخفی به عنوان ورودی به تابع قالببندی داده میشود
|
438 |
-
fn=format_user_message,
|
439 |
-
inputs=[raw_user_message_holder], # ورودی از کامپوننت مخفی
|
440 |
-
outputs=user_message_display # خروجی به کامپوننت HTML
|
441 |
)
|
442 |
|
443 |
gr.Examples(
|
444 |
-
label="✨
|
445 |
examples=[
|
446 |
-
[False, None, "
|
447 |
-
[False, None, "
|
448 |
-
[False, None, "
|
449 |
-
[False, None, "
|
450 |
-
[False, None, "به پادکست هفتگی ما خوش آمدید. این هفته به بررسی عمیق تاثیرات فناوری بر زندگی روزمره خواهیم پرداخت.", "مانند یک میزبان پادکست، صمیمی و متفکر.", "Laomedeia", "alpha_podcast_1", 0.85, 3600, 11],
|
451 |
],
|
452 |
-
inputs=[
|
453 |
use_file_input_cb, uploaded_file_input, text_to_speak_tb,
|
454 |
speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
|
455 |
-
temperature_slider
|
456 |
],
|
457 |
-
# خروجی Examples
|
458 |
-
outputs=[output_audio, download_file_output, raw_user_message_holder],
|
459 |
fn=gradio_tts_interface,
|
460 |
-
# برای Examples، نمیتوانیم به سادگی .then را زنجیر کنیم تا user_message_display آپدیت شود.
|
461 |
-
# پیام وضعیت برای Examples نمایش داده نخواهد شد مگر اینکه یک wrapper پیچیدهتر بنویسیم.
|
462 |
-
# فعلا برای سادگی، پیام وضعیت برای Examples آپدیت نمیشود.
|
463 |
cache_examples=False
|
464 |
)
|
465 |
|
466 |
gr.Markdown(
|
467 |
-
"<div style='text-align: center; margin-top:
|
468 |
"قدرت گرفته از فناوری پیشرفته هوش مصنوعی آلفا.<br>"
|
469 |
-
"لطفاً
|
470 |
"</div>"
|
471 |
)
|
472 |
|
|
|
24 |
]
|
25 |
FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
|
26 |
|
27 |
+
# مقادیر پیشفرض برای تنظیمات فنی که از UI حذف میشوند
|
28 |
+
DEFAULT_MAX_CHUNK_SIZE = 3800
|
29 |
+
DEFAULT_SLEEP_BETWEEN_REQUESTS = 8 # کمی کاهش داده شد چون دیگر قابل تنظیم نیست
|
30 |
+
|
31 |
+
|
32 |
+
def _log(message, log_list): # برای دیباگ داخلی
|
33 |
+
# print(message) # برای نمایش در کنسول Hugging Face Spaces
|
34 |
log_list.append(message)
|
35 |
|
36 |
def save_binary_file(file_name, data, log_list):
|
37 |
try:
|
38 |
with open(file_name, "wb") as f:
|
39 |
f.write(data)
|
40 |
+
_log(f"✅ فایل ذخیره شد: {file_name}", log_list)
|
41 |
return file_name
|
42 |
except Exception as e:
|
43 |
_log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
|
|
|
67 |
for param in parts:
|
68 |
param = param.strip()
|
69 |
if param.lower().startswith("rate="):
|
70 |
+
try: rate = int(param.split("=", 1)[1])
|
71 |
+
except: pass
|
|
|
|
|
72 |
elif param.startswith("audio/L"):
|
73 |
+
try: bits_per_sample = int(param.split("L", 1)[1])
|
74 |
+
except: pass
|
|
|
75 |
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
76 |
|
77 |
def smart_text_split(text, max_size=3800, log_list=None):
|
78 |
+
if len(text) <= max_size: return [text]
|
79 |
+
chunks, current_chunk = [], ""
|
|
|
|
|
80 |
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
81 |
for sentence in sentences:
|
82 |
+
if len(current_chunk) + len(sentence) + 1 > max_size:
|
83 |
+
if current_chunk: chunks.append(current_chunk.strip())
|
|
|
|
|
84 |
current_chunk = sentence
|
85 |
while len(current_chunk) > max_size:
|
86 |
+
split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
|
87 |
+
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:])
|
88 |
+
chunks.append(part.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
else:
|
90 |
current_chunk += (" " if current_chunk else "") + sentence
|
91 |
+
if current_chunk: chunks.append(current_chunk.strip())
|
|
|
|
|
92 |
final_chunks = [c for c in chunks if c]
|
93 |
+
if log_list: _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
|
|
|
94 |
return final_chunks
|
95 |
|
96 |
def merge_audio_files_func(file_paths, output_path, log_list):
|
97 |
if not PYDUB_AVAILABLE:
|
98 |
+
_log("❌ pydub در دسترس نیست.", log_list)
|
99 |
return False
|
100 |
try:
|
101 |
+
_log(f"🔗 ادغام {len(file_paths)} فایل صوتی...", log_list)
|
102 |
combined = AudioSegment.empty()
|
103 |
for i, file_path in enumerate(file_paths):
|
104 |
if os.path.exists(file_path):
|
105 |
+
combined += AudioSegment.from_file(file_path) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
|
106 |
+
else: _log(f"⚠️ فا��ل پیدا نشد: {file_path}", log_list)
|
|
|
|
|
|
|
|
|
107 |
combined.export(output_path, format="wav")
|
108 |
+
_log(f"✅ فایل ادغام شده: {output_path}", log_list)
|
109 |
return True
|
110 |
except Exception as e:
|
111 |
+
_log(f"❌ خطا در ادغام: {e}", log_list)
|
112 |
return False
|
113 |
|
114 |
+
def create_zip_file(file_paths, zip_name, log_list): # این تابع دیگر استفاده نمیشود چون بخش دانلود مجزا حذف شده
|
115 |
try:
|
116 |
with zipfile.ZipFile(zip_name, 'w') as zipf:
|
117 |
for file_path in file_paths:
|
|
|
120 |
_log(f"📦 فایل ZIP ایجاد شد: {zip_name}", log_list)
|
121 |
return True
|
122 |
except Exception as e:
|
123 |
+
_log(f"❌ خطا در ایجاد ZIP: {e}", log_list)
|
124 |
return False
|
125 |
|
126 |
def core_generate_audio(
|
127 |
text_input, prompt_input, selected_voice, output_base_name,
|
128 |
+
temperature_val,
|
129 |
+
log_list # فقط برای لاگهای داخلی
|
130 |
):
|
131 |
+
max_chunk = DEFAULT_MAX_CHUNK_SIZE
|
132 |
+
sleep_time = DEFAULT_SLEEP_BETWEEN_REQUESTS
|
133 |
+
|
134 |
+
_log("🚀 شروع فرآیند...", log_list)
|
135 |
api_key = os.environ.get("GEMINI_API_KEY")
|
136 |
if not api_key:
|
137 |
+
_log("❌ کلید API تنظیم نشده.", log_list)
|
138 |
+
# چون پیام وضعیت حذف شده، کاربر فقط خروجی خالی دریافت میکند.
|
139 |
+
# بهتر است در README.md تاکید زیادی روی تنظیم کلید شود.
|
140 |
+
return None # فقط فایل صوتی برگردانده میشود
|
141 |
|
142 |
try:
|
|
|
143 |
client = genai.Client(api_key=api_key)
|
|
|
144 |
except Exception as e:
|
145 |
+
_log(f"❌ خطا در کلاینت: {e}", log_list)
|
146 |
+
return None
|
147 |
|
148 |
+
if not text_input or not text_input.strip():
|
149 |
+
_log("❌ متن ورودی خالی.", log_list)
|
150 |
+
return None
|
151 |
|
152 |
text_chunks = smart_text_split(text_input, max_chunk, log_list)
|
153 |
if not text_chunks:
|
154 |
+
_log("❌ متن قابل پردازش نیست.", log_list)
|
155 |
+
return None
|
156 |
|
157 |
generated_files = []
|
158 |
model_to_use = FIXED_MODEL_NAME
|
159 |
|
160 |
for i, chunk in enumerate(text_chunks):
|
161 |
+
_log(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...", log_list)
|
162 |
final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
|
163 |
contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
|
164 |
generate_content_config = types.GenerateContentConfig(
|
165 |
+
temperature=temperature_val, response_modalities=["audio"],
|
166 |
+
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
|
167 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice)))
|
|
|
|
|
|
|
|
|
168 |
)
|
169 |
current_chunk_filename_base = f"{output_base_name}_part{i+1:03d}"
|
170 |
try:
|
171 |
+
response = client.models.generate_content(model=model_to_use, contents=contents, config=generate_content_config)
|
|
|
|
|
172 |
if (response.candidates and response.candidates[0].content and
|
173 |
+
response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data):
|
|
|
174 |
inline_data = response.candidates[0].content.parts[0].inline_data
|
175 |
data_buffer = inline_data.data
|
176 |
+
file_extension = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
|
177 |
+
if "audio/L" in inline_data.mime_type and file_extension == ".wav":
|
178 |
+
data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
|
179 |
+
|
180 |
+
# اطمینان از پسوند مناسب برای pydub
|
181 |
+
if not file_extension.startswith("."): file_extension = "." + file_extension
|
182 |
+
if file_extension not in [".wav", ".mp3", ".ogg", ".flac"]: # اگر فرمت ناشناخته بود، به wav تبدیل میکنیم اگر ممکن باشد
|
183 |
+
if PYDUB_AVAILABLE and file_extension != ".wav": # سعی در تبدیل به wav
|
184 |
+
try:
|
185 |
+
temp_path = f"{current_chunk_filename_base}{file_extension}"
|
186 |
+
save_binary_file(temp_path, data_buffer, log_list)
|
187 |
+
audio_seg = AudioSegment.from_file(temp_path)
|
188 |
+
# پاک کردن فایل موقت با پسوند اصلی
|
189 |
+
if os.path.exists(temp_path): os.remove(temp_path)
|
190 |
+
|
191 |
+
file_extension = ".wav" # تغییر پسوند به wav
|
192 |
+
# فایل را با پسوند wav ذخیره میکنیم
|
193 |
+
# data_buffer حالا باید بایتهای wav باشد
|
194 |
+
# این بخش نیاز به بازبینی دارد که چگونه بایتهای wav را از audio_seg بگیریم یا مستقیما ذخیره کنیم
|
195 |
+
# برای سادگی، اگر فرمت اولیه توسط pydub خوانا باشد، همان را ذخیره میکنیم
|
196 |
+
# و اگر قرار است ادغام شود، pydub خودش هندل میکند.
|
197 |
+
# اگر فرمت اولیه mp3 و ... باشد، ذخیره و بعدا توسط pydub خوانده میشود.
|
198 |
+
# فعلا فرض میکنیم فرمت دریافتی از API توسط pydub قابل خواندن است.
|
199 |
+
pass # ادامه با file_extension اصلی
|
200 |
+
except Exception as e_conv:
|
201 |
+
_log(f"⚠️ خطا در تبدیل فرمت {file_extension} به wav برای قطعه {i+1}: {e_conv}", log_list)
|
202 |
+
# اگر تبدیل ناموفق بود، با همان فرمت اولیه ادامه میدهیم و امیدواریم pydub آن را بخواند
|
203 |
+
else: # اگر pydub نباشد و فرمت هم wav نباشد، ممکن است در ادغام مشکل پیش بیاید
|
204 |
+
_log(f"⚠️ فرمت ناشناخته {file_extension} برای قطعه {i+1} و pydub در دسترس نیست یا فرمت wav نیست.", log_list)
|
205 |
+
# اگر فرمت شناخته شدهای برای pydub نباشد و pydub هم نباشد، فقط wav ذخیره میکنیم
|
206 |
+
if file_extension not in [".wav",".mp3"]: file_extension = ".wav"
|
207 |
+
|
208 |
+
|
209 |
generated_file_path = save_binary_file(f"{current_chunk_filename_base}{file_extension}", data_buffer, log_list)
|
210 |
+
if generated_file_path: generated_files.append(generated_file_path)
|
211 |
+
else: _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی.", log_list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
except Exception as e:
|
213 |
_log(f"❌ خطا در تولید قطعه {i+1}: {e}", log_list)
|
214 |
+
# اگر خطایی در یک قطعه رخ دهد، ادامه میدهیم تا بقیه تولید شوند
|
215 |
+
# کاربر در نهایت هرچه تولید شده را دریافت میکند.
|
216 |
continue
|
217 |
+
if i < len(text_chunks) - 1 and len(text_chunks) > 1:
|
|
|
218 |
time.sleep(sleep_time)
|
219 |
|
220 |
if not generated_files:
|
221 |
+
_log("❌ هیچ فایلی تولید نشد.", log_list)
|
222 |
+
return None
|
223 |
|
224 |
+
_log(f"🎉 {len(generated_files)} فایل(های) صوتی تولید شد.", log_list)
|
225 |
+
final_audio_file = None
|
|
|
|
|
226 |
|
227 |
if len(generated_files) > 1:
|
228 |
if PYDUB_AVAILABLE:
|
229 |
+
merged_filename = f"{output_base_name}_final_audio.wav" # همیشه WAV برای ادغام شده
|
230 |
if merge_audio_files_func(generated_files, merged_filename, log_list):
|
231 |
+
final_audio_file = merged_filename
|
232 |
+
for file_path in generated_files: # حذف فایلهای جزئی
|
233 |
+
if os.path.abspath(file_path) != os.path.abspath(merged_filename):
|
234 |
+
try: os.remove(file_path)
|
235 |
+
except: pass
|
236 |
+
else: # اگر ادغام ناموفق بود، اولین فایل را برمیگردانیم
|
237 |
+
final_audio_file = generated_files[0] if generated_files else None
|
238 |
+
else: # اگر pydub نباشد، فقط اولین فایل را برمیگردانیم
|
239 |
+
_log("⚠️ pydub برای ادغام در دسترس نیست. فقط اولین قطعه ارائه میشود.", log_list)
|
240 |
+
final_audio_file = generated_files[0] if generated_files else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
elif len(generated_files) == 1:
|
242 |
+
final_audio_file = generated_files[0]
|
243 |
+
|
244 |
+
if final_audio_file and not os.path.exists(final_audio_file):
|
245 |
+
_log(f"⚠️ فایل نهایی {final_audio_file} وجود ندارد!", log_list)
|
246 |
+
return None
|
247 |
+
|
248 |
+
return final_audio_file
|
249 |
|
|
|
|
|
|
|
|
|
|
|
250 |
|
251 |
def gradio_tts_interface(
|
252 |
use_file_input, uploaded_file, text_to_speak,
|
253 |
speech_prompt, speaker_voice, output_filename_base_in,
|
254 |
+
temperature,
|
255 |
progress=gr.Progress(track_tqdm=True)
|
256 |
):
|
257 |
+
internal_logs = [] # برای دیباگ داخلی
|
258 |
actual_text_input = ""
|
259 |
if use_file_input:
|
260 |
if uploaded_file is not None:
|
261 |
try:
|
262 |
with open(uploaded_file.name, 'r', encoding='utf-8') as f:
|
263 |
actual_text_input = f.read().strip()
|
264 |
+
if not actual_text_input: return None # خطا: فایل خالی
|
|
|
|
|
265 |
except Exception as e:
|
266 |
+
_log(f"❌ خطا در خواندن فایل: {e}", internal_logs)
|
267 |
+
return None # خطا
|
268 |
+
else: return None # خطا: فایل انتخاب نشده
|
|
|
269 |
else:
|
270 |
actual_text_input = text_to_speak
|
271 |
+
if not actual_text_input or not actual_text_input.strip(): return None
|
|
|
272 |
|
273 |
output_filename_base = re.sub(r'[^\w\-_]', '', output_filename_base_in if output_filename_base_in else "alpha_tts_output")
|
274 |
if not output_filename_base: output_filename_base = "alpha_tts_output"
|
275 |
|
276 |
+
# تابع core_generate_audio فقط مسیر فایل صوتی نهایی را برمیگرداند
|
277 |
+
final_audio_path = core_generate_audio(
|
278 |
actual_text_input, speech_prompt, speaker_voice, output_filename_base,
|
279 |
+
temperature, internal_logs
|
280 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
|
282 |
+
# for log_entry in internal_logs: print(log_entry) # برای دیباگ در کنسول HF
|
283 |
+
|
284 |
+
return final_audio_path
|
285 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
|
287 |
css = """
|
288 |
+
body { font-family: 'Tahoma', 'Arial', sans-serif; direction: rtl; background-color: #f4f7f6; color: #333; }
|
289 |
+
.gradio-container { max-width: 90% !important; margin: 20px auto !important; padding: 20px !important; background-color: #ffffff; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.08); }
|
290 |
+
@media (min-width: 768px) { .gradio-container { max-width: 650px !important; } }
|
291 |
footer { display: none !important; }
|
292 |
+
.gr-button { font-weight: bold; background: linear-gradient(135deg, #007bff, #0056b3) !important; color: white !important; border:none !important; border-radius: 8px !important; padding: 12px 25px !important; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.15); }
|
293 |
+
.gr-button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); }
|
294 |
+
.gr-input, .gr-dropdown, .gr-slider, .gr-checkbox, .gr-textbox, .gr-file { border-radius: 8px !important; border: 1px solid #d1d5db; transition: border-color 0.2s ease, box-shadow 0.2s ease; }
|
295 |
+
.gr-input:focus-within, .gr-textbox:focus-within { border-color: #007bff !important; box-shadow: 0 0 0 2px rgba(0,123,255,0.25) !important; }
|
296 |
+
h1 { font-size: 1.9em; margin-bottom: 8px; color: #2c3e50; }
|
297 |
+
h2 { font-size: 1.1em; margin-bottom: 18px; color: #555;}
|
298 |
+
h3 { font-size: 1.3em; color: #0056b3; margin-top: 25px; margin-bottom:15px; border-bottom: 2px solid #007bff30; padding-bottom: 8px;}
|
299 |
+
label { font-weight: 600; color: #4a5568; margin-bottom: 6px; display: block; font-size: 0.95em; }
|
300 |
+
textarea, input[type="text"] { direction: rtl; text-align: right; padding: 12px; font-size: 1em; }
|
301 |
+
.gr-form > div { margin-bottom: 15px !important; } /* فاصله بین ردیفهای فرم */
|
302 |
+
#output_audio_player audio { width: 100%; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
303 |
+
.temperature_description { font-size: 0.85em; color: #666; margin-top: -8px; margin-bottom: 10px; padding-right: 5px; }
|
304 |
+
.main_title_container {text-align:center; padding-bottom:15px; border-bottom: 1px solid #eee; margin-bottom: 20px;}
|
305 |
+
.main_title_container img {width:60px; height:60px; margin-bottom:5px;}
|
|
|
|
|
|
|
|
|
306 |
"""
|
307 |
|
308 |
+
alpha_intro_html = """
|
309 |
+
<div class='main_title_container'>
|
310 |
+
<img src='https://img.icons8.com/fluency/96/artificial-intelligence.png' alt='AI Icon'/>
|
311 |
<h1>تبدیل متن به صدا با هوش مصنوعی آلفا</h1>
|
312 |
+
<p style='font-size:1.1em; color:#555;'>متن خود را به صدای طبیعی و رسا تبدیل کنید!</p>
|
313 |
</div>
|
314 |
"""
|
315 |
|
316 |
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky)) as demo:
|
317 |
+
gr.HTML(alpha_intro_html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
318 |
|
319 |
with gr.Row(elem_classes="gr-form"):
|
320 |
+
with gr.Column(scale=3): # ستون اصلی برای ورودیها
|
321 |
+
gr.Markdown("### ۱. متن و تنظیمات صدا")
|
322 |
+
use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False)
|
323 |
uploaded_file_input = gr.File(label="📂 آپلود فایل متنی (UTF-8)", file_types=['.txt'], visible=False)
|
324 |
text_to_speak_tb = gr.Textbox(
|
325 |
+
label="⌨️ متنی که میخواهید به صدا تبدیل شود:",
|
326 |
placeholder="اینجا بنویسید...",
|
327 |
+
lines=7,
|
328 |
+
value="سلام! من هوش مصنوعی آلفا هستم.",
|
329 |
visible=True
|
330 |
)
|
331 |
use_file_input_cb.change(
|
|
|
339 |
value="با لحنی دوستانه و واضح صحبت کن.",
|
340 |
lines=2
|
341 |
)
|
|
|
|
|
|
|
342 |
speaker_voice_dd = gr.Dropdown(
|
343 |
+
SPEAKER_VOICES, label="🎤 انتخاب نوع صدا (گوینده):", value="Charon"
|
344 |
)
|
345 |
temperature_slider = gr.Slider(
|
346 |
+
minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="🌡️ خلاقیت و تنوع صدا:"
|
347 |
)
|
348 |
+
gr.Markdown("<p class='temperature_description'>مقادیر بالاتر صدایی خلاقانهتر و متنوعتر، و مقادیر پایینتر صدایی قابل پیشبینیتر و یکنواختتر ایجاد میکنند.</p>",
|
349 |
+
elem_classes="temperature_description_container")
|
350 |
+
|
351 |
output_filename_base_tb = gr.Textbox(
|
352 |
+
label="📛 نام فایل خروجی (اختیاری، بدون پسوند):", value="alpha_audio"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
353 |
)
|
354 |
|
355 |
+
generate_button = gr.Button("🎧 تولید و پخش صدا", variant="primary", elem_id="generate_button_main")
|
356 |
|
357 |
+
gr.Markdown("### 🔊 نتیجه تولید صدا")
|
358 |
+
output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player") # لیبل خالی شد
|
359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
360 |
generate_button.click(
|
361 |
fn=gradio_tts_interface,
|
362 |
inputs=[
|
363 |
use_file_input_cb, uploaded_file_input, text_to_speak_tb,
|
364 |
speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
|
365 |
+
temperature_slider
|
366 |
],
|
367 |
+
outputs=[output_audio]
|
|
|
|
|
|
|
|
|
368 |
)
|
369 |
|
370 |
gr.Examples(
|
371 |
+
label="✨ چند نمونه برای شروع ✨",
|
372 |
examples=[
|
373 |
+
[False, None, "به نام خداوند بخشنده مهربان. سلام بر شما شنوندگان عزیز.", "با لحنی آرام و معنوی.", "Achird", "quran_intro_sample", 0.7],
|
374 |
+
[False, None, "خبر فوری! قیمتها در بازار طلا و سکه با نوسانات شدیدی همراه بوده است.", "مانند یک گوینده خبر اقتصادی، سریع و دقیق.", "Orus", "news_flash_sample", 1.0],
|
375 |
+
[False, None, "در این ویدیو قصد داریم به شما آموزش دهیم چگونه یک وبسایت ساده با پایتون بسازید.", "آموزشی، واضح و با سرعت متوسط.", "Vindemiatrix", "tutorial_sample", 0.8],
|
376 |
+
[False, None, "کتاب صوتی «بوف کور» اثر صادق هدایت. فصل اول.", "روایی، با احساس و کمی غمگین.", "Alnilam", "audiobook_sample", 0.85],
|
|
|
377 |
],
|
378 |
+
inputs=[ # ورودیها باید با ورودیهای تابع اصلی مطابقت داشته باشند
|
379 |
use_file_input_cb, uploaded_file_input, text_to_speak_tb,
|
380 |
speech_prompt_tb, speaker_voice_dd, output_filename_base_tb,
|
381 |
+
temperature_slider
|
382 |
],
|
383 |
+
outputs=[output_audio], # خروجی Examples هم فقط پلیر صوتی است
|
|
|
384 |
fn=gradio_tts_interface,
|
|
|
|
|
|
|
385 |
cache_examples=False
|
386 |
)
|
387 |
|
388 |
gr.Markdown(
|
389 |
+
"<div style='text-align: center; margin-top: 40px; padding-top:20px; border-top: 1px solid #eee; font-size: 0.9em; color: #6c757d;'>"
|
390 |
"قدرت گرفته از فناوری پیشرفته هوش مصنوعی آلفا.<br>"
|
391 |
+
"لطفاً به قوانین و مقررات مربوط به تولید محتوا احترام بگذارید."
|
392 |
"</div>"
|
393 |
)
|
394 |
|