Hamed744 commited on
Commit
73c0690
·
verified ·
1 Parent(s): 73c7042

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +523 -434
app.py CHANGED
@@ -1,483 +1,572 @@
1
  import gradio as gr
2
- import edge_tts
3
- import tempfile
4
- import asyncio
5
- import traceback
6
- import os
7
- import google.generativeai as genai
8
- import logging
9
- import time
10
- import threading
11
- import sys
12
- import base64
13
  import mimetypes
 
14
  import re
15
  import struct
16
- from google import genai as google_genai
17
- from google.genai import types
18
- from pydub import AudioSegment
19
-
20
- # --- پیکربندی لاگینگ ---
21
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
22
-
23
- # --- چرخش کلیدهای Gemini API ---
24
- API_KEYS_GEMINI = []
25
- i = 1
26
- while True:
27
- key = os.environ.get(f'GEMINI_API_KEY_{i}')
28
- if key:
29
- API_KEYS_GEMINI.append(key)
30
- i += 1
31
- else:
32
- break
33
-
34
- NUM_GEMINI_KEYS = len(API_KEYS_GEMINI)
35
- current_gemini_key_index = 0
36
- gemini_operations_lock = asyncio.Lock()
37
-
38
- if NUM_GEMINI_KEYS == 0:
39
- logging.error(
40
- 'خطای حیاتی: هیچ Secret با نام GEMINI_API_KEY_n (مثلاً GEMINI_API_KEY_1) یافت نشد! '
41
- 'قابلیت ترجمه غیرفعال خواهد بود. لطفاً Secret ها را در تنظیمات Space خود اضافه کنید.'
42
- )
43
- else:
44
- logging.info(f"تعداد {NUM_GEMINI_KEYS} کلید API جیمینای بارگذاری شد.")
45
-
46
- # --- ریستارت خودکار ---
47
- def auto_restart_service():
48
- RESTART_INTERVAL_SECONDS = 24 * 60 * 60 # 24 ساعت
49
- logging.info(f"سرویس برای ری‌استارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمان‌بندی شده است.")
50
- time.sleep(RESTART_INTERVAL_SECONDS)
51
- logging.info(f"زمان ری‌استارت خودکار ({RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت) فرا رسیده است. برنامه خارج می‌شود تا توسط پلتفرم ری‌استارت شود...")
52
- os._exit(1)
53
-
54
- # --- دیکشنری صداهای انگلیسی ---
55
- language_dict_persian_keys = {
56
- 'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural',
57
- 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
58
- 'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural',
59
- 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
60
- 'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural',
61
- 'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
62
- 'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural',
63
- 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
64
- 'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural',
65
- 'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
66
- 'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural',
67
- 'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
68
- 'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural',
69
- 'انگلیسی (بریتانیا) - میا (زن، جدید)': 'en-GB-MiaNeural',
70
- 'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural',
71
- 'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
72
- 'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural',
73
- 'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
74
- 'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural',
75
- 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
76
- 'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural',
77
- 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
78
- 'انگلیسی (آفریقای جنوبی) - لیا (زن)': 'en-ZA-LeahNeural',
79
- 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
80
- 'انگلیسی (هنگ کنگ) - یان (زن)': 'en-HK-YanNeural',
81
- 'انگلیسی (هنگ کنگ) - سم (مرد)': 'en-HK-SamNeural',
82
- 'انگلیسی (نیوزیلند) - میچل (مرد)': 'en-NZ-MitchellNeural',
83
- 'انگلیسی (فیلیپین) - روزا (زن)': 'en-PH-RosaNeural',
84
- 'انگلیسی (فیلیپین) - جیمز (مرد)': 'en-PH-JamesNeural',
85
- 'انگلیسی (سنگاپور) - لونا (زن)': 'en-SG-LunaNeural',
86
- 'انگلیسی (سنگاپور) - وین (مرد)': 'en-SG-WayneNeural',
87
- 'انگلیسی (کنیا) - آسیلیا (زن)': 'en-KE-AsiliaNeural',
88
- 'انگلیسی (کنیا) - چیلمبا (مرد)': 'en-KE-ChilembaNeural',
89
- 'انگلیسی (نیجریه) - ازینه (زن)': 'en-NG-EzinneNeural',
90
- 'انگلیسی (نیجریه) - آبئو (مرد)': 'en-NG-AbeoNeural',
91
- 'انگلیسی (تانزانیا) - ایمانی (زن)': 'en-TZ-ImaniNeural',
92
- 'انگلیسی (تانزانیا) - الیمو (مرد)': 'en-TZ-ElimuNeural',
93
- }
94
-
95
- # --- توابع اصلی ---
96
- async def translate_text_gemini(text, target_language="English"):
97
- if NUM_GEMINI_KEYS == 0:
98
- return "خطا: سرویس ترجمه پیکربندی نشده (هیچ کلید API جیمینای معتبری یافت نشد).", None
99
- if not text or not text.strip():
100
- return "خطا: متنی برای ترجمه وارد نشده است.", None
101
-
102
- selected_api_key = None
103
- model_to_use = 'gemini-1.5-flash-latest'
104
-
105
- async with gemini_operations_lock:
106
- global current_gemini_key_index
107
- key_index_to_use = current_gemini_key_index
108
- selected_api_key = API_KEYS_GEMINI[key_index_to_use]
109
- current_gemini_key_index = (current_gemini_key_index + 1) % NUM_GEMINI_KEYS
110
- logging.info(f"قفل Gemini گرفته شد. استفاده از کلید API با اندیس: {key_index_to_use} (...{selected_api_key[-4:]}) برای مدل {model_to_use}")
111
 
112
- try:
113
- genai.configure(api_key=selected_api_key)
114
- model_instance = genai.GenerativeModel(model_to_use)
115
- logging.info(f"پیکربندی Gemini با کلید ...{selected_api_key[-4:]} برای مدل {model_to_use} انجام شد.")
116
- prompt = f"Translate the following Persian text to {target_language}. Provide only the translated English text, naturally and fluently, without any extra phrases, explanations, or markdown formatting. Be concise and accurate.\n\nPersian: \"{text}\"\n{target_language}:"
117
- response = await model_instance.generate_content_async(prompt)
118
- translated_text = response.text.strip()
119
- if translated_text.lower().startswith(f"{target_language.lower()}:"):
120
- translated_text = translated_text[len(target_language)+1:].strip()
121
- logging.info(f"ترجمه با کلید ...{selected_api_key[-4:]} موفق. قفل Gemini آزاد می‌شود.")
122
- return "ترجمه موفق", translated_text
123
- except Exception as e:
124
- key_info = f"...{selected_api_key[-4:]}" if selected_api_key else "N/A"
125
- logging.error(f"خطای Gemini با کلید {key_info} (مدل {model_to_use}): {e}\n{traceback.format_exc()}")
126
- return f"خطای ترجمه ({type(e).__name__}) با کلید جاری.", None
127
-
128
- MAX_TTS_RETRIES = 1
129
- TTS_RETRY_DELAY = 0.5
130
-
131
- async def text_to_speech_edge_async(text_to_speak, tts_voice_key, rate, volume, pitch):
132
- voice_id = language_dict_persian_keys.get(tts_voice_key)
133
- if voice_id is None:
134
- logging.error(f"کلید صدای '{tts_voice_key}' در دیکشنری یافت نشد.")
135
- return f"خطای TTS: صدای '{tts_voice_key}' یافت نشد (پیکربندی داخلی).", None
136
-
137
- logging.info(f"تلاش برای تولید صدا با: VoiceKey='{tts_voice_key}', VoiceID='{voice_id}', Text='{text_to_speak[:30]}...'")
138
-
139
- if not text_to_speak or not text_to_speak.strip():
140
- return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- for attempt in range(MAX_TTS_RETRIES + 1):
 
 
 
 
 
 
 
 
143
  try:
