Hamed744 commited on
Commit
1799f89
·
verified ·
1 Parent(s): f99e261

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +333 -32
app.py CHANGED
@@ -1,6 +1,333 @@
1
- # ... (تمام ایمپورت‌ها و تعاریف توابع و کلاس‌ها مانند قبل) ...
 
 
 
 
 
 
 
 
 
 
2
 
3
- # --- تعریف CSS سفارشی با انیمیشن‌های ساده و بهبود ظاهر ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- #examples-separator hr, #examples-separator > div > hr {{ /* هدف قرار دادن hr داخل کامپوننت مارک‌داون */
62
- margin-top: 2rem !important;
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)