Update app.py
Browse files
app.py
CHANGED
@@ -1,6 +1,333 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
FLY_PRIMARY_COLOR_HEX = "#2563EB"; FLY_SECONDARY_COLOR_HEX = "#059669"; FLY_ACCENT_COLOR_HEX = "#D97706";
|
5 |
FLY_TEXT_COLOR_HEX = "#111827"; FLY_SUBTLE_TEXT_HEX = "#4B5563"; FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB";
|
6 |
FLY_WHITE_HEX = "#FFFFFF"; FLY_BORDER_COLOR_HEX = "#E5E7EB"; FLY_INPUT_BG_HEX = "#FFFFFF";
|
@@ -9,7 +336,6 @@ custom_css_v2 = f"""
|
|
9 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
10 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
11 |
:root {{
|
12 |
-
/* ... (تعریف متغیرهای CSS مانند قبل) ... */
|
13 |
--font-persian: 'Vazirmatn', 'Inter', sans-serif; --font-english: 'Inter', sans-serif;
|
14 |
--primary-color: {FLY_PRIMARY_COLOR_HEX}; --secondary-color: {FLY_SECONDARY_COLOR_HEX};
|
15 |
--accent-color: {FLY_ACCENT_COLOR_HEX}; --text-color: {FLY_TEXT_COLOR_HEX};
|
@@ -22,7 +348,6 @@ custom_css_v2 = f"""
|
|
22 |
--shadow-lg: 0 12px 20px -4px rgba(0,0,0,0.08), 0 4px 8px -3px rgba(0,0,0,0.05);
|
23 |
--transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
24 |
}}
|
25 |
-
/* ... (بقیه استایلهای عمومی body, .gradio-container و غیره مانند قبل) ... */
|
26 |
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; }}
|
27 |
.gradio-container {{ max-width: 100% !important; min-height: 100vh; margin:0 auto !important; padding:0 !important; border-radius:0 !important; box-shadow:none !important; }}
|
28 |
|
@@ -56,30 +381,12 @@ footer, .gradio-footer {{ display: none !important; visibility: hidden !importan
|
|
56 |
.compact-group .gr-form {{ gap: 0.9rem !important; }}
|
57 |
#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); }}
|
58 |
#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); }}
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
margin-bottom: 1.5rem !important;
|
64 |
-
height: 1.5px !important;
|
65 |
-
background-color: var(--border-color) !important;
|
66 |
-
border: none !important;
|
67 |
-
opacity: 0.7;
|
68 |
-
}}
|
69 |
-
/* اگر مارکداون "---" را به تگ p تبدیل میکند: */
|
70 |
-
#examples-separator > div > p {{
|
71 |
-
margin-top: 2rem !important;
|
72 |
-
margin-bottom: 1.5rem !important;
|
73 |
-
height: 1.5px !important;
|
74 |
-
background-color: var(--border-color) !important;
|
75 |
-
border: none !important;
|
76 |
-
opacity: 0.7;
|
77 |
-
font-size:0; /* برای مخفی کردن متن "---" اگر به عنوان p رندر شود */
|
78 |
}}
|
79 |
-
|
80 |
-
|
81 |
@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; }} }}
|
82 |
-
/* Keyframe animations */
|
83 |
@keyframes slideInDown {{ from {{ opacity: 0; transform: translateY(-20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
84 |
@keyframes fadeInUp {{ from {{ opacity: 0; transform: translateY(20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
85 |
@keyframes zoomIn {{ from {{ opacity: 0; transform: scale(0.95); }} to {{ opacity: 1; transform: scale(1); }} }}
|
@@ -87,7 +394,6 @@ footer, .gradio-footer {{ display: none !important; visibility: hidden !importan
|
|
87 |
|
88 |
# --- تعریف رابط کاربری Gradio ---
|
89 |
with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "system-ui"]), css=custom_css_v2, title="آواگر جمینای - نسخه پیشرفته") as demo:
|
90 |
-
# ... (بخش HTML هدر مانند قبل) ...
|
91 |
gr.HTML(f"""
|
92 |
<div class="app-header-card">
|
93 |
<h1>💎 آواگر جمینای پلاس</h1>
|
@@ -97,7 +403,6 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "sy
|
|
97 |
|
98 |
with gr.Column(elem_classes="main-content-wrapper"):
|
99 |
with gr.Group(elem_classes="content-panel"):
|
100 |
-
# ... (بخش نمایش هشدار/موفقیت API Key مانند قبل) ...
|
101 |
if not HF_GEMINI_API_KEY:
|
102 |
gr.HTML(f"<div class='api-warning-box'>⚠️ <strong>هشدار حیاتی:</strong> کلید API جمینای (<code>GEMINI_API_KEY</code>) در Hugging Face Secrets یافت نشد. "
|
103 |
"این ابزار برای کار کردن به این کلید نیاز دارد. لطفاً آن را در بخش 'Settings' > 'Secrets' این Space تنظیم کنید.</div>")
|
@@ -105,7 +410,6 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "sy
|
|
105 |
gr.HTML(f"<div class='success-message-box'>"
|
106 |
"🔑 کلید API جمینای با موفقیت از Secrets بارگذاری شد. آواگر جمینای پلاس آماده خدمترسانی است!</div>")
|
107 |
|
108 |
-
# ... (بقیه چیدمان UI با Row و Column مانند قبل) ...
|
109 |
with gr.Row(equal_height=False):
|
110 |
with gr.Column(scale=3, min_width=320):
|
111 |
gr.Markdown("<h3 class='section-title'>۱. متن و سبک گفتار</h3>", elem_id="input-section")
|
@@ -170,7 +474,6 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "sy
|
|
170 |
outputs=[output_audio_player, output_file_downloader, status_log_tb]
|
171 |
)
|
172 |
|
173 |
-
# **تغییر اینجا:** آرگومان style از gr.Markdown حذف شد
|
174 |
gr.Markdown("---", elem_id="examples-separator")
|
175 |
|
176 |
gr.Examples(
|
@@ -190,12 +493,10 @@ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "sy
|
|
190 |
cache_examples=False, elem_id="examples-section"
|
191 |
)
|
192 |
|
193 |
-
# ... (بخش HTML فوتر مانند قبل) ...
|
194 |
gr.HTML("<p class='app-footer-text'>طراحی و توسعه با ❤️ توسط <a href='https://huggingface.co/Hamed744' target='_blank' style='color:var(--primary-color); text-decoration:none; font-weight:600;'>Hamed744 (AIGOLDEN)</a> | نسخه ۱.۲ آواگر جمینای پلاس</p>")
|
195 |
|
196 |
|
197 |
if __name__ == "__main__":
|
198 |
-
# ... (بخش if __name__ == "__main__": مانند قبل) ...
|
199 |
if not PYDUB_AVAILABLE: print("هشدار: کتابخانه pydub نصب نشده یا کار نمیکند.")
|
200 |
if not HF_GEMINI_API_KEY: print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است.")
|
201 |
demo.launch(debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true", share=False)
|
|
|
1 |
+
import base64
|
2 |
+
import mimetypes
|
3 |
+
import os
|
4 |
+
import re
|
5 |
+
import struct
|
6 |
+
import time
|
7 |
+
import zipfile
|
8 |
+
import google.generativeai as genai # ایمپورت برای کتابخانه جدید
|
9 |
+
from google.generativeai import types
|
10 |
+
import traceback
|
11 |
+
import gradio as gr # <--- این ایمپورت باید اینجا یا قبل از اولین استفاده از gr باشد
|
12 |
|
13 |
+
# خواندن کلید API از Hugging Face Secrets
|
14 |
+
HF_GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
15 |
+
|
16 |
+
try:
|
17 |
+
from pydub import AudioSegment
|
18 |
+
PYDUB_AVAILABLE = True
|
19 |
+
except ImportError:
|
20 |
+
PYDUB_AVAILABLE = False
|
21 |
+
print("⚠️ کتابخانه pydub در دسترس نیست. قابلیت ادغام فایلهای صوتی غیرفعال خواهد بود.")
|
22 |
+
|
23 |
+
# --- ثابتها ---
|
24 |
+
# ... (ثابتها مانند قبل) ...
|
25 |
+
SPEAKER_VOICES = [
|
26 |
+
"Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat",
|
27 |
+
"Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima",
|
28 |
+
"Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalthgeti",
|
29 |
+
"Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus",
|
30 |
+
"Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
|
31 |
+
]
|
32 |
+
MODELS = ["gemini-1.5-flash-latest", "gemini-1.5-pro-latest"]
|
33 |
+
MODEL_NAMES_FARSI = {
|
34 |
+
"gemini-1.5-flash-latest": "جمینای ۱.۵ فلش (جدید، سریع، بهینه)",
|
35 |
+
"gemini-1.5-pro-latest": "جمینای ۱.۵ پرو (جدید، قدرتمند)"
|
36 |
+
}
|
37 |
+
SPEAKER_VOICES_FARSI_SAMPLE = {
|
38 |
+
"Charon": "شارون (صدای مردانه، پیشفرض)", "Achernar": "آخرالنهر (صدای مردانه)",
|
39 |
+
"Vindemiatrix": "ویندمیاتريکس (صدای زنانه)", "Schedar": "صدر (صدای مردانه)",
|
40 |
+
"Laomedeia": "لائومدیا (صدای زنانه)", "Sulafat": "سولافات (صدای مردانه)"
|
41 |
+
}
|
42 |
+
|
43 |
+
|
44 |
+
# --- توابع کمکی (مانند قبل) ---
|
45 |
+
# ... (تمام توابع کمکی save_binary_file, convert_to_wav, و غیره مانند قبل) ...
|
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: f.write(data)
|
50 |
+
print(f"✅ فایل در مسیر ذخیره شد: {abs_file_name}"); return abs_file_name
|
51 |
+
except Exception as e: print(f"❌ خطا در ذخیره فایل {abs_file_name}: {e}"); return None
|
52 |
+
def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
|
53 |
+
parameters = parse_audio_mime_type(mime_type)
|
54 |
+
bits_per_sample, rate, num_channels = parameters["bits_per_sample"], parameters["rate"], 1
|
55 |
+
data_size = len(audio_data); bytes_per_sample = bits_per_sample // 8
|
56 |
+
block_align = num_channels * bytes_per_sample; byte_rate = rate * block_align
|
57 |
+
chunk_size = 36 + data_size
|
58 |
+
return struct.pack("<4sI4s4sIHHIIHH4sI", b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1, num_channels, rate, byte_rate, block_align, bits_per_sample, b"data", data_size) + audio_data
|
59 |
+
def parse_audio_mime_type(mime_type: str) -> dict[str, int | None]:
|
60 |
+
bits_per_sample, rate = 16, 24000
|
61 |
+
if mime_type:
|
62 |
+
mime_type_lower = mime_type.lower(); parts = mime_type_lower.split(";")
|
63 |
+
for param in parts:
|
64 |
+
param = param.strip()
|
65 |
+
if param.startswith("rate="):
|
66 |
+
try: rate = int(param.split("=", 1)[1])
|
67 |
+
except: pass
|
68 |
+
elif param.startswith("audio/l"):
|
69 |
+
try:
|
70 |
+
potential_bits = param.split("l", 1)[1].split(";",1)[0]
|
71 |
+
if potential_bits.isdigit(): bits_per_sample = int(potential_bits)
|
72 |
+
except: pass
|
73 |
+
return {"bits_per_sample": bits_per_sample, "rate": rate}
|
74 |
+
def load_text_from_gr_file(file_obj):
|
75 |
+
if file_obj is None: return "", "فایلی برای ورودی متن ارائه نشده است."
|
76 |
+
try:
|
77 |
+
with open(file_obj.name, 'r', encoding='utf-8') as f: content = f.read().strip()
|
78 |
+
if not content: return "", "فایل متنی خالی است."
|
79 |
+
return content, f"متن با موفقیت از فایل '{os.path.basename(file_obj.name)}' ({len(content)} کاراکتر) بارگذاری شد."
|
80 |
+
except Exception as e: return "", f"خطا در خواندن فایل متنی: {e}"
|
81 |
+
def smart_text_split(text, max_size=4000): # بازگشت به مقدار معقولتر برای TTS
|
82 |
+
if len(text) <= max_size: return [text]
|
83 |
+
chunks, current_chunk = [], ""
|
84 |
+
sentences = re.split(r'(?<=[.!?؟])\s+', text)
|
85 |
+
for sentence in sentences:
|
86 |
+
if not sentence: continue
|
87 |
+
if len(current_chunk) + len(sentence) + 1 > max_size:
|
88 |
+
if current_chunk: chunks.append(current_chunk.strip())
|
89 |
+
if len(sentence) > max_size: # اگر خود جمله هم طولانی است
|
90 |
+
temp_sentence_parts = [sentence[i:i+max_size] for i in range(0, len(sentence), max_size)]
|
91 |
+
chunks.extend(temp_sentence_parts)
|
92 |
+
current_chunk = ""
|
93 |
+
else: current_chunk = sentence
|
94 |
+
else: current_chunk += (" " if current_chunk else "") + sentence
|
95 |
+
if current_chunk: chunks.append(current_chunk.strip())
|
96 |
+
return chunks
|
97 |
+
def merge_audio_files_func(file_paths, output_path):
|
98 |
+
if not PYDUB_AVAILABLE: return False, "pydub در دسترس نیست. امکان ادغام فایلها وجود ندارد.", None
|
99 |
+
if not file_paths: return False, "هیچ فایل صوتی برای ادغام وجود ندارد.", None
|
100 |
+
try:
|
101 |
+
combined = AudioSegment.empty()
|
102 |
+
for i, file_path in enumerate(file_paths):
|
103 |
+
if os.path.exists(file_path):
|
104 |
+
try:
|
105 |
+
audio = AudioSegment.from_file(file_path)
|
106 |
+
combined += audio
|
107 |
+
if i < len(file_paths) - 1: combined += AudioSegment.silent(duration=200)
|
108 |
+
except Exception as e_load:
|
109 |
+
msg = f"خطا در بارگذاری فایل صوتی '{os.path.basename(file_path)}' با pydub: {e_load}"
|
110 |
+
print(f"⚠️ {msg}"); return False, msg, None
|
111 |
+
else:
|
112 |
+
msg = f"فایل برای ادغام یافت نشد: {os.path.basename(file_path)}"
|
113 |
+
print(f"⚠️ {msg}"); return False, msg, None
|
114 |
+
abs_output_path = os.path.abspath(output_path)
|
115 |
+
combined.export(abs_output_path, format="wav")
|
116 |
+
return True, f"فایل ادغام شده با موفقیت در '{os.path.basename(abs_output_path)}' (فرمت WAV) ذخیره شد.", abs_output_path
|
117 |
+
except Exception as e:
|
118 |
+
msg = f"خطا در ادغام فایلها: {e}"
|
119 |
+
print(f"❌ {msg}"); return False, msg, None
|
120 |
+
def create_zip_file(file_paths, zip_name):
|
121 |
+
abs_zip_name = os.path.abspath(zip_name)
|
122 |
+
try:
|
123 |
+
with zipfile.ZipFile(abs_zip_name, 'w') as zipf:
|
124 |
+
for file_path in file_paths:
|
125 |
+
if os.path.exists(file_path):
|
126 |
+
zipf.write(file_path, os.path.basename(file_path))
|
127 |
+
return True, f"فایل ZIP با نام '{os.path.basename(abs_zip_name)}' ایجاد شد.", abs_zip_name
|
128 |
+
except Exception as e: return False, f"خطا در ایجاد فایل ZIP: {e}", None
|
129 |
+
|
130 |
+
# --- تابع اصلی تولید صدا (مانند قبل با اصلاحات قبلی برای خطا) ---
|
131 |
+
def generate_audio_for_gradio(
|
132 |
+
use_file_input_checkbox, text_file_obj, speech_prompt_input, text_to_speak_input,
|
133 |
+
max_chunk_slider_ignored, # این پارامتر دیگر مستقیم استفاده نمیشود چون smart_text_split تغییر کرده
|
134 |
+
sleep_slider, temperature_slider, model_dropdown_key,
|
135 |
+
speaker_dropdown, output_filename_base_input, merge_checkbox, delete_partials_checkbox,
|
136 |
+
progress=gr.Progress(track_tqdm=True)
|
137 |
+
):
|
138 |
+
status_messages = ["🚀 فرآیند تبدیل متن به گفتار آغاز شد..."]
|
139 |
+
progress(0, desc="در حال آمادهسازی...")
|
140 |
+
actual_max_chunk_size = 4000
|
141 |
+
|
142 |
+
api_key_to_use = HF_GEMINI_API_KEY
|
143 |
+
if not api_key_to_use:
|
144 |
+
status_messages.extend(["❌ خطا: کلید API جمینای (GEMINI_API_KEY) در تنظیمات Secret این Space یافت نشد.",
|
145 |
+
"⬅️ لطفاً آن را در بخش Settings > Secrets مربوط به این Space تنظیم کنید."])
|
146 |
+
return None, None, "\n".join(status_messages)
|
147 |
+
|
148 |
+
genai.configure(api_key=api_key_to_use)
|
149 |
+
status_messages.append("🔑 کلید API با موفقیت از Secrets بارگذاری و برای استفاده تنظیم شد.")
|
150 |
+
|
151 |
+
actual_text_input = ""
|
152 |
+
if use_file_input_checkbox:
|
153 |
+
if text_file_obj is None:
|
154 |
+
status_messages.append("❌ خطا: گزینه 'استفاده از فایل متنی' انتخاب شده، اما هیچ فایلی آپلود نشده است.")
|
155 |
+
return None, None, "\n".join(status_messages)
|
156 |
+
actual_text_input, msg = load_text_from_gr_file(text_file_obj)
|
157 |
+
status_messages.append(msg)
|
158 |
+
if not actual_text_input: return None, None, "\n".join(status_messages)
|
159 |
+
else:
|
160 |
+
actual_text_input = text_to_speak_input
|
161 |
+
status_messages.append("⌨️ از متن وارد شده به صورت دستی استفاده میشود.")
|
162 |
+
|
163 |
+
if not actual_text_input or actual_text_input.strip() == "":
|
164 |
+
status_messages.append("❌ خطا: متن ورودی خالی است."); return None, None, "\n".join(status_messages)
|
165 |
+
|
166 |
+
status_messages.append("✅ کلاینت جمینای (از طریق genai.configure) آماده است.")
|
167 |
+
|
168 |
+
text_chunks = smart_text_split(actual_text_input, actual_max_chunk_size)
|
169 |
+
status_messages.append(f"📊 متن به {len(text_chunks)} قطعه تقسیم شد (هر قطعه تقریباً {actual_max_chunk_size} کاراکتر).")
|
170 |
+
for i, chunk_text_content in enumerate(text_chunks): status_messages.append(f" 📝 قطعه {i+1}: {len(chunk_text_content)} کاراکتر")
|
171 |
+
|
172 |
+
generated_audio_files = []
|
173 |
+
run_id = base64.urlsafe_b64encode(os.urandom(6)).decode()
|
174 |
+
temp_output_dir = f"temp_audio_{run_id}"; os.makedirs(temp_output_dir, exist_ok=True)
|
175 |
+
output_base_name_safe = re.sub(r'[\s\\\/\:\*\?\"\<\>\|\%]+', '_', output_filename_base_input)
|
176 |
+
|
177 |
+
total_chunks = len(text_chunks)
|
178 |
+
for i, chunk_text_content in enumerate(text_chunks):
|
179 |
+
progress(0.1 + (0.7 * (i / total_chunks)), desc=f"در حال تولید قطعه {i+1} از {total_chunks}...")
|
180 |
+
status_messages.append(f"\n🔊 در حال تولید صدا برای قطعه {i+1}/{total_chunks} با مدل '{model_dropdown_key}'...")
|
181 |
+
|
182 |
+
text_for_tts_api = chunk_text_content
|
183 |
+
# speech_prompt_input فعلا به صورت مستقیم به متن اضافه نمیشود
|
184 |
+
|
185 |
+
contents_for_api = [types.Part.from_text(text_for_tts_api)] # برای TTS، فقط parts کافی است
|
186 |
+
|
187 |
+
current_generation_config = types.GenerationConfig(
|
188 |
+
temperature=float(temperature_slider),
|
189 |
+
)
|
190 |
+
# تلاش برای اضافه کردن speech_config به روش سازگار با API جدید
|
191 |
+
# این بخش بسیار حیاتی است و ممکن است نیاز به بازبینی دقیق مستندات google-generativeai داشته باشد.
|
192 |
+
# نام پارامترها و ساختار ممکن است با آنچه در کتابخانه قدیمی google-genai بود متفاوت باشد.
|
193 |
+
# فعلا از نامهایی استفاده میکنیم که در کدهای نمونه google-generativeai بیشتر دیده شده.
|
194 |
+
if hasattr(types, 'SpeechSettings'): # نام کلاس ممکن است SpeechSettings یا مشابه باشد
|
195 |
+
speech_settings = types.SpeechSettings(voice=speaker_dropdown) # یا voice_name=speaker_dropdown
|
196 |
+
# نحوه اضافه کردن این به درخواست:
|
197 |
+
# ممکن است بخشی از model_kwargs یا generation_config باشد، یا پارامتر جداگانه
|
198 |
+
# فعلا فرض میکنیم به generation_config اضافه میشود اگر چنین فیلدی داشته باشد
|
199 |
+
if hasattr(current_generation_config, 'speech_settings'): # نام فیلد را حدس میزنیم
|
200 |
+
current_generation_config.speech_settings = speech_settings
|
201 |
+
status_messages.append(f"ℹ️ تنظیمات گفتار (آزمایشی با SpeechSettings): گوینده '{speaker_dropdown}'")
|
202 |
+
elif hasattr(current_generation_config, 'speech'): # تلاش با نام دیگر
|
203 |
+
current_generation_config.speech = types.SpeechConfig( # یا SpeechSettings
|
204 |
+
voice=types.VoiceConfig(name=speaker_dropdown) # یا voice_name
|
205 |
+
)
|
206 |
+
status_messages.append(f"ℹ️ تنظیمات گفتار (آزمایشی با speech.voice.name): گوینده '{speaker_dropdown}'")
|
207 |
+
else:
|
208 |
+
status_messages.append("⚠️ کلاس SpeechSettings (یا مشابه) در types یافت نشد. تنظیمات گوینده ممکن است اعمال نشود.")
|
209 |
+
|
210 |
+
|
211 |
+
try:
|
212 |
+
chunk_filename_base = f"{output_base_name_safe}_part_{i+1:03d}"
|
213 |
+
chunk_filepath_prefix = os.path.join(temp_output_dir, chunk_filename_base)
|
214 |
+
audio_data_received = False
|
215 |
+
|
216 |
+
model_instance = genai.GenerativeModel(model_dropdown_key)
|
217 |
+
|
218 |
+
# برای TTS، معمولا `generate_content` کافی است و `stream` ممکن است برای خروجی صوتی مستقیم نباشد
|
219 |
+
# مگر اینکه API صریحا از استریم کردن بایتهای صوتی پشتیبانی کند.
|
220 |
+
# استفاده از حالت غیر استریم برای سادگی اولیه با TTS و اطمینان از دریافت کامل فایل:
|
221 |
+
response = model_instance.generate_content(
|
222 |
+
contents=contents_for_api, # فقط Parts
|
223 |
+
generation_config=current_generation_config,
|
224 |
+
# stream=False # حالت پیشفرض False است
|
225 |
+
# request_options={"response_mime_type": "audio/wav"} # درخواست مستقیم فرمت صوتی
|
226 |
+
)
|
227 |
+
|
228 |
+
# بررسی پاسخ برای داده صوتی
|
229 |
+
# ساختار پاسخ ممکن است با کتابخانه جدید متفاوت باشد
|
230 |
+
if response.parts and response.parts[0].inline_data:
|
231 |
+
inline_data = response.parts[0].inline_data
|
232 |
+
data_buffer, api_mime_type = inline_data.data, inline_data.mime_type
|
233 |
+
audio_data_received = True
|
234 |
+
status_messages.append(f"ℹ️ MIME Type دریافتی از API: {api_mime_type}")
|
235 |
+
file_extension = ".wav"
|
236 |
+
if api_mime_type and ("mp3" in api_mime_type.lower() or "mpeg" in api_mime_type.lower()):
|
237 |
+
file_extension = ".mp3"; status_messages.append(f"ℹ️ ذخیره با فرمت MP3: {api_mime_type}")
|
238 |
+
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()):
|
239 |
+
file_extension = ".wav"; status_messages.append(f"ℹ️ ذخیره با فرمت WAV: {api_mime_type}")
|
240 |
+
else:
|
241 |
+
status_messages.append(f"ℹ️ تبدیل به فرمت WAV برای MIME Type: {api_mime_type or 'نامشخص'}")
|
242 |
+
data_buffer = convert_to_wav(data_buffer, api_mime_type)
|
243 |
+
status_messages.append(f"ℹ️ پسوند فایل نهایی: {file_extension}")
|
244 |
+
generated_file_path = save_binary_file(f"{chunk_filepath_prefix}{file_extension}", data_buffer)
|
245 |
+
if generated_file_path:
|
246 |
+
generated_audio_files.append(generated_file_path)
|
247 |
+
status_messages.append(f"✅ قطعه {i+1} ذخیره شد: {os.path.basename(generated_file_path)}")
|
248 |
+
else: status_messages.append(f"❌ عدم موفقیت در ذخیره قطعه {i+1}.")
|
249 |
+
|
250 |
+
elif response.prompt_feedback and response.prompt_feedback.block_reason:
|
251 |
+
status_messages.append(f"❌ تولید صدا برای قطعه {i+1} مسدود شد. دلیل: {response.prompt_feedback.block_reason_message or response.prompt_feedback.block_reason}")
|
252 |
+
else:
|
253 |
+
status_messages.append(f"❌ پاسخ دریافتی از API برای قطعه {i+1} حاوی داده صوتی نبود. پاسخ: {response.text[:100] if response.text else 'پاسخ متنی خالی'}")
|
254 |
+
|
255 |
+
|
256 |
+
if not audio_data_received:
|
257 |
+
status_messages.append(f"❌ هیچ داده صوتی برای قطعه {i+1} در پاسخ نهایی دریافت نشد.")
|
258 |
+
|
259 |
+
except Exception as e:
|
260 |
+
# ... (مدیریت خطا مانند قبل) ...
|
261 |
+
is_quota_error = False
|
262 |
+
if isinstance(e, genai.errors.GoogleAPIError):
|
263 |
+
status_messages.append(f"❌ خطای API گوگل در قطعه {i+1} ({type(e).__name__}): {e}")
|
264 |
+
if hasattr(e, 'message') and ("QUOTA" in str(e.message).upper() or "RESOURCE_EXHAUSTED" in str(e.message).upper()):
|
265 |
+
status_messages.append("🚫 شما از سهمیه رایگان/فعلی خود برای این مدل فراتر رفتهاید. لطفاً طرح خود را بررسی کنید یا بعداً دوباره امتحان نمایید.")
|
266 |
+
is_quota_error = True
|
267 |
+
elif hasattr(e, 'message'):
|
268 |
+
status_messages.append(f" پیام خطا از API: {e.message}")
|
269 |
+
status_messages.append(traceback.format_exc())
|
270 |
+
elif hasattr(types, 'BlockedPromptError') and isinstance(e, types.BlockedPromptError): # این نام ممکن است دقیق نباشد
|
271 |
+
status_messages.append(f"❌ محتوای قطعه {i+1} توسط API مسدود شد: {e}")
|
272 |
+
elif hasattr(types, 'StopCandidateException') and isinstance(e, types.StopCandidateException): # این نام ممکن است دقیق نباشد
|
273 |
+
status_messages.append(f"❌ تولید صدا برای قطعه {i+1} به دلیل پایان نامناسب متوقف شد: {e}")
|
274 |
+
if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback'):
|
275 |
+
status_messages.append(f" بازخورد API: {e.response.prompt_feedback}")
|
276 |
+
else:
|
277 |
+
status_messages.extend([f"❌ خطا در تولید/پردازش قطعه {i+1}: {type(e).__name__} - {e}", traceback.format_exc()])
|
278 |
+
|
279 |
+
if is_quota_error and model_dropdown_key.endswith("-pro-latest"):
|
280 |
+
status_messages.append("💡 پیشنهاد: از مدل 'جمینای ۱.۵ فلش' که محدودیت کمتری دارد استفاده کنید یا برای استفاده از مدل پرو، طرح خود را در گوگل ارتقا دهید.")
|
281 |
+
|
282 |
+
|
283 |
+
if not audio_data_received and i < total_chunks -1 :
|
284 |
+
status_messages.append(f"⚠️ به دلیل خطا در قطعه {i+1}، ادامه تولید سایر قطعات ممکن است با مشکل مواجه شود.")
|
285 |
+
|
286 |
+
if i < total_chunks - 1 and float(sleep_slider) > 0 :
|
287 |
+
status_messages.append(f"⏱️ انتظار به مدت {sleep_slider} ثانیه..."); time.sleep(float(sleep_slider))
|
288 |
+
|
289 |
+
# ... (بقیه تابع generate_audio_for_gradio برای ادغام و بازگرداندن نتیجه مانند قبل) ...
|
290 |
+
progress(0.85, desc="پردازش فایلهای نهایی...")
|
291 |
+
if not generated_audio_files:
|
292 |
+
status_messages.append("❌ هیچ فایل صوتی با موفقیت تولید یا ذخیره نشد!")
|
293 |
+
progress(1, desc="پایان با خطا."); return None, None, "\n".join(status_messages)
|
294 |
+
status_messages.append(f"\n🎉 {len(generated_audio_files)} فایل(های) صوتی تولید شد!")
|
295 |
+
output_audio_path_for_player, output_path_for_download = None, None
|
296 |
+
if merge_checkbox and len(generated_audio_files) > 1 and PYDUB_AVAILABLE:
|
297 |
+
status_messages.append(f"🔗 در حال ادغام {len(generated_audio_files)} فایل صوتی...")
|
298 |
+
merged_filename_path = os.path.join(temp_output_dir, f"{output_base_name_safe}_merged.wav")
|
299 |
+
success_merge, msg_merge, merged_p = merge_audio_files_func(generated_audio_files, merged_filename_path)
|
300 |
+
status_messages.append(msg_merge)
|
301 |
+
if success_merge:
|
302 |
+
output_audio_path_for_player, output_path_for_download = merged_p, merged_p
|
303 |
+
if delete_partials_checkbox:
|
304 |
+
status_messages.append("🗑️ در حال حذف فایلهای جزئی...")
|
305 |
+
for file_p in generated_audio_files:
|
306 |
+
try: os.remove(file_p); status_messages.append(f" 🗑️ حذف شد: {os.path.basename(file_p)}")
|
307 |
+
except Exception as e_del: status_messages.append(f" ⚠️ عدم موفقیت در حذف {os.path.basename(file_p)}: {e_del}")
|
308 |
+
else:
|
309 |
+
status_messages.append("⚠️ ادغام ناموفق بود. فایل ZIP از قطعات ارائه میشود.")
|
310 |
+
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"))
|
311 |
+
status_messages.append(msg_zip)
|
312 |
+
if success_zip: output_path_for_download = zip_p
|
313 |
+
elif len(generated_audio_files) == 1:
|
314 |
+
single_file_path = generated_audio_files[0]
|
315 |
+
output_audio_path_for_player, output_path_for_download = single_file_path, single_file_path
|
316 |
+
status_messages.append(f"🎵 فایل صوتی تکی: {os.path.basename(single_file_path)}")
|
317 |
+
elif len(generated_audio_files) > 1:
|
318 |
+
if not PYDUB_AVAILABLE and merge_checkbox: status_messages.append("⚠️ pydub در دسترس نیست، امکان ادغام وجود ندارد. فایل ZIP ارائه میشود.")
|
319 |
+
status_messages.append("📦 چندین قطعه تولید شد. در حال ایجاد فایل ZIP...")
|
320 |
+
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"))
|
321 |
+
status_messages.append(msg_zip)
|
322 |
+
if success_zip: output_path_for_download = zip_p
|
323 |
+
final_status = "\n".join(status_messages)
|
324 |
+
print(final_status)
|
325 |
+
progress(1, desc="انجام شد!")
|
326 |
+
return output_audio_path_for_player, output_path_for_download, final_status
|
327 |
+
|
328 |
+
|
329 |
+
# --- تعریف CSS سفارشی (مانند قبل با اصلاح برای جداکننده مثالها) ---
|
330 |
+
# ... (کد CSS مانند قبل با اصلاح برای #examples-separator hr) ...
|
331 |
FLY_PRIMARY_COLOR_HEX = "#2563EB"; FLY_SECONDARY_COLOR_HEX = "#059669"; FLY_ACCENT_COLOR_HEX = "#D97706";
|
332 |
FLY_TEXT_COLOR_HEX = "#111827"; FLY_SUBTLE_TEXT_HEX = "#4B5563"; FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB";
|
333 |
FLY_WHITE_HEX = "#FFFFFF"; FLY_BORDER_COLOR_HEX = "#E5E7EB"; FLY_INPUT_BG_HEX = "#FFFFFF";
|
|
|
336 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
337 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
338 |
:root {{
|
|
|
339 |
--font-persian: 'Vazirmatn', 'Inter', sans-serif; --font-english: 'Inter', sans-serif;
|
340 |
--primary-color: {FLY_PRIMARY_COLOR_HEX}; --secondary-color: {FLY_SECONDARY_COLOR_HEX};
|
341 |
--accent-color: {FLY_ACCENT_COLOR_HEX}; --text-color: {FLY_TEXT_COLOR_HEX};
|
|
|
348 |
--shadow-lg: 0 12px 20px -4px rgba(0,0,0,0.08), 0 4px 8px -3px rgba(0,0,0,0.05);
|
349 |
--transition-ease: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
350 |
}}
|
|
|
351 |
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; }}
|
352 |
.gradio-container {{ max-width: 100% !important; min-height: 100vh; margin:0 auto !important; padding:0 !important; border-radius:0 !important; box-shadow:none !important; }}
|
353 |
|
|
|
381 |
.compact-group .gr-form {{ gap: 0.9rem !important; }}
|
382 |
#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); }}
|
383 |
#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); }}
|
384 |
+
#examples-separator > div > hr, #examples-separator > div > p {{
|
385 |
+
margin-top: 2rem !important; margin-bottom: 1.5rem !important;
|
386 |
+
height: 1.5px !important; background-color: var(--border-color) !important;
|
387 |
+
border: none !important; opacity: 0.7; font-size:0 !important; /* مخفی کردن متن --- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
}}
|
|
|
|
|
389 |
@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; }} }}
|
|
|
390 |
@keyframes slideInDown {{ from {{ opacity: 0; transform: translateY(-20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
391 |
@keyframes fadeInUp {{ from {{ opacity: 0; transform: translateY(20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
392 |
@keyframes zoomIn {{ from {{ opacity: 0; transform: scale(0.95); }} to {{ opacity: 1; transform: scale(1); }} }}
|
|
|
394 |
|
395 |
# --- تعریف رابط کاربری Gradio ---
|
396 |
with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn"), "system-ui"]), css=custom_css_v2, title="آواگر جمینای - نسخه پیشرفته") as demo:
|
|
|
397 |
gr.HTML(f"""
|
398 |
<div class="app-header-card">
|
399 |
<h1>💎 آواگر جمینای پلاس</h1>
|
|
|
403 |
|
404 |
with gr.Column(elem_classes="main-content-wrapper"):
|
405 |
with gr.Group(elem_classes="content-panel"):
|
|
|
406 |
if not HF_GEMINI_API_KEY:
|
407 |
gr.HTML(f"<div class='api-warning-box'>⚠️ <strong>هشدار حیاتی:</strong> کلید API جمینای (<code>GEMINI_API_KEY</code>) در Hugging Face Secrets یافت نشد. "
|
408 |
"این ابزار برای کار کردن به این کلید نیاز دارد. لطفاً آن را در بخش 'Settings' > 'Secrets' این Space تنظیم کنید.</div>")
|
|
|
410 |
gr.HTML(f"<div class='success-message-box'>"
|
411 |
"🔑 کلید API جمینای با موفقیت از Secrets بارگذاری شد. آواگر جمینای پلاس آماده خدمترسانی است!</div>")
|
412 |
|
|
|
413 |
with gr.Row(equal_height=False):
|
414 |
with gr.Column(scale=3, min_width=320):
|
415 |
gr.Markdown("<h3 class='section-title'>۱. متن و سبک گفتار</h3>", elem_id="input-section")
|
|
|
474 |
outputs=[output_audio_player, output_file_downloader, status_log_tb]
|
475 |
)
|
476 |
|
|
|
477 |
gr.Markdown("---", elem_id="examples-separator")
|
478 |
|
479 |
gr.Examples(
|
|
|
493 |
cache_examples=False, elem_id="examples-section"
|
494 |
)
|
495 |
|
|
|
496 |
gr.HTML("<p class='app-footer-text'>طراحی و توسعه با ❤️ توسط <a href='https://huggingface.co/Hamed744' target='_blank' style='color:var(--primary-color); text-decoration:none; font-weight:600;'>Hamed744 (AIGOLDEN)</a> | نسخه ۱.۲ آواگر جمینای پلاس</p>")
|
497 |
|
498 |
|
499 |
if __name__ == "__main__":
|
|
|
500 |
if not PYDUB_AVAILABLE: print("هشدار: کتابخانه pydub نصب نشده یا کار نمیکند.")
|
501 |
if not HF_GEMINI_API_KEY: print("هشدار: متغیر محیطی GEMINI_API_KEY تنظیم نشده است.")
|
502 |
demo.launch(debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true", share=False)
|