144
- rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
145
- communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
146
-
147
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
148
- tmp_path = tmp_file.name
149
- await communicate.save(tmp_path)
150
- logging.info(f"صدای '{voice_id}' (تلاش {attempt + 1}) با موفقیت در '{tmp_path}' ذخیره شد.")
151
- return "TTS موفق", tmp_path
152
-
153
- except edge_tts.exceptions.NoAudioReceived as e_no_audio:
154
- logging.warning(f"خطای NoAudioReceived برای VoiceID='{voice_id}' (تلاش {attempt + 1}/{MAX_TTS_RETRIES + 1}). خطا: {e_no_audio}")
155
- if attempt < MAX_TTS_RETRIES:
156
- await asyncio.sleep(TTS_RETRY_DELAY)
157
- else:
158
- return f"خطای TTS: صدایی برای '{tts_voice_key}' دریافت نشد (NoAudioReceived).", None
159
- except Exception as e:
160
- logging.error(f"خطای Edge-TTS (تلاش {attempt + 1}) برای VoiceID='{voice_id}': {e}\n{traceback.format_exc()}")
161
- return f"خطای TTS ({type(e).__name__}): مشکلی در تولید صدا برای '{tts_voice_key}' پیش آمد.", None
 
 
 
 
 
162
 
163
- return "خطای TTS: تلاش‌ها برای تولید صدا ناموفق بود.", None
 
164
 
165
- async def translate_and_speak_async_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
166
- if NUM_GEMINI_KEYS == 0:
167
- return "خطا: سرویس ترجمه پیکربندی نشده (کلید API جیمینای یافت نشد).", None
168
- if not persian_text or not persian_text.strip():
169
- return "لطفاً متن فارسی را برای ترجمه وارد کنید.", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- translation_status_msg, translated_text = await translate_text_gemini(persian_text, target_language="English")
172
- translated_text_output = translated_text if translated_text else "ترجمه ناموفق بود."
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- if "خطا" in translation_status_msg or not translated_text:
175
- return f"{translated_text_output}\n({translation_status_msg})", None
 
 
176
 
177
- if english_tts_voice_key not in language_dict_persian_keys:
178
- if language_dict_persian_keys:
179
- english_tts_voice_key = list(language_dict_persian_keys.keys())[0]
180
- logging.warning(f"صدای انتخابی نامعتبر، به پیشفرض '{english_tts_voice_key}' تغییر یافت.")
181
- else:
182
- return f"{translated_text_output}\n\n(خطای TTS: هیچ صدایی موجود نیست.)", None
183
 
