Update app.py
Browse files
app.py
CHANGED
@@ -6,11 +6,11 @@ import re
|
|
6 |
import struct
|
7 |
import time
|
8 |
import zipfile
|
9 |
-
|
10 |
-
from google.
|
11 |
-
import traceback
|
12 |
|
13 |
# خواندن کلید API از Hugging Face Secrets
|
|
|
14 |
HF_GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
15 |
|
16 |
try:
|
@@ -19,48 +19,64 @@ try:
|
|
19 |
except ImportError:
|
20 |
PYDUB_AVAILABLE = False
|
21 |
print("⚠️ کتابخانه pydub در دسترس نیست. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
|
|
22 |
|
23 |
# --- ثابتها ---
|
24 |
-
|
25 |
-
"
|
26 |
-
"
|
27 |
-
"
|
28 |
-
"
|
29 |
-
"
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
"
|
35 |
-
"gemini-2.5-pro-preview-tts": "جمینای ۲.۵ پرو (اختصاصی TTS، کیفیت بالا)" # نام اصلی
|
36 |
}
|
37 |
-
|
38 |
-
"
|
39 |
-
"
|
40 |
-
"Laomedeia": "لائومدیا (زنانه)", "Sulafat": "سولافات (مردانه)"
|
41 |
-
# ... میتوانید برای همه گویندهها نام فارسی تعریف کنید
|
42 |
}
|
|
|
|
|
|
|
43 |
|
44 |
# --- توابع کمکی ---
|
45 |
def save_binary_file(file_name, data):
|
46 |
abs_file_name = os.path.abspath(file_name)
|
47 |
try:
|
48 |
-
with open(abs_file_name, "wb") as f:
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
51 |
|
52 |
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
53 |
parameters = parse_audio_mime_type(mime_type)
|
54 |
-
bits_per_sample
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
57 |
chunk_size = 36 + data_size
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
59 |
|
60 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
61 |
-
bits_per_sample
|
|
|
62 |
if mime_type:
|
63 |
-
mime_type_lower = mime_type.lower()
|
|
|
64 |
for param in parts:
|
65 |
param = param.strip()
|
66 |
if param.startswith("rate="):
|
@@ -68,455 +84,386 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
|
68 |
except: pass
|
69 |
elif param.startswith("audio/l"):
|
70 |
try:
|
71 |
-
potential_bits = param.split("l", 1)[1]
|
72 |
if potential_bits.isdigit(): bits_per_sample = int(potential_bits)
|
73 |
except: pass
|
74 |
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
75 |
|
76 |
def load_text_from_gr_file(file_obj):
|
77 |
-
if file_obj is None:
|
|
|
78 |
try:
|
79 |
-
with open(file_obj.name, 'r', encoding='utf-8') as f:
|
80 |
-
|
|
|
|
|
81 |
return content, f"متن با موفقیت از فایل '{os.path.basename(file_obj.name)}' ({len(content)} کاراکتر) بارگذاری شد."
|
82 |
-
except Exception as e:
|
83 |
-
|
84 |
-
# ** تابع smart_text_split با استفاده از max_chunk_size ورودی **
|
85 |
-
def smart_text_split(text, max_chunk_size=3800):
|
86 |
-
if not text: return []
|
87 |
-
if len(text) <= max_chunk_size:
|
88 |
-
return [text.strip()]
|
89 |
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
94 |
-
|
95 |
for sentence in sentences:
|
96 |
-
|
97 |
-
if
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
current_chunk = ""
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
for i in range(0, len(sentence), max_chunk_size):
|
111 |
-
chunks.append(sentence[i:i + max_chunk_size].strip())
|
112 |
-
else:
|
113 |
-
current_chunk = sentence # شروع چانک جدید با این جمله
|
114 |
-
else:
|
115 |
-
# اضافه کردن جمله به چانک فعلی
|
116 |
-
if current_chunk:
|
117 |
-
current_chunk += " " + sentence
|
118 |
-
else:
|
119 |
-
current_chunk = sentence
|
120 |
-
|
121 |
-
# اضافه کردن آخرین چانک اگر چیزی باقی مانده باشد
|
122 |
-
if current_chunk:
|
123 |
-
chunks.append(current_chunk)
|
124 |
-
|
125 |
-
return [c for c in chunks if c] # حذف چانکهای خالی احتمالی
|
126 |
-
|
127 |
|
128 |
def merge_audio_files_func(file_paths, output_path):
|
129 |
if not PYDUB_AVAILABLE: return False, "pydub در دسترس نیست. امکان ادغام فایلها وجود ندارد.", None
|
130 |
-
if not file_paths: return False, "
|
131 |
try:
|
132 |
combined = AudioSegment.empty()
|
133 |
-
for i,
|
134 |
-
if os.path.exists(
|
135 |
try:
|
136 |
-
audio = AudioSegment.from_file(
|
137 |
combined += audio
|
138 |
if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
|
139 |
except Exception as e_load:
|
140 |
-
|
141 |
-
|
142 |
-
else:
|
143 |
-
msg = f"فایل برای ادغام یافت نشد: {os.path.basename(file_path)}"
|
144 |
-
print(f"⚠️ {msg}"); return False, msg, None
|
145 |
abs_output_path = os.path.abspath(output_path)
|
146 |
combined.export(abs_output_path, format="wav")
|
147 |
-
return True, f"فایل ادغام شده با موفقیت در '{os.path.basename(abs_output_path)}'
|
148 |
-
except Exception as e:
|
149 |
-
|
150 |
-
print(f"❌ {msg}"); return False, msg, None
|
151 |
def create_zip_file(file_paths, zip_name):
|
152 |
abs_zip_name = os.path.abspath(zip_name)
|
153 |
try:
|
154 |
with zipfile.ZipFile(abs_zip_name, 'w') as zipf:
|
155 |
-
for
|
156 |
-
if os.path.exists(
|
157 |
-
|
158 |
-
return True, f"فایل ZIP با نام '{os.path.basename(abs_zip_name)}' ایجاد شد.", abs_zip_name
|
159 |
except Exception as e: return False, f"خطا در ایجاد فایل ZIP: {e}", None
|
160 |
|
161 |
# --- تابع اصلی تولید صدا ---
|
162 |
def generate_audio_for_gradio(
|
163 |
-
use_file_input_checkbox, text_file_obj,
|
164 |
-
|
165 |
-
sleep_slider, temperature_slider,
|
166 |
-
|
|
|
167 |
progress=gr.Progress(track_tqdm=True)
|
168 |
):
|
169 |
-
status_messages = ["🚀 فرآیند تبدیل متن به
|
170 |
progress(0, desc="در حال آمادهسازی...")
|
171 |
|
172 |
api_key_to_use = HF_GEMINI_API_KEY
|
173 |
if not api_key_to_use:
|
174 |
-
status_messages.
|
175 |
-
|
176 |
return None, None, "\n".join(status_messages)
|
177 |
-
|
178 |
-
|
179 |
-
status_messages.append("🔑 کلید API با موفقیت از Secrets بارگذاری و برای استفاده تنظیم شد.")
|
180 |
|
181 |
-
actual_text_input = ""
|
182 |
if use_file_input_checkbox:
|
183 |
if text_file_obj is None:
|
184 |
-
status_messages.append("❌ خطا: گزینه 'استفاده از فایل متنی' انتخاب شده، اما
|
185 |
return None, None, "\n".join(status_messages)
|
186 |
actual_text_input, msg = load_text_from_gr_file(text_file_obj)
|
187 |
-
status_messages.append(msg)
|
188 |
-
if not actual_text_input: return None, None, "\n".join(status_messages)
|
189 |
else:
|
190 |
actual_text_input = text_to_speak_input
|
191 |
-
|
|
|
|
|
192 |
|
193 |
-
|
194 |
-
status_messages.append("
|
195 |
-
|
196 |
-
|
|
|
|
|
|
|
|
|
197 |
|
198 |
-
|
199 |
-
|
200 |
-
status_messages.append(f"
|
201 |
-
for i, chunk_text_content in enumerate(text_chunks): status_messages.append(f" 📝 قطعه {i+1}: {len(chunk_text_content)} کاراکتر")
|
202 |
|
203 |
-
generated_audio_files = []
|
204 |
-
|
205 |
-
|
206 |
output_base_name_safe = re.sub(r'[\s\\\/\:\*\?\"\<\>\|\%]+', '_', output_filename_base_input)
|
207 |
|
|
|
|
|
|
|
|
|
208 |
total_chunks = len(text_chunks)
|
209 |
for i, chunk_text_content in enumerate(text_chunks):
|
210 |
-
|
211 |
-
|
212 |
|
213 |
-
|
214 |
-
if speech_prompt_input.strip()
|
215 |
-
# نحوه صحیح ترکیب پرامپت با متن اصلی برای مدلهای TTS باید طبق مستندات باشد.
|
216 |
-
# این روش ساده ممکن است برای برخی مدلها کار کند:
|
217 |
-
text_for_tts_api = f"Prompt: \"{speech_prompt_input}\"\n\nText: \"{chunk_text_content}\""
|
218 |
-
status_messages.append(f"ℹ️ اعمال پرامپت سبک: '{speech_prompt_input}'")
|
219 |
|
220 |
-
|
221 |
-
|
222 |
-
contents_for_api = [
|
223 |
-
types.Content(
|
224 |
-
role="user", # یا "model" اگر پرامپت سبک به عنوان بخشی از تاریخچه چت در نظر گرفته شود
|
225 |
-
parts=[
|
226 |
-
types.Part.from_text(text=text_for_tts_api),
|
227 |
-
],
|
228 |
-
),
|
229 |
-
]
|
230 |
-
generation_config = types.GenerateContentConfig( # استفاده از GenerateContentConfig
|
231 |
temperature=float(temperature_slider),
|
232 |
-
response_modalities=["audio"],
|
233 |
-
speech_config=types.SpeechConfig(
|
234 |
voice_config=types.VoiceConfig(
|
235 |
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
236 |
-
voice_name=speaker_dropdown
|
237 |
-
)
|
238 |
)
|
239 |
-
)
|
240 |
)
|
241 |
-
|
242 |
-
|
243 |
try:
|
244 |
-
|
245 |
-
|
246 |
-
audio_data_received = False
|
247 |
-
|
248 |
-
model_instance = genai.GenerativeModel(model_dropdown_key)
|
249 |
-
|
250 |
-
# استفاده از استریم با generation_config که شامل speech_config است
|
251 |
-
for stream_response_chunk in model_instance.generate_content_stream(
|
252 |
-
contents=contents_for_api, # ارسال contents ساخته شده
|
253 |
-
generation_config=generation_config # ارسال generation_config ساخته شده
|
254 |
):
|
255 |
-
if (
|
256 |
-
|
257 |
-
|
258 |
-
|
|
|
259 |
data_buffer, api_mime_type = inline_data.data, inline_data.mime_type
|
260 |
audio_data_received = True
|
261 |
status_messages.append(f"ℹ️ MIME Type دریافتی از API: {api_mime_type}")
|
262 |
-
|
|
|
263 |
if api_mime_type and ("mp3" in api_mime_type.lower() or "mpeg" in api_mime_type.lower()):
|
264 |
-
|
|
|
265 |
elif api_mime_type and "wav" in api_mime_type.lower() and not ("audio/l16" in api_mime_type.lower() or "audio/l24" in api_mime_type.lower()):
|
266 |
-
|
|
|
267 |
else:
|
268 |
-
status_messages.append(f"ℹ️ تبدیل به
|
269 |
data_buffer = convert_to_wav(data_buffer, api_mime_type)
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
|
|
|
|
|
|
275 |
else: status_messages.append(f"❌ عدم موفقیت در ذخیره قطعه {i+1}.")
|
276 |
break
|
277 |
-
|
278 |
-
|
|
|
279 |
if not audio_data_received:
|
280 |
-
status_messages.append(f"❌
|
281 |
-
if
|
282 |
-
|
283 |
-
hasattr(stream_response_chunk.prompt_feedback, 'block_reason') and stream_response_chunk.prompt_feedback.block_reason:
|
284 |
-
status_messages.append(f"🛑 دلیل مسدود شدن (از بازخورد پرامپت): "
|
285 |
-
f"{stream_response_chunk.prompt_feedback.block_reason_message or stream_response_chunk.prompt_feedback.block_reason}")
|
286 |
-
except Exception as e:
|
287 |
-
is_quota_error = False
|
288 |
-
# نام کلاسهای خطا در کتابخانه google-generativeai ممکن است کمی متفاوت باشد.
|
289 |
-
# BlockedPromptError و StopCandidateException معمولاً در types.generation_types یا مستقیماً types هستند.
|
290 |
-
if hasattr(types, 'BlockedPromptError') and isinstance(e, types.BlockedPromptError):
|
291 |
-
status_messages.append(f"❌ محتوای قطعه {i+1} توسط API مسدود شد: {e}")
|
292 |
-
elif hasattr(types, 'StopCandidateException') and isinstance(e, types.StopCandidateException):
|
293 |
-
status_messages.append(f"❌ تولید صدا برای قطعه {i+1} به دلیل پایان نامناسب متوقف شد: {e}")
|
294 |
-
if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback'):
|
295 |
-
status_messages.append(f" بازخورد API: {e.response.prompt_feedback}")
|
296 |
-
elif isinstance(e, genai.errors.GoogleAPIError): # کلاس والد برای خطاهای API گوگل
|
297 |
-
status_messages.append(f"❌ خطای API گوگل در قطعه {i+1} ({type(e).__name__}): {e}")
|
298 |
-
# بررسی دقیقتر برای خطای سهمیه با استفاده از پیام خطا
|
299 |
-
error_message_upper = str(getattr(e, 'message', '')).upper()
|
300 |
-
if "QUOTA" in error_message_upper or "RESOURCE_EXHAUSTED" in error_message_upper:
|
301 |
-
status_messages.append("🚫 شما از سهمیه رایگان/فعلی خود برای این مدل فراتر رفتهاید. لطفاً طرح خود را بررسی کنید یا بعداً دوباره امتحان نمایید.")
|
302 |
-
is_quota_error = True
|
303 |
-
elif hasattr(e, 'message'):
|
304 |
-
status_messages.append(f" پیام خطا از API: {e.message}")
|
305 |
-
status_messages.append(traceback.format_exc())
|
306 |
-
else:
|
307 |
-
status_messages.extend([f"❌ خطا در تولید/پردازش قطعه {i+1}: {type(e).__name__} - {e}", traceback.format_exc()])
|
308 |
-
|
309 |
-
if is_quota_error and model_dropdown_key.endswith("-pro-preview-tts"):
|
310 |
-
status_messages.append("💡 پیشنهاد: از مدل 'جمینای فلش' که محدودیت کمتری دارد استفاده کنید یا برای استفاده از مدل پرو، طرح خود را در گوگل ارتقا دهید.")
|
311 |
|
312 |
-
|
313 |
-
|
|
|
|
|
314 |
|
315 |
-
if i < total_chunks - 1
|
316 |
-
status_messages.append(f"⏱️ انتظار به مدت {sleep_slider} ثانیه...")
|
|
|
317 |
|
318 |
-
progress(0.85, desc="پردازش فایلهای
|
319 |
if not generated_audio_files:
|
320 |
status_messages.append("❌ هیچ فایل صوتی با موفقیت تولید یا ذخیره نشد!")
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
if
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
try: os.remove(file_p); status_messages.append(f" 🗑️ حذف شد: {os.path.basename(file_p)}")
|
335 |
-
except Exception as e_del: status_messages.append(f" ⚠️ عدم موفقیت در حذف {os.path.basename(file_p)}: {e_del}")
|
336 |
else:
|
337 |
-
status_messages.append("
|
338 |
-
|
339 |
-
|
340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
341 |
elif len(generated_audio_files) == 1:
|
342 |
-
|
343 |
-
|
344 |
-
status_messages.append(f"🎵 فایل صوتی تکی: {os.path.basename(
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
final_status = "\n".join(status_messages)
|
352 |
print(final_status)
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
FLY_PRIMARY_COLOR_HEX = "#2563EB"; FLY_SECONDARY_COLOR_HEX = "#059669"; FLY_ACCENT_COLOR_HEX = "#D97706";
|
358 |
-
FLY_TEXT_COLOR_HEX = "#111827"; FLY_SUBTLE_TEXT_HEX = "#4B5563"; FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB";
|
359 |
-
FLY_WHITE_HEX = "#FFFFFF"; FLY_BORDER_COLOR_HEX = "#E5E7EB"; FLY_INPUT_BG_HEX = "#FFFFFF";
|
360 |
-
custom_css_v2 = f"""
|
361 |
-
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
362 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
363 |
-
:root {{
|
364 |
-
--font-persian: 'Vazirmatn', 'Inter', sans-serif; --font-english: 'Inter', sans-serif;
|
365 |
-
--primary-color: {FLY_PRIMARY_COLOR_HEX}; --secondary-color: {FLY_SECONDARY_COLOR_HEX};
|
366 |
-
--accent-color: {FLY_ACCENT_COLOR_HEX}; --text-color: {FLY_TEXT_COLOR_HEX};
|
367 |
-
--subtle-text-color: {FLY_SUBTLE_TEXT_HEX}; --light-bg-color: {FLY_LIGHT_BACKGROUND_HEX};
|
368 |
-
--white-color: {FLY_WHITE_HEX}; --border-color: {FLY_BORDER_COLOR_HEX};
|
369 |
-
--input-bg-color: {FLY_INPUT_BG_HEX};
|
370 |
-
--radius-sm: 0.375rem; --radius-md: 0.625rem; --radius-lg: 0.875rem;
|
371 |
-
--shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
|
372 |
-
--shadow-md: 0 4px 8px -2px rgba(0,0,0,0.08), 0 2px 4px -2px rgba(0,0,0,0.05);
|
373 |
-
--shadow-lg: 0 12px 20px -4px rgba(0,0,0,0.08), 0 4px 8px -3px rgba(0,0,0,0.05);
|
374 |
-
--transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
375 |
-
}}
|
376 |
-
body, .gradio-container {{ font-family: var(--font-persian); direction: rtl; background-color: var(--light-bg-color); color: var(--text-color); line-height: 1.7; font-size: 16px; scroll-behavior: smooth; }}
|
377 |
-
.gradio-container {{ max-width: 100% !important; min-height: 100vh; margin:0 auto !important; padding:0 !important; border-radius:0 !important; box-shadow:none !important; }}
|
378 |
-
.app-header-card {{ padding: 2.5rem 1.5rem; margin:0; background: linear-gradient(140deg, var(--primary-color) 10%, var(--secondary-color) 90%); color: var(--white-color); border-bottom-left-radius: var(--radius-lg); border-bottom-right-radius: var(--radius-lg); box-shadow: var(--shadow-lg); text-align: center; position:relative; overflow:hidden; }}
|
379 |
-
.app-header-card::before {{ content:''; position:absolute; top:-60px; left:-60px; width:200px; height:200px; background:rgba(255,255,255,0.07); border-radius:50%; filter:blur(10px); animation: pulse-bubble 8s infinite ease-in-out; }}
|
380 |
-
.app-header-card::after {{ content:''; position:absolute; bottom:-70px; right:-70px; width:250px; height:250px; background:rgba(255,255,255,0.05); border-radius:45% 55% 60% 40% / 40% 50% 50% 60% ; filter:blur(15px); animation: pulse-bubble 10s infinite ease-in-out reverse; }}
|
381 |
-
@keyframes pulse-bubble {{ 0%, 100% {{ transform: scale(1); opacity: 0.05; }} 50% {{ transform: scale(1.1); opacity: 0.1; }} }}
|
382 |
-
.app-header-card h1 {{ font-size: 2.2em !important; font-weight: 800 !important; margin-bottom: 0.6rem; text-shadow: 0 2px 5px rgba(0,0,0,0.15); animation: slideInDown 0.8s ease-out; }}
|
383 |
-
.app-header-card .app-subtitle {{ font-size: 1.05em !important; opacity: 0.9; animation: fadeInUp 0.8s 0.2s ease-out backwards; }}
|
384 |
-
.main-content-wrapper {{ padding: 1.5rem 1rem; width:100%; max-width: 960px; margin: -2.5rem auto 2.5rem auto; position:relative; z-index:10; }}
|
385 |
-
.content-panel {{ background-color: var(--white-color); padding: 2rem 1.75rem; border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); animation: zoomIn 0.6s ease-out; }}
|
386 |
-
.section-title {{ font-size: 1.3em; font-weight: 700; color: var(--primary-color); margin-bottom: 1.2rem; border-bottom: 3px solid var(--primary-color); padding-bottom: 0.6rem; display:inline-block; }}
|
387 |
-
.gr-button.lg.primary, button[variant="primary"].generate-button-main {{ background: linear-gradient(135deg, var(--accent-color) 0%, color-mix(in srgb, var(--accent-color) 80%, #A15E00) 100%) !important; color: var(--white-color) !important; font-weight: 700 !important; border-radius: var(--radius-md) !important; border: none !important; box-shadow: 0 3px 6px rgba(0,0,0,0.1), 0 1px 3px rgba(0,0,0,0.08) !important; padding: 0.85rem 1.8rem !important; font-size: 1.05em !important; transition: var(--transition-ease); transform: perspective(1px) translateZ(0); }}
|
388 |
-
.gr-button.lg.primary:hover, button[variant="primary"].generate-button-main:hover {{ background: linear-gradient(135deg, color-mix(in srgb, var(--accent-color) 90%, black) 0%, color-mix(in srgb, var(--accent-color) 70%, #A15E00) 100%) !important; transform: translateY(-2px) scale(1.02); box-shadow: 0 6px 12px rgba(0,0,0,0.12), 0 3px 6px rgba(0,0,0,0.1) !important; }}
|
389 |
-
.gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-number > label + div > input[type="number"] {{ border-radius: var(--radius-md) !important; border: 1.5px solid var(--border-color) !important; background-color: var(--input-bg-color) !important; padding: 0.7rem 0.85rem !important; font-size: 0.98em !important; transition: var(--transition-ease); }}
|
390 |
-
.gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-dropdown > label + div > div > select:focus, .gr-textbox > label + div > textarea:focus, .gr-number > label + div > input[type="number"]:focus {{ border-color: var(--primary-color) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 20%, transparent) !important; background-color: var(--white-color) !important; }}
|
391 |
-
label > .label-text {{ font-weight: 600 !important; color: var(--subtle-text-color) !important; font-size: 0.92em !important; margin-bottom: 0.4rem !important; }}
|
392 |
-
.gr-accordion > .gr-button {{ background-color: var(--light-bg-color) !important; border-radius: var(--radius-md) !important; font-weight: 600 !important; padding: 0.6rem 0.8rem !important; transition: var(--transition-ease); border: 1px solid var(--border-color) !important; }}
|
393 |
-
.gr-accordion > .gr-button:hover {{ background-color: color-mix(in srgb, var(--light-bg-color) 90%, var(--border-color)) !important; }}
|
394 |
-
.gr-accordion > .gr-panel {{ background-color: color-mix(in srgb, var(--light-bg-color) 97%, var(--border-color)) !important; border-radius: var(--radius-md) !important; padding: 1.2rem !important; margin-top:0.5rem; border: 1px solid var(--border-color); transition: var(--transition-ease); }}
|
395 |
-
.status-log-panel {{ background-color: var(--input-bg-color); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 0.75rem 1rem; min-height: 180px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.04); }}
|
396 |
-
.status-log-panel textarea {{ background-color: transparent !important; border: none !important; font-size: 0.88em !important; color: var(--subtle-text-color); line-height:1.6; }}
|
397 |
-
.api-warning-box {{ background-color: color-mix(in srgb, var(--accent-color) 10%, #fff) !important; color: color-mix(in srgb, var(--accent-color) 85%, black) !important; padding: 1rem 1.2rem !important; border-radius: var(--radius-md) !important; border: 1.5px solid color-mix(in srgb, var(--accent-color) 40%, transparent) !important; text-align: center !important; margin-bottom: 1.5rem !important; font-size: 0.92em !important; box-shadow: var(--shadow-sm); }}
|
398 |
-
.success-message-box {{ background-color: color-mix(in srgb, var(--secondary-color) 10%, #fff) !important; color: color-mix(in srgb, var(--secondary-color) 85%, black) !important; padding: 1rem 1.2rem !important; border-radius: var(--radius-md) !important; border: 1.5px solid color-mix(in srgb, var(--secondary-color) 40%, transparent) !important; text-align: center !important; margin-bottom: 1.5rem !important; font-size: 0.92em !important; box-shadow: var(--shadow-sm); }}
|
399 |
-
.app-footer-text {{ text-align: center; font-size: 0.88em; color: var(--subtle-text-color); margin-top: 3rem; padding: 1.5rem 0; border-top: 1px solid var(--border-color); }}
|
400 |
-
footer, .gradio-footer {{ display: none !important; visibility: hidden !important; }}
|
401 |
-
#output_audio_col, #output_download_col {{ padding-top:1.2rem; }}
|
402 |
-
.gr-form {{ gap: 1.5rem !important; }}
|
403 |
-
.compact-group .gr-form {{ gap: 0.9rem !important; }}
|
404 |
-
#examples-section .gr-sample-button {{ background-color: color-mix(in srgb, var(--secondary-color) 12%, transparent) !important; color: var(--secondary-color) !important; border-radius: var(--radius-sm) !important; font-size: 0.88em !important; padding: 0.4rem 0.7rem !important; border: 1.5px solid color-mix(in srgb, var(--secondary-color) 35%, transparent) !important; margin: 0.25rem !important; transition: var(--transition-ease); }}
|
405 |
-
#examples-section .gr-sample-button:hover {{ background-color: color-mix(in srgb, var(--secondary-color) 22%, transparent) !important; transform: translateY(-1px); box-shadow: var(--shadow-sm); }}
|
406 |
-
#examples-separator > div > hr, #examples-separator > div > p {{ margin-top: 2rem !important; margin-bottom: 1.5rem !important; height: 1.5px !important; background-color: var(--border-color) !important; border: none !important; opacity: 0.7; font-size:0 !important; }}
|
407 |
-
@media (max-width: 768px) {{ .main-content-wrapper {{ margin-top: -1.5rem; padding: 0.75rem; }} .content-panel {{ padding: 1.2rem; }} .app-header-card h1 {{ font-size: 1.8em !important; }} .app-header-card .app-subtitle {{ font-size: 0.95em !important; }} .section-title {{ font-size:1.15em; }} }}
|
408 |
-
@keyframes slideInDown {{ from {{ opacity: 0; transform: translateY(-20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
409 |
-
@keyframes fadeInUp {{ from {{ opacity: 0; transform: translateY(20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
410 |
-
@keyframes zoomIn {{ from {{ opacity: 0; transform: scale(0.95); }} to {{ opacity: 1; transform: scale(1); }} }}
|
411 |
-
"""
|
412 |
|
413 |
# --- تعریف رابط کاربری Gradio ---
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
gr.Markdown("<h3 class='section-title'>۲. تنظیمات پیشرفته صدا</h3>", elem_id="settings-section")
|
451 |
-
model_choices_farsi = [(MODEL_NAMES_FARSI.get(key, key), key) for key in MODELS]
|
452 |
-
model_name_dd = gr.Dropdown(choices=model_choices_farsi, label="🤖 انتخاب مدل Gemini (TTS)", value=MODELS[0], elem_id="model-selector", info="مدل پرو کیفیت بالاتری دارد اما ممکن است محدودیت بیشتری داشته باشد.")
|
453 |
-
|
454 |
-
speaker_choices_farsi = [(SPEAKER_VOICES_FARSI_SAMPLE.get(v, v) + f" ({v})", v) for v in SPEAKER_VOICES]
|
455 |
-
speaker_voice_dd = gr.Dropdown(choices=speaker_choices_farsi, label="🎤 انتخاب صدای گوینده", value="Charon", elem_id="speaker-selector")
|
456 |
-
|
457 |
-
temp_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.75, label="🌡️ دمای خلاقیت مدل", elem_id="temperature-slider", info="مقادیر بالاتر (نزدیک به ۱) تنوع بیشتری در صدا ایجاد میکنند.")
|
458 |
-
|
459 |
-
with gr.Accordion("جزئیات بیشتر (تقسیمبندی متن و نام فایل)", open=False, elem_id="advanced-settings-accordion"):
|
460 |
-
# ** اسلایدر max_chunk_size دوباره فعال شد **
|
461 |
-
max_chunk_size_slider_ui = gr.Slider(minimum=1000, maximum=5000, step=100, value=3800, label="🧩 حداکثر کاراکتر در هر قطعه API (برای TTS)", elem_id="chunk-size-slider", info="مقدار پیشنهادی: ۳۰۰۰ تا ۴۰۰۰ کاراکتر.")
|
462 |
-
sleep_slider = gr.Slider(minimum=0, maximum=5, step=0.25, value=0.5, label="⏱️ تاخیر بین درخواستها به API (ثانیه)", elem_id="sleep-slider", info="برای جلوگیری از خطای محدودیت درخواست.")
|
463 |
-
output_filename_tb = gr.Textbox(label="💾 نام پایه برای فایلهای خروجی (انگلیسی، بدون پسوند)", value="gemini_voice_output", elem_id="output-filename-input")
|
464 |
-
|
465 |
-
with gr.Group(elem_classes="compact-group", elem_id="merge-options-group"):
|
466 |
-
gr.Markdown("گزینههای ادغام (در صورت تولید بیش از یک قطعه صوتی):", elem_id="merge-options-title")
|
467 |
-
merge_cb = gr.Checkbox(label="🔗 ادغام خودکار قطعات صوتی به یک فایل WAV", value=True, visible=PYDUB_AVAILABLE, elem_id="merge-checkbox")
|
468 |
-
delete_partials_cb = gr.Checkbox(label="🗑️ حذف فایلهای قطعهبندی شده پس از ادغام موفق", value=True, visible=PYDUB_AVAILABLE, elem_id="delete-partials-checkbox")
|
469 |
-
if PYDUB_AVAILABLE:
|
470 |
-
merge_cb.change(lambda x: gr.update(visible=x), [merge_cb], [delete_partials_cb])
|
471 |
-
else:
|
472 |
-
gr.HTML("<div class='api-warning-box' style='background-color: #FEF3C7 !important; color: #92400E !important; border-color: #FDE68A !important; margin-top:0.5rem;'>⚠️ قابلیت ادغام فایلها به دلیل عدم دسترسی به کتابخانه <code>pydub</code> غیرفعال است. صداها به صورت جداگانه ذخیره خواهند شد.</div>")
|
473 |
-
|
474 |
-
submit_btn = gr.Button("✨ هماکنون صدا را تولید کن! ✨", variant="primary", elem_id="generate-button-main", elem_classes=["generate-button-main"])
|
475 |
-
|
476 |
-
with gr.Accordion("🎧 نتیجه و گزارش فرآیند 📊", open=True, elem_id="output-report-accordion"):
|
477 |
-
with gr.Row():
|
478 |
-
with gr.Column(scale=1, elem_id="output_audio_col"):
|
479 |
-
output_audio_player = gr.Audio(label="🔊 فایل صوتی نهایی (قابل پخش)", type="filepath", autoplay=True, elem_id="audio-player-output", show_label=True)
|
480 |
-
with gr.Column(scale=1, elem_id="output_download_col"):
|
481 |
-
output_file_downloader = gr.File(label="📁 دانلود فایل نهایی (فرمت WAV یا ZIP)", type="filepath", elem_id="file-downloader-output", show_label=True)
|
482 |
-
|
483 |
-
status_log_tb = gr.Textbox(label="📜 گزارش کامل وضعیت و پیامهای سیستم:", lines=10, interactive=False, text_align="right", elem_id="status-log-textbox", elem_classes=["status-log-panel"], show_label=True)
|
484 |
-
|
485 |
-
submit_btn.click(
|
486 |
-
fn=generate_audio_for_gradio,
|
487 |
-
inputs=[
|
488 |
-
use_file_cb, text_file_upload, speech_prompt_tb, text_to_speak_tb,
|
489 |
-
max_chunk_size_slider_ui, # ** ارسال مقدار اسلایدر به تابع **
|
490 |
-
sleep_slider, temp_slider,
|
491 |
-
model_name_dd, speaker_voice_dd, output_filename_tb,
|
492 |
-
merge_cb, delete_partials_cb
|
493 |
-
],
|
494 |
-
outputs=[output_audio_player, output_file_downloader, status_log_tb]
|
495 |
)
|
|
|
|
|
|
|
|
|
|
|
496 |
|
497 |
-
gr.
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
examples=[ # ** مقادیر max_chunk_size در مثالها بهروز شد **
|
502 |
-
[False, None, "یک راوی با صدایی گرم و دلنشین، مناسب برای کتاب صوتی.", "در زمانهای قدیم، در سرزمینی دور، پادشاهی عادل زندگی میکرد که مردمش او را بسیار دوست داشتند.", 3800, 0.5, 0.75, MODELS[0], "Charon", "داستان_پادشاه", True, True],
|
503 |
-
[False, None, "با لحنی پرشور و هیجانانگیز، مانند یک گزارشگر ورزشی.", "و گل! یک گل باورنکردنی در دقیقهی نود! تماشاگران به وجد آمدهاند!", 3500, 0.5, 0.8, MODELS[1], "Achernar", "گزارش_فوتبال", True, True],
|
504 |
-
],
|
505 |
-
fn=generate_audio_for_gradio,
|
506 |
-
inputs=[ # ** ورودی max_chunk_size_slider_ui اضافه شد **
|
507 |
-
use_file_cb, text_file_upload, speech_prompt_tb, text_to_speak_tb,
|
508 |
-
max_chunk_size_slider_ui, sleep_slider, temp_slider,
|
509 |
-
model_name_dd, speaker_voice_dd, output_filename_tb,
|
510 |
-
merge_cb, delete_partials_cb
|
511 |
-
],
|
512 |
-
outputs=[output_audio_player, output_file_downloader, status_log_tb],
|
513 |
-
cache_examples=False, elem_id="examples-section"
|
514 |
)
|
515 |
|
516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
|
518 |
|
519 |
if __name__ == "__main__":
|
520 |
-
if not PYDUB_AVAILABLE: print("هشدار: کتابخانه pydub نصب نشده یا کار نمیکند.")
|
521 |
-
if not HF_GEMINI_API_KEY: print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است.")
|
522 |
-
|
|
|
|
6 |
import struct
|
7 |
import time
|
8 |
import zipfile
|
9 |
+
from google import genai
|
10 |
+
from google.genai import types
|
|
|
11 |
|
12 |
# خواندن کلید API از Hugging Face Secrets
|
13 |
+
# این متغیر محیطی توسط Space در زمان اجرا اگر Secret تنظیم شده باشد، تزریق میشود.
|
14 |
HF_GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
15 |
|
16 |
try:
|
|
|
19 |
except ImportError:
|
20 |
PYDUB_AVAILABLE = False
|
21 |
print("⚠️ کتابخانه pydub در دسترس نیست. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
22 |
+
print("اگر ادغام فایلها مد نظر است، pydub را به requirements.txt اضافه کرده و از وجود ffmpeg در محیط اطمینان حاصل کنید.")
|
23 |
|
24 |
# --- ثابتها ---
|
25 |
+
SPEAKER_VOICES_FA = {
|
26 |
+
"آکیرد (زن)": "Achird", "زُبِنالجُنوبی (مرد)": "Zubenelgenubi", "ویندِمیاطریکس (زن)": "Vindemiatrix",
|
27 |
+
"سَعدالاَخبیه (مرد)": "Sadachbia", "سَعدالتَجر (زن)": "Sadaltager", "سولافات (مرد)": "Sulafat",
|
28 |
+
"لائومِدِیا (زن)": "Laomedeia", "آکِرنار (مرد)": "Achernar", "النِلام (زن)": "Alnilam",
|
29 |
+
"شِدار (مرد)": "Schedar", "گاکراکس (زن)": "Gacrux", "پولکِریما (مرد)": "Pulcherrima",
|
30 |
+
"آمبرِیِل (زن)": "Umbriel", "اَلجِیبا (مرد)": "Algieba", "دِسپینا (زن)": "Despina",
|
31 |
+
"اِرینومه (مرد)": "Erinome", "اَلجِنیب (زن)": "Algenib", "رأسالجاثی (مرد)": "Rasalthgeti",
|
32 |
+
"اوروس (زن)": "Orus", "آئوئِده (مرد)": "Aoede", "کالیرهوئه (زن)": "Callirrhoe",
|
33 |
+
"اوتونوئه (مرد)": "Autonoe", "اِنسِلادوس (زن)": "Enceladus", "یاپِتوس (مرد)": "Iapetus",
|
34 |
+
"زِفیر (زن)": "Zephyr", "پاک (مرد)": "Puck", "کارون (زن، پیشفرض)": "Charon",
|
35 |
+
"کوره (مرد)": "Kore", "فِنریر (زن)": "Fenrir", "لِدا (مرد)": "Leda"
|
|
|
36 |
}
|
37 |
+
MODELS_FA = {
|
38 |
+
"جمینای ۲.۵ فلش (سریعتر، کیفیت خوب)": "gemini-2.5-flash-preview-tts",
|
39 |
+
"جمینای ۲.۵ پرو (کندتر، کیفیت بالاتر)": "gemini-2.5-pro-preview-tts"
|
|
|
|
|
40 |
}
|
41 |
+
SPEAKER_VOICES_LIST = list(SPEAKER_VOICES_FA.keys())
|
42 |
+
MODELS_LIST = list(MODELS_FA.keys())
|
43 |
+
|
44 |
|
45 |
# --- توابع کمکی ---
|
46 |
def save_binary_file(file_name, data):
|
47 |
abs_file_name = os.path.abspath(file_name)
|
48 |
try:
|
49 |
+
with open(abs_file_name, "wb") as f:
|
50 |
+
f.write(data)
|
51 |
+
print(f"✅ فایل در مسیر زیر ذخیره شد: {abs_file_name}")
|
52 |
+
return abs_file_name
|
53 |
+
except Exception as e:
|
54 |
+
print(f"❌ خطا در ذخیره فایل {abs_file_name}: {e}")
|
55 |
+
return None
|
56 |
|
57 |
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
58 |
parameters = parse_audio_mime_type(mime_type)
|
59 |
+
bits_per_sample = parameters["bits_per_sample"]
|
60 |
+
sample_rate = parameters["rate"]
|
61 |
+
num_channels = 1
|
62 |
+
data_size = len(audio_data)
|
63 |
+
bytes_per_sample = bits_per_sample // 8
|
64 |
+
block_align = num_channels * bytes_per_sample
|
65 |
+
byte_rate = sample_rate * block_align
|
66 |
chunk_size = 36 + data_size
|
67 |
+
header = struct.pack(
|
68 |
+
"<4sI4s4sIHHIIHH4sI",
|
69 |
+
b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels,
|
70 |
+
sample_rate, byte_rate, block_align, bits_per_sample, b"data", data_size
|
71 |
+
)
|
72 |
+
return header + audio_data
|
73 |
|
74 |
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
75 |
+
bits_per_sample = 16
|
76 |
+
rate = 24000
|
77 |
if mime_type:
|
78 |
+
mime_type_lower = mime_type.lower()
|
79 |
+
parts = mime_type_lower.split(";")
|
80 |
for param in parts:
|
81 |
param = param.strip()
|
82 |
if param.startswith("rate="):
|
|
|
84 |
except: pass
|
85 |
elif param.startswith("audio/l"):
|
86 |
try:
|
87 |
+
potential_bits = param.split("l", 1)[1]
|
88 |
if potential_bits.isdigit(): bits_per_sample = int(potential_bits)
|
89 |
except: pass
|
90 |
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
91 |
|
92 |
def load_text_from_gr_file(file_obj):
|
93 |
+
if file_obj is None:
|
94 |
+
return "", "فایلی برای ورودی متن انتخاب نشده است."
|
95 |
try:
|
96 |
+
with open(file_obj.name, 'r', encoding='utf-8') as f:
|
97 |
+
content = f.read().strip()
|
98 |
+
if not content:
|
99 |
+
return "", "فایل متنی خالی است."
|
100 |
return content, f"متن با موفقیت از فایل '{os.path.basename(file_obj.name)}' ({len(content)} کاراکتر) بارگذاری شد."
|
101 |
+
except Exception as e:
|
102 |
+
return "", f"خطا در خواندن فایل متنی: {e}"
|
|
|
|
|
|
|
|
|
|
|
103 |
|
104 |
+
def smart_text_split(text, max_size=3800):
|
105 |
+
if len(text) <= max_size: return [text]
|
106 |
+
chunks, current_chunk = [], ""
|
107 |
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
|
|
108 |
for sentence in sentences:
|
109 |
+
if not sentence: continue
|
110 |
+
if len(current_chunk) + len(sentence) + 1 > max_size:
|
111 |
+
if current_chunk: chunks.append(current_chunk.strip())
|
112 |
+
if len(sentence) > max_size:
|
113 |
+
words = sentence.split(' ')
|
114 |
+
temp_part = ""
|
115 |
+
for word in words:
|
116 |
+
if len(temp_part) + len(word) + 1 > max_size:
|
117 |
+
if temp_part: chunks.append(temp_part.strip())
|
118 |
+
if len(word) > max_size:
|
119 |
+
for i in range(0, len(word), max_size): chunks.append(word[i:i+max_size])
|
120 |
+
temp_part = ""
|
121 |
+
else: temp_part = word
|
122 |
+
else: temp_part += (" " if temp_part else "") + word
|
123 |
+
if temp_part: chunks.append(temp_part.strip())
|
124 |
current_chunk = ""
|
125 |
+
else: current_chunk = sentence
|
126 |
+
else: current_chunk += (" " if current_chunk else "") + sentence
|
127 |
+
if current_chunk: chunks.append(current_chunk.strip())
|
128 |
+
return chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
|
130 |
def merge_audio_files_func(file_paths, output_path):
|
131 |
if not PYDUB_AVAILABLE: return False, "pydub در دسترس نیست. امکان ادغام فایلها وجود ندارد.", None
|
132 |
+
if not file_paths: return False, "فایل صوتی برای ادغام وجود ندارد.", None
|
133 |
try:
|
134 |
combined = AudioSegment.empty()
|
135 |
+
for i, fp in enumerate(file_paths):
|
136 |
+
if os.path.exists(fp):
|
137 |
try:
|
138 |
+
audio = AudioSegment.from_file(fp, format=fp.split('.')[-1]) # Guess format from extension
|
139 |
combined += audio
|
140 |
if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
|
141 |
except Exception as e_load:
|
142 |
+
return False, f"خطا در بارگذاری فایل صوتی '{os.path.basename(fp)}': {e_load}", None
|
143 |
+
else: return False, f"فایل برای ادغام یافت نشد: {os.path.basename(fp)}", None
|
|
|
|
|
|
|
144 |
abs_output_path = os.path.abspath(output_path)
|
145 |
combined.export(abs_output_path, format="wav")
|
146 |
+
return True, f"فایل ادغام شده با موفقیت در '{os.path.basename(abs_output_path)}' ذخیره شد.", abs_output_path
|
147 |
+
except Exception as e: return False, f"خطا در ادغام فایلها: {e}", None
|
148 |
+
|
|
|
149 |
def create_zip_file(file_paths, zip_name):
|
150 |
abs_zip_name = os.path.abspath(zip_name)
|
151 |
try:
|
152 |
with zipfile.ZipFile(abs_zip_name, 'w') as zipf:
|
153 |
+
for fp in file_paths:
|
154 |
+
if os.path.exists(fp): zipf.write(fp, os.path.basename(fp))
|
155 |
+
return True, f"فایل ZIP با موفقیت در '{os.path.basename(abs_zip_name)}' ایجاد شد.", abs_zip_name
|
|
|
156 |
except Exception as e: return False, f"خطا در ایجاد فایل ZIP: {e}", None
|
157 |
|
158 |
# --- تابع اصلی تولید صدا ---
|
159 |
def generate_audio_for_gradio(
|
160 |
+
use_file_input_checkbox, text_file_obj,
|
161 |
+
speech_prompt_input, text_to_speak_input,
|
162 |
+
max_chunk_slider, sleep_slider, temperature_slider,
|
163 |
+
model_dropdown_fa, speaker_dropdown_fa, output_filename_base_input,
|
164 |
+
merge_checkbox, delete_partials_checkbox,
|
165 |
progress=gr.Progress(track_tqdm=True)
|
166 |
):
|
167 |
+
status_messages = ["🚀 شروع فرآیند تبدیل متن به گفتار..."]
|
168 |
progress(0, desc="در حال آمادهسازی...")
|
169 |
|
170 |
api_key_to_use = HF_GEMINI_API_KEY
|
171 |
if not api_key_to_use:
|
172 |
+
status_messages.append("❌ خطا: کلید API جمینای (GEMINI_API_KEY) در Hugging Face Secrets یافت نشد.")
|
173 |
+
status_messages.append("⬅️ لطفاً آن را در بخش Settings > Secrets در تنظیمات Space خود اضافه کنید.")
|
174 |
return None, None, "\n".join(status_messages)
|
175 |
+
os.environ["GEMINI_API_KEY"] = api_key_to_use
|
176 |
+
status_messages.append("🔑 کلید API با موفقیت از Secrets بارگذاری شد.")
|
|
|
177 |
|
178 |
+
actual_text_input, msg = ("", "")
|
179 |
if use_file_input_checkbox:
|
180 |
if text_file_obj is None:
|
181 |
+
status_messages.append("❌ خطا: گزینه 'استفاده از فایل متنی' انتخاب شده، اما فایلی آپلود نشده است.")
|
182 |
return None, None, "\n".join(status_messages)
|
183 |
actual_text_input, msg = load_text_from_gr_file(text_file_obj)
|
|
|
|
|
184 |
else:
|
185 |
actual_text_input = text_to_speak_input
|
186 |
+
msg = "⌨️ استفاده از متن وارد شده دستی."
|
187 |
+
status_messages.append(msg)
|
188 |
+
if not actual_text_input: return None, None, "\n".join(status_messages)
|
189 |
|
190 |
+
try:
|
191 |
+
status_messages.append("🛠️ در حال مقداردهی اولیه کلاینت جمینای...")
|
192 |
+
progress(0.1, desc="مقداردهی کلاینت جمینای...")
|
193 |
+
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
|
194 |
+
status_messages.append("✅ کلاینت جمینای با موفقیت مقداردهی شد.")
|
195 |
+
except Exception as e:
|
196 |
+
status_messages.append(f"❌ خطا در مقداردهی کلاینت جمینای: {e}")
|
197 |
+
return None, None, "\n".join(status_messages)
|
198 |
|
199 |
+
text_chunks = smart_text_split(actual_text_input, int(max_chunk_slider))
|
200 |
+
status_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد.")
|
201 |
+
for i, chunk_text in enumerate(text_chunks): status_messages.append(f" 📝 قطعه {i+1}: {len(chunk_text)} کاراکتر")
|
|
|
202 |
|
203 |
+
generated_audio_files, run_id = [], base64.urlsafe_b64encode(os.urandom(6)).decode()
|
204 |
+
temp_output_dir = f"temp_audio_{run_id}"
|
205 |
+
os.makedirs(temp_output_dir, exist_ok=True)
|
206 |
output_base_name_safe = re.sub(r'[\s\\\/\:\*\?\"\<\>\|\%]+', '_', output_filename_base_input)
|
207 |
|
208 |
+
# Map selected FA names to actual API names
|
209 |
+
selected_model_api_name = MODELS_FA[model_dropdown_fa]
|
210 |
+
selected_speaker_api_name = SPEAKER_VOICES_FA[speaker_dropdown_fa]
|
211 |
+
|
212 |
total_chunks = len(text_chunks)
|
213 |
for i, chunk_text_content in enumerate(text_chunks):
|
214 |
+
progress_val = 0.1 + (0.7 * (i / total_chunks))
|
215 |
+
progress(progress_val, desc=f"در حال تولید قطعه {i+1} از {total_chunks}...")
|
216 |
|
217 |
+
status_messages.append(f"\n🔊 تولید صدا برای قطعه {i+1}/{total_chunks}...")
|
218 |
+
final_text_for_api = f'"{speech_prompt_input}"\n{chunk_text_content}' if speech_prompt_input.strip() else chunk_text_content
|
|
|
|
|
|
|
|
|
219 |
|
220 |
+
contents_for_api = [types.Content(role="user", parts=[types.Part.from_text(text=final_text_for_api)])]
|
221 |
+
generate_content_config = types.GenerateContentConfig(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
222 |
temperature=float(temperature_slider),
|
223 |
+
response_modalities=["audio"],
|
224 |
+
speech_config=types.SpeechConfig(
|
225 |
voice_config=types.VoiceConfig(
|
226 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_speaker_api_name)
|
|
|
|
|
227 |
)
|
228 |
+
)
|
229 |
)
|
230 |
+
audio_data_received = False
|
|
|
231 |
try:
|
232 |
+
for stream_resp_chunk in client.models.generate_content_stream(
|
233 |
+
model=selected_model_api_name, contents=contents_for_api, config=generate_content_config,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
234 |
):
|
235 |
+
if (stream_resp_chunk.candidates and stream_resp_chunk.candidates[0].content and
|
236 |
+
stream_resp_chunk.candidates[0].content.parts and
|
237 |
+
stream_resp_chunk.candidates[0].content.parts[0].inline_data):
|
238 |
+
|
239 |
+
inline_data = stream_resp_chunk.candidates[0].content.parts[0].inline_data
|
240 |
data_buffer, api_mime_type = inline_data.data, inline_data.mime_type
|
241 |
audio_data_received = True
|
242 |
status_messages.append(f"ℹ️ MIME Type دریافتی از API: {api_mime_type}")
|
243 |
+
|
244 |
+
file_ext = ".wav" # پیشفرض wav و تبدیل
|
245 |
if api_mime_type and ("mp3" in api_mime_type.lower() or "mpeg" in api_mime_type.lower()):
|
246 |
+
file_ext = ".mp3"
|
247 |
+
status_messages.append(f"ℹ️ ذخیره به صورت MP3 بر اساس MIME: {api_mime_type}")
|
248 |
elif api_mime_type and "wav" in api_mime_type.lower() and not ("audio/l16" in api_mime_type.lower() or "audio/l24" in api_mime_type.lower()):
|
249 |
+
file_ext = ".wav"
|
250 |
+
status_messages.append(f"ℹ️ ذخیره به صورت WAV بر اساس MIME: {api_mime_type}")
|
251 |
else:
|
252 |
+
status_messages.append(f"ℹ️ تبدیل به WAV برای MIME: {api_mime_type or 'نامشخص'}")
|
253 |
data_buffer = convert_to_wav(data_buffer, api_mime_type)
|
254 |
+
|
255 |
+
status_messages.append(f"ℹ️ پسوند فایل تعیین شده: {file_ext}")
|
256 |
+
chunk_fp_prefix = os.path.join(temp_output_dir, f"{output_base_name_safe}_part_{i+1:03d}")
|
257 |
+
gen_file_path = save_binary_file(f"{chunk_fp_prefix}{file_ext}", data_buffer)
|
258 |
+
|
259 |
+
if gen_file_path:
|
260 |
+
generated_audio_files.append(gen_file_path)
|
261 |
+
status_messages.append(f"✅ قطعه {i+1} ذخیره شد: {os.path.basename(gen_file_path)}")
|
262 |
else: status_messages.append(f"❌ عدم موفقیت در ذخیره قطعه {i+1}.")
|
263 |
break
|
264 |
+
|
265 |
+
elif stream_resp_chunk.text: status_messages.append(f"ℹ️ پیام متنی از API (حین استریم): {stream_resp_chunk.text}")
|
266 |
+
|
267 |
if not audio_data_received:
|
268 |
+
status_messages.append(f"❌ داده صوتی برای قطعه {i+1} در استریم دریافت نشد.")
|
269 |
+
if stream_resp_chunk and stream_resp_chunk.prompt_feedback and stream_resp_chunk.prompt_feedback.block_reason:
|
270 |
+
status_messages.append(f"🛑 دلیل بلاک شدن توسط API: {stream_resp_chunk.prompt_feedback.block_reason_message or stream_resp_chunk.prompt_feedback.block_reason}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
271 |
|
272 |
+
except Exception as e:
|
273 |
+
status_messages.append(f"❌ خطا در تولید/پردازش قطعه {i+1}: {e}")
|
274 |
+
import traceback; status_messages.append(traceback.format_exc())
|
275 |
+
continue
|
276 |
|
277 |
+
if i < total_chunks - 1:
|
278 |
+
status_messages.append(f"⏱️ انتظار به مدت {sleep_slider} ثانیه...")
|
279 |
+
time.sleep(float(sleep_slider))
|
280 |
|
281 |
+
progress(0.85, desc="پردازش فایلهای تولید شده...")
|
282 |
if not generated_audio_files:
|
283 |
status_messages.append("❌ هیچ فایل صوتی با موفقیت تولید یا ذخیره نشد!")
|
284 |
+
final_status = "\n".join(status_messages)
|
285 |
+
print(final_status)
|
286 |
+
progress(1, desc="پایان با خطا.")
|
287 |
+
return None, None, final_status
|
288 |
+
status_messages.append(f"\n🎉 {len(generated_audio_files)} فایل صوتی تولید شد!")
|
289 |
+
|
290 |
+
out_audio_player_path, out_download_path = None, None
|
291 |
+
if merge_checkbox and len(generated_audio_files) > 1:
|
292 |
+
if not PYDUB_AVAILABLE:
|
293 |
+
status_messages.append("⚠️ pydub در دسترس نیست. امکان ادغام وجود ندارد. فایل ZIP قطعات ارائه میشود.")
|
294 |
+
success, msg, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
|
295 |
+
status_messages.append(msg)
|
296 |
+
if success: out_download_path = zip_p
|
|
|
|
|
297 |
else:
|
298 |
+
status_messages.append(f"🔗 در حال ادغام {len(generated_audio_files)} فایل...")
|
299 |
+
merged_fp = os.path.join(temp_output_dir, f"{output_base_name_safe}_merged.wav")
|
300 |
+
success, msg, merged_p = merge_audio_files_func(generated_audio_files, merged_fp)
|
301 |
+
status_messages.append(msg)
|
302 |
+
if success:
|
303 |
+
out_audio_player_path, out_download_path = merged_p, merged_p
|
304 |
+
if delete_partials_checkbox:
|
305 |
+
status_messages.append("🗑️ در حال حذف فایلهای جزئی...")
|
306 |
+
for fp in generated_audio_files:
|
307 |
+
try: os.remove(fp); status_messages.append(f" 🗑️ حذف شد: {os.path.basename(fp)}")
|
308 |
+
except Exception as e_del: status_messages.append(f" ⚠️ عدم موفقیت در حذف {os.path.basename(fp)}: {e_del}")
|
309 |
+
else:
|
310 |
+
status_messages.append("⚠️ ادغام ناموفق بود. فایل ZIP قطعات ارائه میشود.")
|
311 |
+
success_zip, msg_zip, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
|
312 |
+
status_messages.append(msg_zip)
|
313 |
+
if success_zip: out_download_path = zip_p
|
314 |
elif len(generated_audio_files) == 1:
|
315 |
+
single_fp = generated_audio_files[0]
|
316 |
+
out_audio_player_path, out_download_path = single_fp, single_fp
|
317 |
+
status_messages.append(f"🎵 فایل صوتی تکی: {os.path.basename(single_fp)}")
|
318 |
+
else:
|
319 |
+
status_messages.append("📦 چندین قطعه تولید شده است. در حال ایجاد فایل ZIP.")
|
320 |
+
success, msg, zip_p = create_zip_file(generated_audio_files, os.path.join(temp_output_dir, f"{output_base_name_safe}_all_parts.zip"))
|
321 |
+
status_messages.append(msg)
|
322 |
+
if success: out_download_path = zip_p
|
323 |
+
|
324 |
final_status = "\n".join(status_messages)
|
325 |
print(final_status)
|
326 |
+
print(f"DEBUG: مسیر فایل برای پخشکننده: {out_audio_player_path}")
|
327 |
+
print(f"DEBUG: مسیر فایل برای دانلود: {out_download_path}")
|
328 |
+
progress(1, desc="پایان!")
|
329 |
+
return out_audio_player_path, out_download_path, final_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
330 |
|
331 |
# --- تعریف رابط کاربری Gradio ---
|
332 |
+
css = """
|
333 |
+
body { font-family: 'Vazirmatn', 'Tahoma', sans-serif; direction: rtl; }
|
334 |
+
.gradio-container { max-width: 900px !important; margin: auto !important; }
|
335 |
+
footer { display: none !important; } /* Hide default Gradio footer */
|
336 |
+
.gr-button { font-weight: bold; }
|
337 |
+
.st-emotion-cache-1uj092c, .st-emotion-cache-1wnczdq { font-family: 'Vazirmatn', 'Tahoma', sans-serif !important; } /* Forcing font on some elements */
|
338 |
+
.rtl-override { direction: rtl !important; text-align: right !important; }
|
339 |
+
.rtl-override input, .rtl-override textarea, .rtl-override select { direction: rtl !important; text-align: right !important; }
|
340 |
+
label > .label-text { font-size: 1.1em !important; margin-bottom: 5px !important; }
|
341 |
+
.gr-input, .gr-dropdown, .gr-slider { margin-bottom: 10px !important; }
|
342 |
+
"""
|
343 |
+
|
344 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky, font=[gr.themes.GoogleFont("Vazirmatn"), "Tahoma", "sans-serif"]), css=css, title="تبدیل متن به گفتار با جمینای") as demo:
|
345 |
+
gr.Markdown("<h1 style='text-align: center; color: #2A7AF2;'>🎵 تبدیل متن به گفتار با API جمینای 🗣️</h1>", elem_classes=["rtl-override"])
|
346 |
+
|
347 |
+
if not HF_GEMINI_API_KEY:
|
348 |
+
gr.Warning(
|
349 |
+
"کلید API جمینای (GEMINI_API_KEY) در Hugging Face Secrets یافت نشد. "
|
350 |
+
"برای کارکرد صحیح اپلیکیشن، لطفاً آن را در بخش 'Settings' > 'Secrets' این Space با نام `GEMINI_API_KEY` اضافه کنید."
|
351 |
+
)
|
352 |
+
else:
|
353 |
+
gr.Info("کلید API جمینای با موفقیت از Secrets بارگذاری شد. اپلیکیشن آماده تولید صدا است!")
|
354 |
+
|
355 |
+
gr.Markdown(
|
356 |
+
"این ابزار متن شما را با استفاده از مدلهای پیشرفته جمینای گوگل به گفتار تبدیل میکند. "
|
357 |
+
"مطمئن شوید که کلید API جمینای خود را در بخش Secrets این Space تنظیم کردهاید."
|
358 |
+
"\n\nمیتوانید کلید API خود را از [Google AI Studio](https://aistudio.google.com/app/apikey) دریافت کنید.",
|
359 |
+
elem_classes=["rtl-override"]
|
360 |
+
)
|
361 |
+
|
362 |
+
with gr.Row(elem_classes=["rtl-override"]):
|
363 |
+
with gr.Column(scale=2):
|
364 |
+
gr.Markdown("### ۱. ورودی متن", elem_classes=["rtl-override"])
|
365 |
+
use_file = gr.Checkbox(label="📁 استفاده از فایل متنی (.txt)", value=False, elem_classes=["rtl-override"])
|
366 |
+
text_file = gr.File(
|
367 |
+
label="بارگذاری فایل متنی", file_types=['.txt'], visible=False, elem_classes=["rtl-override"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
)
|
369 |
+
text_to_speak = gr.Textbox(
|
370 |
+
label="📝 متنی که میخواهید به گفتار تبدیل شود (یا از فایل بالا استفاده کنید):",
|
371 |
+
lines=8, placeholder="متن خود را اینجا وارد کنید...", visible=True, elem_classes=["rtl-override"]
|
372 |
+
)
|
373 |
+
use_file.change(lambda x: (gr.update(visible=x), gr.update(visible=not x)), [use_file], [text_file, text_to_speak])
|
374 |
|
375 |
+
speech_prompt = gr.Textbox(
|
376 |
+
label="🗣️ پرامپت راهنمای گفتار (اختیاری):",
|
377 |
+
placeholder="مثال: «با لحنی دوستانه و پرانرژی، مانند یک یوتیوبر»",
|
378 |
+
info="این پرامپت بر سبک، احساسات و ویژگیهای صدای خروجی تأثیر میگذارد.", elem_classes=["rtl-override"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
379 |
)
|
380 |
|
381 |
+
with gr.Column(scale=1):
|
382 |
+
gr.Markdown("### ۲. تنظیمات تولید صدا", elem_classes=["rtl-override"])
|
383 |
+
model_name_fa = gr.Dropdown(
|
384 |
+
MODELS_LIST, label="🤖 انتخاب مدل:", value=MODELS_LIST[0], elem_classes=["rtl-override"]
|
385 |
+
)
|
386 |
+
speaker_voice_fa = gr.Dropdown(
|
387 |
+
SPEAKER_VOICES_LIST, label="🎤 انتخاب گوینده:", value="کارون (زن، پیشفرض)", elem_classes=["rtl-override"]
|
388 |
+
)
|
389 |
+
temperature = gr.Slider(
|
390 |
+
minimum=0.0, maximum=1.0, step=0.05, value=0.7, label="🌡️ دما (Temperature):",
|
391 |
+
info="میزان خلاقیت و تنوع صدا (0.0 تا 1.0). مقادیر بالاتر تنوع بیشتری ایجاد میکنند.", elem_classes=["rtl-override"]
|
392 |
+
)
|
393 |
+
max_chunk_size = gr.Slider(
|
394 |
+
minimum=1000, maximum=4000, step=100, value=3800, label="🧩 حداکثر کاراکتر در هر قطعه:",
|
395 |
+
info="متن برای ارسال به API به قطعات کوچکتر تقسیم میشود.", elem_classes=["rtl-override"]
|
396 |
+
)
|
397 |
+
sleep_between_requests = gr.Slider(
|
398 |
+
minimum=1, maximum=15, step=0.5, value=3, label="⏱️ تاخیر بین درخواستها (ثانیه):",
|
399 |
+
info="برای مدیریت محدودیتهای API (مثلاً جمینای فلش ۶۰ درخواست در دقیقه).", elem_classes=["rtl-override"]
|
400 |
+
)
|
401 |
+
output_filename_base = gr.Textbox(
|
402 |
+
label="💾 نام پایه فایل خروجی:", value="صدای_جمینای", elem_classes=["rtl-override"]
|
403 |
+
)
|
404 |
+
|
405 |
+
with gr.Group(visible=PYDUB_AVAILABLE):
|
406 |
+
merge_audio = gr.Checkbox(label="🔗 ادغام قطعات صوتی (در صورت وجود بیش از یک قطعه)", value=True, elem_classes=["rtl-override"])
|
407 |
+
delete_partials = gr.Checkbox(label="🗑️ حذف قطعات پس از ادغام", value=True, visible=True, elem_classes=["rtl-override"])
|
408 |
+
merge_audio.change(lambda x: gr.update(visible=x), [merge_audio], [delete_partials])
|
409 |
+
|
410 |
+
if not PYDUB_AVAILABLE:
|
411 |
+
gr.Markdown("<small style='color: orange;'>⚠️ قابلیت ادغام غیرفعال است: کتابخانه `pydub` یافت نشد.</small>", elem_classes=["rtl-override"])
|
412 |
+
|
413 |
+
submit_button = gr.Button("✨ تولید صدا ✨", variant="primary", elem_classes=["rtl-override"], scale=2)
|
414 |
+
gr.Markdown("---", elem_classes=["rtl-override"])
|
415 |
+
gr.Markdown("### ۳. خروجی و گزارش", elem_classes=["rtl-override"])
|
416 |
+
|
417 |
+
with gr.Row(elem_classes=["rtl-override"]):
|
418 |
+
with gr.Column(scale=1):
|
419 |
+
output_audio_player = gr.Audio(label="🎧 فایل صوتی تولید شده:", type="filepath", format="wav")
|
420 |
+
with gr.Column(scale=1):
|
421 |
+
output_file_download = gr.File(label="📥 دانلود فایل خروجی:", type="filepath")
|
422 |
+
|
423 |
+
status_textbox = gr.Textbox(label="📊 گزارش وضعیت:", lines=10, interactive=False, max_lines=20, elem_classes=["rtl-override"])
|
424 |
+
|
425 |
+
submit_button.click(
|
426 |
+
fn=generate_audio_for_gradio,
|
427 |
+
inputs=[
|
428 |
+
use_file, text_file, speech_prompt, text_to_speak,
|
429 |
+
max_chunk_size, sleep_between_requests, temperature,
|
430 |
+
model_name_fa, speaker_voice_fa, output_filename_base, # Use FA dropdowns
|
431 |
+
merge_audio, delete_partials
|
432 |
+
],
|
433 |
+
outputs=[output_audio_player, output_file_download, status_textbox]
|
434 |
+
)
|
435 |
+
|
436 |
+
gr.Markdown("---", elem_classes=["rtl-override"])
|
437 |
+
# The encoded text part:
|
438 |
+
encoded_text_creator = "Q3JlYXRlIGJ5IDogYWlnb2xkZW4=" # "Created by : aigolden"
|
439 |
+
try:
|
440 |
+
decoded_text_creator = base64.b64decode(encoded_text_creator.encode('utf-8')).decode('utf-8')
|
441 |
+
gr.Markdown(f"<p style='text-align:center; font-size:small; color: #555;'><em>{decoded_text_creator} | ترجمه و بهبود توسط مدل هوش مصنوعی</em></p>", elem_classes=["rtl-override"])
|
442 |
+
except: pass
|
443 |
+
|
444 |
+
gr.Examples(
|
445 |
+
examples=[
|
446 |
+
[False, None, "راوی با لحنی دوستانه و آموزنده.", "سلام دنیا! این یک آزمایش برای تبدیل متن به گفتار با استفاده از جمینای و گرادیو است. امیدوارم به خوبی کار کند!", 3800, 3, 0.7, MODELS_LIST[0], "کارون (زن، پیشفرض)", "مثال_سلام", True, True],
|
447 |
+
[False, None, "گوینده خبر هیجانزده.", "خبر فوری! هوش مصنوعی اکنون میتواند گفتاری شبیه به انسان تولید کند. این فناوری به سرعت در حال پیشرفت است!", 3000, 3, 0.8, MODELS_LIST[1], "آکِرنار (مرد)", "مثال_خبر", True, True],
|
448 |
+
[True, "sample_text.txt", "داستانگویی با لحنی آرام.", "", 3500, 4, 0.6, MODELS_LIST[0], "ویندِمیاطریکس (زن)", "مثال_از_فایل", True, False]
|
449 |
+
],
|
450 |
+
fn=generate_audio_for_gradio,
|
451 |
+
inputs=[
|
452 |
+
use_file, text_file, speech_prompt, text_to_speak,
|
453 |
+
max_chunk_size, sleep_between_requests, temperature,
|
454 |
+
model_name_fa, speaker_voice_fa, output_filename_base,
|
455 |
+
merge_audio, delete_partials
|
456 |
+
],
|
457 |
+
outputs=[output_audio_player, output_file_download, status_textbox],
|
458 |
+
cache_examples=False, # API calls
|
459 |
+
label="نمونههای آماده (برای استفاده از مثال فایل، فایل sample_text.txt باید موجود باشد):",
|
460 |
+
elem_classes=["rtl-override"]
|
461 |
+
)
|
462 |
+
gr.Markdown("<small style='display: block; text-align: center;'>برای استفاده از مثال «فایل متنی نمونه»، ابتدا یک فایل با نام `sample_text.txt` حاوی متن دلخواه در ریشه این Space ایجاد کنید، یا فایل متنی خود را بارگذاری نمایید.</small>", elem_classes=["rtl-override"])
|
463 |
|
464 |
|
465 |
if __name__ == "__main__":
|
466 |
+
if not PYDUB_AVAILABLE: print("هشدار: کتابخانه pydub نصب نشده یا کار نمیکند. ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
467 |
+
if not HF_GEMINI_API_KEY: print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است. اپلیکیشن در اجرای محلی ممکن است بدون کلید API کار نکند.")
|
468 |
+
|
469 |
+
demo.launch(debug=True, share=False)
|