Update app.py
Browse files
app.py
CHANGED
@@ -1,76 +1,25 @@
|
|
1 |
import gradio as gr
|
|
|
|
|
2 |
import os
|
|
|
|
|
3 |
import time
|
4 |
-
import
|
5 |
-
import
|
|
|
6 |
import logging
|
7 |
-
import traceback
|
8 |
-
import asyncio # For first script's key rotation, though TTS part is sync
|
9 |
|
10 |
-
|
11 |
-
import base64 # Not directly used in final combined code, but was in original TTS parts
|
12 |
-
import mimetypes
|
13 |
-
import re
|
14 |
-
import struct
|
15 |
-
import zipfile # Not directly used in final combined code
|
16 |
-
from google import genai # For TTS
|
17 |
-
from google.genai import types as genai_types # For TTS
|
18 |
|
19 |
try:
|
20 |
from pydub import AudioSegment
|
21 |
PYDUB_AVAILABLE = True
|
|
|
22 |
except ImportError:
|
23 |
PYDUB_AVAILABLE = False
|
24 |
-
logging.warning("
|
25 |
-
|
26 |
-
# --- START: پیکربندی لاگینگ (From Alpha Translator) ---
|
27 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
|
28 |
-
# --- END: پیکربندی لاگینگ ---
|
29 |
-
|
30 |
-
# --- START: منطق چرخش API Key برای Gemini (Adapted from Alpha Translator for synchronous TTS client) ---
|
31 |
-
API_KEYS_GEMINI = []
|
32 |
-
i = 1
|
33 |
-
while True:
|
34 |
-
key = os.environ.get(f'GEMINI_API_KEY_{i}')
|
35 |
-
if key:
|
36 |
-
API_KEYS_GEMINI.append(key)
|
37 |
-
i += 1
|
38 |
-
else:
|
39 |
-
break
|
40 |
|
41 |
-
NUM_GEMINI_KEYS = len(API_KEYS_GEMINI)
|
42 |
-
current_gemini_key_index = 0
|
43 |
-
gemini_key_lock = threading.Lock() # Use threading.Lock for synchronous operations
|
44 |
-
|
45 |
-
if NUM_GEMINI_KEYS == 0:
|
46 |
-
logging.error(
|
47 |
-
'خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n (مثلاً GEMINI_API_KEY_1) یافت نشد! ' +
|
48 |
-
'قابلیت تبدیل متن به گفتار غیرفعال خواهد بود. لطفاً Secret ها را در تنظیمات Space خود اضافه کنید.'
|
49 |
-
)
|
50 |
-
else:
|
51 |
-
logging.info(f"تعداد {NUM_GEMINI_KEYS} کلید API جیمینای بارگذاری شد.")
|
52 |
-
|
53 |
-
def get_gemini_api_key_sync():
|
54 |
-
if NUM_GEMINI_KEYS == 0:
|
55 |
-
return None
|
56 |
-
with gemini_key_lock:
|
57 |
-
global current_gemini_key_index
|
58 |
-
selected_api_key = API_KEYS_GEMINI[current_gemini_key_index]
|
59 |
-
current_gemini_key_index = (current_gemini_key_index + 1) % NUM_GEMINI_KEYS
|
60 |
-
logging.info(f"TTS Gemini: استفاده از کلید API با اندیس چرخشی: ...{selected_api_key[-4:]}")
|
61 |
-
return selected_api_key
|
62 |
-
# --- END: منطق چرخش API Key ---
|
63 |
-
|
64 |
-
# --- START: تابع ریاستارت خودکار (From Alpha Translator) ---
|
65 |
-
def auto_restart_service():
|
66 |
-
RESTART_INTERVAL_SECONDS = 24 * 60 * 60 # 24 ساعت
|
67 |
-
logging.info(f"سرویس برای ریاستارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمانبندی شده است.")
|
68 |
-
time.sleep(RESTART_INTERVAL_SECONDS)
|
69 |
-
logging.info(f"زمان ریاستارت خودکار ({RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت) فرا رسیده است. برنامه خارج میشود تا توسط پلتفرم ریاستارت شود...")
|
70 |
-
os._exit(1) # خروج فوری برای تحریک ریاستارت توسط پلتفرم
|
71 |
-
# --- END: تابع ریاستارت خودکار ---
|
72 |
-
|
73 |
-
# --- START: TTS Core Logic (From Alpha TTS, adapted for key rotation) ---
|
74 |
SPEAKER_VOICES = [
|
75 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
76 |
"Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
|
@@ -78,22 +27,22 @@ SPEAKER_VOICES = [
|
|
78 |
"Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
|
79 |
"Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
80 |
]
|
81 |
-
FIXED_MODEL_NAME = "gemini-
|
82 |
DEFAULT_MAX_CHUNK_SIZE = 3800
|
83 |
-
DEFAULT_SLEEP_BETWEEN_REQUESTS =
|
84 |
-
DEFAULT_OUTPUT_FILENAME_BASE = "
|
85 |
|
86 |
-
def
|
87 |
log_list_ref.append(message)
|
88 |
-
logging.info(f"[
|
89 |
|
90 |
def save_binary_file(file_name, data, log_list_ref):
|
91 |
try:
|
92 |
with open(file_name, "wb") as f: f.write(data)
|
93 |
-
|
94 |
return file_name
|
95 |
except Exception as e:
|
96 |
-
|
97 |
return None
|
98 |
|
99 |
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
@@ -120,481 +69,276 @@ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
|
|
120 |
def smart_text_split(text, max_size=3800, log_list_ref=None):
|
121 |
if len(text) <= max_size: return [text]
|
122 |
chunks, current_chunk = [], ""
|
123 |
-
sentences = re.split(r'(?<=[
|
124 |
for sentence in sentences:
|
125 |
if len(current_chunk) + len(sentence) + 1 > max_size:
|
126 |
if current_chunk: chunks.append(current_chunk.strip())
|
127 |
current_chunk = sentence
|
128 |
-
while len(current_chunk) > max_size:
|
129 |
-
split_idx = -1
|
130 |
-
|
131 |
-
idx = current_chunk.rfind(punc, max_size // 2, max_size)
|
132 |
-
if idx > split_idx : split_idx = idx
|
133 |
-
|
134 |
-
if split_idx != -1:
|
135 |
-
part, current_chunk = current_chunk[:split_idx+1], current_chunk[split_idx+1:]
|
136 |
-
else:
|
137 |
-
part, current_chunk = current_chunk[:max_size], current_chunk[max_size:]
|
138 |
chunks.append(part.strip())
|
139 |
-
else:
|
140 |
-
current_chunk += (" " if current_chunk and sentence else "") + sentence
|
141 |
if current_chunk: chunks.append(current_chunk.strip())
|
142 |
-
final_chunks = [c for c in chunks if c]
|
143 |
-
if log_list_ref:
|
144 |
return final_chunks
|
145 |
|
146 |
-
|
147 |
def merge_audio_files_func(file_paths, output_path, log_list_ref):
|
148 |
-
if not PYDUB_AVAILABLE:
|
149 |
-
_log_tts("❌ Pydub در دسترس نیست. ادغام فایل انجام نشد.", log_list_ref)
|
150 |
-
return False
|
151 |
try:
|
152 |
-
|
153 |
combined = AudioSegment.empty()
|
154 |
for i, fp in enumerate(file_paths):
|
155 |
-
if os.path.exists(fp):
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
else:
|
161 |
-
_log_tts(f"⚠️ فایل صوتی برای ادغام یافت نشد: {fp}", log_list_ref)
|
162 |
-
|
163 |
-
combined.export(output_path, format="wav")
|
164 |
-
_log_tts(f"✅ فایل صوتی با موفقیت در '{output_path}' ادغام و ذخیره شد.", log_list_ref)
|
165 |
-
return True
|
166 |
-
except Exception as e:
|
167 |
-
_log_tts(f"❌ خطا در هنگام ادغام فایلهای صوتی: {e}\n{traceback.format_exc()}", log_list_ref)
|
168 |
-
return False
|
169 |
|
170 |
def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list_ref):
|
171 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
172 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
api_key
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
try:
|
182 |
-
genai.configure(api_key=api_key)
|
183 |
-
except Exception as e:
|
184 |
-
_log_tts(f"❌ خطا در مقداردهی اولیه کلاینت Gemini: {e}", log_list_ref)
|
185 |
-
return None, f"خطا در ارتباط با Gemini: {e}"
|
186 |
-
|
187 |
-
if not text_input or not text_input.strip():
|
188 |
-
_log_tts("❌ متن ورودی برای تبدیل به گفتار خالی است.", log_list_ref)
|
189 |
-
return None, "خطا: متن ورودی خالی است."
|
190 |
|
|
|
|
|
|
|
191 |
text_chunks = smart_text_split(text_input, max_chunk, log_list_ref)
|
192 |
-
if not text_chunks:
|
193 |
-
_log_tts("❌ متن قابل پردازش برای تبدیل به گفتار نیست.", log_list_ref)
|
194 |
-
return None, "خطا: متن قابل پردازش نیست."
|
195 |
|
196 |
-
|
197 |
for i, chunk in enumerate(text_chunks):
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
|
|
|
|
|
|
|
|
202 |
try:
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
generation_config=genai_types.GenerationConfig(
|
225 |
-
temperature=temperature_val,
|
226 |
-
response_mime_type="audio/wav"
|
227 |
-
),
|
228 |
-
)
|
229 |
-
|
230 |
-
fname_base = f"{output_base_name}_part{i+1:03d}"
|
231 |
-
|
232 |
-
audio_bytes = None
|
233 |
-
mime_type = None
|
234 |
-
|
235 |
-
if response.parts and hasattr(response.parts[0], 'blob') and response.parts[0].blob.mime_type.startswith("audio/"): # More common for new SDK
|
236 |
-
audio_bytes = response.parts[0].blob.data
|
237 |
-
mime_type = response.parts[0].blob.mime_type
|
238 |
-
elif response.candidates and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data: # AlphaTTS way
|
239 |
-
inline_data = response.candidates[0].content.parts[0].inline_data
|
240 |
-
audio_bytes = inline_data.data
|
241 |
-
mime_type = inline_data.mime_type
|
242 |
-
else:
|
243 |
-
audio_part = None
|
244 |
-
if response.parts:
|
245 |
-
for part in response.parts:
|
246 |
-
if hasattr(part, 'mime_type') and part.mime_type.startswith("audio/"): # Check for mime_type attr
|
247 |
-
audio_part = part
|
248 |
-
break
|
249 |
-
if audio_part and hasattr(audio_part, 'data'):
|
250 |
-
audio_bytes = audio_part.data
|
251 |
-
mime_type = audio_part.mime_type
|
252 |
-
elif audio_part and hasattr(audio_part, '_blob'):
|
253 |
-
audio_bytes = audio_part._blob.data
|
254 |
-
mime_type = audio_part._blob.mime_type
|
255 |
-
|
256 |
-
if not audio_bytes:
|
257 |
-
_log_tts(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی معتبر دریافت شد.", log_list_ref)
|
258 |
-
_log_tts(f"ساختار پاسخ (Response structure): {response}", log_list_ref)
|
259 |
-
continue
|
260 |
-
|
261 |
-
if not mime_type: # Safety net if mime_type wasn't extracted
|
262 |
-
_log_tts(f"⚠️ MIME type برای قطعه {i+1} یافت نشد. پیشفرض wav.", log_list_ref)
|
263 |
-
mime_type = "audio/wav"
|
264 |
-
|
265 |
-
|
266 |
-
ext = mimetypes.guess_extension(mime_type) or ".wav"
|
267 |
-
if "audio/L" in mime_type and ext == ".wav":
|
268 |
-
audio_bytes = convert_to_wav(audio_bytes, mime_type)
|
269 |
-
if not ext.startswith("."): ext = "." + ext
|
270 |
-
|
271 |
-
fpath = save_binary_file(f"{fname_base}{ext}", audio_bytes, log_list_ref)
|
272 |
-
if fpath:
|
273 |
-
generated_files.append(fpath)
|
274 |
-
|
275 |
-
except Exception as e:
|
276 |
-
_log_tts(f"❌ خطا در تولید قطعه صوتی {i+1} با Gemini: {e}\n{traceback.format_exc()}", log_list_ref)
|
277 |
-
if hasattr(e, 'response') and e.response:
|
278 |
-
_log_tts(f"جزئیات خطای Gemini API: {e.response}", log_list_ref)
|
279 |
-
continue
|
280 |
-
|
281 |
-
if i < len(text_chunks) - 1 and len(text_chunks) > 1:
|
282 |
-
_log_tts(f"💤 توقف کوتاه ({sleep_time} ثانیه) قبل از پردازش قطعه بعدی...", log_list_ref)
|
283 |
-
time.sleep(sleep_time)
|
284 |
-
|
285 |
-
if not generated_files:
|
286 |
-
_log_tts("❌ هیچ فایل صوتی تولید نشد.", log_list_ref)
|
287 |
-
return None, "تولید صدا ناموفق بود. هیچ فایلی ایجاد نشد."
|
288 |
-
|
289 |
-
_log_tts(f"🎉 {len(generated_files)} فایل(های) صوتی با موفقیت تولید شد.", log_list_ref)
|
290 |
|
291 |
-
|
292 |
-
|
|
|
|
|
293 |
|
294 |
-
|
|
|
295 |
if PYDUB_AVAILABLE:
|
296 |
-
|
297 |
-
|
298 |
-
try: os.remove(merged_fn)
|
299 |
-
except OSError as e: _log_tts(f"⚠️ عدم امکان حذف فایل ادغام شده قبلی '{merged_fn}': {e}", log_list_ref)
|
300 |
-
|
301 |
-
if merge_audio_files_func(generated_files, merged_fn, log_list_ref):
|
302 |
-
final_audio_file = merged_fn
|
303 |
-
for fp in generated_files:
|
304 |
-
if os.path.abspath(fp) != os.path.abspath(merged_fn):
|
305 |
-
try: os.remove(fp)
|
306 |
-
except OSError as e_del: _log_tts(f"⚠️ عدم امکان حذف فایل موقت '{fp}': {e_del}", log_list_ref)
|
307 |
else:
|
308 |
-
|
309 |
-
|
310 |
-
try:
|
311 |
-
first_chunk_path = generated_files[0]
|
312 |
-
target_ext = os.path.splitext(first_chunk_path)[1]
|
313 |
-
fallback_fn = f"{final_output_path_base}_fallback{target_ext}"
|
314 |
-
if os.path.exists(fallback_fn): os.remove(fallback_fn)
|
315 |
-
os.rename(first_chunk_path, fallback_fn)
|
316 |
-
final_audio_file = fallback_fn
|
317 |
-
for i_gf in range(1, len(generated_files)):
|
318 |
-
try: os.remove(generated_files[i_gf])
|
319 |
-
except OSError as e_del: _log_tts(f"⚠️ عدم امکان حذف فایل موقت '{generated_files[i_gf]}': {e_del}", log_list_ref)
|
320 |
-
except Exception as e_rename_fallback:
|
321 |
-
_log_tts(f"خطا در تغییر نام فایل اولین قطعه (fallback): {e_rename_fallback}", log_list_ref)
|
322 |
-
final_audio_file = generated_files[0]
|
323 |
else:
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
first_chunk_path = generated_files[0]
|
328 |
-
target_ext = os.path.splitext(first_chunk_path)[1]
|
329 |
-
single_fallback_fn = f"{final_output_path_base}_single{target_ext}"
|
330 |
-
if os.path.exists(single_fallback_fn): os.remove(single_fallback_fn)
|
331 |
-
os.rename(first_chunk_path, single_fallback_fn)
|
332 |
-
final_audio_file = single_fallback_fn
|
333 |
-
for i_gf in range(1, len(generated_files)):
|
334 |
-
_log_tts(f"قطعه اضافی موجود: {generated_files[i_gf]} (ادغام نشده)", log_list_ref)
|
335 |
-
|
336 |
-
except Exception as e_rename_nopydub:
|
337 |
-
_log_tts(f"خطا در تغییر نام اولین قطعه (بدون pydub): {e_rename_nopydub}", log_list_ref)
|
338 |
-
final_audio_file = generated_files[0]
|
339 |
-
|
340 |
-
elif len(generated_files) == 1:
|
341 |
try:
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
|
|
|
|
|
|
|
|
|
|
359 |
|
360 |
-
return
|
361 |
|
362 |
-
def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature):
|
363 |
-
|
364 |
actual_text = ""
|
365 |
-
status_message = "شروع پردازش..."
|
366 |
-
final_audio_path = None
|
367 |
-
|
368 |
-
if NUM_GEMINI_KEYS == 0:
|
369 |
-
return None, "خطای پیکربندی: هیچ کلید API جیمینای برای سرویس TTS تنظیم نشده است."
|
370 |
-
|
371 |
if use_file_input:
|
372 |
-
if uploaded_file
|
373 |
try:
|
374 |
-
with open(uploaded_file.name, 'r', encoding='utf-8') as f:
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
_log_tts(f"خوانش متن از فایل: {uploaded_file.name}", logs_for_this_run)
|
379 |
-
except Exception as e:
|
380 |
-
_log_tts(f"❌ خطا در خواندن فایل متنی: {e}", logs_for_this_run)
|
381 |
-
return None, f"خطا در خواندن فایل: {e}"
|
382 |
-
else:
|
383 |
-
return None, "خطا: فایل متنی انتخاب نشده است در حالی که گزینه استفاده از فایل فعال است."
|
384 |
else:
|
385 |
actual_text = text_to_speak
|
386 |
-
if not actual_text or not actual_text.strip():
|
387 |
-
return None, "خطا: لطفاً متنی را برای تبدیل به گفتار وارد کنید."
|
388 |
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
if final_audio_path and generation_status_msg == "موفق":
|
398 |
-
status_message = "✅ تبدیل متن به گفتار با موفقیت انجام شد."
|
399 |
-
_log_tts(status_message, logs_for_this_run)
|
400 |
-
return final_audio_path, status_message
|
401 |
-
elif final_audio_path and generation_status_msg != "موفق":
|
402 |
-
status_message = f"⚠️ {generation_status_msg}. فایل صوتی ممکن است ناقص باشد: {final_audio_path}"
|
403 |
-
_log_tts(status_message, logs_for_this_run)
|
404 |
-
return final_audio_path, status_message
|
405 |
-
else:
|
406 |
-
status_message = f"❌ {generation_status_msg}"
|
407 |
-
_log_tts(status_message, logs_for_this_run)
|
408 |
-
return None, status_message
|
409 |
-
|
410 |
-
except Exception as e:
|
411 |
-
_log_tts(f"❌ خطای پیشبینی نشده در gradio_tts_interface: {e}\n{traceback.format_exc()}", logs_for_this_run)
|
412 |
-
return None, f"خطای داخلی سرویس: {e}"
|
413 |
-
|
414 |
-
# --- END: TTS Core Logic ---
|
415 |
-
|
416 |
-
|
417 |
-
# --- START: بخش UI و Gradio (Adapted from Alpha Translator, content from Alpha TTS) ---
|
418 |
-
FLY_PRIMARY_COLOR_HEX = "#4F46E5"
|
419 |
-
FLY_SECONDARY_COLOR_HEX = "#10B981"
|
420 |
-
FLY_ACCENT_COLOR_HEX = "#D97706"
|
421 |
-
FLY_TEXT_COLOR_HEX = "#1F2937"
|
422 |
-
FLY_SUBTLE_TEXT_HEX = "#6B7280"
|
423 |
-
FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
|
424 |
-
FLY_WHITE_HEX = "#FFFFFF"
|
425 |
-
FLY_BORDER_COLOR_HEX = "#D1D5DB"
|
426 |
-
FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6"
|
427 |
-
FLY_PANEL_BG_SIMPLE = "#E0F2FE"
|
428 |
-
|
429 |
-
app_theme_outer = gr.themes.Base(
|
430 |
-
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
|
431 |
-
).set(
|
432 |
-
body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
|
433 |
-
)
|
434 |
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
:root {{
|
440 |
-
--
|
441 |
-
--
|
442 |
-
--
|
443 |
-
--
|
444 |
-
--
|
445 |
-
--
|
446 |
-
--
|
447 |
-
--
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
452 |
}}
|
453 |
-
|
454 |
-
.gradio-
|
455 |
-
.
|
456 |
-
.
|
457 |
-
.
|
458 |
-
.
|
459 |
-
.
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
.content-panel-simple .gr-input > label + div > textarea:focus,.content-panel-simple .gr-dropdown > label + div > div > input:focus,.content-panel-simple .gr-dropdown > label + div > div > select:focus,.content-panel-simple .gr-textbox > label + div > textarea:focus, .content-panel-simple .gr-file > label + div:focus-within {{border-color:var(--fly-primary) !important;box-shadow:0 0 0 3px rgba(var(--fly-primary-rgb),0.12) !important;background-color:var(--fly-bg-white) !important;}}
|
467 |
-
.content-panel-simple .gr-file > label + div {{ text-align:center; border-style: dashed !important; }}
|
468 |
-
.content-panel-simple .gr-dropdown select {{font-family:var(--font-global) !important;width:100%;cursor:pointer;}}
|
469 |
-
.content-panel-simple .gr-textbox[label*="وضعیت"] > label + div > textarea {{background-color:var(--fly-panel-bg-simple) !important;border-color:#A5D5FE !important;min-height:80px;font-family:var(--font-global);font-size:0.9em !important;line-height:1.5;padding:10px !important;}}
|
470 |
-
.content-panel-simple .gr-panel,.content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > .gr-panel {{border-radius:8px !important;border:1px solid var(--fly-border-color) !important;background-color:var(--fly-input-bg-simple) !important;padding:0.8rem 1rem !important;margin-top:0.6rem;box-shadow:none;}}
|
471 |
-
.content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > button.gr-button {{font-weight:500 !important;padding:8px 10px !important;border-radius:6px !important;background-color:#E5E7EB !important;color:var(--fly-text-primary) !important;border:1px solid #D1D5DB !important;}}
|
472 |
-
.content-panel-simple label > span.label-text {{font-weight:500 !important;color:#4B5563 !important;font-size:0.88em !important;margin-bottom:6px !important;display:inline-block;}}
|
473 |
-
.content-panel-simple .gr-slider label span {{font-size:0.82em !important;color:var(--fly-text-secondary);}}
|
474 |
-
.temp-description-tts {{ font-size: 0.82em !important; color: var(--fly-text-secondary) !important; margin-top: -0.5rem; margin-bottom: 1rem; padding-right: 5px; }}
|
475 |
-
.content-panel-simple div[label*="نمونه"] {{margin-top:1.5rem;}}
|
476 |
-
.content-panel-simple div[label*="نمونه"] .gr-button.gr-button-tool,.content-panel-simple div[label*="نمونه"] .gr-sample-button {{background-color:#E0E7FF !important;color:var(--fly-primary) !important;border-radius:6px !important;font-size:0.78em !important;padding:4px 8px !important;}}
|
477 |
-
.content-panel-simple .custom-hr {{height:1px;background-color:var(--fly-border-color);margin:1.5rem 0;border:none;}}
|
478 |
-
.api-warning-message {{background-color:#FFFBEB !important;color:#92400E !important;padding:10px 12px !important;border-radius:8px !important;border:1px solid #FDE68A !important;text-align:center !important;margin:0 0.2rem 1rem 0.2rem !important;font-size:0.85em !important;}}
|
479 |
-
.content-panel-simple #output_audio_tts audio {{ width: 100%; border-radius: var(--radius-md); margin-top:0.5rem; }}
|
480 |
-
@media (min-width:640px) {{.main-content-area {{padding:1.5rem;max-width:700px;}} .content-panel-simple {{padding:1.5rem;}} .app-title-card h1 {{font-size:2.5em !important;}} .app-title-card p {{font-size:1.05em !important;}} }}
|
481 |
-
@media (min-width:768px) {{
|
482 |
-
.main-content-area {{max-width:780px;}} .content-panel-simple {{padding:2rem;}}
|
483 |
-
.content-panel-simple .main-content-row {{display:flex !important;flex-direction:row !important;gap:1.5rem !important;}}
|
484 |
-
.content-panel-simple .main-content-row > .gr-column:nth-child(1) {{flex-basis:60%; min-width:0;}}
|
485 |
-
.content-panel-simple .main-content-row > .gr-column:nth-child(2) {{flex-basis:40%; min-width:0;}}
|
486 |
-
.content-panel-simple .gr-button.lg.primary,.content-panel-simple button[variant="primary"] {{width:auto !important;align-self:flex-start;}}
|
487 |
-
.app-title-card h1 {{font-size:2.75em !important;}} .app-title-card p {{font-size:1.1em !important;}}
|
488 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
489 |
"""
|
490 |
-
logging.info(f"Gradio version: {gr.__version__}")
|
491 |
-
if not PYDUB_AVAILABLE:
|
492 |
-
logging.warning("Pydub (برای ادغام فایلهای صوتی) یافت نشد. لطفاً با `pip install pydub` نصب کنید. در غیر این صورت، فقط اولین قطعه صوتی ارائه خواهد شد.")
|
493 |
-
|
494 |
-
with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آلفا TTS") as demo:
|
495 |
-
gr.HTML(f"""
|
496 |
-
<div class="app-title-card">
|
497 |
-
<h1>🚀 Alpha TTS</h1>
|
498 |
-
<p>جادوی تبدیل متن به صدا با هوش مصنوعی Gemini</p>
|
499 |
-
</div>
|
500 |
-
""")
|
501 |
-
|
502 |
-
with gr.Column(elem_classes=["main-content-area"]):
|
503 |
-
with gr.Group(elem_classes=["content-panel-simple"]):
|
504 |
-
if NUM_GEMINI_KEYS == 0:
|
505 |
-
missing_key_msg = (
|
506 |
-
"⚠️ هشدار: قابلیت تبدیل متن به گفتار غیرفعال است. "
|
507 |
-
"هیچ کلید API جیمینای (با فرمت GEMINI_API_KEY_1, ...) "
|
508 |
-
"در بخش Secrets این Space یافت نشد. "
|
509 |
-
"لطفاً حداقل یک کلید با نام GEMINI_API_KEY_1 تنظیم کنید."
|
510 |
-
)
|
511 |
-
gr.Markdown(f"<div class='api-warning-message'>{missing_key_msg}</div>")
|
512 |
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
label="آپلود فایل متنی",
|
520 |
-
file_types=['.txt'],
|
521 |
-
visible=False
|
522 |
-
)
|
523 |
-
text_to_speak_tb = gr.Textbox(
|
524 |
-
label="📝 متن فارسی برای تبدیل به گفتار",
|
525 |
-
placeholder="مثال: سلام، به پروژه آلفا خوش آمدید.",
|
526 |
-
lines=5,
|
527 |
-
value=""
|
528 |
-
)
|
529 |
-
speech_prompt_tb = gr.Textbox(
|
530 |
-
label="🗣️ سبک و زمینه گفتار (اختیاری)",
|
531 |
-
placeholder="مثال: با لحنی شاد و پرانرژی",
|
532 |
-
value="با لحنی دوستانه و رسا صحبت کن.",
|
533 |
-
lines=2
|
534 |
-
)
|
535 |
-
with gr.Column(scale=2):
|
536 |
-
speaker_voice_dd = gr.Dropdown(
|
537 |
-
SPEAKER_VOICES,
|
538 |
-
label="🎤 انتخاب گوینده",
|
539 |
-
value="Charon"
|
540 |
-
)
|
541 |
-
temperature_slider = gr.Slider(
|
542 |
-
minimum=0.1, maximum=1.5, step=0.05, value=0.9,
|
543 |
-
label="🌡️ میزان خلاقیت صدا (دما)"
|
544 |
-
)
|
545 |
-
gr.Markdown("<p class='temp-description-tts'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایینتر = یکنواختی بیشتر.</p>", elem_classes=["temp-description-tts-container"])
|
546 |
-
|
547 |
-
output_audio = gr.Audio(label="🎧 فایل صوتی خروجی", type="filepath", elem_id="output_audio_tts")
|
548 |
-
|
549 |
-
generate_button = gr.Button("🚀 تولید و پخش صدا", variant="primary", elem_classes=["lg"])
|
550 |
-
|
551 |
-
gr.HTML("<hr class='custom-hr'>")
|
552 |
-
|
553 |
-
gr.Examples(
|
554 |
-
examples=[
|
555 |
-
[False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید. این یک نمونه صدای تولید شده توسط آلفا است.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
|
556 |
-
[False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی پیشرفته جیمینای است.", "با صدایی طبیعی، روان و کمی رسمی.", "Charon", 0.9],
|
557 |
-
[False, None, "آیا میتوانم یک پیتزای پپرونی سفارش دهم؟", "پرسشی و مودبانه.", "Achird", 0.75],
|
558 |
-
],
|
559 |
-
inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
|
560 |
-
outputs=[output_audio, status_message_output],
|
561 |
-
fn=gradio_tts_interface,
|
562 |
-
cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
|
563 |
-
label="💡 نمونههای کاربردی"
|
564 |
-
)
|
565 |
-
|
566 |
-
gr.Markdown("<p class='app-footer-fly'>Alpha TTS © 2024</p>")
|
567 |
-
|
568 |
-
def toggle_file_input(use_file):
|
569 |
-
if use_file:
|
570 |
-
return gr.update(visible=True, label=" "), gr.update(visible=False)
|
571 |
-
else:
|
572 |
-
return gr.update(visible=False), gr.update(visible=True, label="📝 متن فارسی برای تبدیل به گفتار")
|
573 |
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
582 |
fn=gradio_tts_interface,
|
583 |
-
|
584 |
-
outputs=[output_audio, status_message_output]
|
585 |
)
|
586 |
-
|
587 |
-
logging.error("دکمه تولید صدا (generate_button) به درستی مقداردهی اولیه نشده است.")
|
588 |
-
|
589 |
|
590 |
if __name__ == "__main__":
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
demo.launch(
|
596 |
-
server_name="0.0.0.0",
|
597 |
-
server_port=int(os.getenv("PORT", 7860)),
|
598 |
-
debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true",
|
599 |
-
show_error=True
|
600 |
-
)
|
|
|
1 |
import gradio as gr
|
2 |
+
import base64
|
3 |
+
import mimetypes
|
4 |
import os
|
5 |
+
import re
|
6 |
+
import struct
|
7 |
import time
|
8 |
+
import zipfile
|
9 |
+
from google import genai
|
10 |
+
from google.genai import types
|
11 |
import logging
|
|
|
|
|
12 |
|
13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
try:
|
16 |
from pydub import AudioSegment
|
17 |
PYDUB_AVAILABLE = True
|
18 |
+
logging.info("pydub با موفقیت ایمپورت شد.")
|
19 |
except ImportError:
|
20 |
PYDUB_AVAILABLE = False
|
21 |
+
logging.warning("pydub یافت نشد. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
SPEAKER_VOICES = [
|
24 |
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
|
25 |
"Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
|
|
|
27 |
"Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
|
28 |
"Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
29 |
]
|
30 |
+
FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
|
31 |
DEFAULT_MAX_CHUNK_SIZE = 3800
|
32 |
+
DEFAULT_SLEEP_BETWEEN_REQUESTS = 7
|
33 |
+
DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio_gtts"
|
34 |
|
35 |
+
def _log_internal(message, log_list_ref):
|
36 |
log_list_ref.append(message)
|
37 |
+
logging.info(f"[CORE_LOG] {message}")
|
38 |
|
39 |
def save_binary_file(file_name, data, log_list_ref):
|
40 |
try:
|
41 |
with open(file_name, "wb") as f: f.write(data)
|
42 |
+
_log_internal(f"فایل ذخیره شد: {os.path.basename(file_name)}", log_list_ref)
|
43 |
return file_name
|
44 |
except Exception as e:
|
45 |
+
_log_internal(f"خطا در ذخیره فایل {os.path.basename(file_name)}: {e}", log_list_ref)
|
46 |
return None
|
47 |
|
48 |
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
|
|
69 |
def smart_text_split(text, max_size=3800, log_list_ref=None):
|
70 |
if len(text) <= max_size: return [text]
|
71 |
chunks, current_chunk = [], ""
|
72 |
+
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
73 |
for sentence in sentences:
|
74 |
if len(current_chunk) + len(sentence) + 1 > max_size:
|
75 |
if current_chunk: chunks.append(current_chunk.strip())
|
76 |
current_chunk = sentence
|
77 |
+
while len(current_chunk) > max_size:
|
78 |
+
split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
|
79 |
+
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:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
chunks.append(part.strip())
|
81 |
+
else: current_chunk += (" " if current_chunk else "") + sentence
|
|
|
82 |
if current_chunk: chunks.append(current_chunk.strip())
|
83 |
+
final_chunks = [c for c in chunks if c]
|
84 |
+
if log_list_ref: _log_internal(f"متن به {len(final_chunks)} قطعه تقسیم شد.", log_list_ref)
|
85 |
return final_chunks
|
86 |
|
|
|
87 |
def merge_audio_files_func(file_paths, output_path, log_list_ref):
|
88 |
+
if not PYDUB_AVAILABLE: _log_internal("pydub در دسترس نیست، ادغام انجام نشد.", log_list_ref); return False
|
|
|
|
|
89 |
try:
|
90 |
+
_log_internal(f"ادغام {len(file_paths)} فایل صوتی...", log_list_ref)
|
91 |
combined = AudioSegment.empty()
|
92 |
for i, fp in enumerate(file_paths):
|
93 |
+
if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
|
94 |
+
else: _log_internal(f"فایل پیدا نشد برای ادغام: {fp}", log_list_ref)
|
95 |
+
combined.export(output_path, format="wav")
|
96 |
+
_log_internal(f"فایل با موفقیت در {os.path.basename(output_path)} ادغام شد.", log_list_ref); return True
|
97 |
+
except Exception as e: _log_internal(f"خطا در ادغام فایلها: {e}", log_list_ref); return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list_ref):
|
100 |
output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
|
101 |
max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
|
102 |
+
_log_internal("شروع فرآیند تولید صدا...", log_list_ref)
|
103 |
+
api_key = os.environ.get("GEMINI_API_KEY_1")
|
104 |
+
if not api_key: api_key = os.environ.get("GEMINI_API_KEY")
|
105 |
+
if not api_key:
|
106 |
+
_log_internal("خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_1 یا GEMINI_API_KEY یافت نشد!", log_list_ref)
|
107 |
+
return None
|
108 |
+
_log_internal(f"استفاده از کلید API جمینای (...{api_key[-4:] if api_key else 'N/A'})", log_list_ref)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
|
110 |
+
try: client = genai.Client(api_key=api_key)
|
111 |
+
except Exception as e: _log_internal(f"خطا در ایجاد کلاینت جمینای: {e}", log_list_ref); return None
|
112 |
+
if not text_input or not text_input.strip(): _log_internal("متن ورودی خالی است.", log_list_ref); return None
|
113 |
text_chunks = smart_text_split(text_input, max_chunk, log_list_ref)
|
114 |
+
if not text_chunks: _log_internal("پس از تقسیمبندی، متنی برای پردازش وجود ندارد.", log_list_ref); return None
|
|
|
|
|
115 |
|
116 |
+
generated_files_temp = []
|
117 |
for i, chunk in enumerate(text_chunks):
|
118 |
+
_log_internal(f"پردازش قطعه {i+1} از {len(text_chunks)}...", log_list_ref)
|
119 |
+
final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
|
120 |
+
contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
|
121 |
+
config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
|
122 |
+
speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
|
123 |
+
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
|
124 |
+
timestamp = int(time.time() * 1000)
|
125 |
+
temp_fname_base = f"temp_audio_{timestamp}_part{i+1:03d}"
|
126 |
try:
|
127 |
+
response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
|
128 |
+
if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
|
129 |
+
inline_data = response.candidates[0].content.parts[0].inline_data
|
130 |
+
data_buffer = inline_data.data
|
131 |
+
ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
|
132 |
+
if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
|
133 |
+
if not ext.startswith("."): ext = "." + ext
|
134 |
+
temp_fpath = save_binary_file(f"{temp_fname_base}{ext}", data_buffer, log_list_ref)
|
135 |
+
if temp_fpath: generated_files_temp.append(temp_fpath)
|
136 |
+
else: _log_internal(f"پاسخ API برای قطعه {i+1} بدون داده صوتی معتبر.", log_list_ref)
|
137 |
+
except Exception as e:
|
138 |
+
_log_internal(f"خطای بحرانی در تولید قطعه {i+1} با جمینای: {e}\n{traceback.format_exc()}", log_list_ref);
|
139 |
+
for fp_clean in generated_files_temp:
|
140 |
+
if os.path.exists(fp_clean):
|
141 |
+
try: os.remove(fp_clean)
|
142 |
+
except: _log_internal(f"خطا در پاک کردن فایل موقت {fp_clean} پس از خطا", log_list_ref)
|
143 |
+
return None
|
144 |
+
if i < len(text_chunks) - 1 and len(text_chunks) > 1: time.sleep(sleep_time)
|
145 |
+
|
146 |
+
if not generated_files_temp: _log_internal("هیچ فایل صوتی موقتی تولید نشد.", log_list_ref); return None
|
147 |
+
_log_internal(f"{len(generated_files_temp)} قطعه صوتی با موفقیت تولید شد.", log_list_ref)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
|
149 |
+
final_output_path = f"{output_base_name}_final.wav"
|
150 |
+
if os.path.exists(final_output_path):
|
151 |
+
try: os.remove(final_output_path)
|
152 |
+
except Exception as e_del: _log_internal(f"خطا در حذف فایل خروجی قبلی {final_output_path}: {e_del}", log_list_ref)
|
153 |
|
154 |
+
final_audio_file_to_return = None
|
155 |
+
if len(generated_files_temp) > 1:
|
156 |
if PYDUB_AVAILABLE:
|
157 |
+
if merge_audio_files_func(generated_files_temp, final_output_path, log_list_ref):
|
158 |
+
final_audio_file_to_return = final_output_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
else:
|
160 |
+
_log_internal("ادغام ناموفق بود.", log_list_ref)
|
161 |
+
final_audio_file_to_return = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
else:
|
163 |
+
_log_internal("pydub برای ادغام چند قطعه در دسترس نیست.", log_list_ref)
|
164 |
+
final_audio_file_to_return = None
|
165 |
+
elif len(generated_files_temp) == 1:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
try:
|
167 |
+
os.rename(generated_files_temp[0], final_output_path)
|
168 |
+
final_audio_file_to_return = final_output_path
|
169 |
+
except Exception as e_rename:
|
170 |
+
_log_internal(f"خطا در انتقال فایل تکی به مسیر نهایی: {e_rename}", log_list_ref)
|
171 |
+
final_audio_file_to_return = None
|
172 |
+
if os.path.exists(generated_files_temp[0]):
|
173 |
+
try: os.remove(generated_files_temp[0])
|
174 |
+
except: pass
|
175 |
+
|
176 |
+
# پاک کردن تمام فایلهای موقت که در generated_files_temp لیست شدهاند
|
177 |
+
# این حلقه باید در این سطح تورفتگی باشد
|
178 |
+
for temp_f in generated_files_temp:
|
179 |
+
if os.path.exists(temp_f) and (not final_audio_file_to_return or os.path.abspath(temp_f) != os.path.abspath(final_audio_file_to_return)):
|
180 |
+
try:
|
181 |
+
os.remove(temp_f)
|
182 |
+
_log_internal(f"فایل موقت {os.path.basename(temp_f)} پاک شد.", log_list_ref)
|
183 |
+
except Exception as e_clean:
|
184 |
+
_log_internal(f"خطا در پاک کردن فایل موقت {os.path.basename(temp_f)}: {e_clean}", log_list_ref)
|
185 |
+
|
186 |
+
if final_audio_file_to_return and not os.path.exists(final_audio_file_to_return):
|
187 |
+
_log_internal(f"فایل نهایی '{final_audio_file_to_return}' پس از پردازش وجود ندارد!", log_list_ref)
|
188 |
+
return None
|
189 |
|
190 |
+
return final_audio_file_to_return
|
191 |
|
192 |
+
def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
|
193 |
+
logs = []
|
194 |
actual_text = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
if use_file_input:
|
196 |
+
if uploaded_file:
|
197 |
try:
|
198 |
+
with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
|
199 |
+
if not actual_text: _log_internal("فایل آپلود شده خالی است.", logs); return None
|
200 |
+
except Exception as e: _log_internal(f"خطا در خواندن فایل آپلود شده: {e}", logs); return None
|
201 |
+
else: _log_internal("گزینه فایل انتخاب شده اما فایلی آپلود نشده.", logs); return None
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
else:
|
203 |
actual_text = text_to_speak
|
204 |
+
if not actual_text or not actual_text.strip(): _log_internal("متن ورودی برای تبدیل خالی است.", logs); return None
|
|
|
205 |
|
206 |
+
final_audio_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, logs)
|
207 |
+
if final_audio_path:
|
208 |
+
logging.info(f"فایل صوتی نهایی برای ارسال به کاربر: {final_audio_path}")
|
209 |
+
return final_audio_path
|
210 |
+
else:
|
211 |
+
logging.warning("هیچ فایل صوتی نهایی برای ارسال به کاربر تولید نشد.")
|
212 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
213 |
|
214 |
+
APP_HEADER_GRADIENT_START = "#4F46E5"
|
215 |
+
APP_HEADER_GRADIENT_END = "#10B981"
|
216 |
+
PANEL_BACKGROUND = "#FFFFFF"
|
217 |
+
TEXT_INPUT_BG = "#F3F4F6"
|
218 |
+
BUTTON_BG = "#2979FF"
|
219 |
+
MAIN_BACKGROUND = "linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%)"
|
220 |
+
TEXT_PRIMARY = "#1F2937"
|
221 |
+
TEXT_SECONDARY = "#6B7280"
|
222 |
+
BORDER_COLOR = "#D1D5DB"
|
223 |
+
RADIUS_CARD = "20px"
|
224 |
+
RADIUS_INPUT = "10px"
|
225 |
+
SHADOW_CARD = "0 10px 30px -5px rgba(0,0,0,0.1)"
|
226 |
+
SHADOW_BUTTON = f"0 4px 10px -2px rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)},0.5)"
|
227 |
+
|
228 |
+
LABEL_TEXT_INPUT = "📝 متن فارسی برای تبدیل"
|
229 |
+
LABEL_SPEECH_PROMPT = "🗣️ سبک گفتار (اختیاری)"
|
230 |
+
LABEL_SPEAKER_VOICE = "🎤 انتخاب گوینده و لهجه"
|
231 |
+
LABEL_TEMPERATURE = "🌡️ خلاقیت و تنوع صدا"
|
232 |
+
LABEL_FILE_UPLOAD = "📄 استفاده از فایل متنی (.txt)"
|
233 |
+
|
234 |
+
custom_css_final_attempt = f"""
|
235 |
+
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
|
236 |
:root {{
|
237 |
+
--app-font: 'Vazirmatn', sans-serif;
|
238 |
+
--app-header-grad-start: {APP_HEADER_GRADIENT_START}; --app-header-grad-end: {APP_HEADER_GRADIENT_END};
|
239 |
+
--app-panel-bg: {PANEL_BACKGROUND}; --app-input-bg: {TEXT_INPUT_BG};
|
240 |
+
--app-button-bg: {BUTTON_BG}; --app-main-bg: {MAIN_BACKGROUND};
|
241 |
+
--app-text-primary: {TEXT_PRIMARY}; --app-text-secondary: {TEXT_SECONDARY};
|
242 |
+
--app-border-color: {BORDER_COLOR};
|
243 |
+
--radius-card: {RADIUS_CARD}; --radius-input: {RADIUS_INPUT};
|
244 |
+
--shadow-card: {SHADOW_CARD}; --shadow-button: {SHADOW_BUTTON};
|
245 |
+
}}
|
246 |
+
body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 15px; line-height: 1.6; }}
|
247 |
+
.gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
|
248 |
+
.app-header-container {{ padding: 2.8rem 1.5rem 3.5rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.25); }}
|
249 |
+
.app-header-container h1 {{ font-size: 2.3em; font-weight: 800; margin:0 0 0.4rem 0; text-shadow: 0 1px 3px rgba(0,0,0,0.2); }}
|
250 |
+
.app-header-container p {{ font-size: 1.05em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.95; }}
|
251 |
+
.main-content-wrapper-alpha {{ padding: 1.8rem 1.5rem; max-width: 650px; margin: -2.5rem auto 2rem auto; width: 90%; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); position:relative; z-index:10; }}
|
252 |
+
@media (max-width: 768px) {{
|
253 |
+
.main-content-wrapper-alpha {{ width: 92%; padding: 1.5rem 1.2rem; margin-top: -2rem; }}
|
254 |
+
.app-header-container h1 {{font-size:2em;}}
|
255 |
+
.app-header-container p {{font-size:1em;}}
|
256 |
}}
|
257 |
+
footer {{display:none !important;}}
|
258 |
+
.gradio-button.generate-button-final-alpha {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.85rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.25s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.8rem !important; }}
|
259 |
+
.gradio-button.generate-button-final-alpha:hover {{ filter: brightness(1.15); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)},0.65);}}
|
260 |
+
.gradio-textbox > label + div > textarea,
|
261 |
+
.gradio-dropdown > label + div > div > input,
|
262 |
+
.gradio-dropdown select,
|
263 |
+
.gradio-file > label + div {{
|
264 |
+
border-radius: var(--radius-input) !important;
|
265 |
+
border: 1px solid var(--app-border-color) !important;
|
266 |
+
background-color: var(--app-input-bg) !important;
|
267 |
+
box-shadow: inset 0 1px 3px rgba(0,0,0,0.06);
|
268 |
+
padding: 0.8rem !important;
|
269 |
+
font-size: 0.95em !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
270 |
}}
|
271 |
+
.gradio-dropdown .wrap-inner {{ border-radius: var(--radius-input) !important; }}
|
272 |
+
.gradio-file > label + div {{ text-align:center; border-style: dashed !important; padding: 1.2rem !important; }}
|
273 |
+
.gradio-file span[data-testid="block-title"] {{ font-weight:500; color: var(--app-text-secondary); font-size:0.9em; }}
|
274 |
+
.gradio-file button.svelte-116rqfv {{ background: var(--app-button-bg) !important; color:white !important; border-radius:6px !important; padding: 0.4rem 0.8rem !important; font-size:0.85em !important; }}
|
275 |
+
.gradio-textbox > label + div > textarea:focus,
|
276 |
+
.gradio-dropdown > label + div > div > input:focus {{
|
277 |
+
border-color: var(--app-button-bg) !important;
|
278 |
+
box-shadow: 0 0 0 3.5px rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)},0.25) !important;
|
279 |
+
}}
|
280 |
+
label.gradio-label > .label-text {{ font-weight: 500 !important; color: var(--app-text-primary) !important; font-size: 0.98em !important; margin-bottom: 0.6rem !important; display: block; }}
|
281 |
+
.gradio-textbox[elem_id="text_input_alpha_final"] > label > .label-text::before,
|
282 |
+
.gradio-checkbox[elem_id="use_file_cb_alpha_final"] > label > .label-text > span::before,
|
283 |
+
.gradio-textbox[elem_id="speech_prompt_alpha_final"] > label > .label-text::before,
|
284 |
+
.gradio-dropdown[elem_id="speaker_voice_alpha_final"] > label > .label-text::before,
|
285 |
+
.gradio-slider[elem_id="temperature_slider_alpha_final"] > label > .label-text > span::before {{
|
286 |
+
margin-left: 10px; vertical-align: -2px; font-size: 1.1em; opacity: 0.8;
|
287 |
+
}}
|
288 |
+
.gradio-textbox[elem_id="text_input_alpha_final"] > label > .label-text::before {{ content: '📝'; }}
|
289 |
+
.gradio-checkbox[elem_id="use_file_cb_alpha_final"] > label > .label-text > span::before {{ content: '📄'; }}
|
290 |
+
.gradio-textbox[elem_id="speech_prompt_alpha_final"] > label > .label-text::before {{ content: '🗣️'; }}
|
291 |
+
.gradio-dropdown[elem_id="speaker_voice_alpha_final"] > label > .label-text::before {{ content: '🎤'; }}
|
292 |
+
.gradio-slider[elem_id="temperature_slider_alpha_final"] > label > .label-text > span::before {{ content: '🌡️'; }}
|
293 |
+
#output_audio_player_alpha_final audio {{ width: 100%; border-radius: var(--radius-input); margin-top:1rem; box-shadow: 0 2px 5px rgba(0,0,0,0.08); }}
|
294 |
+
.temp_description_class_alpha_final {{ font-size: 0.88em; color: var(--app-text-secondary); margin-top: -0.3rem; margin-bottom: 1.2rem; }}
|
295 |
+
.app-footer-container-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.9; margin-top:3.5rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}
|
296 |
+
.gradio-examples {{ margin-top: 2.5rem !important; }}
|
297 |
+
.gradio-examples > .gradio-label > .label-text {{ font-size: 1.1em !important; font-weight: 700 !important; color: var(--app-text-primary) !important; text-align:center; margin-bottom: 1rem !important; }}
|
298 |
+
.gradio-examples table th {{ background-color: var(--app-input-bg) !important; font-weight:700 !important; font-size:0.9em !important; padding: 0.6rem 0.5rem !important; text-align:right !important; }}
|
299 |
+
.gradio-examples table td {{ padding: 0.6rem 0.5rem !important; font-size:0.9em !important; }}
|
300 |
+
.gradio-examples .gr-sample-button {{ background-color: rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)}, 0.1) !important; color: var(--app-button-bg) !important; border: 1px solid rgba({int(BUTTON_BG[1:3],16)},{int(BUTTON_BG[3:5],16)},{int(BUTTON_BG[5:7],16)}, 0.3) !important; font-weight:500 !important; }}
|
301 |
+
#output_audio_player_alpha_final > .gradio-label {{ display: none !important; }}
|
302 |
+
#file_uploader_alpha_final > .gradio-label {{ display: none !important; }}
|
303 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
|
305 |
+
alpha_header_html_final = """
|
306 |
+
<div class='app-header-container'>
|
307 |
+
<h1>Alpha Translator</h1>
|
308 |
+
<p>جادوی ترجمه و تلفظ در دستان شما</p>
|
309 |
+
</div>
|
310 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
|
312 |
+
with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_final_attempt, title="Alpha TTS") as demo:
|
313 |
+
gr.HTML(alpha_header_html_final)
|
314 |
+
with gr.Column(elem_classes=["main-content-wrapper-alpha"]):
|
315 |
+
use_file_input_cb = gr.Checkbox(label=LABEL_FILE_UPLOAD, value=False, elem_id="use_file_cb_alpha_final")
|
316 |
+
uploaded_file_input = gr.File(label=" ", file_types=['.txt'], visible=False, elem_id="file_uploader_alpha_final" )
|
317 |
+
text_to_speak_tb = gr.Textbox(label=LABEL_TEXT_INPUT, placeholder="مثال: سلام، فردا هوا چطور است؟", lines=5, value="", visible=True, elem_id="text_input_alpha_final")
|
318 |
+
use_file_input_cb.change(fn=lambda x: (gr.update(visible=x), gr.update(visible=not x)), inputs=use_file_input_cb, outputs=[uploaded_file_input, text_to_speak_tb])
|
319 |
+
speech_prompt_tb = gr.Textbox(label=LABEL_SPEECH_PROMPT, placeholder="مثال: با لحنی شاد و پرانرژی", value="با لحنی دوستانه و رسا صحبت کن.", lines=2, elem_id="speech_prompt_alpha_final")
|
320 |
+
speaker_voice_dd = gr.Dropdown(SPEAKER_VOICES, label=LABEL_SPEAKER_VOICE, value="Charon", elem_id="speaker_voice_alpha_final")
|
321 |
+
temperature_slider = gr.Slider(minimum=0.1, maximum=1.5, step=0.05, value=0.9, label=LABEL_TEMPERATURE, elem_id="temperature_slider_alpha_final")
|
322 |
+
gr.Markdown("<p class='temp_description_class_alpha_final'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایینتر = یکنواختی بیشتر.</p>")
|
323 |
+
generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final-alpha"], elem_id="generate_button_alpha_final")
|
324 |
+
output_audio = gr.Audio(type="filepath", elem_id="output_audio_player_alpha_final", label=" ")
|
325 |
+
generate_button.click(fn=gradio_tts_interface, inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], outputs=[output_audio] )
|
326 |
+
gr.Examples(
|
327 |
+
label="نمونههای کاربردی",
|
328 |
+
examples=[
|
329 |
+
[False, None, "قیمت این لباس چقدر است؟", "با لحنی مودبانه و سوالی.", "Zubenelgenubi", 0.75],
|
330 |
+
[False, None, "میتوانید آدرس را روی نقشه به من نشان دهید؟", "واضح و با سرعت متوسط.", "Achird", 0.8],
|
331 |
+
[False, None, "ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", "کمی آهستهتر و شمرده.", "Vindemiatrix", 0.6],
|
332 |
+
],
|
333 |
+
inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
|
334 |
+
outputs=[output_audio],
|
335 |
fn=gradio_tts_interface,
|
336 |
+
cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true"
|
|
|
337 |
)
|
338 |
+
gr.Markdown("<p class='app-footer-container-final'>Alpha Language Learning © 2025</p>")
|
|
|
|
|
339 |
|
340 |
if __name__ == "__main__":
|
341 |
+
logging.info("اپلیکیشن Alpha TTS در حال راهاندازی است...")
|
342 |
+
if not os.environ.get("GEMINI_API_KEY_1") and not os.environ.get("GEMINI_API_KEY"):
|
343 |
+
logging.warning("هشدار: هیچ کلید API جمینای (GEMINI_API_KEY_1 یا GEMINI_API_KEY) در متغیرهای محیطی یافت نشد. اپلیکیشن ممکن است کار نکند.")
|
344 |
+
demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)), debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true", show_error=True )
|
|
|
|
|
|
|
|
|
|
|
|