184
- tts_status_msg, audio_path = await text_to_speech_edge_async(translated_text, english_tts_voice_key, rate, volume, pitch)
185
-
186
- if "خطا" in tts_status_msg or not audio_path:
187
- return f"{translated_text_output}\n\n({tts_status_msg})", None
188
-
189
- return translated_text_output, audio_path
190
-
191
- # --- تنظیمات رابط کاربری ---
192
- APP_HEADER_GRADIENT_START = "#4A00E0"
193
- APP_HEADER_GRADIENT_END = "#8E2DE2"
194
- PANEL_BACKGROUND = "#FFFFFF"
195
- TEXT_INPUT_BG = "#F7F7F7"
196
- BUTTON_BG_IMG = "#2979FF"
197
- MAIN_BACKGROUND_IMG = "linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%)"
198
-
199
- custom_css = f"""
200
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
 
 
 
 
 
 
 
 
 
 
201
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
 
 
202
  :root {{
203
- --app-font: 'Vazirmatn', sans-serif;
204
- --app-font-english: 'Poppins', sans-serif;
205
- --app-header-grad-start: {APP_HEADER_GRADIENT_START};
206
- --app-header-grad-end: {APP_HEADER_GRADIENT_END};
207
- --app-panel-bg: {PANEL_BACKGROUND};
208
- --app-input-bg: {TEXT_INPUT_BG};
209
- --app-button-bg: {BUTTON_BG_IMG};
210
- --app-main-bg: {MAIN_BACKGROUND_IMG};
211
- --app-text-primary: #333;
212
- --app-text-secondary: #555;
213
- --app-border-color: #E0E0E0;
214
- --radius-card: 20px;
215
- --radius-input: 8px;
216
- --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1);
217
- --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);
 
 
 
 
 
 
 
218
  }}
219
- body, .gradio-container {{
220
- font-family: var(--app-font);
 
221
  direction: rtl;
222
- background: var(--app-main-bg);
223
- color: var(--app-text-primary);
224
- font-size: 16px;
225
- line-height: 1.7;
226
  }}
227
- .gradio-container {{
228
- max-width:100% !important;
229
- min-height:100vh;
230
- margin:0 !important;
231
- padding:0 !important;
232
- display:flex;
233
- flex-direction:column;
234
  }}
235
- .app-header-alpha {{
236
- padding: 3rem 1.5rem 4rem 1.5rem;
237
- text-align: center;
238
- background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%);
239
- color: white;
240
- border-bottom-left-radius: var(--radius-card);
241
- border-bottom-right-radius: var(--radius-card);
242
- box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2);
243
  }}
244
- .app-header-alpha h1 {{
245
- font-size: 2.4em;
246
- font-weight: 800;
247
- margin:0 0 0.5rem 0;
248
- text-shadow: 0 2px 4px rgba(0,0,0,0.15);
249
  }}
250
- .app-header-alpha p {{
251
- font-size: 1.1em;
252
- color: rgba(255,255,255,0.9);
253
- margin-top:0;
254
- opacity: 0.9;
255
  }}
256
- .main-content-panel-alpha {{
257
- padding: 1.8rem 1.5rem;
258
- max-width: 780px;
259
- margin: -2.5rem auto 2rem auto;
260
- width: 90%;
261
- background-color: var(--app-panel-bg);
262
- border-radius: var(--radius-card);
263
- box-shadow: var(--shadow-card);
264
- position:relative;
265
- z-index:10;
266
  }}
267
- @media (max-width: 768px) {{
268
- .main-content-panel-alpha {{
269
- width: 95%;
270
- padding: 1.5rem 1rem;
271
- margin-top: -2rem;
272
- }}
273
- .app-header-alpha h1 {{
274
- font-size:2em;
275
- }}
276
- .app-header-alpha p {{
277
- font-size:1em;
278
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  }}
280
- footer, .gradio-footer, .flagging-container {{display:none !important;}}
281
-
282
- .gr-button.generate-button-final {{
283
- background: var(--app-button-bg) !important;
284
- color: white !important;
285
- border:none !important;
286
- border-radius: var(--radius-input) !important;
287
- padding: 0.8rem 1.5rem !important;
288
- font-weight: 700 !important;
289
- font-size:1.05em !important;
290
- transition: all 0.3s ease;
291
- box-shadow: var(--shadow-button);
292
- width:100%;
293
- margin-top:1.5rem !important;
294
  }}
295
- .gr-button.generate-button-final:hover {{
296
- filter: brightness(1.1);
297
- transform: translateY(-2px);
298
- box-shadow: 0 6px 12px -3px rgba(41,121,255,0.6);
299
  }}
300
- .gr-input > label + div > textarea,
301
- .gr-dropdown > label + div > div > input,
302
- .gr-dropdown > label + div > div > select,
303
- .gr-textbox > label + div > textarea {{
304
- border-radius: var(--radius-input) !important;
305
- border: 1px solid var(--app-border-color) !important;
306
- background-color: var(--app-input-bg) !important;
307
- box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
308
- padding: 0.75rem !important;
309
  }}
310
- .gr-input > label + div > textarea:focus,
311
- .gr-dropdown > label + div > div > input:focus,
312
- .gr-textbox > label + div > textarea:focus {{
313
- border-color: var(--app-button-bg) !important;
314
- box-shadow: 0 0 0 3px rgba(41,121,255,0.2) !important;
315
  }}
316
- label > .label-text {{
317
- font-weight: 700 !important;
318
- color: var(--app-text-primary) !important;
319
- font-size: 0.95em !important;
320
- margin-bottom: 0.5rem !important;
321
  }}
322
- .translated-text-output textarea {{
323
- font-family: var(--app-font-english) !important;
324
- direction: ltr !important;
325
- text-align: left !important;
 
 
 
326
  }}
327
- .section-title-main-alpha {{
328
- font-size: 1.1em;
329
- color: var(--app-text-secondary);
330
- margin-bottom:1rem;
331
- padding-bottom: 0.5rem;
332
- border-bottom: 1px solid var(--app-border-color);
333
- font-weight:500;
334
- text-align:right;
335
  }}
336
- label[for="text_input_main"] > .label-text::before {{ content: '📝'; margin-left: 8px; }}
337
- label[for="voice_dropdown"] > .label-text::before {{ content: '🗣️'; margin-left: 8px; }}
338
- label[for="rate_slider"] > .label-text::before {{ content: '⏩'; margin-left: 8px; }}
339
- label[for="volume_slider"] > .label-text::before {{ content: '🔊'; margin-left: 8px; }}
340
- label[for="pitch_slider"] > .label-text::before {{ content: '🎵'; margin-left: 8px; }}
341
- label[for="translated_text_output"] > .label-text::before {{ content: '📜'; margin-left: 8px; }}
342
- label[for="output_audio"] > .label-text::before {{ content: '🎧'; margin-left: 8px; }}
343
-
344
- #output_audio_player audio {{
345
- width: 100%;
346
- border-radius: var(--radius-input);
347
- margin-top:0.8rem;
348
  }}
349
- .app-footer-final {{
350
- text-align:center;
351
- font-size:0.9em;
352
- color: var(--app-text-secondary);
353
- opacity:0.8;
354
- margin-top:3rem;
355
- padding:1.5rem 0;
356
- border-top:1px solid var(--app-border-color);
357
  }}
358
- .advanced-settings-label {{
359
- font-size: 0.85em;
360
- color: #666;
361
- margin-top: -0.5rem;
362
- margin-bottom: 0.5rem;
 
 
 
 
363
  }}
364
  """
365
 
366
- # --- رابط کاربری Gradio ---
367
- default_english_tts_voice = 'انگلیسی (آمریکا) - جنی (زن)'
368
- if not language_dict_persian_keys:
369
- logging.critical("خطای بحرانی: language_dict_persian_keys خالی است! هیچ صدایی در دسترس نیست.")
370
- default_english_tts_voice = "لیست صداها خالی است"
371
- elif default_english_tts_voice not in language_dict_persian_keys:
372
- default_english_tts_voice = list(language_dict_persian_keys.keys())[0]
373
- logging.warning(f"صدای پیشفرض اولیه '{'انگلیسی (آمریکا) - جنی (زن)'}' در لیست جدید یافت نشد، به '{default_english_tts_voice}' تغییر یافت.")
374
-
375
- with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css, title="Alpha Translator") as demo:
376
- gr.HTML("""
377
- <div class='app-header-alpha'>
378
- <h1>Alpha Translator</h1>
379
- <p>جادوی ترجمه و تلفظ در دستان شما</p>
380
- </div>
381
  """)
382
 
383
- with gr.Column(elem_classes=["main-content-panel-alpha"]):
384
- if NUM_GEMINI_KEYS == 0:
385
- missing_key_msg = (
386
- "⚠️ هشدار: قابلیت ترجمه غیرفعال است. "
387
- "هیچ کلید API جیمینای (با فرمت GEMINI_API_KEY_1, ...) "
388
- "در بخش Secrets این Space یافت نشد. "
389
- "لطفاً حداقل یک کلید با نام GEMINI_API_KEY_1 تنظیم کنید."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  )
391
- gr.Markdown(f"<div style='background-color:#FFFBEB; color:#92400E; padding:10px 12px; border-radius:8px; border:1px solid #FDE68A; text-align:center; margin-bottom:1rem;'>{missing_key_msg}</div>")
392
-
393
- with gr.Row():
394
- with gr.Column(scale=3, min_width=300):
395
- input_text_persian = gr.Textbox(
396
- lines=4, label="متن فارسی برای ترجمه",
397
- placeholder="مثال: سلام، فردا هوا چطور است؟",
398
- elem_id="text_input_main"
399
- )
400
-
401
- language_dropdown_tts_english = gr.Dropdown(
402
- choices=list(language_dict_persian_keys.keys()),
403
- value=default_english_tts_voice,
404
- label="انتخاب گوینده و لهجه انگلیسی",
405
- elem_id="voice_dropdown",
406
- interactive=bool(language_dict_persian_keys)
407
-
408
- with gr.Accordion("⚙️ تنظیمات پیشرفته صدا", open=False):
409
- gr.Markdown("<p class='advanced-settings-label'>تنظیمات حرفه‌ای برای کنترل دقیقتر خروجی صدا</p>")
410
- with gr.Row():
411
- rate_slider = gr.Slider(
412
- minimum=-100, maximum=100, step=1, value=0,
413
- label="سرعت (%)", scale=1, elem_id="rate_slider")
414
- volume_slider = gr.Slider(
415
- minimum=-100, maximum=100, step=1, value=0,
416
- label="حجم (%)", scale=1, elem_id="volume_slider")
417
- pitch_slider = gr.Slider(
418
- minimum=-50, maximum=50, step=1, value=0,
419
- label="گام (Hz)", elem_id="pitch_slider")
420
-
421
- submit_button = gr.Button(
422
- "🚀 ترجمه و تلفظ",
423
- variant="primary",
424
- elem_classes=["generate-button-final"])
425
-
426
- with gr.Column(scale=2, min_width=280):
427
- output_text_translated = gr.Textbox(
428
- label="متن ترجمه شده (انگلیسی)",
429
- interactive=False, lines=6,
430
- placeholder="متن انگلیسی ترجمه شده یا پیام‌های خطا...",
431
- elem_id="translated_text_output")
432
-
433
- output_audio = gr.Audio(
434
- type="filepath", label="فایل صوتی",
435
- format="mp3", interactive=False,
436
- autoplay=True, elem_id="output_audio_player")
437
-
438
- if language_dict_persian_keys and default_english_tts_voice and default_english_tts_voice != "لیست صداها خالی است":
439
- gr.HTML("<hr style='height:1px;background-color:var(--app-border-color);margin:1.5rem 0;border:none;'>")
440
 
441
- num_voices = len(language_dict_persian_keys)
442
- voice_keys = list(language_dict_persian_keys.keys())
443
- voice1_idx = 0
444
- voice2_idx = min(1, num_voices - 1) if num_voices > 0 else 0
445
- voice3_idx = min(2, num_voices - 1) if num_voices > 0 else 0
 
 
 
 
 
 
 
 
446
 
447
- example_list = [
448
- ["قیمت این لباس چقدر است؟", voice_keys[voice1_idx] if num_voices > 0 else current_default_voice, 0, 0, 0],
449
- ["می‌توانید آدرس را روی نقشه به من نشان دهید؟", voice_keys[voice2_idx] if num_voices > 0 else current_default_voice, 0, 0, 0],
450
- ["ببخشید، متوجه نشدم. امکان دارد تکرار کنید؟", voice_keys[voice3_idx] if num_voices > 0 else current_default_voice, -10, 0, 0],
451
- ]
452
 
453
- if example_list:
454
- gr.Examples(
455
- examples=example_list,
456
- inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
457
- outputs=[output_text_translated, output_audio],
458
- fn=translate_and_speak_async_wrapper,
459
- cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
460
- label="💡 نمونه‌های کاربردی"
461
- )
462
- else:
463
- gr.Markdown("<p style='text-align:center; color:var(--app-text-secondary); margin-top:1rem;'>نمونه‌ای برای نمایش موجود نیست.</p>")
464
 
465
- gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2025</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
- submit_button.click(
468
- fn=translate_and_speak_async_wrapper,
469
- inputs=[input_text_persian, language_dropdown_tts_english, rate_slider, volume_slider, pitch_slider],
470
- outputs=[output_text_translated, output_audio]
471
- )
472
 
473
- # --- راه‌اندازی برنامه ---
474
- if __name__ == "__main__":
475
- if not language_dict_persian_keys:
476
- logging.critical("خطای بحرانی: language_dict_persian_keys خالی است!")
477
 
478
- # شروع ترد ری‌استارت خودکار
479
- restart_scheduler_thread = threading.Thread(target=auto_restart_service, daemon=True)
480
- restart_scheduler_thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
  demo.launch(
483
  server_name="0.0.0.0",
 
1
  import gradio as gr
2
+ # import base64 # Not used in your original core logic
 
 
 
 
 
 
 
 
 
 
3
  import mimetypes
4
+ import os
5
  import re
6
  import struct
7
+ import time
8
+ # import zipfile # Not used in your original core logic
9
+ from google import genai
10
+ from google.genai import types as genai_types # Aliased your 'types' import to avoid conflict
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # --- Standard Python Logging (very basic) ---
13
+ import logging
14
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
15
+
16
+ try:
17
+ from pydub import AudioSegment
18
+ PYDUB_AVAILABLE = True
19
+ except ImportError:
20
+ PYDUB_AVAILABLE = False
21
+ logging.warning("Pydub (for audio merging) not found. Merging will be disabled if multiple audio chunks are generated.")
22
+
23
+
24
+ # --- START: YOUR EXACT CORE TTS LOGIC (AlphaTTS_Original) ---
25
+ # This entire block is copied VERBATIM from the AlphaTTS_Original code you provided
26
+ # No changes to logic, variable names, or function signatures within this block.
27
+
28
+ SPEAKER_VOICES = [
29
+ "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
30
+ "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
31
+ "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib",
32
+ "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
33
+ "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
34
+ ]
35
+ FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts" # YOUR DEFINED MODEL
36
+ DEFAULT_MAX_CHUNK_SIZE = 3800
37
+ DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
38
+ DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
39
+
40
+ def _log(message, log_list): # YOUR _log function
41
+ log_list.append(message)
42
+ logging.info(f"[AlphaTTS_LOG_INTERNAL] {message}") # Added standard logging for visibility
43
+
44
+ def save_binary_file(file_name, data, log_list): # YOUR save_binary_file
45
+ try:
46
+ with open(file_name, "wb") as f: f.write(data)
47
+ _log(f"✅ فایل ذخیره شد: {file_name}", log_list)
48
+ return file_name
49
+ except Exception as e:
50
+ _log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
51
+ return None
52
+
53
+ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes: # YOUR convert_to_wav
54
+ parameters = parse_audio_mime_type(mime_type)
55
+ bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
56
+ num_channels, data_size = 1, len(audio_data)
57
+ bytes_per_sample, block_align = bits_per_sample // 8, num_channels * (bits_per_sample // 8)
58
+ byte_rate, chunk_size = rate * block_align, 36 + data_size
59
+ header = 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)
60
+ return header + audio_data
61
+
62
+ def parse_audio_mime_type(mime_type: str) -> dict[str, int]: # YOUR parse_audio_mime_type
63
+ bits, rate = 16, 24000
64
+ for param in mime_type.split(";"):
65
+ param = param.strip()
66
+ if param.lower().startswith("rate="):
67
+ try: rate = int(param.split("=", 1)[1])
68
+ except ValueError: pass # Your original had 'except: pass'
69
+ elif param.startswith("audio/L"):
70
+ try: bits = int(param.split("L", 1)[1])
71
+ except ValueError: pass # Your original had 'except: pass'
72
+ return {"bits_per_sample": bits, "rate": rate}
73
+
74
+ def smart_text_split(text, max_size=3800, log_list=None): # YOUR smart_text_split
75
+ if len(text) <= max_size: return [text]
76
+ chunks, current_chunk = [], ""
77
+ sentences = re.split(r'(?<=[.!?؟])\s+', text) # Your original regex
78
+ for sentence in sentences:
79
+ if len(current_chunk) + len(sentence) + 1 > max_size:
80
+ if current_chunk: chunks.append(current_chunk.strip())
81
+ current_chunk = sentence
82
+ while len(current_chunk) > max_size:
83
+ split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
84
+ 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:])
85
+ chunks.append(part.strip())
86
+ else: current_chunk += (" " if current_chunk else "") + sentence
87
+ if current_chunk: chunks.append(current_chunk.strip())
88
+ final_chunks = [c for c in chunks if c]
89
+ if log_list: _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
90
+ return final_chunks
91
+
92
+ def merge_audio_files_func(file_paths, output_path, log_list): # YOUR merge_audio_files_func
93
+ if not PYDUB_AVAILABLE: _log("❌ pydub در دسترس نیست.", log_list); return False
94
+ try:
95
+ _log(f"🔗 ادغام {len(file_paths)} فایل صوتی...", log_list)
96
+ combined = AudioSegment.empty()
97
+ for i, fp in enumerate(file_paths):
98
+ if os.path.exists(fp):
99
+ try:
100
+ segment = AudioSegment.from_file(fp) # Pydub infers format
101
+ combined += segment
102
+ if i < len(file_paths) - 1:
103
+ combined += AudioSegment.silent(duration=150) # Your original logic
104
+ except Exception as e_pydub: # More specific exception handling for pydub
105
+ _log(f"⚠️ خطای Pydub در پردازش فایل '{fp}': {e_pydub}. این فایل نادیده گرفته می شود.", log_list)
106
+ continue
107
+ else: _log(f"⚠️ فایل پیدا نشد: {fp}", log_list)
108
+ if len(combined) == 0: # Check if anything was combined
109
+ _log("❌ هیچ قطعه صوتی معتبری برای ادغام یافت نشد.", log_list)
110
+ return False
111
+ combined.export(output_path, format="wav")
112
+ _log(f"✅ فایل ادغام شده: {output_path}", log_list); return True
113
+ except Exception as e: _log(f"❌ خطا در ادغام: {e}", log_list); return False
114
+
115
+ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list): # YOUR core_generate_audio
116
+ output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
117
+ max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
118
+ _log(f"🚀 شروع فرآیند با مدل: {FIXED_MODEL_NAME}...", log_list)
119
+ api_key = os.environ.get("GEMINI_API_KEY") # YOUR API KEY METHOD
120
+ if not api_key: _log("❌ کلید API تنظیم نشده.", log_list); return None
121
+ try: client = genai.Client(api_key=api_key) # YOUR CLIENT INSTANTIATION
122
+ except Exception as e: _log(f"❌ خطا در کلاینت: {e}", log_list); return None # Your original error handling
123
+ if not text_input or not text_input.strip(): _log("❌ متن ورودی خالی.", log_list); return None
124
+ text_chunks = smart_text_split(text_input, max_chunk, log_list)
125
+ if not text_chunks: _log("❌ متن قابل پردازش نیست.", log_list); return None
126
 
127
+ generated_files = []
128
+ for i, chunk in enumerate(text_chunks):
129
+ _log(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)} (صدا: {selected_voice}, دما: {temperature_val})...", log_list)
130
+ final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
131
+ contents = [genai_types.Content(role="user", parts=[genai_types.Part.from_text(text=final_text)])]
132
+ config = genai_types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
133
+ speech_config=genai_types.SpeechConfig(voice_config=genai_types.VoiceConfig(
134
+ prebuilt_voice_config=genai_types.PrebuiltVoiceConfig(voice_name=selected_voice))))
135
+ fname_base = f"{output_base_name}_part{i+1:03d}"
136
  try:
137
+ # YOUR API CALL
138
+ response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
139
+ # YOUR RESPONSE HANDLING
140
+ if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
141
+ inline_data = response.candidates[0].content.parts[0].inline_data
142
+ data_buffer = inline_data.data
143
+ mime_type = inline_data.mime_type
144
+ _log(f"داده صوتی در candidate.part[0].inline_data برای قطعه {i+1} یافت شد. MIME: {mime_type}", log_list)
145
+ ext = mimetypes.guess_extension(mime_type) or ".wav"
146
+ if "audio/L" in mime_type and ext == ".wav":
147
+ _log(f"تبدیل صدای خام PCM (MIME: {mime_type}) به WAV برای قطعه {i+1}.", log_list)
148
+ data_buffer = convert_to_wav(data_buffer, mime_type)
149
+ if not ext.startswith("."): ext = "." + ext
150
+ fpath = save_binary_file(f"{fname_base}{ext}", data_buffer, log_list)
151
+ if fpath: generated_files.append(fpath)
152
+ else: _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی.", log_list) # Your original log message
153
+ except Exception as e: _log(f" خطا در تولید قطعه {i+1}: {e}", log_list); continue # YOUR ORIGINAL ERROR HANDLING
154
+ if i < len(text_chunks) - 1 and len(text_chunks) > 1:
155
+ _log(f"💤 توقف کوتاه ({sleep_time} ثانیه) قبل از قطعه بعدی...", log_list)
156
+ time.sleep(sleep_time)
157
+
158
+ if not generated_files: _log("❌ هیچ فایلی تولید نشد.", log_list); return None
159
+ _log(f"🎉 {len(generated_files)} فایل(های) صوتی تولید شد.", log_list)
160
 
161
+ final_audio_file = None
162
+ final_output_path_base = f"{output_base_name}_final"
163
 
164
+ # YOUR FILE MERGING AND RENAMING LOGIC (VERBATIM)
165
+ if len(generated_files) > 1:
166
+ if PYDUB_AVAILABLE:
167
+ merged_fn = f"{final_output_path_base}.wav"
168
+ if os.path.exists(merged_fn):
169
+ try: os.remove(merged_fn)
170
+ except Exception as e_rm: _log(f"⚠️ خطا در حذف فایل ادغام شده قبلی '{merged_fn}': {e_rm}", log_list) # Added log
171
+ if merge_audio_files_func(generated_files, merged_fn, log_list):
172
+ final_audio_file = merged_fn
173
+ for fp in generated_files:
174
+ if os.path.abspath(fp) != os.path.abspath(merged_fn):
175
+ try: os.remove(fp)
176
+ except: pass # Your original silent pass
177
+ else:
178
+ if generated_files:
179
+ try:
180
+ # Ensuring target name doesn't overwrite source if paths are somehow the same initially
181
+ source_path = generated_files[0]
182
+ target_path = f"{final_output_path_base}{os.path.splitext(source_path)[1]}"
183
+ if os.path.abspath(source_path) != os.path.abspath(target_path):
184
+ if os.path.exists(target_path): os.remove(target_path)
185
+ os.rename(source_path, target_path)
186
+ final_audio_file = target_path
187
+ # Clean up other parts only if the first one was successfully moved/renamed
188
+ if final_audio_file == target_path:
189
+ for i_gf in range(1, len(generated_files)):
190
+ try: os.remove(generated_files[i_gf])
191
+ except: pass
192
+ except Exception as e_rename: # Your original variable name
193
+ _log(f"خطا در تغییر نام فایل اولین قطعه: {e_rename}", log_list)
194
+ final_audio_file = generated_files[0]
195
+ else:
196
+ _log("⚠️ pydub نیست. اولین قطعه ارائه می‌شود.", log_list)
197
+ if generated_files:
198
+ try:
199
+ source_path = generated_files[0]
200
+ target_path = f"{final_output_path_base}{os.path.splitext(source_path)[1]}"
201
+ if os.path.abspath(source_path) != os.path.abspath(target_path):
202
+ if os.path.exists(target_path): os.remove(target_path)
203
+ os.rename(source_path, target_path)
204
+ final_audio_file = target_path
205
+ if final_audio_file == target_path:
206
+ for i_gf in range(1, len(generated_files)):
207
+ try: os.remove(generated_files[i_gf])
208
+ except: pass
209
+ except Exception as e_rename_single: # Your original variable name
210
+ _log(f"خطا در تغییر نام فایل اولین قطعه (بدون pydub): {e_rename_single}", log_list)
211
+ final_audio_file = generated_files[0]
212
+ elif len(generated_files) == 1:
213
+ try:
214
+ source_path = generated_files[0]
215
+ target_path = f"{final_output_path_base}{os.path.splitext(source_path)[1]}"
216
+ if os.path.abspath(source_path) != os.path.abspath(target_path):
217
+ if os.path.exists(target_path): os.remove(target_path)
218
+ os.rename(source_path, target_path)
219
+ final_audio_file = target_path
220
+ except Exception as e_rename_single_final: # Your original variable name
221
+ _log(f"خطا در تغییر نام فایل تکی نهایی: {e_rename_single_final}", log_list)
222
+ final_audio_file = generated_files[0]
223
+
224
+ if final_audio_file and not os.path.exists(final_audio_file):
225
+ _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!", log_list)
226
+ return None
227
+ return final_audio_file # YOUR ORIGINAL RETURN
228
 
229
+ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)): # YOUR ORIGINAL SIGNATURE
230
+ logs = []
231
+ actual_text = ""
232
+ if use_file_input:
233
+ if uploaded_file: # In Gradio, uploaded_file is a TemporaryFileWrapper object
234
+ try:
235
+ # uploaded_file.name gives the path to the temporary file
236
+ with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
237
+ if not actual_text: return None
238
+ except Exception as e: _log(f"❌ خطا خواندن فایل: {e}", logs); return None
239
+ else: return None # No file provided when checkbox is true
240
+ else:
241
+ actual_text = text_to_speak
242
+ if not actual_text or not actual_text.strip(): return None
243
 
244
+ final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, logs)
245
+ # for log_entry in logs: print(log_entry) # Your original commented-out log printing
246
+ return final_path # YOUR ORIGINAL RETURN (returns only audio path)
247
+ # --- END: YOUR EXACT CORE TTS LOGIC (AlphaTTS_Original) ---
248
 
 
 
 
 
 
 
249
 
250
+ # --- START: Styling and UI (Applying AlphaTranslator_Styled look to YOUR UI structure) ---
251
+ # (Using CSS variables from AlphaTranslator_Styled for colors and fonts)
252
+ FLY_PRIMARY_COLOR_HEX = "#4F46E5"
253
+ FLY_SECONDARY_COLOR_HEX = "#10B981"
254
+ FLY_ACCENT_COLOR_HEX = "#D97706" # Orange accent from AlphaTranslator_Styled (for buttons)
255
+ FLY_TEXT_COLOR_HEX = "#1F2937"
256
+ FLY_SUBTLE_TEXT_HEX = "#6B7280"
257
+ FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB" # Overall page bg
258
+ FLY_WHITE_HEX = "#FFFFFF" # Panel bg
259
+ FLY_BORDER_COLOR_HEX = "#D1D5DB"
260
+ FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6" # Input bg
261
+
262
+ # Theme for Gradio Blocks (using a base font from AlphaTranslator_Styled)
263
+ # Your original `gr.Blocks` used `theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")])`
264
+ # We'll use `app_theme_applied_styled` for body background and apply Vazirmatn in CSS.
265
+ app_theme_applied_styled = gr.themes.Base(
266
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
267
+ ).set(
268
+ body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
269
+ )
270
+
271
+ # Combined and adapted CSS
272
+ # Goal: Make YOUR UI components look like the AlphaTranslator_Styled components.
273
+ # Your original component IDs are like "use_file_cb_alpha_v3", "generate_button_alpha_v3", etc.
274
+ # CSS will target these IDs where possible.
275
+ final_combined_css_v2 = f"""
276
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
277
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
278
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
279
+
280
  :root {{
281
+ --fly-primary: {FLY_PRIMARY_COLOR_HEX};
282
+ --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
283
+ --fly-accent: {FLY_ACCENT_COLOR_HEX};
284
+ --fly-text-primary: {FLY_TEXT_COLOR_HEX};
285
+ --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX};
286
+ --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
287
+ --fly-bg-white: {FLY_WHITE_HEX};
288
+ --fly-border-color: {FLY_BORDER_COLOR_HEX};
289
+ --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE};
290
+ --fly-primary-rgb: 79,70,229;
291
+ --fly-accent-rgb: 217,119,6;
292
+
293
+ --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem;
294
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);
295
+ --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1);
296
+
297
+ --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
298
+ --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
299
+
300
+ /* Your original AlphaTTS CSS variables if different and needed for exact match */
301
+ --app-button-bg-original: #2979FF; /* Blue from your original AlphaTTS button */
302
+ --radius-input-original: 8px; /* Your original input radius */
303
  }}
304
+
305
+ body {{
306
+ font-family: var(--font-global);
307
  direction: rtl;
308
+ background-color: var(--fly-bg-light) !important; /* Ensure override */
309
+ color: var(--fly-text-primary);
310
+ line-height: 1.7;
311
+ font-size: 16px;
312
  }}
313
+
314
+ .gradio-container {{
315
+ max-width:100% !important; width:100% !important; min-height:100vh;
316
+ margin:0 auto !important; padding:0 !important; border-radius:0 !important;
317
+ box-shadow:none !important; background:linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%) !important; /* Ensure override */
318
+ display:flex; flex-direction:column;
 
319
  }}
320
+
321
+ /* Header styling from AlphaTranslator_Styled */
322
+ .app-header-alphatts-v2 {{ /* Unique class for this app's header */
323
+ text-align:center; padding:2.5rem 1rem; margin:0;
324
+ background:linear-gradient(135deg, var(--fly-primary) 0%, var(--fly-secondary) 100%);
325
+ color:var(--fly-bg-white); border-bottom-left-radius:var(--radius-xl);
326
+ border-bottom-right-radius:var(--radius-xl); box-shadow:var(--shadow-lg);
327
+ position:relative; overflow:hidden;
328
  }}
329
+ .app-header-alphatts-v2::before {{
330
+ content:''; position:absolute; top:-50px; right:-50px; width:150px; height:150px;
331
+ background:rgba(255,255,255,0.1); border-radius:9999px;
332
+ opacity:0.5; transform:rotate(45deg);
 
333
  }}
334
+ .app-header-alphatts-v2 h1 {{
335
+ font-size:2.25em !important; font-weight:800 !important; margin:0 0 0.5rem 0;
336
+ font-family:var(--font-english); letter-spacing:-0.5px; text-shadow:0 2px 4px rgba(0,0,0,0.1);
 
 
337
  }}
338
+ .app-header-alphatts-v2 p {{
339
+ font-size:1em !important; margin-top:0.25rem; font-weight:400;
340
+ color:rgba(255,255,255,0.85) !important;
341
+ }}
342
+
343
+ /* Main content panel structure from AlphaTranslator_Styled */
344
+ .main-content-area-alphatts-v2 {{
345
+ flex-grow:1; padding:0.75rem; width:100%; margin:0 auto; box-sizing:border-box;
 
 
346
  }}
347
+ /* This is the direct parent of your UI elements if you keep gr.Column as the main wrapper */
348
+ .content-panel-alphatts-v2 {{
349
+ background-color:var(--fly-bg-white); padding:1rem; border-radius:var(--radius-xl);
350
+ box-shadow:var(--shadow-xl); margin-top:-2rem; position:relative; z-index:10;
351
+ margin-bottom:2rem; width:100%; box-sizing:border-box;
352
+ }}
353
+
354
+ /* Styling YOUR UI elements to match AlphaTranslator_Styled look */
355
+ /* Inputs: Targeting specific elem_ids from your code where possible */
356
+ .content-panel-alphatts-v2 #{generate_button_alpha_v3} + .gr-input > label + div > textarea, /* For text_to_speak_tb */
357
+ .content-panel-alphatts-v2 #{generate_button_alpha_v3} + .gr-dropdown > label + div > div > input,
358
+ .content-panel-alphatts-v2 #{generate_button_alpha_v3} + .gr-dropdown > label + div > div > select,
359
+ .content-panel-alphatts-v2 #{generate_button_alpha_v3} + .gr-textbox > label + div > textarea, /* For speech_prompt_tb */
360
+ .content-panel-alphatts-v2 #{generate_button_alpha_v3} + .gr-file > label + div, /* For uploaded_file_input */
361
+ .content-panel-alphatts-v2 .gr-slider /* General slider */
362
+ {{
363
+ border-radius:var(--radius-input-original) !important; /* YOUR original radius */
364
+ border:1.5px solid var(--fly-border-color) !important;
365
+ font-size:0.95em !important;
366
+ background-color:var(--fly-input-bg-simple) !important;
367
+ padding:10px 12px !important;
368
+ color:var(--fly-text-primary) !important;
369
+ box-shadow: none !important;
370
+ }}
371
+
372
+ /* Focus styles for inputs */
373
+ .content-panel-alphatts-v2 .gr-input > label + div > textarea:focus,
374
+ .content-panel-alphatts-v2 .gr-dropdown > label + div > div > input:focus,
375
+ .content-panel-alphatts-v2 .gr-dropdown > label + div > div > select:focus,
376
+ .content-panel-alphatts-v2 .gr-textbox > label + div > textarea:focus,
377
+ .content-panel-alphatts-v2 .gr-file > label + div:focus-within
378
+ {{
379
+ border-color:var(--fly-primary) !important;
380
+ box-shadow:0 0 0 3px rgba(var(--fly-primary-rgb),0.12) !important;
381
+ background-color:var(--fly-bg-white) !important;
382
  }}
383
+ .content-panel-alphatts-v2 .gr-file > label + div {{ text-align:center; border-style: dashed !important; }}
384
+
385
+ /* Button: Targeting YOUR button by its elem_id with AlphaTranslator_Styled primary button look */
386
+ .content-panel-alphatts-v2 .gr-button#{generate_button_alpha_v3} /* YOUR BUTTON ID */
387
+ {{
388
+ background:var(--fly-accent) !important; /* Orange accent */
389
+ margin-top:1.5rem !important; padding:12px 20px !important; /* Adjusted padding */
390
+ transition:all 0.25s ease-in-out !important; color:white !important; font-weight:600 !important;
391
+ border-radius:var(--radius-input-original) !important; /* Your original radius */ border:none !important;
392
+ box-shadow:0 3px 8px -1px rgba(var(--fly-accent-rgb),0.3) !important;
393
+ width:100% !important; font-size:1.05em !important; /* Your original font size */
394
+ display:flex; align-items:center; justify-content:center;
 
 
395
  }}
396
+ .content-panel-alphatts-v2 .gr-button#{generate_button_alpha_v3}:hover
397
+ {{
398
+ background:#B45309 !important; transform:translateY(-1px) !important;
399
+ box-shadow:0 5px 10px -1px rgba(var(--fly-accent-rgb),0.4) !important;
400
  }}
401
+
402
+ /* Labels: General style (less specific to avoid breaking Gradio's structure) */
403
+ .content-panel-alphatts-v2 .gr-form label > .label-text span /* Try to target the span inside label */
404
+ {{
405
+ font-weight:500 !important; color: var(--fly-text-secondary) !important;
406
+ font-size:0.88em !important; margin-bottom:6px !important; display:inline-block;
 
 
 
407
  }}
408
+ /* Your temperature description class */
409
+ .content-panel-alphatts-v2 .temp_description_class_alpha_v3 {{
410
+ font-size: 0.85em; color: var(--fly-text-secondary); margin-top: -0.4rem; margin-bottom: 1rem;
 
 
411
  }}
412
+
413
+ /* Audio Player: Targeting your specific elem_id */
414
+ .content-panel-alphatts-v2 #output_audio_player_alpha_v3 audio
415
+ {{
416
+ width: 100%; border-radius: var(--radius-input-original); margin-top:0.8rem;
417
  }}
418
+
419
+ /* Examples styling from AlphaTranslator_Styled */
420
+ .content-panel-alphatts-v2 .gr-examples .gr-button.gr-button-tool,
421
+ .content-panel-alphatts-v2 .gr-examples .gr-sample-button
422
+ {{
423
+ background-color:#E0E7FF !important; color:var(--fly-primary) !important;
424
+ border-radius:var(--radius-sm) !important; font-size:0.78em !important; padding:4px 8px !important;
425
  }}
426
+ .content-panel-alphatts-v2 .custom-hr {{height:1px;background-color:var(--fly-border-color);margin:1.5rem 0;border:none;}}
427
+
428
+ /* Footer styling from AlphaTranslator_Styled */
429
+ .app-footer-alphatts-v2 {{
430
+ text-align:center;font-size:0.85em;color:var(--fly-text-secondary);margin-top:2.5rem;
431
+ padding:1rem 0;background-color:rgba(255,255,255,0.3);backdrop-filter:blur(5px);
432
+ border-top:1px solid var(--fly-border-color);
 
433
  }}
434
+ /* Hide default Gradio watermarks/footers more aggressively */
435
+ footer, .gradio-footer, .flagging-container, .footer-utils, .footer, div[class*="svelte-"], .spacer.svelte-164SbmI {{
436
+ display:none !important; visibility:hidden !important; opacity: 0 !important; height: 0 !important;
 
 
 
 
 
 
 
 
 
437
  }}
438
+
439
+
440
+ /* Responsive adjustments from AlphaTranslator_Styled */
441
+ @media (min-width:640px) {{
442
+ .main-content-area-alphatts-v2 {{padding:1.5rem;max-width:700px;}}
443
+ .content-panel-alphatts-v2 {{padding:1.5rem;}}
444
+ .app-header-alphatts-v2 h1 {{font-size:2.5em !important;}}
445
+ .app-header-alphatts-v2 p {{font-size:1.05em !important;}}
446
  }}
447
+ @media (min-width:768px) {{
448
+ .main-content-area-alphatts-v2 {{max-width:780px;}}
449
+ .content-panel-alphatts-v2 {{padding:2rem;}}
450
+ .content-panel-alphatts-v2 .gr-button#{generate_button_alpha_v3} /* Your button ID */
451
+ {{
452
+ width:auto !important; align-self:flex-start;
453
+ }}
454
+ .app-header-alphatts-v2 h1 {{font-size:2.75em !important;}}
455
+ .app-header-alphatts-v2 p {{font-size:1.1em !important;}}
456
  }}
457
  """
458
 
459
+ # --- Gradio UI Definition (YOUR UI structure, with new CSS applied) ---
460
+ # Using app_theme_applied_styled for base theme, and final_combined_css_v2 for specifics
461
+ # The theme for gr.Blocks is `gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")])` from YOUR original code.
462
+ # We will use `app_theme_applied_styled` for the overall page, and `final_combined_css_v2` for detailed styling.
463
+ with gr.Blocks(theme=app_theme_applied_styled, css=final_combined_css_v2, title=f"آلفا TTS ({FIXED_MODEL_NAME.split('-')[1]})") as demo:
464
+ # Header from AlphaTranslator_Styled structure, with a unique class name
465
+ gr.HTML(f"""
466
+ <div class='app-header-alphatts-v2'>
467
+ <h1>🚀 Alpha TTS</h1>
468
+ <p>جادوی تبدیل متن به صدا در دستان شما (Gemini {FIXED_MODEL_NAME.split('-')[1]})</p>
469
+ </div>
 
 
 
 
470
  """)
471
 
472
+ # Main content area wrapper from AlphaTranslator_Styled structure
473
+ with gr.Column(elem_classes=["main-content-area-alphatts-v2"]):
474
+ # Content panel wrapper from AlphaTranslator_Styled structure
475
+ # This will wrap YOUR gr.Column that contains all UI elements.
476
+ with gr.Column(elem_classes=["content-panel-alphatts-v2"]):
477
+
478
+ # YOUR ORIGINAL UI LAYOUT (from your provided AlphaTTS_Original code)
479
+ # All `elem_id`s are from your original code.
480
+
481
+ # Optional: Warning if GEMINI_API_KEY is not set (simple version)
482
+ if not os.environ.get("GEMINI_API_KEY"):
483
+ gr.Markdown("<p style='color:red; text-align:center; margin-bottom:1rem;'>⚠️ <b>هشدار:</b> متغیر محیطی GEMINI_API_KEY تنظیم نشده است. برنامه کار نخواهد کرد.</p>")
484
+
485
+ use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
486
+
487
+ uploaded_file_input = gr.File(
488
+ label=" ",
489
+ file_types=['.txt'],
490
+ visible=False,
491
+ elem_id="file_uploader_alpha_main_v3"
492
+ )
493
+
494
+ text_to_speak_tb = gr.Textbox(
495
+ label="متن فارسی برای تبدیل",
496
+ placeholder="مثال: سلام، فردا هوا چطور است؟",
497
+ lines=5,
498
+ value="",
499
+ visible=True,
500
+ elem_id="text_input_main_alpha_v3"
501
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
+ # YOUR ORIGINAL change function for checkbox
504
+ use_file_input_cb.change(
505
+ fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)),
506
+ inputs=use_file_input_cb,
507
+ outputs=[uploaded_file_input, text_to_speak_tb]
508
+ )
509
+
510
+ speech_prompt_tb = gr.Textbox(
511
+ label="سبک گفتار (اختیاری)",
512
+ placeholder="مثال: با لحنی شاد و پرانرژی",
513
+ value="با لحنی دوستانه و رسا صحبت کن.",
514
+ lines=2, elem_id="speech_prompt_alpha_v3"
515
+ )
516
 
517
+ speaker_voice_dd = gr.Dropdown(
518
+ SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3"
519
+ )
 
 
520
 
521
+ temperature_slider = gr.Slider(
522
+ minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا",
523
+ elem_id="temperature_slider_alpha_v3"
524
+ )
525
+ # Your original temperature description
526
+ gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
 
 
 
 
 
527
 
528
+ # Your original button, CSS will target its elem_id
529
+ generate_button = gr.Button("🚀 تولید و پخش صدا", elem_id="generate_button_alpha_v3")
530
+
531
+ # Your original audio output, CSS will target its elem_id
532
+ output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3")
533
+
534
+ # Your original Examples section
535
+ gr.HTML("<hr class='custom-hr'>") # Using the styled HR
536
+ gr.Markdown( # Simplified title styling
537
+ "<h3 style='text-align:center; font-weight:500; color:var(--fly-text-secondary); margin-top:1.5rem; margin-bottom:1rem;'>نمونه‌های کاربردی</h3>"
538
+ )
539
+ gr.Examples(
540
+ examples=[
541
+ [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
542
+ [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبیعی و روان.", "Charon", 0.9],
543
+ ],
544
+ inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
545
+ outputs=[output_audio], # YOUR ORIGINAL OUTPUTS FOR EXAMPLES
546
+ fn=gradio_tts_interface,
547
+ cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true"
548
+ )
549
 
550
+ # Footer from AlphaTranslator_Styled structure, with unique class name
551
+ gr.Markdown(f"<p class='app-footer-alphatts-v2'>Alpha TTS © 2024 - Model: {FIXED_MODEL_NAME.split('-')[0].upper()} {FIXED_MODEL_NAME.split('-')[1]}</p>")
 
 
 
552
 
 
 
 
 
553
 
554
+ # --- Event Handlers (YOUR ORIGINAL EVENT HANDLERS) ---
555
+ if generate_button is not None:
556
+ generate_button.click(
557
+ fn=gradio_tts_interface,
558
+ inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider, gr.Progress(track_tqdm=True)], # Added progress back as per your original signature
559
+ outputs=[output_audio]
560
+ )
561
+ else:
562
+ logging.error("دکمه تولید صدا (generate_button_alpha_v3) در UI یافت نشد.")
563
+
564
+
565
+ if __name__ == "__main__":
566
+ if not PYDUB_AVAILABLE:
567
+ logging.warning("Pydub (for audio merging) not found. Merging will be disabled if multiple audio chunks are generated.")
568
+ if not os.environ.get("GEMINI_API_KEY"):
569
+ logging.warning("GEMINI_API_KEY environment variable not set. TTS functionality WILL FAIL.")
570
 
571
  demo.launch(
572
  server_name="0.0.0.0",